1 Commits
Author SHA1 Message Date
sneak 16a57547b8 Rewrite TODO.md sentence by sentence (closes #135)
check / check (push) Canceled after 0s
TODO.md is the project's own task list, judged by the same rules as
README.md: short sentences, lists as bullets, nothing a newcomer reads
twice. Every completed step is kept with its date and its issue or
branch, and every fact the old entries recorded is kept too, in short
sentences. The 2026-07-22 policy-compliance checklist is folded into
the step that did the work, and the accepted divergence from the
layout policy gets a section of its own. Workflow, Status and Next
Step keep their rules unchanged.

Model: fable-5-1
2026-10-26 07:23:22 +00:00
+154 -116
View File
@@ -14,9 +14,9 @@
# Status # Status
- Pre-1.0. - Pre-1.0.
- The tracker is authoritative for the backlog. The open issues under the - The tracker is authoritative for the pre-1.0 backlog. The open issues under
`1.0.0` milestone are what remains before the tag. This file records history the `1.0.0` milestone are what remains before the tag. This file records
and process, not the queue. history and process, not the queue.
# Next Step # Next Step
@@ -26,11 +26,12 @@
# Completed Steps # Completed Steps
Newest first. Each entry ends with its date and its issue or branch. Newest first. An entry ends with its date and its issue or branch, where those
are known.
- `TODO.md` rewritten from scratch by the same rules as `README.md`: short - `TODO.md` rewritten from scratch by the same rules as `README.md`: short
sentences, lists as bullets, nothing a newcomer reads twice. Every completed sentences, lists as bullets, nothing a newcomer reads twice. Every completed
step is kept, with its date and issue, in fewer words. The policy-compliance step is kept, with its date and its issue or branch. The policy-compliance
checklist of 2026-07-22 is folded into its step (2026-10-26, checklist of 2026-07-22 is folded into its step (2026-10-26,
https://git.eeqj.de/sneak/sfdupes/issues/135) https://git.eeqj.de/sneak/sfdupes/issues/135)
@@ -59,18 +60,24 @@ Newest first. Each entry ends with its date and its issue or branch.
- Every file the walk records gets three hashes in the hash phase. `head` and - Every file the walk records gets three hashes in the hash phase. `head` and
`tail` are the SHA-256 of its first and last 64 KiB. `content` is the SHA-256 `tail` are the SHA-256 of its first and last 64 KiB. `content` is the SHA-256
of the whole file under 100 MiB, and of 1 MiB at each gigabyte from 100 MiB of the whole file under 100 MiB, and of 1 MiB at each gigabyte from 100 MiB
up. The 10 MiB and 50 MiB thresholds, the size census, the records without up. Gone are:
hashes and the content phase are gone. A scan has four phases, and a rescan - the 10 MiB and 50 MiB thresholds;
reads only new and changed files. `info` no longer counts the records with - the size census;
hashes, since every record has them (2026-10-25, - the records without hashes;
- the content phase.
A scan has four phases, and a rescan reads only new and changed files.
`info` no longer counts the records with `head` and `tail` hashes, nor those
with a content hash, since every record has all three (2026-10-25,
https://git.eeqj.de/sneak/sfdupes/issues/132) https://git.eeqj.de/sneak/sfdupes/issues/132)
- `REPO_POLICIES.md` and the four scripts that pass `CACHEBUST` re-vendored from - `REPO_POLICIES.md` and the four scripts that pass `CACHEBUST` re-vendored from
`sneak/prompts` `next` at `0653579`. The module download in the `lint`, `test` `sneak/prompts` `next` at `0653579`. Two steps now come before
and `builder` stages of the `Dockerfile`, and the package install in `ARG CACHEBUST`: the module download in the `lint`, `test` and `builder`
`builder`, now come before `ARG CACHEBUST`. Docker keeps them between builds. stages of the `Dockerfile`, and the package install in `builder`. Docker keeps
them until the base image, the install line, `go.mod` or `go.sum` changes.
Every step that lints, tests or compiles still comes after it. Every step that lints, tests or compiles still comes after it.
`golangci-lint run` also keeps golangci-lint's cache. README says the same `golangci-lint run` also keeps the linter's own cache. README says the same
(2026-10-18, https://git.eeqj.de/sneak/sfdupes/issues/109) (2026-10-18, https://git.eeqj.de/sneak/sfdupes/issues/109)
- Release rule set: until 1.0, each tip of `main` that passes the build gets the - Release rule set: until 1.0, each tip of `main` that passes the build gets the
@@ -100,44 +107,51 @@ Newest first. Each entry ends with its date and its issue or branch.
- `scan` never records or reads anything on a filesystem that is not persistent: - `scan` never records or reads anything on a filesystem that is not persistent:
`tmpfs`, `proc`, `sysfs` and the other kernel or memory-backed types. It `tmpfs`, `proc`, `sysfs` and the other kernel or memory-backed types. It
matches them by device ID, listed as the scan starts from matches them by device ID. As the scan starts, it lists the mounted
`/proc/self/mountinfo` on Linux and from the mount list on macOS. No flag filesystems of those types, from `/proc/self/mountinfo` on Linux and from the
turns that off. Below an operand, the walk does not enter the default excludes mount list on macOS. No flag turns that off. Below an operand, the walk does
either, so `sfdupes scan /` skips the kernel's view of the running system and not enter the default excludes either, so `sfdupes scan /` skips the kernel's
the temporary directories. On Linux those are `/proc`, `/sys`, `/dev`, `/run`, view of the running system and the temporary directories. On Linux those are
`/tmp` and `/var/tmp`. On macOS they are `/dev`, `/private/tmp`, `/proc`, `/sys`, `/dev`, `/run`, `/tmp` and `/var/tmp`. On macOS they are
`/private/var/tmp`, `/private/var/vm`, `/System/Volumes/VM` and `/dev`, `/private/tmp`, `/private/var/tmp`, `/private/var/vm`,
`/System/Volumes/Data`. A Windows list exists in the code but is not used. An `/System/Volumes/VM` and `/System/Volumes/Data`. A Windows list exists in the
operand at or inside a default exclude is still walked. The scan prints a line code but is not used. An operand at or inside a default exclude is still
for each path it leaves out, without counting it as skipped, and the records walked. The scan prints a line for each path it leaves out, without counting
there count as outside the scanned operands, as for `-x`. it as skipped, and the records there count as outside the scanned operands, as
`--no-default-excludes` turns off the default excludes only, and `scan --help` for `-x`. `--no-default-excludes` turns off the default excludes only, and
lists them (2026-10-17, https://git.eeqj.de/sneak/sfdupes/issues/124) `scan --help` lists them (2026-10-17,
https://git.eeqj.de/sneak/sfdupes/issues/124)
- The rate a progress display shows covers the items completed over the last 5 - The rate a progress display shows covers the items completed over the last 5
seconds, or since the line before when stderr is not a terminal. It falls to 0 seconds, or since the line before when stderr is not a terminal. It falls to 0
while nothing completes, and a burst after a wait no longer reads as a steady while nothing completes, and a burst after a wait no longer reads as a steady
rate. The time remaining still comes from the phase's average. Once no item rate of over 1,000 files a second. The time remaining still comes from the
has completed for 10 seconds, the word `stalled` and where the phase waits phase's average. Once no item has completed for 10 seconds, the word `stalled`
take the rate's place, as `stalled disk/db`. The places are `disk`, `db`, and where the phase waits take the rate's place, as `stalled disk/db`. The
`out` for stdout and `cpu`, or two of them for a phase that may wait on places are `disk`, `db`, `out` for stdout and `cpu`, or two of them for a
either. A line too wide for the terminal loses its bar first, then its rate or phase that may wait on either. Each phase gives its word where it is defined.
those words, so the phase, progress, elapsed time and time remaining stay A line too wide for the terminal loses its bar first, then its rate or those
whole. The content phase's last partial batch is written under a display of words, so the phase, progress, elapsed time and ETA stay whole. On an
80-column terminal, at counts like those of README's example, the content
phase's checks and reads lose those words at most times in minutes. Every
other phase keeps them whenever the elapsed time and the ETA are both in
minutes. The content phase's last partial batch is written under a display of
its own (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/125) its own (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/125)
- New subcommand `info` prints the database's details on stdout, one per line as - New subcommand `info` prints the database's details on stdout, one per line as
a name, a tab and the value (2026-10-16, a name, a tab and the value:
https://git.eeqj.de/sneak/sfdupes/issues/117):
- the path and where it came from; - the path and where it came from;
- the sizes of the database file and of its `-wal` file, when there is one; - the sizes of the database file and of its `-wal` file, when there is one;
- when the database file was last changed; - when the database file was last changed;
- the schema version, the record counts and the sizes of all records added - the schema version;
up. - the number of records, how many have `head` and `tail` hashes, and how
many have a content hash;
- the sizes of all records added up.
It opens the database as `report` does and shows two phases on stderr, It opens the database as `report` does and shows two phases on stderr,
`[1/2] open` and `[2/2] count`. The schema version and the counts come from `[1/2] open` and `[2/2] count`. The schema version and the counts come from
one read-only transaction, the counts from one query. one read-only transaction, the counts from one query (2026-10-16,
https://git.eeqj.de/sneak/sfdupes/issues/117)
- `report` shows its work on stderr in two phases, `[1/2] open` and - `report` shows its work on stderr in two phases, `[1/2] open` and
`[2/2] read`, with the progress display `scan` and `trees` use. The display `[2/2] read`, with the progress display `scan` and `trees` use. The display
@@ -164,19 +178,20 @@ Newest first. Each entry ends with its date and its issue or branch.
(2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/113) (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/113)
- Each `scan` phase's progress display names the phase and its place among the - Each `scan` phase's progress display names the phase and its place among the
five, as `[2/5] walk`, from the moment it starts, with its progress, elapsed five, as `[2/5] walk`. It runs from the moment the phase starts and shows its
time and ETA. `load` counts the records first. The walk's total is estimated progress, elapsed time and ETA. `load` counts the records first. The walk's
from the records under the operands. The content phase counts the records it total is estimated from the records under the operands. The content phase
checks from SQLite's grouping alone, then says it is searching the database counts the records it checks from SQLite's grouping alone, then says it is
until the first one is returned. On a terminal a phase with an exact total searching the database until the first one is returned. On a terminal a phase
draws its line followed by the library's bar, and any other a spinner followed with an exact total draws its line followed by the library's bar. Any other
by the line. Every display is drawn again several times a second. No line phase draws a spinner followed by the line. Every display is drawn again
reaches the terminal's last column, however narrow, also after the terminal is several times a second. No line reaches the terminal's last column, however
made narrower during a phase. A line too wide leaves out its rate, then its narrow, also after the terminal is made narrower during a phase. A line too
bar, keeping the phase, progress, elapsed time and ETA whole. It is cut short wide leaves out its rate, then its bar, keeping the phase, progress, elapsed
only when those alone do not fit, as below about 70 columns late in a large time and ETA whole. It is cut short only when those alone do not fit, as below
scan. In plain mode the line is printed every 5 seconds whether or not an item about 70 columns late in a large scan. In plain mode the line is printed every
completed (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/112) 5 seconds whether or not an item completed (2026-10-16,
https://git.eeqj.de/sneak/sfdupes/issues/112)
- `scan`, `report` and `trees` first print a line on stderr naming the database - `scan`, `report` and `trees` first print a line on stderr naming the database
they use, as an absolute path, and whether that path is the default or comes they use, as an absolute path, and whether that path is the default or comes
@@ -185,8 +200,8 @@ Newest first. Each entry ends with its date and its issue or branch.
https://git.eeqj.de/sneak/sfdupes/issues/111) https://git.eeqj.de/sneak/sfdupes/issues/111)
- The canonical files and model scripts re-vendored from `sneak/prompts` `next` - The canonical files and model scripts re-vendored from `sneak/prompts` `next`
at `e3f79c4` (2026-10-15, https://git.eeqj.de/sneak/sfdupes/issues/95): at `e3f79c4`:
- golangci-lint v2.14.0; - `golangci-lint` v2.14.0;
- lint and test are phases of the `Dockerfile` that write no image, and - lint and test are phases of the `Dockerfile` that write no image, and
`make test` runs the suite under the race detector. `Dockerfile.lint`, `make test` runs the suite under the race detector. `Dockerfile.lint`,
`script/verify-lint-image-pin` and `make test-race` are gone; `script/verify-lint-image-pin` and `make test-race` are gone;
@@ -194,18 +209,18 @@ Newest first. Each entry ends with its date and its issue or branch.
every stage of the `Dockerfile` declares first, so every `RUN` step runs every stage of the `Dockerfile` declares first, so every `RUN` step runs
on every build. The steps that compile keep Go's build cache, and the on every build. The steps that compile keep Go's build cache, and the
tests run with `-count=1`; tests run with `-count=1`;
- prettier runs on the host, from the node and yarn `script/bootstrap` - `prettier` runs on the host, from the `node` and `yarn` that
installs, so the `prettier` and `markdown` stages are gone and the build `script/bootstrap` installs, so the `prettier` and `markdown` stages are
stage installs `git` and `make` itself; gone and the build stage installs `git` and `make` itself;
- a new push cancels the workflow's older run on the same branch, and a run - a new push cancels the workflow's older run on the same branch, and a run
stops after 20 minutes; stops after 20 minutes;
- `.claude/settings.json` is deleted. - `.claude/settings.json` is deleted.
Since https://git.eeqj.de/sneak/sfdupes/issues/109 the module download in Since https://git.eeqj.de/sneak/sfdupes/issues/109 two steps come before
`lint`, `test` and the build stage, and the install of `git` and `make` in `ARG CACHEBUST`: the module download in `lint`, `test` and the build stage,
the build stage, come before `ARG CACHEBUST`. Docker keeps them between and the install of `git` and `make` in the build stage. Docker keeps them
builds, and every step that lints, tests or compiles still runs on every between builds, and every step that lints, tests or compiles still runs on
build. every build (2026-10-15, https://git.eeqj.de/sneak/sfdupes/issues/95)
- With `-x`, an operand under another operand is no longer dropped when the - With `-x`, an operand under another operand is no longer dropped when the
outer operand's walk cannot reach it. That is the case when a directory from outer operand's walk cannot reach it. That is the case when a directory from
@@ -258,8 +273,8 @@ Newest first. Each entry ends with its date and its issue or branch.
`script/` pass a new `CACHEBUST` value, so theirs always run (2026-10-04, `script/` pass a new `CACHEBUST` value, so theirs always run (2026-10-04,
https://git.eeqj.de/sneak/sfdupes/issues/39) https://git.eeqj.de/sneak/sfdupes/issues/39)
- `make fmt` and `make fmt-check` run prettier over all Markdown, and CI checks - `make fmt` and `make fmt-check` run `prettier` over all Markdown, and CI
it. All Markdown was reformatted (2026-10-04, checks it. All Markdown was reformatted (2026-10-04,
https://git.eeqj.de/sneak/sfdupes/issues/19) https://git.eeqj.de/sneak/sfdupes/issues/19)
- `script/lint` writes no image, so a run no longer leaves an untagged one - `script/lint` writes no image, so a run no longer leaves an untagged one
@@ -333,7 +348,7 @@ Newest first. Each entry ends with its date and its issue or branch.
https://git.eeqj.de/sneak/sfdupes/issues/53) https://git.eeqj.de/sneak/sfdupes/issues/53)
- Tests cover stdout write failures in `report` and `trees`. README states that - Tests cover stdout write failures in `report` and `trees`. README states that
`| head` ends sfdupes by `SIGPIPE` and that `>&-` writes to `/dev/null` `| head` ends `sfdupes` by `SIGPIPE` and that `>&-` writes to `/dev/null`
(2026-10-03, https://git.eeqj.de/sneak/sfdupes/issues/30) (2026-10-03, https://git.eeqj.de/sneak/sfdupes/issues/30)
- `report` and `trees` open the database read-only, and `scan` leaves it out of - `report` and `trees` open the database read-only, and `scan` leaves it out of
@@ -352,16 +367,16 @@ Newest first. Each entry ends with its date and its issue or branch.
`make build` (2026-10-02, branch `next`, `make build` (2026-10-02, branch `next`,
https://git.eeqj.de/sneak/sfdupes/issues/67) https://git.eeqj.de/sneak/sfdupes/issues/67)
- The 1 KiB end-window sampling was replaced with the head/tail plus - The 1 KiB end-window sampling was replaced with the hashes chosen by file
content-hash ladder, and README "Duplicate detection" documented every rung. A size, and README "Duplicate detection" documented every size threshold. A file
file under 10 MiB was hashed in full, and its `head`, `tail` and `content` all under 10 MiB was hashed in full, and its `head`, `tail` and `content` all held
held that hash. A larger file got only its 64 KiB `head` and `tail` in the that hash. A larger file got only its 64 KiB `head` and `tail` in the hash
hash phase. The content phase, after the update phase, read it for `content` phase. The content phase, after the update phase, read it for `content` only
only when its size, `head` and `tail` matched another record's. `content` was when its size, `head` and `tail` matched another record's, from this scan or
the whole file below 50 MiB, and gigabyte-spaced 1 MiB samples at or above. A an earlier one. `content` was the whole file below 50 MiB, and gigabyte-spaced
file gone or changed since its record was written was never read. `report` and 1 MiB samples at or above. A file gone or changed since its record was written
`trees` left out any record without a `content` hash. The `content` column is was never read. `report` and `trees` left out any record without a `content`
part of the version 1 schema. Superseded by hash. The `content` column is part of the version 1 schema. Superseded by
https://git.eeqj.de/sneak/sfdupes/issues/132 (2026-09-22, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/132 (2026-09-22, branch `next`,
https://git.eeqj.de/sneak/sfdupes/issues/61) https://git.eeqj.de/sneak/sfdupes/issues/61)
@@ -374,11 +389,11 @@ Newest first. Each entry ends with its date and its issue or branch.
`# image:vX.Y.Z, YYYY-MM-DD` comment over a bare `FROM image@sha256:...`, `# image:vX.Y.Z, YYYY-MM-DD` comment over a bare `FROM image@sha256:...`,
without the false `(Debian-based)` note or the tag. Since without the false `(Debian-based)` note or the tag. Since
https://git.eeqj.de/sneak/sfdupes/issues/95 the `Dockerfile`'s `lint` phase https://git.eeqj.de/sneak/sfdupes/issues/95 the `Dockerfile`'s `lint` phase
holds the only golangci-lint pin, in that form. `Dockerfile.lint` and holds the only `golangci-lint` pin, in that form. `Dockerfile.lint` and
`script/verify-lint-image-pin`, which compared the two pins, are gone `script/verify-lint-image-pin`, which compared the two pins, are gone
(2026-08-10, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/25) (2026-08-10, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/25)
- All linting runs in Docker, via `Dockerfile.lint` and `script/lint`. Per the - All linting ran in Docker, via `Dockerfile.lint` and `script/lint`. Per the
owner ruling the linter is never installed on a host, and owner ruling the linter is never installed on a host, and
`golangci-lint config verify` runs before `golangci-lint run`. `golangci-lint config verify` runs before `golangci-lint run`.
`script/bootstrap` stopped installing or pinning the linter, and `script/bootstrap` stopped installing or pinning the linter, and
@@ -387,7 +402,7 @@ Newest first. Each entry ends with its date and its issue or branch.
`Dockerfile`'s `lint` phase, which `script/lint` builds alone and the build `Dockerfile`'s `lint` phase, which `script/lint` builds alone and the build
stage depends on through `COPY --from=lint /src/go.sum /dev/null`. stage depends on through `COPY --from=lint /src/go.sum /dev/null`.
`Dockerfile.lint` and `script/verify-lint-image-pin` are gone. Nothing inside `Dockerfile.lint` and `script/verify-lint-image-pin` are gone. Nothing inside
an image build may run docker, so the phase calls `golangci-lint` directly an image build may run `docker`, so the phase calls `golangci-lint` directly
(2026-08-10, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/46) (2026-08-10, branch `next`, https://git.eeqj.de/sneak/sfdupes/issues/46)
- The Docker build stage's prerequisites were installed by running - The Docker build stage's prerequisites were installed by running
@@ -414,18 +429,19 @@ Newest first. Each entry ends with its date and its issue or branch.
(2026-08-09, branch `cibuild-cache-bust`, (2026-08-09, branch `cibuild-cache-bust`,
https://git.eeqj.de/sneak/sfdupes/issues/32) https://git.eeqj.de/sneak/sfdupes/issues/32)
- `script/bootstrap` checked the installed golangci-lint version instead of only - `script/bootstrap` checked the installed `golangci-lint` version instead of
its presence. The version lived only in `GOLANGCI_LINT_VERSION`, with the only its presence. The version lived only in `GOLANGCI_LINT_VERSION`, with the
`go install` module ref derived from it, and any installed version that was `go install` module ref derived from it. Any installed version that was not
not the pin was reinstalled. `go install` writes into `GOBIN` (or the pin was reinstalled: older, newer, absent or unparseable. `go install`
`GOPATH/bin`) while `make lint` ran the first `golangci-lint` on `PATH`, so writes into `GOBIN` (or `GOPATH/bin`) while `make lint` ran the first
bootstrap re-read the effective version after installing. On a mismatch it `golangci-lint` on `PATH`, so bootstrap re-read the effective version after
printed both paths and both versions and exited non-zero; it did not reorder installing. On a mismatch it printed both paths and both versions and exited
`PATH` or delete anyone's binary. The `--version` call kept its stderr and was non-zero; it did not reorder `PATH` or delete anyone's binary. The `--version`
bounded by `timeout(1)` where that exists. `git`, `make` and `go` kept call kept its stderr and was bounded by `timeout(1)` where that exists. `git`,
presence-only checks. Superseded by `make` and `go` kept presence-only checks. `make check` and `make lint` were
https://git.eeqj.de/sneak/sfdupes/issues/46: the linter is no longer installed clean at v2.12.2, so v2.10.1 had not been hiding findings on `main`.
on a host (2026-08-09, branch `bootstrap-version-check`, Superseded by https://git.eeqj.de/sneak/sfdupes/issues/46: the linter is no
longer installed on a host (2026-08-09, branch `bootstrap-version-check`,
https://git.eeqj.de/sneak/sfdupes/issues/24) https://git.eeqj.de/sneak/sfdupes/issues/24)
- The hash worker pool unwinds on the error path. The pool is an owned, - The hash worker pool unwinds on the error path. The pool is an owned,
@@ -437,13 +453,17 @@ Newest first. Each entry ends with its date and its issue or branch.
pools and the whole database layer, as the first parameter everywhere. The pools and the whole database layer, as the first parameter everywhere. The
walk pool gets the same treatment, plus a `ctx.Err()` guard after the walk. A walk pool gets the same treatment, plus a `ctx.Err()` guard after the walk. A
cancelled walk yields a partial size census, and every file it never reached cancelled walk yields a partial size census, and every file it never reached
looks vanished to the update phase. The guard is the barrier that still holds looks vanished to the update phase. That phase's own `BeginTx` also fails on
once an interrupted scan may commit what it has. Tests drive `run(scan)` the cancelled context before deleting anything, but the guard is the barrier
against a database whose insert trigger aborts, and assert that the scan fails that still holds once an interrupted scan may commit what it has. Tests drive
instead of hanging and that the goroutine count returns to its pre-scan `run(scan)` against a database whose insert trigger aborts, and assert that
baseline. Other tests cancel a scan part-way through the walk and assert that the scan fails instead of hanging and that the goroutine count returns to its
it stops at the guard with every record intact (2026-08-09, branch pre-scan baseline. Other tests cancel a scan part-way through the walk and
`hash-pool-cleanup`, https://git.eeqj.de/sneak/sfdupes/issues/6) assert that it stops at the guard with every record intact. Direct tests of
`sendEvent`, the walk workers, `dispatchDirs`, `feedHashJobs`, `hashWorker`
and `hashPhase` cover the remaining cancellation branches of both pools
(2026-08-09, branch `hash-pool-cleanup`,
https://git.eeqj.de/sneak/sfdupes/issues/6)
- The database is closed on every fatal exit path. `fatalf` and its `os.Exit(1)` - The database is closed on every fatal exit path. `fatalf` and its `os.Exit(1)`
are gone, so the deferred `db.Close()`, and with it the SQLite WAL checkpoint, are gone, so the deferred `db.Close()`, and with it the SQLite WAL checkpoint,
@@ -451,13 +471,14 @@ Newest first. Each entry ends with its date and its issue or branch.
`loadRecords` and `resolveRoots` return errors instead. The single exit point `loadRecords` and `resolveRoots` return errors instead. The single exit point
is `run` in `main.go`. It maps a `fatalError` to exit 1 and cobra's own is `run` in `main.go`. It maps a `fatalError` to exit 1 and cobra's own
argument and flag errors to exit 2, so a runtime failure is never reported as argument and flag errors to exit 2, so a runtime failure is never reported as
a usage error. `main_test.go` drives the CLI in-process and asserts the exit a usage error or printed with the usage text. `main_test.go` drives the CLI
codes from README "Error handling" and the stdout/stderr split. That includes in-process and asserts the exit codes from README "Error handling" and the
a fatal error after the database is open leaving no `-wal` or `-shm` file stdout/stderr split. That includes a fatal error after the database is open
behind (2026-08-09, branch `db-close-on-fatal`, leaving no `-wal` or `-shm` file behind, for `scan`, `report` or `trees`
(2026-08-09, branch `db-close-on-fatal`,
https://git.eeqj.de/sneak/sfdupes/issues/4) https://git.eeqj.de/sneak/sfdupes/issues/4)
- golangci-lint updated to v2.12.2 with the canonical config, merged as - `golangci-lint` updated to v2.12.2 with the canonical config, merged as
`38a01bd`. The pinned linter in the `Dockerfile` lint stage and `38a01bd`. The pinned linter in the `Dockerfile` lint stage and
`script/bootstrap` went from v2.12.1 to v2.12.2, and `.golangci.yml` was `script/bootstrap` went from v2.12.1 to v2.12.2, and `.golangci.yml` was
replaced with the canonical file. The linter settings (`lll`, `funlen`, replaced with the canonical file. The linter settings (`lll`, `funlen`,
@@ -466,9 +487,13 @@ Newest first. Each entry ends with its date and its issue or branch.
`golangci-v2.12.2`, https://git.eeqj.de/sneak/sfdupes/issues/3) `golangci-v2.12.2`, https://git.eeqj.de/sneak/sfdupes/issues/3)
- Makefile targets converted to scripts-to-rule-them-all `script/` entrypoints, - Makefile targets converted to scripts-to-rule-them-all `script/` entrypoints,
like the other managed repos. All 12 entrypoints exist, and every Makefile like the other managed repos. All 12 entrypoints exist:
target is a thin shim over them (2026-07-26, commit `3abeacf`, - `bootstrap`, `setup` and `projectname`;
https://git.eeqj.de/sneak/sfdupes/issues/1) - `test`, `lint`, `fmt`, `fmt-check` and `check`;
- `docker`, `cibuild`, `precommit` and `install-precommit`.
Every Makefile target is a thin shim over them (2026-07-26, commit
`3abeacf`, https://git.eeqj.de/sneak/sfdupes/issues/1)
- The binary is the default Make target: plain `make` builds `sfdupes`, where it - The binary is the default Make target: plain `make` builds `sfdupes`, where it
used to run `check` plus `build`. `make build` remains as an alias used to run `check` plus `build`. `make build` remains as an alias
@@ -483,10 +508,15 @@ Newest first. Each entry ends with its date and its issue or branch.
required (2026-07-24, branch `scan-wide-phases`) required (2026-07-24, branch `scan-wide-phases`)
- The stat pass split back out of the walk. Phases are strictly sequential - The stat pass split back out of the walk. Phases are strictly sequential
again, walk, stat, hash and update per operand, with parallelism only inside again, per operand, with parallelism only inside each phase:
each phase. The walk enumerates paths with per-directory workers, and the stat - walk;
pass lstats them with per-file workers, restoring the exact total and ETA of - stat;
the stat bar (2026-07-24, branch `parallel-phases`) - hash;
- update.
The walk enumerates paths with per-directory workers, and the stat pass
lstats them with per-file workers, restoring the exact total and ETA of the
stat bar (2026-07-24, branch `parallel-phases`)
- Each operand is announced on stderr before its passes. With per-operand walk, - Each operand is announced on stderr before its passes. With per-operand walk,
hash and update cycles, a multi-operand run such as `scan /srv/*` showed pass hash and update cycles, a multi-operand run such as `scan /srv/*` showed pass
@@ -519,26 +549,34 @@ Newest first. Each entry ends with its date and its issue or branch.
at filesystem boundaries, which are crossed by default (2026-07-23, branch at filesystem boundaries, which are crossed by default (2026-07-23, branch
`scan-required-paths`) `scan-required-paths`)
- The repo brought into compliance with `REPO_POLICIES.md` (2026-07-06), the - The repo was brought into compliance with `REPO_POLICIES.md` (the version of
existing repo checklist and the Go styleguide, audited 2026-07-22 (2026-07-23, 2026-07-06), the existing repo checklist and the Go styleguide. The audit of
branch `repo-policy-compliance`). Added or fixed: 2026-07-22 found these gaps, and the branch closed them:
- `.gitignore`, with OS, editor, Go and secrets patterns; the compiled - `.gitignore`, with OS, editor, Go and secrets patterns; the compiled
`sfdupes` binary and `files.dat` had been sitting untracked in the tree; `sfdupes` binary and `files.dat` had been sitting untracked in the tree;
- `.editorconfig`; - `.editorconfig`;
- `LICENSE` (MIT) and the README License section; - `LICENSE` and the README License section. MIT was assumed from house
convention, for the owner to confirm;
- `REPO_POLICIES.md` in the repo root; - `REPO_POLICIES.md` in the repo root;
- the canonical `.golangci.yml`, with the 150 findings it raised fixed; - the canonical `.golangci.yml`, with the 150 findings it raised fixed;
- the `Makefile` targets `test`, `lint`, `fmt`, `fmt-check`, `docker` and - the `Makefile` targets `test`, `lint`, `fmt`, `fmt-check`, `docker` and
`hooks`, and a `check` target that no longer depends on `build`; `hooks`, and a `check` target that no longer depends on `build`;
- a test suite covering parsing, grouping, digests, suppression, hashing and - a test suite, which the policy required with a 30-second timeout and the
the scan pipeline, at 64% coverage; conditional `-v` rerun. It covers parsing, grouping, digests, suppression,
- a multistage `Dockerfile` with hash-pinned images, and `.dockerignore`; hashing and the scan pipeline, at 64% coverage;
- a multistage `Dockerfile` with hash-pinned images, a fail-fast lint stage
and a build stage running `make check`, plus `.dockerignore`;
- `.gitea/workflows/check.yml`, running `docker build .` on push with the - `.gitea/workflows/check.yml`, running `docker build .` on push with the
checkout action pinned by commit SHA; checkout action pinned by commit SHA;
- the README sections the policy requires; - the README sections the policy requires: a Description first line with the
name, purpose, category, license and author, then Getting Started,
Rationale, TODO, License and Author;
- the stale README non-goal "no git repository setup and no CI" removed; - the stale README non-goal "no git repository setup and no CI" removed;
- the pre-commit hook installed. - the pre-commit hook installed.
The code was already `gofmt`-clean, so no standalone formatting commit was
needed (2026-07-23, branch `repo-policy-compliance`)
- `git init` with a README-only first commit; the code baseline committed on - `git init` with a README-only first commit; the code baseline committed on
`main` (2026-07-22) `main` (2026-07-22)