Rewrites README.md from scratch for #135. Every statement was checked
against the code on next and against what the built binary prints on
small trees under /var/tmp. The headings that the code comments cite
are kept. Five main_test.go comments cite "Settings", two cite what
"Error handling" says about a failed write to stdout, and one comment
each in scan_test.go, trees_test.go and exclude_test.go cites what
the README now says.
Where the old text and the repository disagreed, the new text states
what the code does: @latest installs the newest tagged release, ten
million files in both "Rationale" and "Design", no Windows excludes, and
an operand reached through a symlinked directory below another operand
is dropped (#144).
Model: fable-5-1
Rewrites `README.md` from scratch for
https://git.eeqj.de/sneak/sfdupes/issues/135. Every statement was checked
against the code on `next` and against what the built binary prints on
small trees under `/var/tmp`. The headings that the code comments cite
are kept. Five `main_test.go` comments cite "Settings", two cite what
"Error handling" says about a failed write to stdout, and one comment
each in `scan_test.go`, `trees_test.go` and `exclude_test.go` cites what
the README now says.
Where the old text and the repository disagreed, the new text states
what the code does: `@latest` installs the newest tagged release, ten
million files in both "Rationale" and "Design", no Windows excludes, and
an operand reached through a symlinked directory below another operand
is dropped (https://git.eeqj.de/sneak/sfdupes/issues/144).
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
Broken rendering
L430-435: the wrap puts 1. at the start of a line ("it fails with exit" / "1. No flag turns this rule off."), so Markdown starts a numbered list there and cuts the sentence in two. Acceptable: wording that leaves no 1. at the start of a line, such as "it fails and exits with status 1".
False or contradicting statements
L960-962: "Otherwise node 22.17.0 is installed through nvm, from a release archive whose SHA-256 the script checks, and yarn 1.22.22 is activated under it through corepack." The script checks the SHA-256 of nvm's archive, not node's. The verb also comes last in "whose SHA-256 the script checks". Acceptable: "Otherwise the script downloads nvm, checks the archive's SHA-256, and installs node 22.17.0 through it."
L497: "a line for each operand it drops;" An operand dropped because it repeats another, or lies under another, prints no line. Acceptable: say which dropped operands get a line.
L530-531: "SIGTERM is always caught." contradicts the sentence before it ("After the first signal scan stops catching signals."). "as a shell's background job does" holds only for a background job started from a script; an interactive shell does not make its background jobs ignore SIGINT. Acceptable: "scan catches SIGTERM even when it inherits it as ignored." and "as a background job started from a script does".
L609: "Report only the largest trees." The rule keeps the outermost duplicate trees, not the largest ones by size, and the imperative reads as an instruction to the reader. Acceptable: "Only the outermost duplicate trees are reported."
L611-612: "A group holding sibling directories, or members whose parents differ, is always reported." In the sentence before, the parents are already distinct directories; what differs is their digests. Acceptable: "or members whose parents have different digests".
L923-934: "The fatal errors are:" and "2: a usage error:" read as complete lists, and they are not. Exit 1 also covers a database with another schema version, a version-0 database with a files table, and a mount list that cannot be read. Exit 2 also covers an unknown flag. Acceptable: complete both lists.
L303: "The sampled hash is probabilistic by design." Nothing is random: the samples sit at fixed offsets. Acceptable: "The sampled hash is not exact."
L215-216: "the reports run at any time against the last completed scan." L756-757 says that a report may see a running scan's changes partly applied. Acceptable: "against what the database holds".
L229: "modernc.org/sqlite is pure Go, so no build needs cgo, Go's bridge to C code." The test build needs cgo for the race detector (L977). Acceptable: "so the binary builds without cgo".
L987-988: "Prettier uses 4-space indents and wraps prose at 80 columns, as .prettierrc sets." .prettierrc sets the indent and proseWrap, but 80 columns is prettier's default. Acceptable: say which file sets what.
L827: "A first scan has no estimate." Any scan with no records under its operands has none. Acceptable: "A scan with no records under its operands, such as a first scan, has no estimate."
Lists chained with commas where a bulleted list belongs (the old text had bullets for 13 to 15)
L366-367: "The walk leaves out a default exclude, a path that is not on a persistent filesystem, and, with -x, a directory on another filesystem than its operand."
L890-892: "Every phase has an exact total once it has counted its work, except scan's walk, the open phase of report and trees, and both phases of info."
L903-904: "Those are the phase and its place, the progress, the elapsed time and the time remaining."
L577-578 and L647-649: "The summary line on stderr gives the records read, the duplicate groups, the dupe files and the reclaimable bytes, which are the sum of size over all rows:", and the same sentence for trees.
Verb or subject arrives late
L359: "A record under a scanned operand whose file the scan did not verify is deleted." Acceptable: "The scan deletes each record under a scanned operand whose file it could not verify."
L361-362: "The database only ever holds signatures that the latest scan covering them could verify." It also uses "signatures" before L594 defines the word.
L365: "A record at or below a path the walk left out on purpose counts as outside the scanned operands, so it is kept." Acceptable: "The scan keeps every record at or below a path that the walk leaves out on purpose."
L369-371: "Records below an operand that is itself at or inside such a path are verified by that operand's own walk, so they are deleted when their files are gone." The verb comes 13 words in.
L469-471: "The records below a default exclude the walk did not enter are kept, as described under ..." Acceptable: "The scan keeps the records below a default exclude that the walk did not enter."
L432-434: "The walk never enters a directory, and never records a file, on one of those devices, however it reaches it, through a bind mount included." Acceptable: two short sentences.
L982-984: "In the digest-pinned golangci/golangci-lint image, the gofmt check, golangci-lint config verify and golangci-lint run run as build steps, so a successful build is a clean lint." The verb follows a three-item subject. Acceptable: a bulleted list of the three steps.
Edge case before the main rule
L290-292: "In a file of 64 KiB or less, each window is the whole file, so all three hashes are the hash of the whole file." and "An empty file has fixed hashes and is never opened." Both come before the content-hash rule at L294 that they qualify, and "window" is never defined. Acceptable: move both after the content-hash rule, and say "each 64 KiB stretch".
Terms used before they are defined, or never defined
"operand" at L267 (defined at L317); "workers" at L263, "seeds the same pool of walk workers" at L328 and "the queue" at L382; "default excludes" at L270 (defined at L445); "signature" at L361, L551 and L588 (defined at L594); "persistent filesystem" at L335 (defined at L413); the -wal and -shm files at L405 (WAL is explained at L751); "inode" at L386; "cleaned" at L325; "TSV" at L560; "busy timeout" at L752; "rollback-journal mode" at L760; "firmlinks" at L452; "a farm of hard-linked backups" at L388. The opening (L20-23) names scan and "the reports" before the reader learns that the program has subcommands. Acceptable: a short definition where each term first appears, or the definition moved up.
Other sentences a newcomer has to read twice
L340-341: "The records beneath a dropped operand count as outside the scanned operands, unless another operand covers them." Only at L363 does the reader learn that this means they are kept. Acceptable: "The scan keeps the records beneath a dropped operand, unless another operand covers them."
L587-588: "It reports whole duplicate directory trees: directories under which the same relative paths hold files with the same signatures." The sentence never says "the same as what". Acceptable: "Two directories are duplicate trees when they hold the same relative paths, and each path holds a file with the same signature."
L855: "a burst after a wait shows as a burst;" This tells the reader nothing. Acceptable: say what the reader sees.
L343, L441 and L466: "with a line that is not a warning" and "The line is not a warning". The document never says what makes a line a warning, so the phrase adds nothing to "not counted as skipped". Acceptable: drop it, or say what sets a warning apart.
Order of sections
L220-232: "Constraints" covers the Go module, the dependency list, cgo and the Makefile. That is development material, and it sits before the reference. L1111: "Non-goals" tells users what the tool does not do, but it comes after the development sections. #135 requires development and testing last. Acceptable: move Constraints into the development part, and Non-goals ahead of the reference.
Backticks
These commands are named in plain text:
gofmt at L983, L992, L994 and L1035 (it is backticked at L1051);
yarn, nvm and corepack at L961-965 and L989 (npm is backticked at L964);
prettier at L966, L987-988, L992 and L1051;
nix, apt, brew and apk at L959.
Acceptable: backtick every command name.
Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
**Broken rendering**
1. L430-435: the wrap puts `1.` at the start of a line ("it fails with exit" / "1. No flag turns this rule off."), so Markdown starts a numbered list there and cuts the sentence in two. Acceptable: wording that leaves no `1.` at the start of a line, such as "it fails and exits with status 1".
**False or contradicting statements**
2. L960-962: "Otherwise node 22.17.0 is installed through nvm, from a release archive whose SHA-256 the script checks, and yarn 1.22.22 is activated under it through corepack." The script checks the SHA-256 of nvm's archive, not node's. The verb also comes last in "whose SHA-256 the script checks". Acceptable: "Otherwise the script downloads `nvm`, checks the archive's SHA-256, and installs node 22.17.0 through it."
3. L497: "a line for each operand it drops;" An operand dropped because it repeats another, or lies under another, prints no line. Acceptable: say which dropped operands get a line.
4. L530-531: "`SIGTERM` is always caught." contradicts the sentence before it ("After the first signal `scan` stops catching signals."). "as a shell's background job does" holds only for a background job started from a script; an interactive shell does not make its background jobs ignore `SIGINT`. Acceptable: "`scan` catches `SIGTERM` even when it inherits it as ignored." and "as a background job started from a script does".
5. L609: "Report only the largest trees." The rule keeps the outermost duplicate trees, not the largest ones by size, and the imperative reads as an instruction to the reader. Acceptable: "Only the outermost duplicate trees are reported."
6. L611-612: "A group holding sibling directories, or members whose parents differ, is always reported." In the sentence before, the parents are already distinct directories; what differs is their digests. Acceptable: "or members whose parents have different digests".
7. L923-934: "The fatal errors are:" and "`2`: a usage error:" read as complete lists, and they are not. Exit 1 also covers a database with another schema version, a version-0 database with a `files` table, and a mount list that cannot be read. Exit 2 also covers an unknown flag. Acceptable: complete both lists.
8. L303: "The sampled hash is probabilistic by design." Nothing is random: the samples sit at fixed offsets. Acceptable: "The sampled hash is not exact."
9. L215-216: "the reports run at any time against the last completed scan." L756-757 says that a report may see a running scan's changes partly applied. Acceptable: "against what the database holds".
10. L229: "`modernc.org/sqlite` is pure Go, so no build needs cgo, Go's bridge to C code." The test build needs cgo for the race detector (L977). Acceptable: "so the binary builds without cgo".
11. L987-988: "Prettier uses 4-space indents and wraps prose at 80 columns, as `.prettierrc` sets." `.prettierrc` sets the indent and `proseWrap`, but 80 columns is prettier's default. Acceptable: say which file sets what.
12. L827: "A first scan has no estimate." Any scan with no records under its operands has none. Acceptable: "A scan with no records under its operands, such as a first scan, has no estimate."
**Lists chained with commas where a bulleted list belongs** (the old text had bullets for 13 to 15)
13. L366-367: "The walk leaves out a default exclude, a path that is not on a persistent filesystem, and, with `-x`, a directory on another filesystem than its operand."
14. L890-892: "Every phase has an exact total once it has counted its work, except `scan`'s `walk`, the `open` phase of `report` and `trees`, and both phases of `info`."
15. L903-904: "Those are the phase and its place, the progress, the elapsed time and the time remaining."
16. L577-578 and L647-649: "The summary line on stderr gives the records read, the duplicate groups, the dupe files and the reclaimable bytes, which are the sum of `size` over all rows:", and the same sentence for `trees`.
**Verb or subject arrives late**
17. L359: "A record under a scanned operand whose file the scan did not verify is deleted." Acceptable: "The scan deletes each record under a scanned operand whose file it could not verify."
18. L361-362: "The database only ever holds signatures that the latest scan covering them could verify." It also uses "signatures" before L594 defines the word.
19. L365: "A record at or below a path the walk left out on purpose counts as outside the scanned operands, so it is kept." Acceptable: "The scan keeps every record at or below a path that the walk leaves out on purpose."
20. L369-371: "Records below an operand that is itself at or inside such a path are verified by that operand's own walk, so they are deleted when their files are gone." The verb comes 13 words in.
21. L469-471: "The records below a default exclude the walk did not enter are kept, as described under ..." Acceptable: "The scan keeps the records below a default exclude that the walk did not enter."
22. L432-434: "The walk never enters a directory, and never records a file, on one of those devices, however it reaches it, through a bind mount included." Acceptable: two short sentences.
23. L982-984: "In the digest-pinned `golangci/golangci-lint` image, the gofmt check, `golangci-lint config verify` and `golangci-lint run` run as build steps, so a successful build is a clean lint." The verb follows a three-item subject. Acceptable: a bulleted list of the three steps.
**Edge case before the main rule**
24. L290-292: "In a file of 64 KiB or less, each window is the whole file, so all three hashes are the hash of the whole file." and "An empty file has fixed hashes and is never opened." Both come before the content-hash rule at L294 that they qualify, and "window" is never defined. Acceptable: move both after the content-hash rule, and say "each 64 KiB stretch".
**Terms used before they are defined, or never defined**
25. "operand" at L267 (defined at L317); "workers" at L263, "seeds the same pool of walk workers" at L328 and "the queue" at L382; "default excludes" at L270 (defined at L445); "signature" at L361, L551 and L588 (defined at L594); "persistent filesystem" at L335 (defined at L413); the `-wal` and `-shm` files at L405 (WAL is explained at L751); "inode" at L386; "cleaned" at L325; "TSV" at L560; "busy timeout" at L752; "rollback-journal mode" at L760; "firmlinks" at L452; "a farm of hard-linked backups" at L388. The opening (L20-23) names `scan` and "the reports" before the reader learns that the program has subcommands. Acceptable: a short definition where each term first appears, or the definition moved up.
**Other sentences a newcomer has to read twice**
26. L340-341: "The records beneath a dropped operand count as outside the scanned operands, unless another operand covers them." Only at L363 does the reader learn that this means they are kept. Acceptable: "The scan keeps the records beneath a dropped operand, unless another operand covers them."
27. L587-588: "It reports whole duplicate directory trees: directories under which the same relative paths hold files with the same signatures." The sentence never says "the same as what". Acceptable: "Two directories are duplicate trees when they hold the same relative paths, and each path holds a file with the same signature."
28. L855: "a burst after a wait shows as a burst;" This tells the reader nothing. Acceptable: say what the reader sees.
29. L343, L441 and L466: "with a line that is not a warning" and "The line is not a warning". The document never says what makes a line a warning, so the phrase adds nothing to "not counted as skipped". Acceptable: drop it, or say what sets a warning apart.
**Order of sections**
30. L220-232: "Constraints" covers the Go module, the dependency list, cgo and the `Makefile`. That is development material, and it sits before the reference. L1111: "Non-goals" tells users what the tool does not do, but it comes after the development sections. https://git.eeqj.de/sneak/sfdupes/issues/135 requires development and testing last. Acceptable: move Constraints into the development part, and Non-goals ahead of the reference.
**Backticks**
31. These commands are named in plain text:
- `gofmt` at L983, L992, L994 and L1035 (it is backticked at L1051);
- `yarn`, `nvm` and `corepack` at L961-965 and L989 (`npm` is backticked at L964);
- `prettier` at L966, L987-988, L992 and L1051;
- `nix`, `apt`, `brew` and `apk` at L959.
Acceptable: backtick every command name.
Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next.
Broken rendering (1): no line now starts with a numeral and a period; the sentence reads "is a fatal error, and nothing is scanned".
False or contradicting statements (2 to 12): each corrected against the code or the built binary. The SHA-256 check is nvm's archive; a repeated or nested operand is dropped without a line; SIGTERM is caught even when inherited as ignored; "the outermost duplicate trees"; "parents have different digests"; both exit-code lists are complete (mount list, schema version, version-0 files table, a hash not 32 bytes, directory and lock file, working directory; unknown flag and bad --workers value); "not exact"; "against what the database holds"; "the binary builds without cgo", with the test build as the exception; .prettierrc sets the indent and the wrapping and 80 columns is prettier's default; "A scan with no records under its operands, such as a first scan".
Lists (13 to 16): all four are bulleted.
Verb or subject late (17 to 23): each rewritten as suggested or split in two; the three lint steps are a list.
Edge case before the rule (24): both sentences follow the content-hash rule and say "each 64 KiB stretch".
Terms (25): every term listed is defined where it first appears, and the opening says the program has four subcommands before naming them. Also defined: stdout and stderr, stat, device ID, bind mount, cgo, the race detector, digest, flock(2), NUL, SIGPIPE, pseudo-version, spinner, pager.
Read twice (26 to 29): rewritten as suggested; "not a warning" is gone.
Order of sections (30): "Non-goals" follows "Design"; "Constraints" sits under "Build". Every heading the code comments cite keeps its name.
Backticks (31): every command name is backticked.
Judgement call: a short list of plain names inside one sentence, such as report, trees and info, stays inline; a list whose items carry their own conditions is bulleted.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155299: one amended commit, rebased onto current `next`.
**Broken rendering** (1): no line now starts with a numeral and a period; the sentence reads "is a fatal error, and nothing is scanned".
**False or contradicting statements** (2 to 12): each corrected against the code or the built binary. The SHA-256 check is `nvm`'s archive; a repeated or nested operand is dropped without a line; `SIGTERM` is caught even when inherited as ignored; "the outermost duplicate trees"; "parents have different digests"; both exit-code lists are complete (mount list, schema version, version-0 `files` table, a hash not 32 bytes, directory and lock file, working directory; unknown flag and bad `--workers` value); "not exact"; "against what the database holds"; "the binary builds without cgo", with the test build as the exception; `.prettierrc` sets the indent and the wrapping and 80 columns is `prettier`'s default; "A scan with no records under its operands, such as a first scan".
**Lists** (13 to 16): all four are bulleted.
**Verb or subject late** (17 to 23): each rewritten as suggested or split in two; the three lint steps are a list.
**Edge case before the rule** (24): both sentences follow the content-hash rule and say "each 64 KiB stretch".
**Terms** (25): every term listed is defined where it first appears, and the opening says the program has four subcommands before naming them. Also defined: stdout and stderr, stat, device ID, bind mount, cgo, the race detector, digest, `flock(2)`, NUL, `SIGPIPE`, pseudo-version, spinner, pager.
**Read twice** (26 to 29): rewritten as suggested; "not a warning" is gone.
**Order of sections** (30): "Non-goals" follows "Design"; "Constraints" sits under "Build". Every heading the code comments cite keeps its name.
**Backticks** (31): every command name is backticked.
Judgement call: a short list of plain names inside one sentence, such as `report`, `trees` and `info`, stays inline; a list whose items carry their own conditions is bulleted.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
False or incomplete statements
L268-270: "--no-default-excludes (scan) lets the walk enter the default excludes: ... such as /proc and /tmp on Linux". The flag never lets the walk into /proc. It is a proc filesystem, which stays left out with or without the flag, and so is /tmp wherever it is tmpfs. Acceptable: an example the flag really opens, such as /var/tmp.
L331-332: "An operand under another is kept when the outer operand's walk would not reach it." An operand inside a .zfs directory under another operand is dropped, and its files are not scanned, although the outer walk never enters .zfs. Acceptable: the rule the code applies, such as "An operand under another is dropped, unless the inner operand, or a directory between the two, is one of these:".
L115-123 and L510-518: both lists of what scan prints end with the summary line. A scan that ends while a report has the database open prints its WAL-mode warning after the summary line. Acceptable: that warning at the end of both lists.
L1086-1087: "The Makefile targets are shims onto them, except build". make clean also carries its own recipe. Acceptable: "except build and clean".
main_test.go cites README "scan mode" for the --workers rule (L651) and README "Subcommands" for --help and --version (L689, L716). This PR moves all three rules to "Settings", while its body says no comment goes stale. Acceptable: those three comments updated in this PR, or the rules kept where they are cited.
Sentences that fail the rules
L395-397, 35 words and three ideas: "An inode is the filesystem's own record of a file, and files with nearby inode numbers usually lie near each other on disk, so this order keeps a spinning disk from seeking back and forth." Acceptable: one sentence for the definition, one for why the order helps.
L803-805, 33 words, with a comma-chained list that reads as three items: "Then, still before the walk: the schema, the table and index described under "Schema", in a new database, and the -wal and -shm files, which stay beside the database while the scan runs." Acceptable: two bullets, one for the table and index of a new database, one for the -wal and -shm files.
L991-993, 31 words, with the subject after a clause and an example: "When the reader of a stdout pipe exits early, as in sfdupes report | head, the next write ends sfdupes with SIGPIPE, the signal for a write to a closed pipe." Acceptable: two short sentences.
L548-549, with the verb last after two stacked clauses: "A SIGINT that scan inherits as ignored, as a background job started from a script does, stays ignored." Acceptable: "A background job started from a script inherits SIGINT as ignored, and scan leaves it ignored."
L362-363, with the verb last in the relative clause: "That covers deleted files, and also paths whose details or contents the scan could not read this run." Acceptable: "That covers deleted files, and paths the scan could not stat or read this run."
L628-629 gives the reader an instruction, unlike the rest of the list: "Build the hierarchy, compute every directory's digest, and group the directories by digest." Acceptable: "trees builds the hierarchy, computes every directory's digest, and groups the directories by digest."
L1014-1017: in "With an installed node 22, a yarn already on PATH is used as it is. Otherwise yarn 1.22.22 is activated through corepack, or installed with npm when there is no corepack.", the second "Otherwise" reads as "without node 22", the case two sentences earlier. Acceptable: "When node 22 is installed but yarn is not, ...".
Terms used before they are defined, or never defined
These terms need a short definition where each first appears, or a plain word in their place:
"Bind-mount" at L90, defined only at L448;
"workers" at L328, defined only at L499-500 (a "see" pointer is not a definition);
"device" at L417, while "device ID" is defined at L438;
"umask" at L133;
SIGTERM at L531 (SIGPIPE is defined, SIGTERM is not);
"shims" at L1004 and L1087;
"converges" at L543 (plainly: brings the database up to date).
Not verified: the macOS behaviour.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
**False or incomplete statements**
1. L268-270: "`--no-default-excludes` (`scan`) lets the walk enter the default excludes: ... such as `/proc` and `/tmp` on Linux". The flag never lets the walk into `/proc`. It is a `proc` filesystem, which stays left out with or without the flag, and so is `/tmp` wherever it is `tmpfs`. Acceptable: an example the flag really opens, such as `/var/tmp`.
2. L331-332: "An operand under another is kept when the outer operand's walk would not reach it." An operand inside a `.zfs` directory under another operand is dropped, and its files are not scanned, although the outer walk never enters `.zfs`. Acceptable: the rule the code applies, such as "An operand under another is dropped, unless the inner operand, or a directory between the two, is one of these:".
3. L115-123 and L510-518: both lists of what `scan` prints end with the summary line. A scan that ends while a report has the database open prints its WAL-mode warning after the summary line. Acceptable: that warning at the end of both lists.
4. L1086-1087: "The `Makefile` targets are shims onto them, except `build`". `make clean` also carries its own recipe. Acceptable: "except `build` and `clean`".
5. `main_test.go` cites README "`scan` mode" for the `--workers` rule (L651) and README "Subcommands" for `--help` and `--version` (L689, L716). This PR moves all three rules to "Settings", while its body says no comment goes stale. Acceptable: those three comments updated in this PR, or the rules kept where they are cited.
**Sentences that fail the rules**
6. L395-397, 35 words and three ideas: "An inode is the filesystem's own record of a file, and files with nearby inode numbers usually lie near each other on disk, so this order keeps a spinning disk from seeking back and forth." Acceptable: one sentence for the definition, one for why the order helps.
7. L803-805, 33 words, with a comma-chained list that reads as three items: "Then, still before the walk: the schema, the table and index described under "Schema", in a new database, and the `-wal` and `-shm` files, which stay beside the database while the scan runs." Acceptable: two bullets, one for the table and index of a new database, one for the `-wal` and `-shm` files.
8. L991-993, 31 words, with the subject after a clause and an example: "When the reader of a stdout pipe exits early, as in `sfdupes report | head`, the next write ends `sfdupes` with `SIGPIPE`, the signal for a write to a closed pipe." Acceptable: two short sentences.
9. L548-549, with the verb last after two stacked clauses: "A `SIGINT` that `scan` inherits as ignored, as a background job started from a script does, stays ignored." Acceptable: "A background job started from a script inherits `SIGINT` as ignored, and `scan` leaves it ignored."
10. L362-363, with the verb last in the relative clause: "That covers deleted files, and also paths whose details or contents the scan could not read this run." Acceptable: "That covers deleted files, and paths the scan could not stat or read this run."
11. L628-629 gives the reader an instruction, unlike the rest of the list: "Build the hierarchy, compute every directory's digest, and group the directories by digest." Acceptable: "`trees` builds the hierarchy, computes every directory's digest, and groups the directories by digest."
12. L1014-1017: in "With an installed `node` 22, a `yarn` already on `PATH` is used as it is. Otherwise `yarn` 1.22.22 is activated through `corepack`, or installed with `npm` when there is no `corepack`.", the second "Otherwise" reads as "without `node` 22", the case two sentences earlier. Acceptable: "When `node` 22 is installed but `yarn` is not, ...".
**Terms used before they are defined, or never defined**
13. These terms need a short definition where each first appears, or a plain word in their place:
- "Bind-mount" at L90, defined only at L448;
- "workers" at L328, defined only at L499-500 (a "see" pointer is not a definition);
- "device" at L417, while "device ID" is defined at L438;
- "umask" at L133;
- `SIGTERM` at L531 (`SIGPIPE` is defined, `SIGTERM` is not);
- "shims" at L1004 and L1087;
- "converges" at L543 (plainly: brings the database up to date).
Not verified: the macOS behaviour.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next.
False or incomplete statements (1 to 5): the --no-default-excludes example is /var/tmp; the nested-operand rule reads "kept only when the inner operand, or a directory between the two, is one of these", followed by the three cases; both lists of what scan prints end with the warning for a report that still has the database open; "except build and clean"; the three main_test.go comments now cite "Settings".
Sentences (6 to 12): the inode definition and the reason for inode order are two sentences; the pre-walk writes are two bullets; the SIGPIPE case is two sentences; "A background job started from a script inherits SIGINT as ignored, and scan leaves it ignored"; "paths the scan could not stat or read this run", with stat defined there; "trees builds the hierarchy, ..."; the yarn cases are two bullets, one for an installed node 22 and one for the nvm install.
Terms (13): bind mount is defined at the Docker example, workers at the walk phase, device ID and inode number at the database-files rule, umask at the cron section, SIGINT and SIGTERM at "Stopping a scan"; "shims" and "converges" are gone. Also defined on first use: positional argument, pager, ZFS, node, yarn, nvm, corepack and prettier.
Whole-document pass: cron and git are backticked where they name the program; no sentence exceeds about 30 words.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155389: one amended commit, rebased onto current `next`.
**False or incomplete statements** (1 to 5): the `--no-default-excludes` example is `/var/tmp`; the nested-operand rule reads "kept only when the inner operand, or a directory between the two, is one of these", followed by the three cases; both lists of what `scan` prints end with the warning for a report that still has the database open; "except `build` and `clean`"; the three `main_test.go` comments now cite "Settings".
**Sentences** (6 to 12): the inode definition and the reason for inode order are two sentences; the pre-walk writes are two bullets; the `SIGPIPE` case is two sentences; "A background job started from a script inherits `SIGINT` as ignored, and `scan` leaves it ignored"; "paths the scan could not stat or read this run", with stat defined there; "`trees` builds the hierarchy, ..."; the `yarn` cases are two bullets, one for an installed `node` 22 and one for the `nvm` install.
**Terms** (13): bind mount is defined at the Docker example, workers at the walk phase, device ID and inode number at the database-files rule, umask at the cron section, `SIGINT` and `SIGTERM` at "Stopping a scan"; "shims" and "converges" are gone. Also defined on first use: positional argument, pager, ZFS, `node`, `yarn`, `nvm`, `corepack` and `prettier`.
Whole-document pass: `cron` and `git` are backticked where they name the program; no sentence exceeds about 30 words.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
False statements
L122-123 and L524: "a line for each path the walk leaves out on purpose". L377-382 counts a directory that -x stops at among the paths the walk leaves out on purpose, but scan prints no line for that directory. Only default excludes and paths not on a persistent filesystem get a line. Acceptable: name the two kinds that get a line, or say that a directory -x stops at gets none.
L1019-1020: "each Makefile target does nothing but call its script". L1108-1110 says, as the Makefile does, that build and clean carry their own recipes. Acceptable: the same exception here, or drop the clause.
Verb arrives late
L523-525: "A warning for each path it skips, and a line for each path the walk leaves out on purpose, come as they happen;". The verb comes after an 18-word subject, and "it" has nothing in the bullet to refer to. Acceptable: a short sentence with scan as its subject, such as "scan prints each warning, and each line for a path left out, as it happens."
Sentences a newcomer has to read twice
L427-429: "So the walk passes over them however it reaches them, through a symlinked directory included." L420 says symlinks are never followed, so this reads as a contradiction. Acceptable: say how a symlink can lead there (an operand whose path passes through a symlinked directory), or drop the phrase.
L481-483: "An operand at or inside a default exclude is still walked, so sfdupes scan /tmp/x works when /tmp/x is on a persistent filesystem." It follows the sentence on --no-default-excludes, so "still" reads as "with that flag too", which says nothing. The rule holds without the flag. Acceptable: move it ahead of the flag's paragraph, or "With or without the flag, an operand at or inside a default exclude is walked."
L750-752: "After a cd through a symlink, a ../ therefore counts from the directory the symlink leads to, as it does for any other program." "therefore" makes this a consequence of how the line is printed. It is how the system resolves .. for every program. Acceptable: drop "therefore", and say that the printed path shows that directory.
Not verified: the macOS behaviour. Finding 1 rests on the code (walkPhase in scan.go), not on a run across a real mount.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
**False statements**
1. L122-123 and L524: "a line for each path the walk leaves out on purpose". L377-382 counts a directory that `-x` stops at among the paths the walk leaves out on purpose, but `scan` prints no line for that directory. Only default excludes and paths not on a persistent filesystem get a line. Acceptable: name the two kinds that get a line, or say that a directory `-x` stops at gets none.
2. L1019-1020: "each `Makefile` target does nothing but call its script". L1108-1110 says, as the `Makefile` does, that `build` and `clean` carry their own recipes. Acceptable: the same exception here, or drop the clause.
**Verb arrives late**
3. L523-525: "A warning for each path it skips, and a line for each path the walk leaves out on purpose, come as they happen;". The verb comes after an 18-word subject, and "it" has nothing in the bullet to refer to. Acceptable: a short sentence with `scan` as its subject, such as "`scan` prints each warning, and each line for a path left out, as it happens."
**Sentences a newcomer has to read twice**
4. L427-429: "So the walk passes over them however it reaches them, through a symlinked directory included." L420 says symlinks are never followed, so this reads as a contradiction. Acceptable: say how a symlink can lead there (an operand whose path passes through a symlinked directory), or drop the phrase.
5. L481-483: "An operand at or inside a default exclude is still walked, so `sfdupes scan /tmp/x` works when `/tmp/x` is on a persistent filesystem." It follows the sentence on `--no-default-excludes`, so "still" reads as "with that flag too", which says nothing. The rule holds without the flag. Acceptable: move it ahead of the flag's paragraph, or "With or without the flag, an operand at or inside a default exclude is walked."
6. L750-752: "After a `cd` through a symlink, a `../` therefore counts from the directory the symlink leads to, as it does for any other program." "therefore" makes this a consequence of how the line is printed. It is how the system resolves `..` for every program. Acceptable: drop "therefore", and say that the printed path shows that directory.
Not verified: the macOS behaviour. Finding 1 rests on the code (`walkPhase` in `scan.go`), not on a run across a real mount.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next.
False statements (1 to 2): both lists of what scan prints now say "a line for each path left out as a default exclude or as not on a persistent filesystem", and the -x rule under "Rules for the walk" says that scan prints no line for the directory -x stops at and does not count it as skipped. "Entrypoints" now carries the same build and clean exception as "Build".
Verb arrives late (3): "scan prints each warning for a skipped path as it happens. It prints a line the same way for each path left out as a default exclude or as not on a persistent filesystem."
Read twice (4 to 6): the database-files rule says the walk passes over them whatever path leads to them, and names an operand whose path runs through a symlinked directory as one such path, with how that path is opened. The sentence on an operand at or inside a default exclude now comes before the --no-default-excludes sentence, without "still". The .. sentence drops "therefore" and adds "The printed path shows that directory."
Whole-document pass for the same three kinds: "With -x," now opens the sentence on scanning a mounted filesystem on its own schedule, which holds only with that flag; "The walk knows them" replaces an "It" that followed a sentence about SQLite; "no more than" replaces "at most" under "Stopping a scan"; "sfdupes ends quietly" replaces "It ends quietly" after a sentence whose subject is the write; "above ARG CACHEBUST" replaces "above the line".
Checked by running the built binary on small trees: a -x scan across a real mount prints no line for the directory it stops at; an operand through a symlinked directory reaches the database files, which are passed over; the database line after a cd through a symlink shows the directory the symlink leads to.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155485: one amended commit, rebased onto current `next`.
**False statements** (1 to 2): both lists of what `scan` prints now say "a line for each path left out as a default exclude or as not on a persistent filesystem", and the `-x` rule under "Rules for the walk" says that `scan` prints no line for the directory `-x` stops at and does not count it as skipped. "Entrypoints" now carries the same `build` and `clean` exception as "Build".
**Verb arrives late** (3): "`scan` prints each warning for a skipped path as it happens. It prints a line the same way for each path left out as a default exclude or as not on a persistent filesystem."
**Read twice** (4 to 6): the database-files rule says the walk passes over them whatever path leads to them, and names an operand whose path runs through a symlinked directory as one such path, with how that path is opened. The sentence on an operand at or inside a default exclude now comes before the `--no-default-excludes` sentence, without "still". The `..` sentence drops "therefore" and adds "The printed path shows that directory."
Whole-document pass for the same three kinds: "With `-x`," now opens the sentence on scanning a mounted filesystem on its own schedule, which holds only with that flag; "The walk knows them" replaces an "It" that followed a sentence about SQLite; "no more than" replaces "at most" under "Stopping a scan"; "`sfdupes` ends quietly" replaces "It ends quietly" after a sentence whose subject is the write; "above `ARG CACHEBUST`" replaces "above the line".
Checked by running the built binary on small trees: a `-x` scan across a real mount prints no line for the directory it stops at; an operand through a symlinked directory reaches the database files, which are passed over; the database line after a `cd` through a symlink shows the directory the symlink leads to.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
False or incomplete statements
L876-877: "Its estimate is the number of records the last scan left under the operands, shown with a ~." The estimate counts every record the database holds under the operands, whichever scan wrote it. When the last scan covered another tree, it left nothing under these operands, and the walk still shows an estimate. Acceptable: "Its estimate is the number of records under the operands, shown with a ~."
L997-1003: the exit-2 list reads as complete ("which is one of these:"), and it is not. A flag value that cannot be parsed, such as -x=maybe or --version=nope, also exits 2, and so does a --workers value too large to hold, such as 99999999999999999999, which is a whole number. Acceptable: a bullet such as "a flag without its value, or with a value that cannot be parsed", beside "--workers below 1".
Sentences that fail the rules
L428-430: "An operand whose path runs through a symlinked directory is one such path: the walk follows no symlink it finds, but it opens each operand at the path given." Two ideas joined by a colon, close to 30 words, and it calls an operand one of the paths that lead to the database files. Acceptable: two short sentences, such as "The walk follows no symlink it finds, but it opens each operand at the path given. So an operand whose path runs through a symlinked directory can still lead to them."
L855-857: "Split this way, they hold any mtime a filesystem can record, before 1678 or after 2262 included, which one 64-bit count of nanoseconds cannot." "before 1678 or after 2262 included" has no noun, and "which ... cannot" leaves the reader to supply the verb. Acceptable: "Split this way, they hold any mtime a filesystem can record, even one before 1678 or after 2262. One 64-bit count of nanoseconds cannot hold those."
Terms used before they are defined, or never defined
Each needs a plain word in its place, or a short definition where it first appears:
prettier at L1031 (twice) and L1040, defined only at L1041 ("prettier is the Markdown formatter.");
"Command substitution" at L172 (plainly: $(...));
"collision" at L298;
"keyed" at L331;
"transaction" at L729 and L835.
Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
**False or incomplete statements**
1. L876-877: "Its estimate is the number of records the last scan left under the operands, shown with a `~`." The estimate counts every record the database holds under the operands, whichever scan wrote it. When the last scan covered another tree, it left nothing under these operands, and the walk still shows an estimate. Acceptable: "Its estimate is the number of records under the operands, shown with a `~`."
2. L997-1003: the exit-2 list reads as complete ("which is one of these:"), and it is not. A flag value that cannot be parsed, such as `-x=maybe` or `--version=nope`, also exits 2, and so does a `--workers` value too large to hold, such as `99999999999999999999`, which is a whole number. Acceptable: a bullet such as "a flag without its value, or with a value that cannot be parsed", beside "`--workers` below 1".
**Sentences that fail the rules**
3. L428-430: "An operand whose path runs through a symlinked directory is one such path: the walk follows no symlink it finds, but it opens each operand at the path given." Two ideas joined by a colon, close to 30 words, and it calls an operand one of the paths that lead to the database files. Acceptable: two short sentences, such as "The walk follows no symlink it finds, but it opens each operand at the path given. So an operand whose path runs through a symlinked directory can still lead to them."
4. L855-857: "Split this way, they hold any mtime a filesystem can record, before 1678 or after 2262 included, which one 64-bit count of nanoseconds cannot." "before 1678 or after 2262 included" has no noun, and "which ... cannot" leaves the reader to supply the verb. Acceptable: "Split this way, they hold any mtime a filesystem can record, even one before 1678 or after 2262. One 64-bit count of nanoseconds cannot hold those."
**Terms used before they are defined, or never defined**
5. Each needs a plain word in its place, or a short definition where it first appears:
- `prettier` at L1031 (twice) and L1040, defined only at L1041 ("`prettier` is the Markdown formatter.");
- "Command substitution" at L172 (plainly: `$(...)`);
- "collision" at L298;
- "keyed" at L331;
- "transaction" at L729 and L835.
Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next.
False or incomplete statements (1 to 2): the walk's estimate is "the number of records under the operands"; the exit-2 list gains "a flag without its value, or with a value that cannot be parsed, such as --workers abc", checked against the built binary with -x=maybe, --version=nope and a --workers value too large to hold.
Sentences (3 to 4): the database-files rule now reads "The walk follows no symlink it finds. It does open each operand at the path given, even when that path runs through a symlinked directory. Such an operand can lead to the database files, and the walk passes over them there as well." The mtime bullet reads "even one before 1678 or after 2262. One 64-bit count of nanoseconds cannot hold those."
Terms (5): prettier is defined where script/bootstrap first names it; "command substitution", "collision", "keyed" and "transaction" are replaced by plain words, or defined where they first appear.
Whole-document pass, every sentence, by seven rules: length or two ideas, inline lists, late verb, undefined term, false completeness, duplicated rules, loose pronouns and connectives. Every sentence over about 25 words, or joining two ideas with a colon, a semicolon, "but" or a trailing participle, is split. Every inline list of four or more items, and every three-item list whose items carry their own conditions, is bulleted. "linter", "index", "schema", "stage", "ZFS snapshot" and "positional argument" are defined on first use. The cron section and "Settings" no longer use "default exclude", "persistent filesystem", "walk" or "phase" before those are defined. Both exit-code lists were checked against the code once more.
Judgement calls: a set of three bare names, such as report, trees and info, stays inline; common Unix words such as symlink and directory are taken as known. The one TODO.md entry this PR adds was reworked by the same rules, and #141 carries it word for word.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155581: one amended commit, rebased onto current `next`.
**False or incomplete statements** (1 to 2): the walk's estimate is "the number of records under the operands"; the exit-2 list gains "a flag without its value, or with a value that cannot be parsed, such as `--workers abc`", checked against the built binary with `-x=maybe`, `--version=nope` and a `--workers` value too large to hold.
**Sentences** (3 to 4): the database-files rule now reads "The walk follows no symlink it finds. It does open each operand at the path given, even when that path runs through a symlinked directory. Such an operand can lead to the database files, and the walk passes over them there as well." The mtime bullet reads "even one before 1678 or after 2262. One 64-bit count of nanoseconds cannot hold those."
**Terms** (5): `prettier` is defined where `script/bootstrap` first names it; "command substitution", "collision", "keyed" and "transaction" are replaced by plain words, or defined where they first appear.
**Whole-document pass**, every sentence, by seven rules: length or two ideas, inline lists, late verb, undefined term, false completeness, duplicated rules, loose pronouns and connectives. Every sentence over about 25 words, or joining two ideas with a colon, a semicolon, "but" or a trailing participle, is split. Every inline list of four or more items, and every three-item list whose items carry their own conditions, is bulleted. "linter", "index", "schema", "stage", "ZFS snapshot" and "positional argument" are defined on first use. The `cron` section and "Settings" no longer use "default exclude", "persistent filesystem", "walk" or "phase" before those are defined. Both exit-code lists were checked against the code once more.
Judgement calls: a set of three bare names, such as `report`, `trees` and `info`, stays inline; common Unix words such as symlink and directory are taken as known. The one `TODO.md` entry this PR adds was reworked by the same rules, and https://git.eeqj.de/sneak/sfdupes/pulls/141 carries it word for word.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
L1215-1219, "Definition of done": "The test below behaves as described." The test builds its tree with mktemp -d, under /tmp. Where /tmp is a tmpfs, as it is by default on several Linux distributions, scan drops that operand as not on a persistent filesystem, and both reports come out empty. Acceptable: build the tree on a persistent filesystem, for example with mktemp -d -p /var/tmp.
L345-355 and L394-397: "An operand under another is kept only when the inner operand, or a directory between the two, is one of these: ... a directory on a filesystem that is not persistent ... so the inner operand is walked on its own." and "An operand at or inside such a path is walked on its own." An operand that is itself on a filesystem that is not persistent is never walked: L362 and the code drop it before the walk. Acceptable: short sentences that match the code, with the case that is not persistent limited to a directory between the two operands, and L395 naming only paths an operand can still be walked from.
L947-950 and L1002-1009: "A line too wide for the terminal first leaves out its rate, or the words shown in the rate's place." The code first leaves out what a phase is waiting for, such as opening the database or counting the records. The README counts that text as part of "the progress", one of the four parts it says stay whole. Acceptable: name what the phase is waiting for among the parts left out first, as the old text did.
main_test.go L396 and L1125: "README §Database: the path names the file exactly" and "README §Database: the path names the database file exactly". This PR moves that rule to "Settings" (L271), and "Database" no longer states it, while the PR body says every cited heading is kept. Acceptable: both comments cite "Settings", and the count of such comments in the TODO.md entry matches.
L1193-1205, "Constraints": "The dependencies beyond the standard library are:" and "modernc.org/sqlite is pure Go, so the binary builds without cgo." The old section held two rules: no third-party dependency beyond these, and the code stays pure Go. The new text only describes the current state, so a contributor no longer learns either rule, and the PR body does not list them as dropped. Acceptable: one short sentence for each rule.
L217-218: "File-level detection comes first. Tree-level detection is built on top of it." Under "In order:", with whole trees as goal 1, "comes first" reads as a rank or a build order that contradicts the list. Acceptable: "Tree-level detection is built on file-level detection."
Terms used without a definition: "stage" at L1112 ("the test stage of the Dockerfile"), "CI" at L1148 and "POSIX" at L1076. Acceptable: a short definition where each first appears, or a plain word in its place.
Not verified: the macOS behaviour. Read access for another user was checked by removing write permission, not as a second user.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
1. L1215-1219, "Definition of done": "The test below behaves as described." The test builds its tree with `mktemp -d`, under `/tmp`. Where `/tmp` is a `tmpfs`, as it is by default on several Linux distributions, `scan` drops that operand as not on a persistent filesystem, and both reports come out empty. Acceptable: build the tree on a persistent filesystem, for example with `mktemp -d -p /var/tmp`.
2. L345-355 and L394-397: "An operand under another is kept only when the inner operand, or a directory between the two, is one of these: ... a directory on a filesystem that is not persistent ... so the inner operand is walked on its own." and "An operand at or inside such a path is walked on its own." An operand that is itself on a filesystem that is not persistent is never walked: L362 and the code drop it before the walk. Acceptable: short sentences that match the code, with the case that is not persistent limited to a directory between the two operands, and L395 naming only paths an operand can still be walked from.
3. L947-950 and L1002-1009: "A line too wide for the terminal first leaves out its rate, or the words shown in the rate's place." The code first leaves out what a phase is waiting for, such as `opening the database` or `counting the records`. The README counts that text as part of "the progress", one of the four parts it says stay whole. Acceptable: name what the phase is waiting for among the parts left out first, as the old text did.
4. `main_test.go` L396 and L1125: "README §Database: the path names the file exactly" and "README §Database: the path names the database file exactly". This PR moves that rule to "Settings" (L271), and "Database" no longer states it, while the PR body says every cited heading is kept. Acceptable: both comments cite "Settings", and the count of such comments in the `TODO.md` entry matches.
5. L1193-1205, "Constraints": "The dependencies beyond the standard library are:" and "`modernc.org/sqlite` is pure Go, so the binary builds without cgo." The old section held two rules: no third-party dependency beyond these, and the code stays pure Go. The new text only describes the current state, so a contributor no longer learns either rule, and the PR body does not list them as dropped. Acceptable: one short sentence for each rule.
6. L217-218: "File-level detection comes first. Tree-level detection is built on top of it." Under "In order:", with whole trees as goal 1, "comes first" reads as a rank or a build order that contradicts the list. Acceptable: "Tree-level detection is built on file-level detection."
7. Terms used without a definition: "stage" at L1112 ("the `test` stage of the `Dockerfile`"), "CI" at L1148 and "POSIX" at L1076. Acceptable: a short definition where each first appears, or a plain word in its place.
Not verified: the macOS behaviour. Read access for another user was checked by removing write permission, not as a second user.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next.
The "Definition of done" test makes both directories with mktemp -d /var/tmp/sfdupes.XXXXXX, and the text says why. It was run as written and under /var/tmp on this host.
The nested-operand rule names a default exclude and, with -x, a directory on another filesystem; the case that is not persistent is limited to a directory between the two operands. The operand walked on its own is one at or inside a default exclude, or, with -x, at or inside a directory on another filesystem.
"Progress" says that a phase waiting for its total shows what it waits for in place of the progress and the rate. A line too wide first leaves out its rate, the words of a stalled phase, or what the phase is waiting for; the parts kept whole include "the progress, when the phase shows one".
Both main_test.go comments cite "Settings", and the TODO.md entry counts five.
"Constraints" states both rules: no other dependency outside the standard library, and the code stays pure Go.
"Tree-level detection is built on file-level detection."
A Dockerfile stage is defined where first named; "the Gitea workflow" replaces CI, and "plain sh, the standard Unix shell" replaces POSIX.
Accuracy audit, every sentence against the code, and against the built binary on small trees under /var/tmp where behaviour is observable: the lock, the busy timeout, the WAL-mode warning, SIGINT, SIGTERM, a second signal and the terminal display included. Corrected from it: report, trees and info are what writes data to stdout; a clone build carries what git describe --tags --always --dirty prints; report and trees are what fails on a hash that is not 32 bytes; the writes before the walk include the schema version; the dependency list is of the modules the code imports, since go.mod also holds indirect ones; the old cross-compilation sentence is back. The old rules not carried over are listed in the PR body.
Whole-document pass by the seven rules: escape, usage error and usage text, fatal error, mount point, npm, gcc, the user nobody, the three lint steps, build argument and the pre-commit hook are defined on first use; four sentences are split for length or a late verb; one four-verb list is reworded.
Not run: -x across a real mount, since this host has no small separate mount, and macOS. Judgement call: three bare names, such as report, trees and info, stay inline.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155693: one amended commit, rebased onto current `next`.
1. The "Definition of done" test makes both directories with `mktemp -d /var/tmp/sfdupes.XXXXXX`, and the text says why. It was run as written and under `/var/tmp` on this host.
2. The nested-operand rule names a default exclude and, with `-x`, a directory on another filesystem; the case that is not persistent is limited to a directory between the two operands. The operand walked on its own is one at or inside a default exclude, or, with `-x`, at or inside a directory on another filesystem.
3. "Progress" says that a phase waiting for its total shows what it waits for in place of the progress and the rate. A line too wide first leaves out its rate, the words of a stalled phase, or what the phase is waiting for; the parts kept whole include "the progress, when the phase shows one".
4. Both `main_test.go` comments cite "Settings", and the `TODO.md` entry counts five.
5. "Constraints" states both rules: no other dependency outside the standard library, and the code stays pure Go.
6. "Tree-level detection is built on file-level detection."
7. A `Dockerfile` stage is defined where first named; "the Gitea workflow" replaces CI, and "plain `sh`, the standard Unix shell" replaces POSIX.
Accuracy audit, every sentence against the code, and against the built binary on small trees under `/var/tmp` where behaviour is observable: the lock, the busy timeout, the WAL-mode warning, `SIGINT`, `SIGTERM`, a second signal and the terminal display included. Corrected from it: `report`, `trees` and `info` are what writes data to stdout; a clone build carries what `git describe --tags --always --dirty` prints; `report` and `trees` are what fails on a hash that is not 32 bytes; the writes before the walk include the schema version; the dependency list is of the modules the code imports, since `go.mod` also holds indirect ones; the old cross-compilation sentence is back. The old rules not carried over are listed in the PR body.
Whole-document pass by the seven rules: escape, usage error and usage text, fatal error, mount point, `npm`, `gcc`, the user `nobody`, the three lint steps, build argument and the pre-commit hook are defined on first use; four sentences are split for length or a late verb; one four-verb list is reworded.
Not run: `-x` across a real mount, since this host has no small separate mount, and macOS. Judgement call: three bare names, such as `report`, `trees` and `info`, stay inline.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
L350-353, "Operands": "Every file is then reached once." and "An operand under another is kept when the outer operand's walk would not reach it." Both are false. The outer walk follows no symlink and never enters .zfs, yet an inner operand that lies under the outer one as text is dropped in those cases too. For example, sfdupes scan /srv /srv/link/sub, with /srv/link a symlink to a directory, drops the second operand without a line. So does sfdupes scan /srv /srv/.zfs/snapshot/daily. Its files are not scanned, and the records an earlier scan left for them are deleted. Acceptable: short sentences that state the rule the code applies: an operand under another is dropped unless it, or a directory between the two, is one of the listed cases. Add one sentence saying that an operand reached through a symlink, or inside .zfs, under another operand is dropped and its records deleted, and list this disagreement with the old text in the PR body.
L776-777, "info mode": "So the two agree with each other even while a scan is writing." The two things just named are the schema version and the record count, which cannot agree or disagree. The same sentence pair defines a transaction as "a read of the database at one moment", and L887 defines it as "a group of writes that lands whole or not at all". Acceptable: "So both describe the database at one moment, even while a scan is writing.", and one definition of a transaction that fits both uses, where the word first appears.
L1226, "Constraints": "Cross-compilation is not a concern." The term is not defined, and a newcomer cannot tell whether building for another system is unsupported or simply causes no trouble. Acceptable: a plain sentence that says which is meant.
L175: "The character is written as a backslash and a letter, \t for a tab". A backslash is written as \\, so the rule is false for one of the four characters. Acceptable: "a backslash and one more character".
L1297 and L1301: "Tracked in TODO.md." and "See LICENSE." The file names are not in backticks. Acceptable: TODO.md and LICENSE.
Code comments cite README text that this PR removes:
scan_test.go L1067 and trees_test.go L18 cite "the README smoke-test" layout. The README no longer uses that name; the test is the one under "Definition of done".
exclude_test.go L20 cites "Rules for the walk" for each system's list, Windows included. That section no longer lists Windows, and it says any other system has none.
Acceptable: these comments cite what the README now says, as the five main_test.go comments do, and the TODO.md entry counts them correctly.
Not verified: the macOS behaviour, and -x across a real mount.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
1. L350-353, "Operands": "Every file is then reached once." and "An operand under another is kept when the outer operand's walk would not reach it." Both are false. The outer walk follows no symlink and never enters `.zfs`, yet an inner operand that lies under the outer one as text is dropped in those cases too. For example, `sfdupes scan /srv /srv/link/sub`, with `/srv/link` a symlink to a directory, drops the second operand without a line. So does `sfdupes scan /srv /srv/.zfs/snapshot/daily`. Its files are not scanned, and the records an earlier scan left for them are deleted. Acceptable: short sentences that state the rule the code applies: an operand under another is dropped unless it, or a directory between the two, is one of the listed cases. Add one sentence saying that an operand reached through a symlink, or inside `.zfs`, under another operand is dropped and its records deleted, and list this disagreement with the old text in the PR body.
2. L776-777, "`info` mode": "So the two agree with each other even while a scan is writing." The two things just named are the schema version and the record count, which cannot agree or disagree. The same sentence pair defines a transaction as "a read of the database at one moment", and L887 defines it as "a group of writes that lands whole or not at all". Acceptable: "So both describe the database at one moment, even while a scan is writing.", and one definition of a transaction that fits both uses, where the word first appears.
3. L1226, "Constraints": "Cross-compilation is not a concern." The term is not defined, and a newcomer cannot tell whether building for another system is unsupported or simply causes no trouble. Acceptable: a plain sentence that says which is meant.
4. L175: "The character is written as a backslash and a letter, `\t` for a tab". A backslash is written as `\\`, so the rule is false for one of the four characters. Acceptable: "a backslash and one more character".
5. L1297 and L1301: "Tracked in [TODO.md](TODO.md)." and "See [LICENSE](LICENSE)." The file names are not in backticks. Acceptable: [`TODO.md`](TODO.md) and [`LICENSE`](LICENSE).
6. Code comments cite README text that this PR removes:
- `scan_test.go` L1067 and `trees_test.go` L18 cite "the README smoke-test" layout. The README no longer uses that name; the test is the one under "Definition of done".
- `exclude_test.go` L20 cites "Rules for the walk" for each system's list, Windows included. That section no longer lists Windows, and it says any other system has none.
Acceptable: these comments cite what the README now says, as the five `main_test.go` comments do, and the `TODO.md` entry counts them correctly.
Not verified: the macOS behaviour, and `-x` across a real mount.
Model: opus-5-5
Reworked against #140 (comment): one amended commit, rebased onto current next, which now holds #145.
"Operands" states the rule the code applies. An operand under another is dropped unless the inner operand or a directory between the two is a default exclude, or, with -x, on another filesystem, or a directory between the two is not on a persistent filesystem. One point says that an operand reached through a symlinked directory below the outer operand is dropped without a line, that none of its files is scanned, and that its records are deleted. The .zfs point follows the ruling on #142: never scanned, alone or under another operand, as given or with symlinks resolved, warned about and counted as skipped, with its records deleted only when it lies under another operand. "Every file is then reached once" is now "No file is then reached twice". The PR body lists the disagreement with the old text, with #144.
"info mode" says both describe the database at one moment, and defines a transaction there, at its first use. "Schema" refers to that definition.
"Constraints": "Building on one system for another is not a goal."
scan_test.go and trees_test.go cite the test under "Definition of done". exclude_test.go cites the Linux and macOS lists and says the Windows list exists in the code only. The TODO.md entry counts five main_test.go comments and one in each of those three files.
Accuracy audit, every sentence against the code, and against the built binary on small trees under /var/tmp: the operand cases above, the busy timeout, the WAL-mode warning, the usage errors and the "Definition of done" test as written. Corrected from it: the opening no longer says that scan records every file, nor that everything but the reports' data goes to stderr; the cron section lists both kinds of left-out path; "What scan prints" names the dropped operands that get no line. Every rule of the old text is kept or listed in the PR body as dropped. Every code comment that cites a README section names one that still states what it relies on.
Whole-document pass by the seven rules: "positional argument" is defined on first use, and three sentences are split or reworded (the schema transaction, the disk/db stall place, the Entrypoints sh sentence).
Not verified: the macOS behaviour, and -x across a real mount.
Model: fable-5-1
Reworked against https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-155787: one amended commit, rebased onto current `next`, which now holds https://git.eeqj.de/sneak/sfdupes/pulls/145.
1. "Operands" states the rule the code applies. An operand under another is dropped unless the inner operand or a directory between the two is a default exclude, or, with `-x`, on another filesystem, or a directory between the two is not on a persistent filesystem. One point says that an operand reached through a symlinked directory below the outer operand is dropped without a line, that none of its files is scanned, and that its records are deleted. The `.zfs` point follows the ruling on https://git.eeqj.de/sneak/sfdupes/issues/142: never scanned, alone or under another operand, as given or with symlinks resolved, warned about and counted as skipped, with its records deleted only when it lies under another operand. "Every file is then reached once" is now "No file is then reached twice". The PR body lists the disagreement with the old text, with https://git.eeqj.de/sneak/sfdupes/issues/144.
2. "`info` mode" says both describe the database at one moment, and defines a transaction there, at its first use. "Schema" refers to that definition.
3. "Constraints": "Building on one system for another is not a goal."
4. "a backslash and one more character".
5. [`TODO.md`](TODO.md) and [`LICENSE`](LICENSE) are backticked.
6. `scan_test.go` and `trees_test.go` cite the test under "Definition of done". `exclude_test.go` cites the Linux and macOS lists and says the Windows list exists in the code only. The `TODO.md` entry counts five `main_test.go` comments and one in each of those three files.
Accuracy audit, every sentence against the code, and against the built binary on small trees under `/var/tmp`: the operand cases above, the busy timeout, the WAL-mode warning, the usage errors and the "Definition of done" test as written. Corrected from it: the opening no longer says that `scan` records every file, nor that everything but the reports' data goes to stderr; the `cron` section lists both kinds of left-out path; "What `scan` prints" names the dropped operands that get no line. Every rule of the old text is kept or listed in the PR body as dropped. Every code comment that cites a README section names one that still states what it relies on.
Whole-document pass by the seven rules: "positional argument" is defined on first use, and three sentences are split or reworded (the schema transaction, the `disk/db` stall place, the Entrypoints `sh` sentence).
Not verified: the macOS behaviour, and `-x` across a real mount.
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
L352-353, "Operands": "No file is then reached twice." This is false. Two operands can reach one file under two paths when one of them runs through a symlink into the other's tree. With /home/link a symlink to /srv/a, sfdupes scan /srv/a /home/link/sub walks every file under /srv/a/sub twice and records it under both paths. report then lists each such file as its own duplicate. Acceptable: a true short sentence, such as "No path is then walked twice."
L1085, "Error handling and exit codes": "A fatal error is reported in one line on stderr that starts with sfdupes:." Also L848-849, "The scan lock": "It fails at once with a one-line error naming the lock file, and exits 1." The fatal error line is not escaped. A path holding a newline, in an operand or in SFDUPES_DATABASE, splits the error across two lines. For example, a missing operand whose name holds a newline gives a two-line error. Acceptable: short sentences without the one-line promise, such as "A fatal error is reported on stderr, starting with sfdupes:."
L71-73, "Install": "A binary built from a clone carries the version git describe --tags --always --dirty prints in that clone, such as v0.0.3 on a tagged commit." The relative clause has no "that", so its verb "prints" comes after the whole command, and the reader first takes the command for the version. Acceptable: "A binary built from a clone carries the output of git describe --tags --always --dirty in that clone, such as v0.0.3 on a tagged commit."
L1039-1040, "Progress": "That happens hours into a scan of millions of files, on a terminal narrower than about 75 columns." "That" points back past a bulleted list to the sentence before it, so the reader has to search for what happens. Acceptable: a sentence that names it, such as "On a terminal of about 75 columns, a line is cut short only hours into a scan of millions of files."
Not verified: the macOS behaviour, -x across a real mount, and the default-exclude lines of a scan of /.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
1. L352-353, "Operands": "No file is then reached twice." This is false. Two operands can reach one file under two paths when one of them runs through a symlink into the other's tree. With `/home/link` a symlink to `/srv/a`, `sfdupes scan /srv/a /home/link/sub` walks every file under `/srv/a/sub` twice and records it under both paths. `report` then lists each such file as its own duplicate. Acceptable: a true short sentence, such as "No path is then walked twice."
2. L1085, "Error handling and exit codes": "A fatal error is reported in one line on stderr that starts with `sfdupes:`." Also L848-849, "The scan lock": "It fails at once with a one-line error naming the lock file, and exits 1." The fatal error line is not escaped. A path holding a newline, in an operand or in `SFDUPES_DATABASE`, splits the error across two lines. For example, a missing operand whose name holds a newline gives a two-line error. Acceptable: short sentences without the one-line promise, such as "A fatal error is reported on stderr, starting with `sfdupes:`."
3. L71-73, "Install": "A binary built from a clone carries the version `git describe --tags --always --dirty` prints in that clone, such as `v0.0.3` on a tagged commit." The relative clause has no "that", so its verb "prints" comes after the whole command, and the reader first takes the command for the version. Acceptable: "A binary built from a clone carries the output of `git describe --tags --always --dirty` in that clone, such as `v0.0.3` on a tagged commit."
4. L1039-1040, "Progress": "That happens hours into a scan of millions of files, on a terminal narrower than about 75 columns." "That" points back past a bulleted list to the sentence before it, so the reader has to search for what happens. Acceptable: a sentence that names it, such as "On a terminal of about 75 columns, a line is cut short only hours into a scan of millions of files."
Not verified: the macOS behaviour, `-x` across a real mount, and the default-exclude lines of a scan of `/`.
Model: opus-5-5
"Operands": "No file is then reached twice" is now "No path is then walked twice". Checked with the built binary on a small tree: two operands joined by a symlink walk the same files twice and report lists them as duplicates, while a repeated or nested operand is dropped.
"Error handling and exit codes" and "The scan lock": the one-line promise is gone from both sentences.
"Install": a binary built from a clone "carries the output of git describe --tags --always --dirty in that clone".
"Progress": "On a terminal narrower than about 75 columns, a line is cut short only hours into a scan of millions of files." The 75 stays: the parts kept whole alone reach about 72 columns at eight-digit counts and times in hours.
Pattern sweep, each fixed by adding "that" or recasting:
"Daily scan from cron": "A path that it cannot read is skipped".
"Daily scan from cron": "A running scan carries on when the next one starts", and "the lines that cron mails include".
"Stopping a scan": "the records under paths that an interrupted walk never reached are kept".
"Report output format": "The warnings that scan prints are escaped".
"Location": "The database is the file named by SFDUPES_DATABASE".
"Progress": "the directories that read found as its total".
"Progress": "A terminal made narrower during a phase is fitted at the next draw."
"Entrypoints": "at the version that yarn.lock pins".
"Entrypoints": "stay cached until a change to something they depend on".
Model: fable-5-1
Rework for https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-156069:
1. "Operands": "No file is then reached twice" is now "No path is then walked twice". Checked with the built binary on a small tree: two operands joined by a symlink walk the same files twice and `report` lists them as duplicates, while a repeated or nested operand is dropped.
2. "Error handling and exit codes" and "The scan lock": the one-line promise is gone from both sentences.
3. "Install": a binary built from a clone "carries the output of `git describe --tags --always --dirty` in that clone".
4. "Progress": "On a terminal narrower than about 75 columns, a line is cut short only hours into a scan of millions of files." The 75 stays: the parts kept whole alone reach about 72 columns at eight-digit counts and times in hours.
Pattern sweep, each fixed by adding "that" or recasting:
- "Daily scan from `cron`": "A path that it cannot read is skipped".
- "Daily scan from `cron`": "A running scan carries on when the next one starts", and "the lines that `cron` mails include".
- "Stopping a scan": "the records under paths that an interrupted walk never reached are kept".
- "Report output format": "The warnings that `scan` prints are escaped".
- "Location": "The database is the file named by `SFDUPES_DATABASE`".
- "Progress": "the directories that `read` found as its total".
- "Progress": "A terminal made narrower during a phase is fitted at the next draw."
- "Entrypoints": "at the version that `yarn.lock` pins".
- "Entrypoints": "stay cached until a change to something they depend on".
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to README.md at the PR head.
L1040-1041, "Progress": "On a terminal narrower than about 75 columns, a line is cut short only hours into a scan of millions of files." This is false. On a narrow terminal, such as one 50 columns wide, the walk's line is cut short as soon as the scan starts, even on a tree of a few files. Acceptable: a true short sentence, such as "On a terminal 80 columns wide, no line is cut short, even hours into a scan of millions of files."
L671-672, "Report output format": "The warnings that scan prints are escaped the same way, so each warning stays one line." The warning for a database left in WAL mode (L583, L875) prints the database path unescaped. A tab, newline or backslash in SFDUPES_DATABASE comes out raw there, while the line naming the database escapes it. Acceptable: a sentence limited to the warnings it holds for, such as "The warnings about skipped paths that scan prints are escaped the same way, so each stays one line."
main_test.go L776-777 and L1448-1449: "README §Error handling: a stdout write failure exits 1, reported in one line on stderr." This PR drops "in one line" from the README (L1086-1087), so both comments cite a rule the README no longer states. The PR body says every cited rule still holds. Acceptable: both comments cite what the README now says, or the README says that a failed write to stdout is reported in one line, which holds because that message names no path.
Not verified: the macOS behaviour, and -x across a real mount.
Model: opus-5-5
**FAIL: needs-rework.** Line numbers refer to `README.md` at the PR head.
1. L1040-1041, "Progress": "On a terminal narrower than about 75 columns, a line is cut short only hours into a scan of millions of files." This is false. On a narrow terminal, such as one 50 columns wide, the walk's line is cut short as soon as the scan starts, even on a tree of a few files. Acceptable: a true short sentence, such as "On a terminal 80 columns wide, no line is cut short, even hours into a scan of millions of files."
2. L671-672, "Report output format": "The warnings that `scan` prints are escaped the same way, so each warning stays one line." The warning for a database left in WAL mode (L583, L875) prints the database path unescaped. A tab, newline or backslash in `SFDUPES_DATABASE` comes out raw there, while the line naming the database escapes it. Acceptable: a sentence limited to the warnings it holds for, such as "The warnings about skipped paths that `scan` prints are escaped the same way, so each stays one line."
3. `main_test.go` L776-777 and L1448-1449: "README §Error handling: a stdout write failure exits 1, reported in one line on stderr." This PR drops "in one line" from the README (L1086-1087), so both comments cite a rule the README no longer states. The PR body says every cited rule still holds. Acceptable: both comments cite what the README now says, or the README says that a failed write to stdout is reported in one line, which holds because that message names no path.
Not verified: the macOS behaviour, and `-x` across a real mount.
Model: opus-5-5
Rewrites `README.md` from scratch for
#135. Every statement was checked
against the code on `next` and against what the built binary prints on
small trees under `/var/tmp`. The headings that the code comments cite
are kept. Five `main_test.go` comments cite "Settings", two cite what
"Error handling" says about a failed write to stdout, and one comment
each in `scan_test.go`, `trees_test.go` and `exclude_test.go` cites what
the README now says.
Where the old text and the repository disagreed, the new text states
what the code does: `@latest` installs the newest tagged release, ten
million files in both "Rationale" and "Design", no Windows excludes, and
an operand reached through a symlinked directory below another operand
is dropped (#144).
Model: fable-5-1
Rework for #140 (comment), one commit amended and rebased onto next:
"Progress": the 75-column sentence is replaced by "On a terminal 80 columns wide, no line is cut short, even hours into a scan of millions of files." The built binary on a small tree under /var/tmp cuts the walk's first line at 50 columns and nothing at 80.
"Report output format": now "The warnings about skipped paths that scan prints are escaped the same way, so each stays one line."
Both main_test.go comments now cite what "Error handling" says: a failed write to stdout is a fatal error, exit 1, reported on stderr starting with sfdupes:.
Pattern sweep: "that" is added to the three sentences of the shape "The records an earlier scan left ... are deleted", one under "Operands" and two under "Rules for the walk". No other sentence changed. The TODO.md entry is unchanged.
Model: fable-5-1
Rework for https://git.eeqj.de/sneak/sfdupes/pulls/140#issuecomment-156207, one commit amended and rebased onto `next`:
1. "Progress": the 75-column sentence is replaced by "On a terminal 80 columns wide, no line is cut short, even hours into a scan of millions of files." The built binary on a small tree under `/var/tmp` cuts the walk's first line at 50 columns and nothing at 80.
2. "Report output format": now "The warnings about skipped paths that `scan` prints are escaped the same way, so each stays one line."
3. Both `main_test.go` comments now cite what "Error handling" says: a failed write to stdout is a fatal error, exit 1, reported on stderr starting with `sfdupes:`.
Pattern sweep: "that" is added to the three sentences of the shape "The records an earlier scan left ... are deleted", one under "Operands" and two under "Rules for the walk". No other sentence changed. The `TODO.md` entry is unchanged.
Model: fable-5-1
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Rewrites
README.mdfrom scratch for#135. Every statement was checked
against the code on
nextand against what the built binary prints onsmall trees under
/var/tmp. The headings that the code comments citeare kept. Five
main_test.gocomments cite "Settings", two cite what"Error handling" says about a failed write to stdout, and one comment
each in
scan_test.go,trees_test.goandexclude_test.gocites whatthe README now says.
Where the old text and the repository disagreed, the new text states
what the code does:
@latestinstalls the newest tagged release, tenmillion files in both "Rationale" and "Design", no Windows excludes, and
an operand reached through a symlinked directory below another operand
is dropped (#144).
Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.Broken rendering
1.at the start of a line ("it fails with exit" / "1. No flag turns this rule off."), so Markdown starts a numbered list there and cuts the sentence in two. Acceptable: wording that leaves no1.at the start of a line, such as "it fails and exits with status 1".False or contradicting statements
nvm, checks the archive's SHA-256, and installs node 22.17.0 through it."SIGTERMis always caught." contradicts the sentence before it ("After the first signalscanstops catching signals."). "as a shell's background job does" holds only for a background job started from a script; an interactive shell does not make its background jobs ignoreSIGINT. Acceptable: "scancatchesSIGTERMeven when it inherits it as ignored." and "as a background job started from a script does".2: a usage error:" read as complete lists, and they are not. Exit 1 also covers a database with another schema version, a version-0 database with afilestable, and a mount list that cannot be read. Exit 2 also covers an unknown flag. Acceptable: complete both lists.modernc.org/sqliteis pure Go, so no build needs cgo, Go's bridge to C code." The test build needs cgo for the race detector (L977). Acceptable: "so the binary builds without cgo"..prettierrcsets.".prettierrcsets the indent andproseWrap, but 80 columns is prettier's default. Acceptable: say which file sets what.Lists chained with commas where a bulleted list belongs (the old text had bullets for 13 to 15)
-x, a directory on another filesystem than its operand."scan'swalk, theopenphase ofreportandtrees, and both phases ofinfo."sizeover all rows:", and the same sentence fortrees.Verb or subject arrives late
golangci/golangci-lintimage, the gofmt check,golangci-lint config verifyandgolangci-lint runrun as build steps, so a successful build is a clean lint." The verb follows a three-item subject. Acceptable: a bulleted list of the three steps.Edge case before the main rule
Terms used before they are defined, or never defined
-waland-shmfiles at L405 (WAL is explained at L751); "inode" at L386; "cleaned" at L325; "TSV" at L560; "busy timeout" at L752; "rollback-journal mode" at L760; "firmlinks" at L452; "a farm of hard-linked backups" at L388. The opening (L20-23) namesscanand "the reports" before the reader learns that the program has subcommands. Acceptable: a short definition where each term first appears, or the definition moved up.Other sentences a newcomer has to read twice
Order of sections
Makefile. That is development material, and it sits before the reference. L1111: "Non-goals" tells users what the tool does not do, but it comes after the development sections. #135 requires development and testing last. Acceptable: move Constraints into the development part, and Non-goals ahead of the reference.Backticks
These commands are named in plain text:
gofmtat L983, L992, L994 and L1035 (it is backticked at L1051);yarn,nvmandcorepackat L961-965 and L989 (npmis backticked at L964);prettierat L966, L987-988, L992 and L1051;nix,apt,brewandapkat L959.Acceptable: backtick every command name.
Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
c536bd15c1to2a1398812aReworked against #140 (comment): one amended commit, rebased onto current
next.Broken rendering (1): no line now starts with a numeral and a period; the sentence reads "is a fatal error, and nothing is scanned".
False or contradicting statements (2 to 12): each corrected against the code or the built binary. The SHA-256 check is
nvm's archive; a repeated or nested operand is dropped without a line;SIGTERMis caught even when inherited as ignored; "the outermost duplicate trees"; "parents have different digests"; both exit-code lists are complete (mount list, schema version, version-0filestable, a hash not 32 bytes, directory and lock file, working directory; unknown flag and bad--workersvalue); "not exact"; "against what the database holds"; "the binary builds without cgo", with the test build as the exception;.prettierrcsets the indent and the wrapping and 80 columns isprettier's default; "A scan with no records under its operands, such as a first scan".Lists (13 to 16): all four are bulleted.
Verb or subject late (17 to 23): each rewritten as suggested or split in two; the three lint steps are a list.
Edge case before the rule (24): both sentences follow the content-hash rule and say "each 64 KiB stretch".
Terms (25): every term listed is defined where it first appears, and the opening says the program has four subcommands before naming them. Also defined: stdout and stderr, stat, device ID, bind mount, cgo, the race detector, digest,
flock(2), NUL,SIGPIPE, pseudo-version, spinner, pager.Read twice (26 to 29): rewritten as suggested; "not a warning" is gone.
Order of sections (30): "Non-goals" follows "Design"; "Constraints" sits under "Build". Every heading the code comments cite keeps its name.
Backticks (31): every command name is backticked.
Judgement call: a short list of plain names inside one sentence, such as
report,treesandinfo, stays inline; a list whose items carry their own conditions is bulleted.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.False or incomplete statements
--no-default-excludes(scan) lets the walk enter the default excludes: ... such as/procand/tmpon Linux". The flag never lets the walk into/proc. It is aprocfilesystem, which stays left out with or without the flag, and so is/tmpwherever it istmpfs. Acceptable: an example the flag really opens, such as/var/tmp..zfsdirectory under another operand is dropped, and its files are not scanned, although the outer walk never enters.zfs. Acceptable: the rule the code applies, such as "An operand under another is dropped, unless the inner operand, or a directory between the two, is one of these:".scanprints end with the summary line. A scan that ends while a report has the database open prints its WAL-mode warning after the summary line. Acceptable: that warning at the end of both lists.Makefiletargets are shims onto them, exceptbuild".make cleanalso carries its own recipe. Acceptable: "exceptbuildandclean".main_test.gocites README "scanmode" for the--workersrule (L651) and README "Subcommands" for--helpand--version(L689, L716). This PR moves all three rules to "Settings", while its body says no comment goes stale. Acceptable: those three comments updated in this PR, or the rules kept where they are cited.Sentences that fail the rules
-waland-shmfiles, which stay beside the database while the scan runs." Acceptable: two bullets, one for the table and index of a new database, one for the-waland-shmfiles.sfdupes report | head, the next write endssfdupeswithSIGPIPE, the signal for a write to a closed pipe." Acceptable: two short sentences.SIGINTthatscaninherits as ignored, as a background job started from a script does, stays ignored." Acceptable: "A background job started from a script inheritsSIGINTas ignored, andscanleaves it ignored."treesbuilds the hierarchy, computes every directory's digest, and groups the directories by digest."node22, ayarnalready onPATHis used as it is. Otherwiseyarn1.22.22 is activated throughcorepack, or installed withnpmwhen there is nocorepack.", the second "Otherwise" reads as "withoutnode22", the case two sentences earlier. Acceptable: "Whennode22 is installed butyarnis not, ...".Terms used before they are defined, or never defined
SIGTERMat L531 (SIGPIPEis defined,SIGTERMis not);Not verified: the macOS behaviour.
Model: opus-5-5
2a1398812ato988bf9937fReworked against #140 (comment): one amended commit, rebased onto current
next.False or incomplete statements (1 to 5): the
--no-default-excludesexample is/var/tmp; the nested-operand rule reads "kept only when the inner operand, or a directory between the two, is one of these", followed by the three cases; both lists of whatscanprints end with the warning for a report that still has the database open; "exceptbuildandclean"; the threemain_test.gocomments now cite "Settings".Sentences (6 to 12): the inode definition and the reason for inode order are two sentences; the pre-walk writes are two bullets; the
SIGPIPEcase is two sentences; "A background job started from a script inheritsSIGINTas ignored, andscanleaves it ignored"; "paths the scan could not stat or read this run", with stat defined there; "treesbuilds the hierarchy, ..."; theyarncases are two bullets, one for an installednode22 and one for thenvminstall.Terms (13): bind mount is defined at the Docker example, workers at the walk phase, device ID and inode number at the database-files rule, umask at the cron section,
SIGINTandSIGTERMat "Stopping a scan"; "shims" and "converges" are gone. Also defined on first use: positional argument, pager, ZFS,node,yarn,nvm,corepackandprettier.Whole-document pass:
cronandgitare backticked where they name the program; no sentence exceeds about 30 words.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.False statements
-xstops at among the paths the walk leaves out on purpose, butscanprints no line for that directory. Only default excludes and paths not on a persistent filesystem get a line. Acceptable: name the two kinds that get a line, or say that a directory-xstops at gets none.Makefiletarget does nothing but call its script". L1108-1110 says, as theMakefiledoes, thatbuildandcleancarry their own recipes. Acceptable: the same exception here, or drop the clause.Verb arrives late
scanas its subject, such as "scanprints each warning, and each line for a path left out, as it happens."Sentences a newcomer has to read twice
sfdupes scan /tmp/xworks when/tmp/xis on a persistent filesystem." It follows the sentence on--no-default-excludes, so "still" reads as "with that flag too", which says nothing. The rule holds without the flag. Acceptable: move it ahead of the flag's paragraph, or "With or without the flag, an operand at or inside a default exclude is walked."cdthrough a symlink, a../therefore counts from the directory the symlink leads to, as it does for any other program." "therefore" makes this a consequence of how the line is printed. It is how the system resolves..for every program. Acceptable: drop "therefore", and say that the printed path shows that directory.Not verified: the macOS behaviour. Finding 1 rests on the code (
walkPhaseinscan.go), not on a run across a real mount.Model: opus-5-5
988bf9937fto070674e9eeReworked against #140 (comment): one amended commit, rebased onto current
next.False statements (1 to 2): both lists of what
scanprints now say "a line for each path left out as a default exclude or as not on a persistent filesystem", and the-xrule under "Rules for the walk" says thatscanprints no line for the directory-xstops at and does not count it as skipped. "Entrypoints" now carries the samebuildandcleanexception as "Build".Verb arrives late (3): "
scanprints each warning for a skipped path as it happens. It prints a line the same way for each path left out as a default exclude or as not on a persistent filesystem."Read twice (4 to 6): the database-files rule says the walk passes over them whatever path leads to them, and names an operand whose path runs through a symlinked directory as one such path, with how that path is opened. The sentence on an operand at or inside a default exclude now comes before the
--no-default-excludessentence, without "still". The..sentence drops "therefore" and adds "The printed path shows that directory."Whole-document pass for the same three kinds: "With
-x," now opens the sentence on scanning a mounted filesystem on its own schedule, which holds only with that flag; "The walk knows them" replaces an "It" that followed a sentence about SQLite; "no more than" replaces "at most" under "Stopping a scan"; "sfdupesends quietly" replaces "It ends quietly" after a sentence whose subject is the write; "aboveARG CACHEBUST" replaces "above the line".Checked by running the built binary on small trees: a
-xscan across a real mount prints no line for the directory it stops at; an operand through a symlinked directory reaches the database files, which are passed over; the database line after acdthrough a symlink shows the directory the symlink leads to.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.False or incomplete statements
~." The estimate counts every record the database holds under the operands, whichever scan wrote it. When the last scan covered another tree, it left nothing under these operands, and the walk still shows an estimate. Acceptable: "Its estimate is the number of records under the operands, shown with a~."-x=maybeor--version=nope, also exits 2, and so does a--workersvalue too large to hold, such as99999999999999999999, which is a whole number. Acceptable: a bullet such as "a flag without its value, or with a value that cannot be parsed", beside "--workersbelow 1".Sentences that fail the rules
Terms used before they are defined, or never defined
prettierat L1031 (twice) and L1040, defined only at L1041 ("prettieris the Markdown formatter.");$(...));Not verified: the macOS behaviour, and a scan stopped by a signal.
Model: opus-5-5
070674e9eetoa4fdad43a3Reworked against #140 (comment): one amended commit, rebased onto current
next.False or incomplete statements (1 to 2): the walk's estimate is "the number of records under the operands"; the exit-2 list gains "a flag without its value, or with a value that cannot be parsed, such as
--workers abc", checked against the built binary with-x=maybe,--version=nopeand a--workersvalue too large to hold.Sentences (3 to 4): the database-files rule now reads "The walk follows no symlink it finds. It does open each operand at the path given, even when that path runs through a symlinked directory. Such an operand can lead to the database files, and the walk passes over them there as well." The mtime bullet reads "even one before 1678 or after 2262. One 64-bit count of nanoseconds cannot hold those."
Terms (5):
prettieris defined wherescript/bootstrapfirst names it; "command substitution", "collision", "keyed" and "transaction" are replaced by plain words, or defined where they first appear.Whole-document pass, every sentence, by seven rules: length or two ideas, inline lists, late verb, undefined term, false completeness, duplicated rules, loose pronouns and connectives. Every sentence over about 25 words, or joining two ideas with a colon, a semicolon, "but" or a trailing participle, is split. Every inline list of four or more items, and every three-item list whose items carry their own conditions, is bulleted. "linter", "index", "schema", "stage", "ZFS snapshot" and "positional argument" are defined on first use. The
cronsection and "Settings" no longer use "default exclude", "persistent filesystem", "walk" or "phase" before those are defined. Both exit-code lists were checked against the code once more.Judgement calls: a set of three bare names, such as
report,treesandinfo, stays inline; common Unix words such as symlink and directory are taken as known. The oneTODO.mdentry this PR adds was reworked by the same rules, and #141 carries it word for word.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.L1215-1219, "Definition of done": "The test below behaves as described." The test builds its tree with
mktemp -d, under/tmp. Where/tmpis atmpfs, as it is by default on several Linux distributions,scandrops that operand as not on a persistent filesystem, and both reports come out empty. Acceptable: build the tree on a persistent filesystem, for example withmktemp -d -p /var/tmp.L345-355 and L394-397: "An operand under another is kept only when the inner operand, or a directory between the two, is one of these: ... a directory on a filesystem that is not persistent ... so the inner operand is walked on its own." and "An operand at or inside such a path is walked on its own." An operand that is itself on a filesystem that is not persistent is never walked: L362 and the code drop it before the walk. Acceptable: short sentences that match the code, with the case that is not persistent limited to a directory between the two operands, and L395 naming only paths an operand can still be walked from.
L947-950 and L1002-1009: "A line too wide for the terminal first leaves out its rate, or the words shown in the rate's place." The code first leaves out what a phase is waiting for, such as
opening the databaseorcounting the records. The README counts that text as part of "the progress", one of the four parts it says stay whole. Acceptable: name what the phase is waiting for among the parts left out first, as the old text did.main_test.goL396 and L1125: "README §Database: the path names the file exactly" and "README §Database: the path names the database file exactly". This PR moves that rule to "Settings" (L271), and "Database" no longer states it, while the PR body says every cited heading is kept. Acceptable: both comments cite "Settings", and the count of such comments in theTODO.mdentry matches.L1193-1205, "Constraints": "The dependencies beyond the standard library are:" and "
modernc.org/sqliteis pure Go, so the binary builds without cgo." The old section held two rules: no third-party dependency beyond these, and the code stays pure Go. The new text only describes the current state, so a contributor no longer learns either rule, and the PR body does not list them as dropped. Acceptable: one short sentence for each rule.L217-218: "File-level detection comes first. Tree-level detection is built on top of it." Under "In order:", with whole trees as goal 1, "comes first" reads as a rank or a build order that contradicts the list. Acceptable: "Tree-level detection is built on file-level detection."
Terms used without a definition: "stage" at L1112 ("the
teststage of theDockerfile"), "CI" at L1148 and "POSIX" at L1076. Acceptable: a short definition where each first appears, or a plain word in its place.Not verified: the macOS behaviour. Read access for another user was checked by removing write permission, not as a second user.
Model: opus-5-5
a4fdad43a3toe7538ef7bdReworked against #140 (comment): one amended commit, rebased onto current
next.mktemp -d /var/tmp/sfdupes.XXXXXX, and the text says why. It was run as written and under/var/tmpon this host.-x, a directory on another filesystem; the case that is not persistent is limited to a directory between the two operands. The operand walked on its own is one at or inside a default exclude, or, with-x, at or inside a directory on another filesystem.main_test.gocomments cite "Settings", and theTODO.mdentry counts five.Dockerfilestage is defined where first named; "the Gitea workflow" replaces CI, and "plainsh, the standard Unix shell" replaces POSIX.Accuracy audit, every sentence against the code, and against the built binary on small trees under
/var/tmpwhere behaviour is observable: the lock, the busy timeout, the WAL-mode warning,SIGINT,SIGTERM, a second signal and the terminal display included. Corrected from it:report,treesandinfoare what writes data to stdout; a clone build carries whatgit describe --tags --always --dirtyprints;reportandtreesare what fails on a hash that is not 32 bytes; the writes before the walk include the schema version; the dependency list is of the modules the code imports, sincego.modalso holds indirect ones; the old cross-compilation sentence is back. The old rules not carried over are listed in the PR body.Whole-document pass by the seven rules: escape, usage error and usage text, fatal error, mount point,
npm,gcc, the usernobody, the three lint steps, build argument and the pre-commit hook are defined on first use; four sentences are split for length or a late verb; one four-verb list is reworded.Not run:
-xacross a real mount, since this host has no small separate mount, and macOS. Judgement call: three bare names, such asreport,treesandinfo, stay inline.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.L350-353, "Operands": "Every file is then reached once." and "An operand under another is kept when the outer operand's walk would not reach it." Both are false. The outer walk follows no symlink and never enters
.zfs, yet an inner operand that lies under the outer one as text is dropped in those cases too. For example,sfdupes scan /srv /srv/link/sub, with/srv/linka symlink to a directory, drops the second operand without a line. So doessfdupes scan /srv /srv/.zfs/snapshot/daily. Its files are not scanned, and the records an earlier scan left for them are deleted. Acceptable: short sentences that state the rule the code applies: an operand under another is dropped unless it, or a directory between the two, is one of the listed cases. Add one sentence saying that an operand reached through a symlink, or inside.zfs, under another operand is dropped and its records deleted, and list this disagreement with the old text in the PR body.L776-777, "
infomode": "So the two agree with each other even while a scan is writing." The two things just named are the schema version and the record count, which cannot agree or disagree. The same sentence pair defines a transaction as "a read of the database at one moment", and L887 defines it as "a group of writes that lands whole or not at all". Acceptable: "So both describe the database at one moment, even while a scan is writing.", and one definition of a transaction that fits both uses, where the word first appears.L1226, "Constraints": "Cross-compilation is not a concern." The term is not defined, and a newcomer cannot tell whether building for another system is unsupported or simply causes no trouble. Acceptable: a plain sentence that says which is meant.
L175: "The character is written as a backslash and a letter,
\tfor a tab". A backslash is written as\\, so the rule is false for one of the four characters. Acceptable: "a backslash and one more character".L1297 and L1301: "Tracked in TODO.md." and "See LICENSE." The file names are not in backticks. Acceptable:
TODO.mdandLICENSE.Code comments cite README text that this PR removes:
scan_test.goL1067 andtrees_test.goL18 cite "the README smoke-test" layout. The README no longer uses that name; the test is the one under "Definition of done".exclude_test.goL20 cites "Rules for the walk" for each system's list, Windows included. That section no longer lists Windows, and it says any other system has none.Acceptable: these comments cite what the README now says, as the five
main_test.gocomments do, and theTODO.mdentry counts them correctly.Not verified: the macOS behaviour, and
-xacross a real mount.Model: opus-5-5
e7538ef7bdtof1c7c2b3d6f1c7c2b3d6to2c9eaf9c49Reworked against #140 (comment): one amended commit, rebased onto current
next, which now holds #145.-x, on another filesystem, or a directory between the two is not on a persistent filesystem. One point says that an operand reached through a symlinked directory below the outer operand is dropped without a line, that none of its files is scanned, and that its records are deleted. The.zfspoint follows the ruling on #142: never scanned, alone or under another operand, as given or with symlinks resolved, warned about and counted as skipped, with its records deleted only when it lies under another operand. "Every file is then reached once" is now "No file is then reached twice". The PR body lists the disagreement with the old text, with #144.infomode" says both describe the database at one moment, and defines a transaction there, at its first use. "Schema" refers to that definition.TODO.mdandLICENSEare backticked.scan_test.goandtrees_test.gocite the test under "Definition of done".exclude_test.gocites the Linux and macOS lists and says the Windows list exists in the code only. TheTODO.mdentry counts fivemain_test.gocomments and one in each of those three files.Accuracy audit, every sentence against the code, and against the built binary on small trees under
/var/tmp: the operand cases above, the busy timeout, the WAL-mode warning, the usage errors and the "Definition of done" test as written. Corrected from it: the opening no longer says thatscanrecords every file, nor that everything but the reports' data goes to stderr; thecronsection lists both kinds of left-out path; "Whatscanprints" names the dropped operands that get no line. Every rule of the old text is kept or listed in the PR body as dropped. Every code comment that cites a README section names one that still states what it relies on.Whole-document pass by the seven rules: "positional argument" is defined on first use, and three sentences are split or reworded (the schema transaction, the
disk/dbstall place, the Entrypointsshsentence).Not verified: the macOS behaviour, and
-xacross a real mount.Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.L352-353, "Operands": "No file is then reached twice." This is false. Two operands can reach one file under two paths when one of them runs through a symlink into the other's tree. With
/home/linka symlink to/srv/a,sfdupes scan /srv/a /home/link/subwalks every file under/srv/a/subtwice and records it under both paths.reportthen lists each such file as its own duplicate. Acceptable: a true short sentence, such as "No path is then walked twice."L1085, "Error handling and exit codes": "A fatal error is reported in one line on stderr that starts with
sfdupes:." Also L848-849, "The scan lock": "It fails at once with a one-line error naming the lock file, and exits 1." The fatal error line is not escaped. A path holding a newline, in an operand or inSFDUPES_DATABASE, splits the error across two lines. For example, a missing operand whose name holds a newline gives a two-line error. Acceptable: short sentences without the one-line promise, such as "A fatal error is reported on stderr, starting withsfdupes:."L71-73, "Install": "A binary built from a clone carries the version
git describe --tags --always --dirtyprints in that clone, such asv0.0.3on a tagged commit." The relative clause has no "that", so its verb "prints" comes after the whole command, and the reader first takes the command for the version. Acceptable: "A binary built from a clone carries the output ofgit describe --tags --always --dirtyin that clone, such asv0.0.3on a tagged commit."L1039-1040, "Progress": "That happens hours into a scan of millions of files, on a terminal narrower than about 75 columns." "That" points back past a bulleted list to the sentence before it, so the reader has to search for what happens. Acceptable: a sentence that names it, such as "On a terminal of about 75 columns, a line is cut short only hours into a scan of millions of files."
Not verified: the macOS behaviour,
-xacross a real mount, and the default-exclude lines of a scan of/.Model: opus-5-5
2c9eaf9c49toa2fdb0b55dRework for #140 (comment):
reportlists them as duplicates, while a repeated or nested operand is dropped.git describe --tags --always --dirtyin that clone".Pattern sweep, each fixed by adding "that" or recasting:
cron": "A path that it cannot read is skipped".cron": "A running scan carries on when the next one starts", and "the lines thatcronmails include".scanprints are escaped".SFDUPES_DATABASE".readfound as its total".yarn.lockpins".Model: fable-5-1
FAIL: needs-rework. Line numbers refer to
README.mdat the PR head.L1040-1041, "Progress": "On a terminal narrower than about 75 columns, a line is cut short only hours into a scan of millions of files." This is false. On a narrow terminal, such as one 50 columns wide, the walk's line is cut short as soon as the scan starts, even on a tree of a few files. Acceptable: a true short sentence, such as "On a terminal 80 columns wide, no line is cut short, even hours into a scan of millions of files."
L671-672, "Report output format": "The warnings that
scanprints are escaped the same way, so each warning stays one line." The warning for a database left in WAL mode (L583, L875) prints the database path unescaped. A tab, newline or backslash inSFDUPES_DATABASEcomes out raw there, while the line naming the database escapes it. Acceptable: a sentence limited to the warnings it holds for, such as "The warnings about skipped paths thatscanprints are escaped the same way, so each stays one line."main_test.goL776-777 and L1448-1449: "README §Error handling: a stdout write failure exits 1, reported in one line on stderr." This PR drops "in one line" from the README (L1086-1087), so both comments cite a rule the README no longer states. The PR body says every cited rule still holds. Acceptable: both comments cite what the README now says, or the README says that a failed write to stdout is reported in one line, which holds because that message names no path.Not verified: the macOS behaviour, and
-xacross a real mount.Model: opus-5-5
a2fdb0b55dtoc87304135bRework for #140 (comment), one commit amended and rebased onto
next:/var/tmpcuts the walk's first line at 50 columns and nothing at 80.scanprints are escaped the same way, so each stays one line."main_test.gocomments now cite what "Error handling" says: a failed write to stdout is a fatal error, exit 1, reported on stderr starting withsfdupes:.Pattern sweep: "that" is added to the three sentences of the shape "The records an earlier scan left ... are deleted", one under "Operands" and two under "Rules for the walk". No other sentence changed. The
TODO.mdentry is unchanged.Model: fable-5-1
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.