Repository navigation
Scan, DryRun, Deduplicate and Stats each copy the path prompt and the scan→hash→group pipeline, so every fix to that path must be made four times #157
Description
What's wrong
The four verbs in FileDeduplicator/Verbs/ duplicate two blocks almost word for word.
- The path prompt.
ValidateArgs()inScan.cs:14-30,DryRun.cs:14-30,Deduplicate.cs:14-30andStats.cs:15-31is the same method:PathString is "." or ""→ prompt →Console.ReadLine()?.Trim()→ abort when the input is empty. - The pipeline. Steps 1-3 of each
Run(Scan.cs:37-61,DryRun.cs:37-61,Deduplicate.cs:37-61,Stats.cs:38-56) are the same sequence:FileScanner.ScanForFiles→ print the count → return on zero →FileHasher.HashFiles→Deduplicator.GroupByHash→Deduplicator.FindDuplicates. For Scan and DryRun, lines 14-61 differ only in the verb type and one banner line.
On top of that, Scan.cs:64-83 renders its own KEEP/DELETE listing and wasted-space total instead of using DuplicateReport.PlanDeletions. DuplicateReport's own remarks say all verbs should build this listing in one place so they cannot drift apart.
Why it matters
Several open issues change exactly this duplicated code. Each fix has to land four times, identically, or the verbs start to behave differently:
- Passing
-p .explicitly is treated as "no path" and prompts, swallowing the next stdin line as the path #145: an explicit-p .is treated as "no path" (thePathString is "." or ""check, in four places). - A quoted path pasted at the "Enter the path" prompt is scanned as <cwd>/"<path>" and reported as "Directory not found" #152: a quoted path pasted at the prompt (the
ReadLine()?.Trim(), in four places). - A file deleted or moved during hashing crashes DryRun/Deduplicate/Stats with FileNotFoundException after all the hashing is done #141: a file vanishes during hashing (the pipeline; PR Tolerate a file that vanishes during the hash pass #156 is open).
- Every verb reads and SHA-256-hashes every file in full, even files whose size no other file shares #147: pre-group files by size before hashing (the pipeline, in four places).
- Every verb exits with code 0 even when the directory is missing, deletions fail, or the arguments are invalid #144: exit codes (the early
returns in each copy).
If a fix reaches three verbs and misses the fourth, one verb reports a different set of duplicates, or accepts different input, than the others. The codebase already treats that kind of divergence as its worst defect: DryRunEquivalenceTests exists because DryRun has to predict Deduplicate exactly.
Suggested fix
- Move the prompt into
BaseVerb<T>, and have each verb supply only its prompt text (for example, through an abstractPromptTextproperty). - Add one method, for example
DuplicateFinder.Find(AbsoluteDirectoryPath) -> ScanResult, that returns the scanned files, the hashes and the duplicate groups. Each verb calls it once and keeps only its own reporting. - Make Scan render from
DuplicateReport.PlanDeletions, like DryRun and Deduplicate do. It can keep its[KEEP]/[DELETE]suffix format.
Acceptance criteria
ValidateArgsand the scan/hash/group sequence each exist in exactly one place.- Scan's KEEP/DELETE decisions and byte total come from
DuplicateReport. - The existing
VerbOutputTests,VerbReuseTestsandDryRunEquivalenceTestspass unchanged. - Ideally, land this before or together with the fixes for Passing
-p .explicitly is treated as "no path" and prompts, swallowing the next stdin line as the path #145, A quoted path pasted at the "Enter the path" prompt is scanned as <cwd>/"<path>" and reported as "Directory not found" #152, Every verb reads and SHA-256-hashes every file in full, even files whose size no other file shares #147 and Every verb exits with code 0 even when the directory is missing, deletions fail, or the arguments are invalid #144, so each of those is made once.
Activity
matt-edmondson commentedon Sep 29, 2026
Triage
-p .explicitly is treated as "no path" and prompts, swallowing the next stdin line as the path #145, A quoted path pasted at the "Enter the path" prompt is scanned as <cwd>/"<path>" and reported as "Directory not found" #152, Every verb reads and SHA-256-hashes every file in full, even files whose size no other file shares #147 and Every verb exits with code 0 even when the directory is missing, deletions fail, or the arguments are invalid #144: without it, each of those fixes has to be made four times identically, and a miss makes the verbs disagree. That kind of drift is the codebase's worst class of defect, which is whyDryRunEquivalenceTestsexists.FileDeduplicator/Verbs/),BaseVerb<T>andDuplicateReport. Whoever owns the verbs should take it.DuplicateFinder.Find, so it isn't lost.-p .explicitly is treated as "no path" and prompts, swallowing the next stdin line as the path #145, A quoted path pasted at the "Enter the path" prompt is scanned as <cwd>/"<path>" and reported as "Directory not found" #152, Every verb reads and SHA-256-hashes every file in full, even files whose size no other file shares #147 and Every verb exits with code 0 even when the directory is missing, deletions fail, or the arguments are invalid #144 so each of them is fixed in one place. Keep the existingVerbOutputTests,VerbReuseTestsandDryRunEquivalenceTestsunchanged as the regression gate.Generated by Claude Code
Check the root before a verb runs, so the exit code adds no lines to …