script/test omitted -count=1, so Go's test result cache could satisfy the gate outright. A cached package prints `ok <pkg> (cached)`, and that is an `ok` line: two back-to-back `make test` runs on the parent commit produced the same 14 `ok` lines, the second in 0.42s with every line marked (cached), having executed no test at all. The evidence signal this repo leans on was therefore forgeable. This sits one level below the Docker layer cache #85 addressed: CHECK_EPOCH forces the `RUN make test` step to re-execute, but a GOCACHE baked into an earlier image layer would survive into the re-executed step, so the step can re-run and still do no work. Add -count=1 unconditionally rather than only in the containerised path. The pre-commit hook runs this same script, and a gate that is honest only in CI is dishonest exactly where people rely on it most. test-coverage gets the same flag -- a coverage profile assembled from cached results describes a run that did not happen -- and script/check inherits it by calling script/test. Delete make test-integration rather than making it real (option (b) of issue #69). No file in the repo carries a build tag, so -tags=integration selected nothing extra and the target was an exact duplicate of make test, while internal/vaultik/integration_test.go ran unconditionally on every make test -- the opposite of what a reader of the Makefile would conclude. Tagging a subset was rejected because the entire suite runs in well under a minute, so gating would save seconds in exchange for a build-tag scheme and a second CI path that must be kept wired up; in a repo that has now found five distinct ways for a gate to report an unearned green, a mechanism whose failure mode is "some tests silently stopped running" is a bad trade. Deleting it also means nothing can drop out of CI coverage, since nothing is conditional. The Makefile and README now say plainly that make test runs everything. Raise -timeout from 30s to 120s, after measuring rather than assuming. The standing claim that cold-cache compilation is charged against -timeout is false: -timeout reaches the test binary as -test.timeout and its clock starts inside testing.M.Run, after compilation and linking. A containerised run with an empty GOCACHE spent 46s compiling and still reported per-package durations within noise of a warm host run. The real exposure was margin, not compilation: against the slowest package's worst observed time, 10.2s for internal/database on a cold run, 30s left only 2.9x, thin for a loaded CI runner, and each fresh measurement of that package has come in above the last. -timeout is a hang backstop, not a performance budget, so it should sit far above the slowest legitimate runtime; 120s leaves about 12x while still bounding a hung package, including the verbose rerun, to a few minutes. 120s is a deliberate divergence from REPO_POLICIES.md:192, which mandates a 30-second timeout, and from that file's canonical Go recipe at :212-214. REPO_POLICIES.md is org-canonical and not editable from this repo, so the divergence and its reasoning are recorded in script/test's comment and in TODO.md, and issue #101 proposes amending the policy text upstream. That issue also carries the trade this surfaces: the verbose rerun makes a hung package pay the timeout twice, which at 120s puts a hang-case Docker build over the same policy's five-minute build limit. Both invocations in script/test now share one run_tests function so the quiet run and the verbose rerun cannot drift apart in flags. Measurements and the full verification are recorded once, on the pull request, and deliberately not restated here or in TODO.md.
15 KiB
Workflow
- branch (from
main) - do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (
TODO.mdchanges in the same commit as the work) - merge to
mainif the branch is not protected, otherwise open a PR - push
Status
pre-1.0
Next Step
Define remaining scope for a first tagged release and cut v0.1.0.
Completed Steps
-
2026-08-09: Closed the fifth false-green mechanism (issues #93, #69).
script/testomitted-count=1, so Go's test result cache could satisfy the gate outright: a second back-to-backmake testprinted the full set of 14oklines, every one marked(cached), having executed no test at all. Sinceok <pkg> (cached)is anokline, the "14oklines means the suite ran" signal this repo leans on was forgeable, one level below the Docker layer cache that #85 addressed. Fixed with-count=1unconditionally rather than only in the container, because the pre-commit hook runs the same script and a gate honest only in CI is dishonest where people rely on it most;test-coveragegot the same flag, andscript/checkinherits it by callingscript/test. In the same area,make test-integrationwas deleted rather than made real: no file in the repo carried a build tag, so-tags=integrationselected nothing and the target was an exact duplicate ofmake test. Tagging a subset was rejected because the entire suite runs in well under a minute, and a scheme whose failure mode is "some tests silently stopped running" is a poor trade for those seconds in a repo with this particular history. The-timeoutwas raised from 30s after measuring rather than after assuming: the standing claim that cold-cache compilation is charged against-timeoutis false, disproved by a containerised run that spent 46s compiling and still reported per-package durations within noise of a warm host run.-timeoutreaches the test binary as-test.timeoutand its clock starts insidetesting.M.Run, after the build. The real exposure was margin, not compilation. The 120s landed on is a deliberate, documented divergence fromREPO_POLICIES.md:192, which mandates 30s, and from that file's canonical recipe at:212-214; the divergence is recorded inscript/test's comment becauseREPO_POLICIES.mdis org-canonical and not editable here, and issue #101 proposes amending the policy text upstream. Numbers and the full verification are recorded once, on the pull request, and are deliberately not restated here. -
2026-08-09: Triaged all fifteen stale remote branches (issue #71) and deleted fourteen of them; the full per-branch disposition with evidence is recorded on that issue. Method mattered more than the outcome here: a three-dot
git diff main...branchdiffs from the merge base, so it replays everything that landed onmainafter the branch diverged and makes any old branch look like it holds unlanded work. That artifact is what madegolangci-v2.12.2appear to carry 126 files of unpushed changes when its tree was byte-identical tomain's. Every containment claim here therefore rests on two-dot tip diffs, tree-hash equality,git cherry, andgit branch -r --merged. Nine branches were plain ancestors ofmainwith zerogit cherry+commits.golangci-v2.12.2had landed squashed ascc58583, whose tree hash equals the branch tip's exactly; note the hash recorded in the issue had gone stale becausemainadvanced, so the check had to be redone rather than repeated.fix/sync-snapshot-cleanupwas redundant, its one line already onmaininsyncWithRemote.feature/restore-progress-barwas superseded byprintRestoreProgressand the disk-backed blob cache, and had become actively regressive — it would have deletedinternal/blobgen/compress_test.go, the #28 regression test that landed separately. The two branches this issue was filed for both turned out to be closed questions thatmainhad already moved past by a recorded decision, so neither was landed and no regression test was owed:ctimeno longer exists anywhere in the codebase after1c72a37removed the column, theFile.CTimefield and every use (#54/#55), and change detection compares size, mtime, mode, uid and gid only, exactly asARCHITECTURE.mddocuments — so the silently-skipped-file data-loss risk that made this a 1.0 item does not exist. The SQL allow-list branch would have revertedbfd7334, which replaced that very allow-list with regex sanitisation on review feedback, and would have brokengetTableCount("snapshots")because its allow-list omits that table.feature/daemon-modeis untouched and deferred to #94 pending an owner decision, so it is the one branch besidesmainstill on the remote. The staleTODO.mdentry named in the issue needed no fix:e496aa3had already removed it. No product code changed. -
2026-08-09: Adopted the remaining upstream
CHECK_EPOCHhardening (issue #91), closing the gap #85 knowingly left open. Four changes, all four decided as adopt upstream insneak/prompts#26. (1) Each check stage now asserts[ -n "$CHECK_EPOCH" ] || exit 1before running anything, so a build that supplies no--build-argfails instead of lying. This is the item that mattered: an unsetARGis an empty string and an empty string is a stable cache key, so the second and every later baredocker build .on an unchanged tree replayed all three check layers and still exited 0 — anddocker build .is the commandREPO_POLICIES.mdnames verbatim as a thing that must be green, so the documented command was precisely the one that lied. Failed steps are never cached, which is what makes the guard fire on every invocation rather than once. (2) The epoch is now expanded into each check command rather than left as a bare declaration, so the cache miss no longer depends on BuildKit's unreferenced-ARGhandling staying as it is, and the value appears in the build log. (3)script/cibuildusesepoch="$(date +%s%N)$$", unique per invocation rather than per second;%Nalone is insufficient because busybox drops it silently and exits 0, and$$is what makes the guarantee hold regardless. The bare-assignment form is kept deliberately — inlined in an argument, a failing substitution does not abort underset -euand would yield an empty constant epoch, restoring the exact false green being fixed. (4)script/dockerpasses the same fresh arg, so the two entrypoints cannot disagree about whether the tree is green; local builds are almost always warm, which made it the likelier fooling in practice. TheARGplacement from #85 is unchanged, belowapk add,COPY go.mod go.sumandgo mod download, so dependency layers still cache and the build is not cold. Verified by negative control rather than inspection — a baredocker build .run twice back to back, plus back-to-back pairs of both scripts and a host-sidemake check; the measurements are recorded once, in the PR verification comment, rather than restated here..golangci.yml, the lint-stageFROMline and its digest,script/lint,REPO_POLICIES.mdand.gitea/workflows/check.ymlare all untouched. -
2026-08-09: Stopped
script/cibuildfrom reporting a green it did not earn (issue #85). A baredocker build .let Docker serve the check layers from the layer cache whenever the tree had not changed: the checks never executed and the build still exited 0. The fix is anARG CHECK_EPOCHdeclared immediately above the checkRUNs in both the lint stage and the builder stage (ARGscope is per-stage, so each declares its own), withscript/cibuildassigningepoch="$(date +%s)"and passing--build-arg CHECK_EPOCH="$epoch". The assignment is separate on purpose: underset -eua command substitution that fails inside an argument does not abort the script, which would leave an empty constantCHECK_EPOCHand restore the very false green being fixed. Placement is the rest of the point — theARGsits below theapk add,COPY go.mod go.sum, andgo mod downloadlayers, so only the checks are invalidated and the dependency layers still cache. The guarantee is conditional on a fresh value rather than absolute: a baredocker build .gets an emptyCHECK_EPOCHand can still serve the check layers from cache, whichREADME.mdand theDockerfilenow say plainly, with issue #91 tracking the upstream hardening (expandedARGform, unset guard, per-invocation epoch,script/docker) that would close it. Verified by re-running the reproduction plus the withheld---build-argcounterfactual; the measurements are recorded once, in the PR #89 verification comment, rather than restated here..golangci.yml, the lint-stageFROMline and its digest,script/lint, and.gitea/workflows/check.ymlare all untouched. -
2026-08-09: Corrected the
Vaultik.UIdoc comment (issue #84). It claimed the cli layer replaces the writer with a discarding one in--cronmode; the actual mechanism isUI.SetQuiet(true)insetupGlobals, which drops Begin/Complete/Info/Notice/Detail/ Progress/Banner but still emits Warning and Error. The--cronline inREADME.mdsaid "Silent unless error", which understated what survives, and now names warnings too. The other--croncomments (internal/log/log.go,internal/cli/snapshot.go,internal/vaultik/snapshot.go) were audited and already accurate. Comments and docs only, no behavior change. -
2026-08-09: Made
snapshot listlist the destination store without the private key (issue #64). The listing is now the union of the local index and a single streamed listing of themetadata/prefix, with noage_secret_keygate — the manifest is unencrypted, so a host holding only the public key can enumerate its own backups and a host that lost its local index can still see them. A remote-only snapshot's hostname and name are deliberately not recovered (they are not recoverable without the private key, and making them so would undo the privacy property tracked in issue #81); such rows are labelled by an abbreviation of their remote key and carry the real timestamp and compressed size from the manifest, with<remote only>in the two columns that require the local index. Local-only snapshots are reported as drift, and the hint now namesvaultik prune, which exists, instead ofvaultik snapshot cleanup, which does not.reportRemoteDriftcollapsed into the merged view. Every remote manifest read in the codebase now goes throughdownloadManifestByKey, so issue #81 has one call site to change. Review rework: snapshot timestamps now normalize to UTC inscanSnapshotRows, the one place they enter the domain, so the merged TIMESTAMP column cannot show local time for a locally tracked row and UTC for a remote-only row on a non-UTC host;GetIncompleteByHostnamewas folded onto that same scanner.--jsonnow reports the unreadable-manifest count and the 1000-row truncation on stderr instead of returning a silently short document (the document's shape is unchanged). The two per-snapshotlog.Warncalls on the listing path now route through the same JSON-aware writer as the existing workaround, so one corrupt manifest can no longer put a log line on stdout ahead of the document and break| jq— still a local workaround pending issue #82. Verified withscript/cibuildand with an uncachedmake check(0 issues., no cached test packages), plus end to end against afile://destination with no secret key present. -
2026-08-09: Closed the gap between
make lintand CI (issue #78).script/lintnow runs the digest-pinnedgolangci-lintimage taken from theDockerfilelint stage, which is the single source of truth for the linter version; the duplicate pin in theMakefiledepstarget and the unpinnedgolangci-lintinstall inscript/bootstrapare gone. Agolangci-lintonPATHis used only when its version is exactly the pinned one (which is how the lint stage runs it inside the container); anything else goes through Docker, and a missing or unreachable Docker daemon is a hard error rather than a silent fallback.make checkis therefore now as trustworthy asscript/cibuild. -
2026-08-09: Finished the lint remediation under the canonical
.golangci.yml(issue #61, which also unblocks issue #59). The remaining findings were fixed behavior-preservingly:wsl_v5whitespace,sqlclosecheck, andprealloc. Thesqlclosechecksites now closesql.Rowsin a deferred closure instead of via theCloseRowshelper, which the linter could not see through. Only therevivepackage-name findings remain suppressed, with per-site//nolintdirectives; the package-rename question behind them is tracked in issue #76. Verified withscript/cibuild, which exits 0 — that is the only trustworthy gate, becausescript/lintruns whatevergolangci-linthappens to be onPATHrather than the pinned v2.12.2 that CI and theDockerfileuse, somake checkcan report green on findings CI still fails. That tooling gap is tracked in issue #78. -
2026-08-09: The earlier next step "reconcile the uncommitted
ARCHITECTURE.mdedits onmain" needed no work: the working tree is clean andARCHITECTURE.mdis committed onmain. -
2026-08-07: Updated golangci-lint to v2.12.2 everywhere it is pinned (
Dockerfilelint stage,Makefiledeps target), replaced.golangci.ymlwith the canonical config (v2 schema,default: all), and remediated the bulk of the lint findings it surfaced (issue #61): behavior-preserving fixes across every package, 2,990 findings down to 80.make testandmake fmt-checkwere green at that point butmake lintwas still red; the commit message claimingmake checkwas green was wrong. -
2026-08-07: Added the standard
.golangci.ymland.editorconfig(issue #59); lint findings under the new config are tracked in issue #61.script/bootstrapnow installs sqlite3 (needed by tests). -
2026-07-07 Adopted scripts-to-rule-them-all:
script/entrypoints, Makefile shims, README Entrypoints section -
2026-07-02: Consolidated CLI verbs, retired overlapping commands; bound the local index to its backup destination URL.
-
2026-06-28: snapshot rm now removes metadata only and prints the prune command; restore skips chown when running as non-root.
-
2026-06-26: Snapshot IDs hashed at the storage boundary; snapshot list made resilient to bad remote entries.
-
2026-06-24: Collapsed snapshot prune into vaultik prune; restore streams blobs to disk and restores files in blob-locality order; cron output fixes.
-
2026-06-17: Restore overhaul: ReadAt chunk reads from cached blobs, reference-counted blob sweeper, integration tests; new internal/ui output layer, banner, and progress lines.
-
2025-12-18: Added ARCHITECTURE.md and godoc coverage for exported API.
-
2025-07-26: End-to-end integration tests; manifest format refactor; renamed backup to snapshot; afero filesystem abstraction.
-
2025-07-20: Initial design and implementation: cobra + fx CLI skeleton, SQLite index database, UUID blob storage with streaming chunking.
Future Steps
None queued; the release-scoping item is now the Next Step.