Files
sfdupes/TODO.md
T
sneak 16a57547b8
check / check (push) Canceled after 0s
Rewrite TODO.md sentence by sentence (closes #135)
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

33 KiB

Workflow

  • Take an issue from the 1.0.0 milestone on the tracker. Work that is not on the tracker yet is filed as an issue first.
  • Branch from next.
  • Do the work, with tests, in small focused commits.
  • Record the step at the top of Completed Steps, in the same commit as the work.
  • Push the branch and open a PR against next. Its title ends with (closes #N).
  • An independent review gates each merge to next. Every finding is addressed or rebutted on the PR.
  • Only the owner merges next into main.

Status

  • Pre-1.0.
  • The tracker is authoritative for the pre-1.0 backlog. The open issues under the 1.0.0 milestone are what remains before the tag. This file records history and process, not the queue.

Next Step

Completed Steps

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 sentences, lists as bullets, nothing a newcomer reads twice. Every completed 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, #135)

  • README.md rewritten from scratch, sentence by sentence, as a newcomer reads it. It leads with what the program is and how to use it. A reference section covers the settings, the database, the output formats and the exit codes; development and testing come last. Every statement was checked against the code on next (2026-10-26, #135)

  • A file or directory removed while the scan runs is not an error, in the walk or in the hash phase. The scan prints no line for it and does not count it as skipped. When the first of several hard links to a file is gone, the hash phase reads the next one instead. On a terminal every progress display starts with the spinner, the hash phase's included. A phase with an exact total ends its line with its percent and a 10-column bar at the terminal's right edge, which no longer moves as the line's length changes. A line too wide for the terminal now loses its rate before its bar. The time remaining is labelled remain, not eta (2026-10-26, #133)

  • The head, tail and content columns are BLOBs, each holding the raw 32 bytes of a SHA-256, half the size of the hex text they held. The hashes stay raw from the hash phase through the database to report and trees. Reading a record whose hash is not 32 bytes is an error (2026-10-26, #134)

  • 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 of the whole file under 100 MiB, and of 1 MiB at each gigabyte from 100 MiB up. Gone are:

    • the 10 MiB and 50 MiB thresholds;
    • the size census;
    • 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, #132)

  • REPO_POLICIES.md and the four scripts that pass CACHEBUST re-vendored from sneak/prompts next at 0653579. Two steps now come before ARG CACHEBUST: the module download in the lint, test and builder 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. golangci-lint run also keeps the linter's own cache. README says the same (2026-10-18, #109)

  • Release rule set: until 1.0, each tip of main that passes the build gets the next v0.0.N tag, so go install sneak.berlin/go/sfdupes@latest installs it. go.mod retracts v0.0.1, the early build without a database. When the linker set no version, sfdupes --version prints the version Go recorded in the binary: the tag, or the pseudo-version of a go install build. It prints dev only when neither is known. README "Install" documented the rule, what @main and v0.0.1 give, and how to check an installed binary (2026-10-17, #116)

  • scan creates the database's directory where the database path leads. The directory is the path with its last element cut off as text, never cleaned. A .. after a symlinked directory therefore goes through the symlink, as it does when the file is opened. With SFDUPES_DATABASE=/srv/link/../new/db.sqlite and /srv/link a symlink to /data/x, it now creates /data/new and the database in it. It used to create /srv/new and then fail (2026-10-17, #119)

  • scan never records its own database. Once the database and its .lock file are open, it notes the device and inode of the database file, its -wal and -shm files and the .lock file. The walk passes over a file with one of them wherever it reaches it, through a symlinked directory included, printing no line and counting nothing. A record an earlier scan left for one of them is deleted (2026-10-17, #115)

  • 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 matches them by device ID. As the scan starts, it lists the mounted filesystems of those types, from /proc/self/mountinfo on Linux and from the mount list on macOS. No flag turns that off. Below an operand, the walk does not enter the default excludes either, so sfdupes scan / skips the kernel's view of the running system and the temporary directories. On Linux those are /proc, /sys, /dev, /run, /tmp and /var/tmp. On macOS they are /dev, /private/tmp, /private/var/tmp, /private/var/vm, /System/Volumes/VM and /System/Volumes/Data. A Windows list exists in the code but is not used. An operand at or inside a default exclude is still walked. The scan prints a line for each path it leaves out, without counting it as skipped, and the records there count as outside the scanned operands, as for -x. --no-default-excludes turns off the default excludes only, and scan --help lists them (2026-10-17, #124)

  • 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 while nothing completes, and a burst after a wait no longer reads as a steady rate of over 1,000 files a second. The time remaining still comes from the phase's average. Once no item has completed for 10 seconds, the word stalled and where the phase waits take the rate's place, as stalled disk/db. The places are disk, db, out for stdout and cpu, or two of them for a phase that may wait on either. Each phase gives its word where it is defined. A line too wide for the terminal loses its bar first, then its rate or those 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, #125)

  • New subcommand info prints the database's details on stdout, one per line as a name, a tab and the value:

    • the path and where it came from;
    • the sizes of the database file and of its -wal file, when there is one;
    • when the database file was last changed;
    • the schema version;
    • 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, [1/2] open and [2/2] count. The schema version and the counts come from one read-only transaction, the counts from one query (2026-10-16, #117)

  • 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 runs from the moment report starts until the summary line. open says it is opening the database, also while it waits on a busy one. read counts the records, then reads them against that count in signature order. SQLite takes that order from the files_signature index without sorting, so the first record comes at once. report forms the duplicate groups itself, holding only those of the size it is reading, and writes them ordered by first path before it reads the next size. stdout and the summary line are unchanged, and SQLite no longer needs a temporary file to sort in (2026-10-16, #114)

  • trees shows its work on stderr in four phases, [1/4] open, [2/4] read, [3/4] group and [4/4] write, each with the progress display scan uses. The display runs from the moment trees starts until the summary line. open says it is opening the database, also while it waits on a busy one. read counts the records, then reads them against that count. group counts the directories it groups, and write the groups it writes. A display's rate names what its phase counts: files, records, directories or groups. stdout and the summary line are unchanged. When stdout and stderr are the same file, as one terminal is, no line of stdout shares a line with the display. When they are not, the display keeps being drawn while stdout waits on a slow reader (2026-10-16, #113)

  • Each scan phase's progress display names the phase and its place among the five, as [2/5] walk. It runs from the moment the phase starts and shows its progress, elapsed time and ETA. load counts the records first. The walk's total is estimated from the records under the operands. The content phase counts the records it checks from SQLite's grouping alone, then says it is searching the database until the first one is returned. On a terminal a phase with an exact total draws its line followed by the library's bar. Any other phase draws a spinner followed by the line. Every display is drawn again several times a second. No line reaches the terminal's last column, however narrow, also after the terminal is made narrower during a phase. A line too wide leaves out its rate, then its bar, keeping the phase, progress, elapsed time and ETA whole. It is cut short only when those alone do not fit, as below about 70 columns late in a large scan. In plain mode the line is printed every 5 seconds whether or not an item completed (2026-10-16, #112)

  • 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 from SFDUPES_DATABASE. scan says when the database is new. README "Database" says when scan writes each file (2026-10-16, #111)

  • The canonical files and model scripts re-vendored from sneak/prompts next at e3f79c4:

    • golangci-lint v2.14.0;
    • lint and test are phases of the Dockerfile that write no image, and make test runs the suite under the race detector. Dockerfile.lint, script/verify-lint-image-pin and make test-race are gone;
    • every docker build in script/ passes a new CACHEBUST value, which 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 tests run with -count=1;
    • prettier runs on the host, from the node and yarn that script/bootstrap installs, so the prettier and markdown stages are 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 stops after 20 minutes;
    • .claude/settings.json is deleted.

    Since #109 two steps come before ARG CACHEBUST: the module download in lint, test and the build stage, and the install of git and make in the build stage. Docker keeps them between builds, and every step that lints, tests or compiles still runs on every build (2026-10-15, #95)

  • 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 the inner operand up to, but not including, the outer one is on a different filesystem than the outer one. The inner operand is then walked on its own. For a regular-file operand the directories checked start at its directory. The records of files gone from below it are deleted (2026-10-14, #106)

  • With -x, scan keeps the records below a directory on another filesystem under its operand, and counts their sizes in the size census, instead of deleting them (2026-10-14, #99)

  • The hash and content phases open each file without blocking and check that it is still a regular file. A file replaced by a FIFO or device node after it was statted is warned about and skipped instead of hanging the scan (2026-10-14, #100)

  • The scan lock file is named from the database's path with symlinks resolved, after scan creates the database file if it is missing. A scan through a symlink to the database therefore fails while another scan holds the lock (2026-10-14, #101)

  • On a terminal, a warning for the last file of a phase no longer erases the phase's finished bar. The bar is drawn again below the warning (2026-10-14, #102)

  • scan records the mtime to the nanosecond, as whole seconds in mtime plus mtime_nsec, and compares it at that resolution. A same-size rewrite within the same second is hashed again (2026-10-07, #12)

  • The narration was cut from TODO.md Completed Steps and from the comments in script/, Dockerfile and Dockerfile.lint (gone since #95). The Workflow section now branches from and merges to next (2026-10-04, #49)

  • make test-race ran the test suite under the race detector in a cgo-enabled container, outside make check. Since #95 make test itself runs the suite under the race detector, in the Dockerfile's Debian-based test phase, and make test-race is gone (2026-10-04, #18)

  • A bare docker build . failed, naming script/cibuild and script/docker, rather than serve the gates from cache. Since #95 a bare build succeeds, as REPO_POLICIES.md requires, and may serve the gates from cache. The builds in script/ pass a new CACHEBUST value, so theirs always run (2026-10-04, #39)

  • make fmt and make fmt-check run prettier over all Markdown, and CI checks it. All Markdown was reformatted (2026-10-04, #19)

  • script/lint writes no image, so a run no longer leaves an untagged one behind (2026-10-04, #48)

  • Tests cover a missing database, scan keeping stdout empty, its skip warning, the report and trees summary lines, and every subcommand going through runE (2026-10-04, #16)

  • .golangci.yml replaced with the current canonical copy, which uses gomodguard_v2, so lint no longer prints a deprecation warning (2026-10-04, #26)

  • A test fails when either hashWorker cancellation check in scan.go is removed (2026-10-04, #83)

  • A database path holding ?, # or % opens exactly the file it names (2026-10-04, #55)

  • scan rejects --workers below 1 as a usage error instead of running single-threaded (2026-10-04, #10)

  • A test fails when either walk cancellation check in scan.go is removed (2026-10-04, #81)

  • A test checks that scan refuses a database with another schema version (2026-10-04, #64)

  • Four inaccurate comments in cancel_test.go corrected, and walkCancelInFlightDirs renamed to walkCancelInFlightFiles (2026-10-04, #33)

  • The -x filesystem-boundary rules in subdirJob are tested (2026-10-04, #17)

  • scan creates the schema in one transaction. A version-0 database with a files table is refused with a clear schema-version error (2026-10-04, #11)

  • README documents install, Docker, a daily cron scan and how to read and check the reports (2026-10-04, #54)

  • The Dockerfile build stage kept the Go module cache out of builder's home and copied the sources with --chown, so no chown -R walked them. Since #95 there is no builder user and nothing changes owner: the build stage only compiles, as root, and the tests run as nobody in the test phase (2026-10-04, #43)

  • --version prints sfdupes VERSION to stdout. README documents it and --help (2026-10-04, #15)

  • scan stops cleanly on SIGINT or SIGTERM: it commits what it has hashed, deletes nothing more, and exits 1 (2026-10-04, #5)

  • report and trees stream the records instead of holding them all in memory. The schema gains the files_signature index (2026-10-04, #14)

  • Progress prints at once on a non-terminal, uses a real terminal test, and prints warnings through a spinner instead of racing its redraw (2026-10-03, #13)

  • Symlink, socket, FIFO, device and .zfs operands are warned about and skipped, and the records beneath them are kept (2026-10-03, #9)

  • scan holds a lock on a lock file beside the database for its whole run, so a second scan fails at once with exit 1 (2026-10-03, #53)

  • Tests cover stdout write failures in report and trees. README states that | head ends sfdupes by SIGPIPE and that >&- writes to /dev/null (2026-10-03, #30)

  • report and trees open the database read-only, and scan leaves it out of WAL mode, so reading needs only read access (2026-10-03, #8)

  • Tabs, newlines, carriage returns and backslashes are escaped in report, trees and warning paths. The root directory's path is / (2026-10-03, #7)

  • A plain docker build . stamps the git tag or short commit instead of dev. .dockerignore sends .git without .git/config. The build stage stamps the VERSION build argument, else git describe --tags --always. It fails if the context carries .git and the version is still empty, dev or unknown. CI checks out the full history (fetch-depth: 0), so it stamps the same value as make build (2026-10-02, branch next, #67)

  • The 1 KiB end-window sampling was replaced with the hashes chosen by file size, and README "Duplicate detection" documented every size threshold. A file under 10 MiB was hashed in full, and its head, tail and content all held that hash. A larger file got only its 64 KiB head and tail in the hash phase. The content phase, after the update phase, read it for content only when its size, head and tail matched another record's, from this scan or an earlier one. content was the whole file below 50 MiB, and gigabyte-spaced 1 MiB samples at or above. A file gone or changed since its record was written was never read. report and trees left out any record without a content hash. The content column is part of the version 1 schema. Superseded by #132 (2026-09-22, branch next, #61)

  • The dead files.dat references removed from Makefile, .gitignore and .dockerignore (2026-09-21, branch next, #22)

  • The lint-image pin comments and FROM form in Dockerfile and Dockerfile.lint fixed. Both pins became the policy # image:vX.Y.Z, YYYY-MM-DD comment over a bare FROM image@sha256:..., without the false (Debian-based) note or the tag. Since #95 the Dockerfile's lint phase holds the only golangci-lint pin, in that form. Dockerfile.lint and script/verify-lint-image-pin, which compared the two pins, are gone (2026-08-10, branch next, #25)

  • All linting ran in Docker, via Dockerfile.lint and script/lint. Per the owner ruling the linter is never installed on a host, and golangci-lint config verify runs before golangci-lint run. script/bootstrap stopped installing or pinning the linter, and script/verify-linter-pin was retired. Since #95 both commands run in the Dockerfile's lint phase, which script/lint builds alone and the build stage depends on through COPY --from=lint /src/go.sum /dev/null. 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 (2026-08-10, branch next, #46)

  • The Docker build stage's prerequisites were installed by running script/bootstrap instead of apk add --no-cache make inline. script/verify-linter-pin failed the build unless the linter copied from the lint stage was the version script/bootstrap pinned. Builds then took up to 5m14s cold, mostly in a chown -R of the module cache, filed as #43. Since #95 the build stage installs git and make with apk add --no-cache and only compiles; the lint and test phases are the gates (2026-08-09, branch dockerfile-bootstrap, #42)

  • The Docker layer cache is busted for the gate steps, so script/cibuild and script/docker cannot report a green they did not earn. The Dockerfile copies the tree before its gates, so on an unchanged tree Docker served them from cache and the build exited 0 having run nothing. The fix was a CHECK_EPOCH build argument. Since #95 every docker build in script/ passes a new CACHEBUST value instead, which every stage of the Dockerfile declares before every step that lints, tests or compiles. Run as root, the tests fail TestScanHardlinkRunFailsTogether, because root reads through the chmod(0) the test relies on, so the test phase runs them as nobody (2026-08-09, branch cibuild-cache-bust, #32)

  • script/bootstrap checked the installed golangci-lint version instead of only its presence. The version lived only in GOLANGCI_LINT_VERSION, with the go install module ref derived from it. Any installed version that was not the pin was reinstalled: older, newer, absent or unparseable. go install writes into GOBIN (or GOPATH/bin) while make lint ran the first golangci-lint on PATH, so bootstrap re-read the effective version after installing. On a mismatch it printed both paths and both versions and exited non-zero; it did not reorder PATH or delete anyone's binary. The --version call kept its stderr and was bounded by timeout(1) where that exists. git, make and go kept presence-only checks. make check and make lint were clean at v2.12.2, so v2.10.1 had not been hiding findings on main. Superseded by #46: the linter is no longer installed on a host (2026-08-09, branch bootstrap-version-check, #24)

  • The hash worker pool unwinds on the error path. The pool is an owned, context-aware hashPool. Every blocking send in the feeder and the workers selects on ctx.Done(), and jobs is closed on every path out. hashPhase defers pool.stop(), which cancels and then drains results until the last goroutine has exited; draining is what frees a worker parked on a send. ctx is threaded from cmd.Context() through runScan, syncScan, both worker 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 cancelled walk yields a partial size census, and every file it never reached looks vanished to the update phase. That phase's own BeginTx also fails on the cancelled context before deleting anything, but the guard is the barrier that still holds once an interrupted scan may commit what it has. Tests drive run(scan) against a database whose insert trigger aborts, and assert that the scan fails instead of hanging and that the goroutine count returns to its pre-scan baseline. Other tests cancel a scan part-way through the walk and 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, #6)

  • 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, runs when a subcommand fails. runScan, runReport, runTrees, 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 argument and flag errors to exit 2, so a runtime failure is never reported as a usage error or printed with the usage text. main_test.go drives the CLI in-process and asserts the exit codes from README "Error handling" and the stdout/stderr split. That includes a fatal error after the database is open leaving no -wal or -shm file behind, for scan, report or trees (2026-08-09, branch db-close-on-fatal, #4)

  • golangci-lint updated to v2.12.2 with the canonical config, merged as 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 replaced with the canonical file. The linter settings (lll, funlen, cyclop, dupl thresholds) now live under linters.settings, per the v2 schema, so they are applied. No new lint findings surfaced (2026-08-09, branch golangci-v2.12.2, #3)

  • Makefile targets converted to scripts-to-rule-them-all script/ entrypoints, like the other managed repos. All 12 entrypoints exist:

    • bootstrap, setup and projectname;
    • 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, #1)

  • 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 (2026-07-24, branch make-default-target)

  • Scan-wide phases, concurrent operands and batched updates. All operands seed the shared walk pool, and every pass runs once over the whole scan, so totals and ETAs are scan-global. The per-operand walk, hash and update cycles and their stderr announcements are gone. The update pass commits in batched transactions. The filesystem is authoritative and the database an eventually-consistent reflection of it, so scan-level atomicity is not required (2026-07-24, branch scan-wide-phases)

  • The stat pass split back out of the walk. Phases are strictly sequential again, per operand, with parallelism only inside each phase:

    • walk;
    • stat;
    • 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, hash and update cycles, a multi-operand run such as scan /srv/* showed pass totals that looked like the whole run's (2026-07-24, branch scan-operand-progress)

  • Parallel walk. The walk pass was a single goroutine and took hours at about 20 million files on a busy pool: 22 million files in 4 hours on a ZFS server. It is now a per-directory worker-pool traversal that records size and mtime during the walk, folding away the separate stat pass and halving metadata I/O. Each PATH operand commits in its own transaction, so an interrupted scan keeps completed operands (2026-07-24, branch parallel-walk)

  • Persistent scan database. scan maintains a SQLite database (modernc.org/sqlite, pure Go, cgo stays disabled), keyed by absolute path, that survives between runs. A rescan hashes only new or changed files, by mtime and size. It deletes the records of files vanished from under the scanned operands and leaves records outside them untouched, so scan can be cronned daily. report and trees read the database, with no positional arguments, instead of a scan stream. The database is at /var/lib/sfdupes/db.sqlite, overridable via SFDUPES_DATABASE. WAL journaling plus a single-transaction update keep a report run during a scan safe (2026-07-24, branch persistent-database)

  • The origin remote (git@git.eeqj.de:sneak/sfdupes.git) added, v0.0.1 tagged, and main plus tags pushed (2026-07-23)

  • scan CLI rework. Required PATH... operands via cobra replace the /srv -root default. The new -x/--one-file-system flag (GNU convention) stops at filesystem boundaries, which are crossed by default (2026-07-23, branch scan-required-paths)

  • The repo was brought into compliance with REPO_POLICIES.md (the version of 2026-07-06), the existing repo checklist and the Go styleguide. The audit of 2026-07-22 found these gaps, and the branch closed them:

    • .gitignore, with OS, editor, Go and secrets patterns; the compiled sfdupes binary and files.dat had been sitting untracked in the tree;
    • .editorconfig;
    • LICENSE and the README License section. MIT was assumed from house convention, for the owner to confirm;
    • REPO_POLICIES.md in the repo root;
    • the canonical .golangci.yml, with the 150 findings it raised fixed;
    • the Makefile targets test, lint, fmt, fmt-check, docker and hooks, and a check target that no longer depends on build;
    • a test suite, which the policy required with a 30-second timeout and the conditional -v rerun. It covers parsing, grouping, digests, suppression, 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 checkout action pinned by commit SHA;
    • 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 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 main (2026-07-22)

  • scan, report and trees subcommands implemented (pre-git history)

Future Steps

  • Possible later features, out of scope per README: full-content verification of candidates, and helper scripts for removal.

Accepted Divergences

  • Flat single-package layout, with the .go files in the repo root. That is fine for a small single-binary tool per the Go styleguide, and the tracker audit agrees.