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
36 KiB
Workflow
- Take an issue from the
1.0.0milestone 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
nextintomain.
Status
- Pre-1.0.
- The tracker is authoritative for the pre-1.0 backlog. The open issues under
the
1.0.0milestone are what remains before the tag. This file records history and process, not the queue.
Next Step
- Take the next issue from the
1.0.0milestone, https://git.eeqj.de/sneak/sfdupes/milestone/17. The issues are not restated here, because a copy drifts as soon as the tracker moves.
Completed Steps
Newest first. An entry ends with its date and its issue or branch, where those are known.
-
TODO.mdrewritten from scratch by the same rules asREADME.md, sentence by sentence. 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.mdrewritten 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 every setting and every subcommand. Development and testing come last. Every statement was checked against the code onnext. Five comments inmain_test.gocite "Settings", where their rules now live (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. The bar 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, noteta(2026-10-26, #133) -
The
head,tailandcontentcolumns are BLOBs, each holding the raw 32 bytes of a SHA-256. That is half the size of the hex text they held. The hashes stay raw from the hash phase through the database toreportandtrees. 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.
headandtailare the SHA-256 of its first and last 64 KiB.contentis 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, which counted the sizes of every walked file and of every record outside the operands. A file was hashed only when the census showed another file of its size;
- the records without hashes;
- the content phase.
A scan has four phases, and a rescan reads only new and changed files.
infono longer counts the records withheadandtailhashes, nor those with a content hash, since every record has all three (2026-10-25, #132) -
REPO_POLICIES.mdand the four scripts that passCACHEBUSTcopied again fromsneak/promptsnextat0653579. Two steps now come beforeARG CACHEBUST:- the module download in the
lint,testandbuilderstages of theDockerfile; - the package install in
builder.
Docker keeps them until one of these changes:
- the base image;
- the install line;
go.modorgo.sum.
Every step that checks or compiles the code still comes after
ARG CACHEBUST.golangci-lint runalso keeps the linter's own cache. README says the same (2026-10-18, #109) - the module download in the
-
The release rule was set. Until 1.0, each tip of
mainthat passes the build gets the nextv0.0.Ntag.go install sneak.berlin/go/sfdupes@latestthen installs it.go.modretractsv0.0.1, the early build without a database. When the linker set no version,sfdupes --versionprints the version Go recorded in the binary. That is the tag, or the pseudo-version of ago installbuild. It printsdevonly when neither is known. README "Install" documented:- the rule;
- what
@mainandv0.0.1give; - how to check an installed binary.
(2026-10-17, #116)
-
scancreates the database's directory where the database path leads. The directory is the path with its last element cut off as text, so a..in it stays. A..after a symlinked directory therefore goes through the symlink, as it does when the file is opened. WithSFDUPES_DATABASE=/srv/link/../new/db.sqliteand/srv/linka symlink to/data/x, it now creates/data/newand the database in it. It used to create/srv/newand then fail (2026-10-17, #119) -
scannever records its own database. Once the database and its.lockfile are open, it notes the device and inode of each of these:- the database file;
- its
-waland-shmfiles; - the
.lockfile.
The walk passes over any file with the device and inode of one of them, however it reaches the file. It prints no line for such a file and counts nothing. A record an earlier scan left for one of them is deleted (2026-10-17, #115)
-
scannever records or reads anything on a filesystem that is not persistent, such astmpfsorproc. Such a filesystem is one the kernel makes up or keeps in memory.scanmatches them by device ID, however the walk or the content phase reaches a path. As the scan starts, it lists the mounted filesystems of those types, from/proc/self/mountinfoon 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. Sosfdupes scan /skips the kernel's view of the running system and the temporary directories. On Linux those are:/proc, /sys, /dev, /run, /tmp, /var/tmpOn macOS they are:
/dev, /private/tmp, /private/var/tmp, /private/var/vm /System/Volumes/VM, /System/Volumes/DataA 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. The records there count as outside the scanned operands, as for
-x.--no-default-excludesturns off the default excludes only, andscan --helplists them (2026-10-17, #124) -
The rate a progress display shows covers the items completed over the last 5 seconds. When stderr is not a terminal, it covers the items since the line before. It falls to 0 while nothing completes. 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
stalledand where the phase waits take the rate's place, asstalled disk/db. The places are:disk;db;out, for stdout;cpu.
A phase that may wait in two places names both. Each phase's word is set in the code that defines the phase. A line too wide for the terminal loses its bar first, then its rate or those words. Four parts stay whole:
- the phase;
- the progress;
- the elapsed time;
- the ETA, the time remaining.
On an 80-column terminal, at counts like those of README's example, the content phase's displays for its checks and its reads lost those words. That happened for most elapsed times and ETAs in minutes. Every other phase kept them whenever the elapsed time and the ETA were both in minutes. The content phase writes its last partial batch, the records not yet written, under a display of its own (2026-10-16, #125)
-
New subcommand
infoprints 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
-walfile, when there is one; - when the database file was last changed;
- the schema version;
- the number of records;
- how many records have
headandtailhashes; - how many records have a content hash;
- the sizes of all records added up.
It opens the database as
reportdoes and shows two phases on stderr,[1/2] openand[2/2] count. The schema version and the counts come from one read-only transaction, a read of the database at one moment. The counts come from one query (2026-10-16, #117) -
reportshows its work on stderr in two phases,[1/2] openand[2/2] read, with the progress displayscanandtreesuse. The display runs from the momentreportstarts until the summary line.opensays it is opening the database, also while it waits on a busy one.readcounts the records, then reads them against that count in signature order. SQLite takes that order from thefiles_signatureindex without sorting, so the first record comes at once.reportforms the duplicate groups itself and holds only those of the size it is reading. It 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) -
treesshows its work on stderr in four phases, from[1/4] opento[4/4] write, each with the progress displayscanuses. The display runs from the momenttreesstarts until the summary line.opensays it is opening the database, also while it waits on a busy one.readcounts the records, then reads them against that count.groupcounts the directories it groups, andwritethe groups it writes. A display's rate names what its phase counts, such as records 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
scanphase'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:- the progress;
- the elapsed time;
- the ETA, the time remaining.
loadcounts the records first. The walk's total is estimated from the records under the operands. The content phase takes its total from SQLite's grouping alone. Until the first record is returned, it says it is searching the database. On a terminal a phase with an exact total draws its line followed by the bar of the progress-bar library. 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 the terminal is, even after it is made narrower during a phase. A line too wide leaves out its rate, then its bar. The rest stays whole. The line is cut short only when the rest alone does not fit, as below about 70 columns late in a large scan. When stderr is not a terminal, the line is printed every 5 seconds whether or not an item completed (2026-10-16, #112) -
scan,reportandtreesfirst print a line on stderr naming the database they use, as an absolute path. The line says whether that path is the default or comes fromSFDUPES_DATABASE.scansays when the database is new. README "Database" says whenscanwrites each file (2026-10-16, #111) -
The canonical files and model scripts copied again from
sneak/promptsnextate3f79c4:golangci-lintv2.14.0;- lint and test are phases of the
Dockerfilethat write no image, andmake testruns the suite under the race detector. Gone areDockerfile.lint,script/verify-lint-image-pinandmake test-race; - every
docker buildinscript/passes a newCACHEBUSTvalue. Every stage of theDockerfiledeclares it first, so everyRUNstep runs on every build. The steps that compile keep Go's build cache, and the tests run with-count=1; prettierruns on the host, from thenodeandyarnthatscript/bootstrapinstalls. Theprettierandmarkdownstages are gone, and the build stage installsgitandmakeitself;- a new push cancels the workflow's older run on the same branch, and a run stops after 20 minutes;
- a coding agent's settings file is deleted.
Since #109 two steps come before
ARG CACHEBUST:- the module download in
lint,testand the build stage; - the install of
gitandmakein the build stage.
Docker keeps them between builds. Every step that checks or compiles the code 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 between the two operands, the inner one included, 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,scankeeps the records below a directory on another filesystem under its operand, instead of deleting them. It counts their sizes in the size census (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
scancreates 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)
-
scanrecords the mtime to the nanosecond, as whole seconds inmtimeplusmtime_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.mdCompleted Steps and from the comments inscript/,DockerfileandDockerfile.lint(gone since #95). The Workflow section now branches from and merges tonext(2026-10-04, #49) -
make test-raceran the test suite under the race detector in a cgo-enabled container, outsidemake check. Since #95make testitself runs the suite under the race detector, in theDockerfile's Debian-basedtestphase.make test-raceis gone (2026-10-04, #18) -
A bare
docker build .failed, namingscript/cibuildandscript/docker, rather than serve the gates, the lint and test steps, from cache. Since #95 a bare build succeeds, asREPO_POLICIES.mdrequires, and may serve the gates from cache. The builds inscript/pass a newCACHEBUSTvalue, so theirs always run (2026-10-04, #39) -
make fmtandmake fmt-checkrunprettierover all Markdown, and the Gitea workflow checks it. All Markdown was reformatted (2026-10-04, #19) -
script/lintwrites no image, so a run no longer leaves an untagged one behind (2026-10-04, #48) -
Tests cover:
- a missing database;
scankeeping stdout empty;- its skip warning;
- the
reportandtreessummary lines; - every subcommand going through
runE.
(2026-10-04, #16)
-
.golangci.ymlreplaced with the current canonical copy, which usesgomodguard_v2, so lint no longer prints a deprecation warning (2026-10-04, #26) -
A test fails when either
hashWorkercancellation check inscan.gois removed (2026-10-04, #83) -
A database path holding
?,#or%opens exactly the file it names (2026-10-04, #55) -
scanrejects--workersbelow 1 as a usage error instead of running single-threaded (2026-10-04, #10) -
A test fails when either walk cancellation check in
scan.gois removed (2026-10-04, #81) -
A test checks that
scanrefuses a database with another schema version (2026-10-04, #64) -
Four inaccurate comments in
cancel_test.gocorrected, andwalkCancelInFlightDirsrenamed towalkCancelInFlightFiles(2026-10-04, #33) -
The
-xfilesystem-boundary rules insubdirJobare tested (2026-10-04, #17) -
scancreates the schema in one transaction. A version-0 database with afilestable is refused with a clear schema-version error (2026-10-04, #11) -
README documents install, Docker, a daily
cronscan and how to read and check the reports (2026-10-04, #54) -
The
Dockerfilebuild stage kept the Go module cache out ofbuilder's home and copied the sources with--chown, so nochown -Rwalked them. Since #95 there is nobuilderuser and nothing changes owner. The build stage only compiles, as root, and the tests run asnobodyin thetestphase (2026-10-04, #43) -
--versionprintssfdupes VERSIONto stdout. README documents it and--help(2026-10-04, #15) -
scanstops cleanly onSIGINTorSIGTERM: it commits what it has hashed, deletes nothing more, and exits 1 (2026-10-04, #5) -
reportandtreesstream the records instead of holding them all in memory. The schema gains thefiles_signatureindex (2026-10-04, #14) -
Progress prints at once on a non-terminal and uses a real terminal test. Warnings print through a spinner instead of racing its redraw (2026-10-03, #13)
-
These operands are warned about and skipped, and the records beneath them are kept:
- symlinks;
- sockets;
- FIFOs;
- device nodes;
.zfsdirectories.
(2026-10-03, #9)
-
scanholds a lock on a lock file beside the database for its whole run, so a secondscanfails at once with exit 1 (2026-10-03, #53) -
Tests cover stdout write failures in
reportandtrees. README states that| headendssfdupesbySIGPIPEand that>&-writes to/dev/null(2026-10-03, #30) -
reportandtreesopen the database read-only, andscanleaves it out of WAL mode, so reading needs only read access (2026-10-03, #8) -
Four characters are escaped in
report,treesand warning paths:- tab;
- newline;
- carriage return;
- backslash.
The root directory's path is
/(2026-10-03, #7) -
A plain
docker build .stamps thegittag or short commit instead ofdev..dockerignoresends.gitwithout.git/config. The build stage stamps theVERSIONbuild argument, elsegit describe --tags --always. It fails if the context carries.gitand the version is still empty,devorunknown. The Gitea workflow checks out the full history (fetch-depth: 0), so it stamps the same value asmake build(2026-10-02, branchnext, #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,tailandcontentall held that hash. A larger file got only its 64 KiBheadandtailin the hash phase. The content phase ran after the update phase. It read a file forcontentonly when its size,headandtailmatched another record's, from this scan or an earlier one.contentwas 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.reportandtreesleft out any record without acontenthash. Thecontentcolumn is part of the version 1 schema. Superseded by #132 (2026-09-22, branchnext, #61) -
The dead
files.datreferences removed fromMakefile,.gitignoreand.dockerignore(2026-09-21, branchnext, #22) -
The lint-image pin comments and
FROMform inDockerfileandDockerfile.lintfixed. Both pins became the policy# image:vX.Y.Z, YYYY-MM-DDcomment over a bareFROM image@sha256:..., without the false(Debian-based)note or the tag. Since #95 theDockerfile'slintphase holds the onlygolangci-lintpin, in that form.Dockerfile.lintis gone, and so isscript/verify-lint-image-pin, which compared the two pins (2026-08-10, branchnext, #25) -
All linting ran in Docker, via
Dockerfile.lintandscript/lint. Per the owner ruling the linter is never installed on a host, andgolangci-lint config verifyruns beforegolangci-lint run.script/bootstrapstopped installing or pinning the linter, andscript/verify-linter-pinwas retired. Since #95 both commands run in theDockerfile'slintphase.script/lintbuilds that phase alone, and the build stage depends on it throughCOPY --from=lint /src/go.sum /dev/null.Dockerfile.lintandscript/verify-lint-image-pinare gone. Nothing inside an image build may rundocker, so the phase callsgolangci-lintdirectly (2026-08-10, branchnext, #46) -
The Docker build stage's prerequisites were installed by running
script/bootstrapinstead ofapk add --no-cache makeinline.script/verify-linter-pinfailed the build unless the linter copied from the lint stage was the versionscript/bootstrappinned. Builds then took up to 5m14s from an empty cache, mostly in achown -Rof the module cache, filed as #43. Since #95 the build stage installsgitandmakewithapk add --no-cacheand only compiles. Thelintandtestphases are the gates (2026-08-09, branchdockerfile-bootstrap, #42) -
The Docker layer cache is defeated for the gate steps, so
script/cibuildandscript/dockercannot report a pass they did not earn. TheDockerfilecopies the tree before its gates. On an unchanged tree Docker served them from cache, and the build exited 0 without running them. The fix was aCHECK_EPOCHbuild argument. Since #95 everydocker buildinscript/passes a newCACHEBUSTvalue instead. Every stage of theDockerfiledeclares it before every step that checks or compiles the code. Run as root, the tests failTestScanHardlinkRunFailsTogether, because root reads through thechmod(0)the test relies on. So thetestphase runs them asnobody(2026-08-09, branchcibuild-cache-bust, #32) -
script/bootstrapchecked the installedgolangci-lintversion instead of only its presence. The version lived only inGOLANGCI_LINT_VERSION, with thego installmodule reference derived from it. Any installed version that was not the pin was reinstalled, whether:- older;
- newer;
- absent;
- unparseable.
go installwrites intoGOBIN, orGOPATH/bin, whilemake lintran the firstgolangci-lintonPATH. 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 reorderPATHor delete anyone's binary. The--versioncall kept its stderr and was bounded bytimeout(1)where that exists.git,makeandgokept presence-only checks. The version check was verified by bootstrapping this host from v2.10.1 to v2.12.2, and again to a no-op. Stub runs of the script underdashcovered:- a thirteen-input version-parse matrix;
- a shadowed install, which must exit non-zero;
- an install destination not on
PATH; GOBINset;- a wedged binary, which must hit the timeout.
make checkandmake lintwere clean at v2.12.2, so v2.10.1 had not been hiding findings onmain. Superseded by #46, since the linter is no longer installed on a host (2026-08-09, branchbootstrap-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 onctx.Done(), andjobsis closed on every path out.hashPhasedeferspool.stop(), which cancels and then drainsresultsuntil the last goroutine has exited. Draining is what frees a worker parked on a send.ctxis passed fromcmd.Context()as the first parameter, through:runScan;syncScan;- both worker pools;
- the whole database layer.
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 ownBeginTxalso fails on the cancelled context before deleting anything. The guard is still the barrier once an interrupted scan may commit what it has. Tests driverun(scan)against a database whose insert trigger aborts. They 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, deterministically, by counting the scan's own checks ofctx.Done(). They assert that it stops at the guard with every record intact. It then holds a partial size census and a still-populated record index. Direct tests cover the remaining cancellation branches of both pools. They test:sendEvent;- the walk workers;
dispatchDirs;feedHashJobs;hashWorker;hashPhase.
(2026-08-09, branch
hash-pool-cleanup, #6) -
The database is closed on every fatal exit path.
fatalfand itsos.Exit(1)are gone. So the deferreddb.Close()runs when a subcommand fails, and with it the SQLite WAL checkpoint. These return errors instead:runScan;runReport;runTrees;loadRecords;resolveRoots.
The single exit point is
runinmain.go. It maps afatalError, anything a subcommand returned, 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, nor printed with the usage text.main_test.godrives the CLI in-process and asserts the exit codes from README "Error handling" and the stdout/stderr split. It also checks that a fatal error after the database is open leaves no-walor-shmfile behind, forscan,reportortrees(2026-08-09, branchdb-close-on-fatal, #4) -
golangci-lintupdated to v2.12.2 with the canonical config, merged as38a01bd. The pinned linter in theDockerfilelint stage andscript/bootstrapwent from v2.12.1 to v2.12.2, and.golangci.ymlwas replaced with the canonical file. Four thresholds now live underlinters.settings, as the v2 schema requires, so they are applied:lll;funlen;cyclop;dupl.
No new lint findings surfaced (2026-08-09, branch
golangci-v2.12.2, #3) -
Makefiletargets converted to scripts-to-rule-them-allscript/entrypoints, like the other managed repos. All 12 entrypoints exist:bootstrap,setupandprojectname;test,lint,fmt,fmt-checkandcheck;docker,cibuild,precommitandinstall-precommit.
Every
Makefiletarget does nothing but call its script (2026-07-26, commit3abeacf, #1) -
The binary is the default
maketarget. Plainmakebuildssfdupes, where it used to runcheckplusbuild.make buildremains as an alias (2026-07-24, branchmake-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 the totals and ETAs cover the whole scan. The per-operand walk, hash and update cycles and their stderr announcements are gone. The update pass writes in batches, each in its own transaction. The filesystem is authoritative and the database follows it, so one scan's changes need not land all at once (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. That restores 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, branchscan-operand-progress) -
Parallel walk. The walk pass was a single goroutine and took hours at about 20 million files on a busy pool. One ZFS server took 4 hours for 22 million files. It is now a walk by a pool of per-directory workers that records size and mtime as it goes. That folds away the separate stat pass and halves the metadata reads. Each
PATHoperand commits in its own transaction, so an interrupted scan keeps completed operands (2026-07-24, branchparallel-walk) -
Persistent scan database.
scanmaintains an SQLite database that survives between runs, with the absolute path as each record's key. The driver ismodernc.org/sqlite, pure Go, so cgo stays disabled. 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. Soscancan run daily fromcron.reportandtreesread the database, with no positional arguments, instead of a scan stream. The database is at/var/lib/sfdupes/db.sqlite, andSFDUPES_DATABASEoverrides that. WAL journaling plus a single-transaction update keep a report run during a scan safe (2026-07-24, branchpersistent-database) -
The
originremote,git@git.eeqj.de:sneak/sfdupes.git, added.v0.0.1tagged, andmainand the tags pushed (2026-07-23) -
scancommand-line rework. RequiredPATH...operands, parsed by cobra, replace the/srv-rootdefault. The new-x,--one-file-systemflag follows the GNU convention and stops at filesystem boundaries, which the walk crosses by default (2026-07-23, branchscan-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 the standard patterns for secrets and for OS, editor and Go files. The compiledsfdupesbinary andfiles.dathad been sitting untracked in the tree; -
.editorconfig; -
LICENSEand the README License section. MIT was assumed from house convention, for the owner to confirm; -
REPO_POLICIES.mdin the repo root; -
the canonical
.golangci.yml, with the 150 findings it raised fixed, so thatmake lintis clean; -
the
Makefiletargets the policy requires:testandlint;fmtandfmt-check;dockerandhooks.
checkdepended onbuild, which wrote the binary.make checkmust not modify files, socheckno longer depends onbuild; -
a test suite, which the policy required with a 30-second timeout and the conditional
-vrerun. At 64% coverage, it covers:- parsing and grouping;
- digests and suppression;
- hashing and the scan pipeline;
-
a multistage
Dockerfileand.dockerignore. TheDockerfilehad:- hash-pinned images;
- a lint stage that failed fast;
- a build stage that ran
make check;
-
.gitea/workflows/check.yml, runningdocker build .on push with the checkout action pinned by commit SHA; -
the README sections the policy requires. The first is a Description whose first line names the project, its purpose and category, its license and its author. Then come Getting Started and Rationale, and at the end TODO, License and Author;
-
the stale README non-goal "no git repository setup and no CI" removed;
-
the pre-commit hook, installed with
make hooksonce that target existed.
The code was already
gofmt-clean, so no standalone formatting commit was needed (2026-07-23, branchrepo-policy-compliance) -
-
git initwith a README-only first commit; the code baseline committed onmain(2026-07-22) -
scan,reportandtreessubcommands implemented (pre-git history)
Future Steps
- Possible later features, out of scope per README, are a byte-for-byte check of candidates and helper scripts for removal.
Accepted Divergences
- Flat single-package layout, with the
.gofiles in the repo root. That is fine for a small single-binary tool per the Go styleguide, and the tracker audit agrees.