next -> main #43

Merged
clawbot merged 4 commits from next into main 2026-08-10 15:33:22 +02:00
Collaborator

Release-cycle branch. One commit per work unit accumulates here; this is the single open next -> main PR, not a per-issue PR.

Landed so far

  • 599286a — run golangci-lint in a pinned container via script/lint (#41)
  • 329c03f — add --target and --output=type=cacheonly, so a bad stage name cannot silently no-op
  • 20cfb47 — define the stage name once, so the two flags cannot diverge (corrects an overclaim in 329c03f)
  • 3eb9f81 — name the two required flags, and record .dockerignore as part of the gate (comments and docs only)

The last three are reworks after independent review. None of them closes an issue on its own; #41 is closed by 599286a.

599286a — run golangci-lint in a pinned container via script/lint (#41)

golangci-lint is no longer invoked on the host anywhere in the repo. Dockerfile.lint pins golangci/golangci-lint:v2.12.2 by digest and runs the linter as a build step, so a successful build IS a clean lint; make lint is now a thin shim over script/lint (POSIX sh, set -eu, mode 100755, resolves its own repo root). .dockerignore excludes .git only — the lint reads the Go sources, go.mod/go.sum and .golangci.yml, none of which come from there. .golangci.yml is untouched; the gomodguard deprecation warning in the output is #29 and is sneak's to decide. TODO.md's scaffold-exemption note is narrowed rather than dropped: Dockerfile.lint, script/lint and .dockerignore are now permitted and required, while CI config, REPO_POLICIES.md, an application Dockerfile and any other script/ entrypoint still are not.

Two deliberate divergences from the sneak/homoicon reference shape:

  1. Two stages, not one. A cached deps stage holds WORKDIR /src, COPY go.mod go.sum ./ and RUN go mod download; then FROM deps AS lint carries COPY . . and the lint run. script/lint builds with --no-cache-filter rather than a bare docker build. Caching of the lint result is explicitly waived — a cached build lints nothing — but a single-stage --no-cache would also re-fetch the whole module cache over the network on every lint. Splitting the stages busts only the stage that lints.
  2. No golangci-lint config verify step (homoicon has one). It resolves its JSON schema over a live, unpinned HTTPS call: an unpinned network input inside the one step whose entire purpose is a pinned, reproducible gate, and a schema-host outage would surface as a red build. golangci-lint run already fails on a malformed config, and the config here is the shared canonical one, verified where it is maintained. The reason is recorded in a comment in Dockerfile.lint.

Verified at the time: two consecutive runs on an unchanged tree with the golangci-lint run layer executing and never CACHED, a deliberate indent-error-flow violation failing the build and naming that finding before a revert went clean, and make check green with real test durations.

329c03f--target and --output=type=cacheonly

First rework, against the first review's finding 1.

--no-cache-filter is silently ignored by BuildKit when no stage matches its argument, so the anti-false-green mechanism of 599286a rested on one unvalidated string: a rename or typo of the lint stage would have left the lint layer served from cache and script/lint reporting green having linted nothing — the same false green that started #41 , relocated. Reproduced in this repo: with the filter pointed at a nonexistent stage and no --target, an unchanged tree built with RUN golangci-lint run ... CACHED at exit 0.

--target was added because it fails loudly on a name that is not in the file (target stage "nosuchstage" could not be found, exit 1).

--output=type=cacheonly was adopted on the first review's ruling. Nothing consumes the image — the deliverable is an exit code — and the export cost seconds per run and left one dangling image behind every time, on a host where pruning is prohibited. Verified before adoption that the lint stage still executes and a lint failure still exits non-zero; a positive control with the flag removed produced exactly one cmd=[golangci-lint] dangling image plus an exporting layers step, proving the measurement could detect one.

TODO.md in the same commit: the 2026-08-07 entry's now-false claim that the repo has no linter pin and lints on the host is marked superseded in place rather than rewritten; the narrowed exemption names .dockerignore as the third file; and the specific wall-clock timings are replaced by the durable property they were evidence for (the lint stage executes every run and is never served from cache), since wall clock varies per host and per run — the same principle as #22 on hard-coded test counts in prose.

This commit also introduced an overclaim, corrected by the next one: its comments said the two flags "validate each other's magic string". They did not.

20cfb47 — the stage name is defined once

Second rework, against the second review's finding.

--target validates only its own argument. A typo confined to --no-cache-filter left the build green and linting nothing — the original defect surviving in the one edit path the previous commit did not cover:

docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint .
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 CACHED
EXIT=0

Correcting only the prose would have left the hazard live and merely documented, so the duplication itself is gone. The stage name is written once in script/lint and passed to both flags:

# Must match the stage name in Dockerfile.lint.
stage=lint

main() {
    cd "$ROOT"
    docker build \
        --target "$stage" \
        --no-cache-filter="$stage" \
        --output=type=cacheonly \
        -f Dockerfile.lint .
}

Divergence between the flags is now unrepresentable rather than warned about. The true property, which is what the files say: there is a single name to get wrong, and --target rejects it loudly when it is not a stage in Dockerfile.lint, which covers the filter too because it is the same string. The same typo that previously passed silently now fails:

ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?)
EXIT_TYPO=1

Non-zero, with zero CACHED lines anywhere in the build. The third review confirmed the cross-file seam is genuinely guarded by renaming the stage in Dockerfile.lint to lintstage while leaving stage=lint, which also exited 1.

3eb9f81 — prose accuracy around the gate

Third rework, against the third review's two low findings. Comments and documentation only: the docker build invocation and its flags are byte-identical and .dockerignore's effective rules are unchanged (git diff on both files yields no non-comment lines; the only rule is still .git).

  1. "Both flags below must stay" had lost its anchor — it ended a paragraph naming only --no-cache-filter, with --target introduced later and three flags on the command below. It now names --target and --no-cache-filter explicitly. --output=type=cacheonly is deliberately not in that pair: removing it would cost efficiency and hygiene, not correctness.

  2. .dockerignore sits in the same trust boundary as $stage and was missing from the "what the tooling does not check" list. Only what reaches the container is linted, so excluding a Go source there removes it from the lint with no warning. Reproduced rather than asserted: a planted violation plus that one path in .dockerignore gives 0 issues. at exit 0 with the violation still in the working tree. The complementary case was also checked so the warning is not overstated — excluding a file other code still references fails loudly on undefined: typecheck errors, so it is specifically the self-contained file that drops out silently. The warning is recorded both in script/lint and in .dockerignore itself, since that file is where such an edit gets made and the first review on this PR suggested extending it for build artifacts.

TODO.md gained no new dated paragraph; the existing "Corrected 2026-08-10" entry already enumerated what is left to the editor and was incomplete without .dockerignore, so the clause went into that sentence.

Residual hazards, stated in script/lint and Dockerfile.lint

--target verifies that the stage name exists, not that it names the stage which actually runs golangci-lint, and it stops the build there — so relocating the lint step, or adding a stage after lint, would not be caught. And .dockerignore governs what is linted at all. Both are on whoever edits those files together.

Standing notes

Full captured evidence for each rework is in the corresponding comment on this PR: the silent-no-op reproductions, the stage=lnit hard failure, the attribution control showing the same build with --no-cache-filter removed reports COPY . . CACHED and RUN golangci-lint ... CACHED on an identical tree, negative controls under four different linters, dangling-image attribution with a positive control, and uncached make check runs. git status was empty around every evidence run and all scratch lived outside the clone.

No prune of any kind was run at any point during this cycle. .golangci.yml is untouched by all four commits, and the branch adds no CI config, no REPO_POLICIES.md, and no script/ entrypoint other than lint.

Porting --target, --output=type=cacheonly and the single-variable shape back into sneak/homoicon before it is cloned into further repos is tracked separately by the manager and is not part of this cycle.

Release-cycle branch. One commit per work unit accumulates here; this is the single open `next` -> `main` PR, not a per-issue PR. ## Landed so far - `599286a` — run golangci-lint in a pinned container via script/lint (https://git.eeqj.de/sneak/rgoue/issues/41) - `329c03f` — add `--target` and `--output=type=cacheonly`, so a bad stage name cannot silently no-op - `20cfb47` — define the stage name once, so the two flags cannot diverge (corrects an overclaim in `329c03f`) - `3eb9f81` — name the two required flags, and record `.dockerignore` as part of the gate (comments and docs only) The last three are reworks after independent review. None of them closes an issue on its own; https://git.eeqj.de/sneak/rgoue/issues/41 is closed by `599286a`. ### `599286a` — run golangci-lint in a pinned container via script/lint (https://git.eeqj.de/sneak/rgoue/issues/41) `golangci-lint` is no longer invoked on the host anywhere in the repo. `Dockerfile.lint` pins `golangci/golangci-lint:v2.12.2` by digest and runs the linter as a build step, so a successful build IS a clean lint; `make lint` is now a thin shim over `script/lint` (POSIX `sh`, `set -eu`, mode `100755`, resolves its own repo root). `.dockerignore` excludes `.git` only — the lint reads the Go sources, `go.mod`/`go.sum` and `.golangci.yml`, none of which come from there. `.golangci.yml` is untouched; the `gomodguard` deprecation warning in the output is https://git.eeqj.de/sneak/rgoue/issues/29 and is sneak's to decide. `TODO.md`'s scaffold-exemption note is narrowed rather than dropped: `Dockerfile.lint`, `script/lint` and `.dockerignore` are now permitted and required, while CI config, `REPO_POLICIES.md`, an application `Dockerfile` and any other `script/` entrypoint still are not. **Two deliberate divergences from the `sneak/homoicon` reference shape:** 1. **Two stages, not one.** A cached `deps` stage holds `WORKDIR /src`, `COPY go.mod go.sum ./` and `RUN go mod download`; then `FROM deps AS lint` carries `COPY . .` and the lint run. `script/lint` builds with `--no-cache-filter` rather than a bare `docker build`. Caching of the lint result is explicitly waived — a cached build lints nothing — but a single-stage `--no-cache` would also re-fetch the whole module cache over the network on every lint. Splitting the stages busts only the stage that lints. 2. **No `golangci-lint config verify` step** (homoicon has one). It resolves its JSON schema over a live, unpinned HTTPS call: an unpinned network input inside the one step whose entire purpose is a pinned, reproducible gate, and a schema-host outage would surface as a red build. `golangci-lint run` already fails on a malformed config, and the config here is the shared canonical one, verified where it is maintained. The reason is recorded in a comment in `Dockerfile.lint`. Verified at the time: two consecutive runs on an unchanged tree with the `golangci-lint run` layer executing and never `CACHED`, a deliberate `indent-error-flow` violation failing the build and naming that finding before a revert went clean, and `make check` green with real test durations. ### `329c03f` — `--target` and `--output=type=cacheonly` First rework, against the first review's finding 1. `--no-cache-filter` is **silently ignored** by BuildKit when no stage matches its argument, so the anti-false-green mechanism of `599286a` rested on one unvalidated string: a rename or typo of the `lint` stage would have left the lint layer served from cache and `script/lint` reporting green having linted nothing — the same false green that started https://git.eeqj.de/sneak/rgoue/issues/41 , relocated. Reproduced in this repo: with the filter pointed at a nonexistent stage and no `--target`, an unchanged tree built with `RUN golangci-lint run ... CACHED` at exit 0. `--target` was added because it fails loudly on a name that is not in the file (`target stage "nosuchstage" could not be found`, exit 1). `--output=type=cacheonly` was adopted on the first review's ruling. Nothing consumes the image — the deliverable is an exit code — and the export cost seconds per run and left one dangling image behind every time, on a host where pruning is prohibited. Verified before adoption that the lint stage still executes and a lint failure still exits non-zero; a positive control with the flag removed produced exactly one `cmd=[golangci-lint]` dangling image plus an `exporting layers` step, proving the measurement could detect one. `TODO.md` in the same commit: the 2026-08-07 entry's now-false claim that the repo has no linter pin and lints on the host is marked superseded in place rather than rewritten; the narrowed exemption names `.dockerignore` as the third file; and the specific wall-clock timings are replaced by the durable property they were evidence for (the lint stage executes every run and is never served from cache), since wall clock varies per host and per run — the same principle as https://git.eeqj.de/sneak/rgoue/issues/22 on hard-coded test counts in prose. **This commit also introduced an overclaim, corrected by the next one:** its comments said the two flags "validate each other's magic string". They did not. ### `20cfb47` — the stage name is defined once Second rework, against the second review's finding. `--target` validates only its own argument. A typo confined to `--no-cache-filter` left the build green and linting nothing — the original defect surviving in the one edit path the previous commit did not cover: ``` docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint . #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 CACHED EXIT=0 ``` Correcting only the prose would have left the hazard live and merely documented, so the duplication itself is gone. The stage name is written once in `script/lint` and passed to both flags: ``` # Must match the stage name in Dockerfile.lint. stage=lint main() { cd "$ROOT" docker build \ --target "$stage" \ --no-cache-filter="$stage" \ --output=type=cacheonly \ -f Dockerfile.lint . } ``` Divergence between the flags is now unrepresentable rather than warned about. The true property, which is what the files say: there is a single name to get wrong, and `--target` rejects it loudly when it is not a stage in `Dockerfile.lint`, which covers the filter too because it is the same string. The same typo that previously passed silently now fails: ``` ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?) EXIT_TYPO=1 ``` Non-zero, with zero `CACHED` lines anywhere in the build. The third review confirmed the cross-file seam is genuinely guarded by renaming the stage in `Dockerfile.lint` to `lintstage` while leaving `stage=lint`, which also exited 1. ### `3eb9f81` — prose accuracy around the gate Third rework, against the third review's two low findings. **Comments and documentation only:** the `docker build` invocation and its flags are byte-identical and `.dockerignore`'s effective rules are unchanged (`git diff` on both files yields no non-comment lines; the only rule is still `.git`). 1. **"Both flags below must stay" had lost its anchor** — it ended a paragraph naming only `--no-cache-filter`, with `--target` introduced later and three flags on the command below. It now names `--target` and `--no-cache-filter` explicitly. `--output=type=cacheonly` is deliberately not in that pair: removing it would cost efficiency and hygiene, not correctness. 2. **`.dockerignore` sits in the same trust boundary as `$stage` and was missing from the "what the tooling does not check" list.** Only what reaches the container is linted, so excluding a Go source there removes it from the lint with no warning. Reproduced rather than asserted: a planted violation plus that one path in `.dockerignore` gives `0 issues.` at exit 0 with the violation still in the working tree. The complementary case was also checked so the warning is not overstated — excluding a file other code still references fails loudly on `undefined:` typecheck errors, so it is specifically the self-contained file that drops out silently. The warning is recorded both in `script/lint` and in `.dockerignore` itself, since that file is where such an edit gets made and the first review on this PR suggested extending it for build artifacts. `TODO.md` gained no new dated paragraph; the existing "Corrected 2026-08-10" entry already enumerated what is left to the editor and was incomplete without `.dockerignore`, so the clause went into that sentence. ## Residual hazards, stated in `script/lint` and `Dockerfile.lint` `--target` verifies that the stage name exists, not that it names the stage which actually runs `golangci-lint`, and it stops the build there — so relocating the lint step, or adding a stage after `lint`, would not be caught. And `.dockerignore` governs what is linted at all. Both are on whoever edits those files together. ## Standing notes Full captured evidence for each rework is in the corresponding comment on this PR: the silent-no-op reproductions, the `stage=lnit` hard failure, the attribution control showing the same build with `--no-cache-filter` removed reports `COPY . . CACHED` and `RUN golangci-lint ... CACHED` on an identical tree, negative controls under four different linters, dangling-image attribution with a positive control, and uncached `make check` runs. `git status` was empty around every evidence run and all scratch lived outside the clone. No prune of any kind was run at any point during this cycle. `.golangci.yml` is untouched by all four commits, and the branch adds no CI config, no `REPO_POLICIES.md`, and no `script/` entrypoint other than `lint`. Porting `--target`, `--output=type=cacheonly` and the single-variable shape back into `sneak/homoicon` before it is cloned into further repos is tracked separately by the manager and is not part of this cycle.
clawbot added 1 commit 2026-08-10 14:34:22 +02:00
golangci-lint is no longer invoked on the host anywhere in the repo.
Dockerfile.lint pins golangci/golangci-lint:v2.12.2 by digest and runs
the linter as a build step, so a successful build IS a clean lint, and
`make lint` becomes a thin shim over script/lint. This removes the host
linter install that produced a false green here, where a branch that was
genuinely red with a goconst finding reported "0 issues" off the shared
host cache; a container per run has its own cache and lock.

Two deliberate divergences from the sneak/homoicon reference shape:

  - Two stages rather than one. A cached `deps` stage holds
    `go mod download`, then `FROM deps AS lint` carries the source copy
    and the lint run, and script/lint builds with
    `--no-cache-filter=lint`. Caching of the lint result is explicitly
    waived (a cached build lints nothing), and splitting the stages means
    busting the lint layer does not re-fetch the module cache over the
    network on every run.

  - No `golangci-lint config verify` step. It resolves its JSON schema
    over a live, unpinned HTTPS call: an unpinned network input inside
    the one step whose purpose is a pinned, reproducible gate, and a
    schema-host outage would surface as a red build. `golangci-lint run`
    already fails on a malformed config. The reason is recorded in a
    comment in Dockerfile.lint.

.dockerignore excludes .git only; the lint reads the Go sources,
go.mod/go.sum and .golangci.yml, none of which come from there.

The TODO.md scaffold-exemption note is narrowed rather than dropped:
Dockerfile.lint and script/lint are now permitted and required, while CI
config, REPO_POLICIES.md, an application Dockerfile and any other
script/ entrypoint still are not.

Verified, since a green docker build is the classic false green: two
consecutive script/lint runs on an unchanged tree each showed the
`golangci-lint run` layer executing (9.8s and 7.9s, both "0 issues.")
while the deps layers reported CACHED; a deliberate indent-error-flow
violation failed the build naming that finding and the unused one, and a
revert went clean again. `make check` green.
clawbot changed title from next -> main to next -> main 2026-08-10 14:34:31 +02:00
clawbot self-assigned this 2026-08-10 14:34:32 +02:00
clawbot added the needs-review label 2026-08-10 14:34:32 +02:00
Author
Collaborator

Independent adversarial review of 599286a (commit only, not the whole next branch). Reproduced from my own fresh clone; the author's pasted evidence was not taken on trust.

Verdict: PASS. Two findings below are hardening/doc items, neither a present false-green.

Reproduced, not trusted

  • Two consecutive script/lint runs, unchanged tree: #10 [lint 2/2] RUN golangci-lint run ... = DONE 14.1s then DONE 9.5s, 0 issues. after 10.2s/7.7s of real work. Never CACHED. deps layers CACHED both times — the two-stage split does what it claims.
  • Negative control of my own choosing (unchecked f.Close() in a new game/ file, not the author's indent-error-flow): exit 1, naming exactly that finding — game/zzrevcheck.go:7:9: Error return value of f.Close is not checked (errcheck). Removed the file, clean again.
  • Exit propagation: with the violation present, make lint and make check both exit 2 (make: *** [Makefile:35: lint] Error 1), make check stopping before test. set -eu, docker build last in main(), nothing swallows status.
  • make check in full, green: lint layer DONE 15.8s (not CACHED), ok cmd/rogue 1.026s / ok game 3.164s — real durations, no (cached).
  • Digest: queried Docker Hub directly — docker-content-digest: sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 for tag v2.12.2, OCI image index. Tag and digest agree, and the line is byte-identical to sneak/homoicon@main.
  • Config applied: .golangci.yml sets default: all, which is why gomodguard's deprecation warning appears — stock golangci-lint would not enable it. That warning is proof the repo's config reached the container and is in force. .golangci.yml untouched by this commit.
  • DOCKER_BUILDKIT=0 ./script/lint: exits 125, unknown flag: --no-cache-filter, no build runs. Fails loudly — no false green.
  • POSIX: dash -n and sh -n clean, no bashisms. Mode 100755 in the index. Root resolution works from a subdirectory.
  • Scope, terminology, formatting, mergeability (one commit ahead of origin/main, zero behind, git merge-tree clean), commit title ends exactly (closes #41), no vendor references or attribution trailers anywhere in the commit, tree, or PR body: all clean.
  • Docs: Makefile header, README sentence, and Future Steps note are each true as written now. The author is right that it is note 2, not note 3.

Findings

1 (medium, hardening — latent false-green, not a present one). --no-cache-filter=<stage> is silently ignored when the stage name does not match. Proven on a scratch two-stage Dockerfile: docker build --no-cache-filter=nosuchstage built with the final stage reporting CACHED and exited 0. BuildKit does not validate the filter argument against the stage list. So the entire anti-false-green mechanism of this change hangs on one unvalidated magic string: rename the lint stage in Dockerfile.lint, or typo it, and script/lint reports green having linted nothing — the exact defect class this PR exists to kill. Nothing in the repo would catch it.

Why it matters more than usual: this shape is about to be copied into other repos, so the hazard propagates.

What acceptable looks like: add --target lint alongside the filter in script/lint, so both flags name the same stage from the same string. Verified: docker build --target nosuchstage fails loudly — ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found, exit 1 — and --target lint --no-cache-filter=lint still re-executes the stage. One flag converts a silent no-op into a hard error. Not a blocker for this commit (the stage is named lint and I proved it executes), but it should land before the shape is cloned.

2 (low, docs). TODO.md line 736 now asserts something false. Inside the dated 2026-08-07 Completed Steps entry: "The repo has no golangci-lint version pin to bump (no Dockerfile or CI; make lint runs whatever golangci-lint is on the host)." That is the last remaining statement in the repo describing a host lint path, and it is written in the present tense. It is a historical log entry, so not a violation of the definition of done, but a reader landing there gets the pre-change answer. Acceptable: append a superseded-by pointer to #41.

Minor / anomalies that pass anyway

  • The narrowed exemption note (Future Steps 2) permits Dockerfile.lint and script/lint by name but does not mention .dockerignore, which this commit also adds. The file is justified and in substance part of the lint container, but the note as written reads as though it should not be there. Worth naming it in the note.
  • TODO.md records the two consecutive runs as 11.6s and 9.7s; the commit message and PR body say 9.8s and 7.9s. Different runs, presumably — but three different numbers for one claim in a change whose entire value is that the evidence is captured rather than asserted. Pick one set.
  • Invoked through a symlink, script/lint resolves ROOT to the symlink's directory and fails (failed to read dockerfile: open Dockerfile.lint: no such file or directory, exit 1). Loud failure, not a false green, and identical to sneak/homoicon. Noted, not a defect.
  • .dockerignore does not exclude .gitignored build artifacts (/rogue, *.test, *.out), so a developer with a built binary ships it into the context each run. Cosmetic.
  • main "$@" takes arguments it never uses. Matches the reference shape.

The three documented divergences

Two-stage split: sound, and required by the implementation brief — verified go mod download stayed CACHED across every build I ran while lint re-executed each time.

--no-cache-filter: sound, with the caveat in finding 1.

Omission of golangci-lint config verify: I agree with the ruling, and would rule the same way independently. An unpinned live HTTPS fetch inside the one step whose entire value is pinned reproducibility is a contradiction, and it converts a schema-host outage into a red build indistinguishable from a real finding. golangci-lint run rejects an unparseable config on its own, and this repo carries a stronger constraint than schema validity anyway: MEMORY.md requires .golangci.yml to stay byte-identical to the canonical shared config, so drift is caught by comparison, not by a schema. Honestly disclosed in Dockerfile.lint lines 27-31.

Disclosure: I did not independently confirm that config verify performs a live network fetch — I did not run it. The claim is consistent with the tool's documented behaviour and it is the manager's ruling regardless.

Ruling on the open question (dangling images)

Adopt --output=type=cacheonly. Not a close call.

  • Nothing consumes the image. The deliverable of this build is an exit code; the export produces an artifact with no reader.
  • Measured on this repo: the export step costs 4.2s and 3.5s of runs that took 21.8s and 14.1s wall — 20-30% of every lint. The cacheonly variant of the same build finished in 10.4s with the lint stage still executing (0 issues. after 8.3s of real linting) and no exporting to image step at all.
  • It introduces no false-green path, verified rather than assumed: a deliberately failing RUN under --output=type=cacheonly --no-cache-filter=lint exited 1 and named the failing step. The stage still executes; only the export is skipped.
  • Tagging does not solve it. Retagging leaves the previous image untagged, i.e. dangling. Skipping the export is the only fix.
  • Scale: this host currently carries 626 dangling images. Per-run marginal disk is modest since layers are shared, but the count grows without bound across concurrent sessions and pruning is prohibited here.
  • Divergence cost is near zero: it is the same class of divergence already taken and documented in the same file (--no-cache-filter), for the same reason — the reference shape assumes a build with a product, and this build has none. It requires BuildKit, which --no-cache-filter already makes a hard requirement.

Recommendation: fold --output=type=cacheonly and --target lint into script/lint together — one line, both justified by the same argument — and push both back into sneak/homoicon before the shape is copied further.

Could not verify

Gitea Actions runs are not queryable as clawbot (403, not repo owner). There is no CI to be green: the repo carries no workflow files by standing exemption, and this change correctly adds none.

Independent adversarial review of `599286a` (commit only, not the whole `next` branch). Reproduced from my own fresh clone; the author's pasted evidence was not taken on trust. **Verdict: PASS.** Two findings below are hardening/doc items, neither a present false-green. ## Reproduced, not trusted - Two consecutive `script/lint` runs, unchanged tree: `#10 [lint 2/2] RUN golangci-lint run ...` = `DONE 14.1s` then `DONE 9.5s`, `0 issues.` after 10.2s/7.7s of real work. Never `CACHED`. `deps` layers `CACHED` both times — the two-stage split does what it claims. - Negative control of my own choosing (unchecked `f.Close()` in a new `game/` file, not the author's `indent-error-flow`): exit 1, naming exactly that finding — `game/zzrevcheck.go:7:9: Error return value of f.Close is not checked (errcheck)`. Removed the file, clean again. - Exit propagation: with the violation present, `make lint` and `make check` both exit 2 (`make: *** [Makefile:35: lint] Error 1`), `make check` stopping before `test`. `set -eu`, `docker build` last in `main()`, nothing swallows status. - `make check` in full, green: lint layer `DONE 15.8s` (not `CACHED`), `ok cmd/rogue 1.026s` / `ok game 3.164s` — real durations, no `(cached)`. - Digest: queried Docker Hub directly — `docker-content-digest: sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240` for tag `v2.12.2`, OCI image index. Tag and digest agree, and the line is byte-identical to `sneak/homoicon@main`. - Config applied: `.golangci.yml` sets `default: all`, which is why `gomodguard`'s deprecation warning appears — stock golangci-lint would not enable it. That warning is proof the repo's config reached the container and is in force. `.golangci.yml` untouched by this commit. - `DOCKER_BUILDKIT=0 ./script/lint`: exits 125, `unknown flag: --no-cache-filter`, no build runs. Fails loudly — no false green. - POSIX: `dash -n` and `sh -n` clean, no bashisms. Mode `100755` in the index. Root resolution works from a subdirectory. - Scope, terminology, formatting, mergeability (one commit ahead of `origin/main`, zero behind, `git merge-tree` clean), commit title ends exactly ` (closes #41)`, no vendor references or attribution trailers anywhere in the commit, tree, or PR body: all clean. - Docs: Makefile header, README sentence, and Future Steps note are each true as written now. The author is right that it is note **2**, not note 3. ## Findings **1 (medium, hardening — latent false-green, not a present one). `--no-cache-filter=<stage>` is silently ignored when the stage name does not match.** Proven on a scratch two-stage Dockerfile: `docker build --no-cache-filter=nosuchstage` built with the final stage reporting `CACHED` and exited **0**. BuildKit does not validate the filter argument against the stage list. So the entire anti-false-green mechanism of this change hangs on one unvalidated magic string: rename the `lint` stage in `Dockerfile.lint`, or typo it, and `script/lint` reports green having linted nothing — the exact defect class this PR exists to kill. Nothing in the repo would catch it. Why it matters more than usual: this shape is about to be copied into other repos, so the hazard propagates. What acceptable looks like: add `--target lint` alongside the filter in `script/lint`, so both flags name the same stage from the same string. Verified: `docker build --target nosuchstage` fails loudly — `ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found`, exit 1 — and `--target lint --no-cache-filter=lint` still re-executes the stage. One flag converts a silent no-op into a hard error. Not a blocker for this commit (the stage *is* named `lint` and I proved it executes), but it should land before the shape is cloned. **2 (low, docs). `TODO.md` line 736 now asserts something false.** Inside the dated 2026-08-07 Completed Steps entry: "The repo has no golangci-lint version pin to bump (no Dockerfile or CI; `make lint` runs whatever `golangci-lint` is on the host)." That is the last remaining statement in the repo describing a host lint path, and it is written in the present tense. It is a historical log entry, so not a violation of the definition of done, but a reader landing there gets the pre-change answer. Acceptable: append a superseded-by pointer to https://git.eeqj.de/sneak/rgoue/issues/41. ## Minor / anomalies that pass anyway - The narrowed exemption note (Future Steps 2) permits `Dockerfile.lint` and `script/lint` by name but does not mention `.dockerignore`, which this commit also adds. The file is justified and in substance part of the lint container, but the note as written reads as though it should not be there. Worth naming it in the note. - `TODO.md` records the two consecutive runs as `11.6s and 9.7s`; the commit message and PR body say `9.8s and 7.9s`. Different runs, presumably — but three different numbers for one claim in a change whose entire value is that the evidence is captured rather than asserted. Pick one set. - Invoked through a symlink, `script/lint` resolves `ROOT` to the symlink's directory and fails (`failed to read dockerfile: open Dockerfile.lint: no such file or directory`, exit 1). Loud failure, not a false green, and identical to `sneak/homoicon`. Noted, not a defect. - `.dockerignore` does not exclude `.gitignore`d build artifacts (`/rogue`, `*.test`, `*.out`), so a developer with a built binary ships it into the context each run. Cosmetic. - `main "$@"` takes arguments it never uses. Matches the reference shape. ## The three documented divergences Two-stage split: sound, and required by the implementation brief — verified `go mod download` stayed `CACHED` across every build I ran while `lint` re-executed each time. `--no-cache-filter`: sound, with the caveat in finding 1. Omission of `golangci-lint config verify`: **I agree with the ruling, and would rule the same way independently.** An unpinned live HTTPS fetch inside the one step whose entire value is pinned reproducibility is a contradiction, and it converts a schema-host outage into a red build indistinguishable from a real finding. `golangci-lint run` rejects an unparseable config on its own, and this repo carries a stronger constraint than schema validity anyway: `MEMORY.md` requires `.golangci.yml` to stay byte-identical to the canonical shared config, so drift is caught by comparison, not by a schema. Honestly disclosed in `Dockerfile.lint` lines 27-31. Disclosure: I did not independently confirm that `config verify` performs a live network fetch — I did not run it. The claim is consistent with the tool's documented behaviour and it is the manager's ruling regardless. ## Ruling on the open question (dangling images) **Adopt `--output=type=cacheonly`.** Not a close call. - Nothing consumes the image. The deliverable of this build is an exit code; the export produces an artifact with no reader. - Measured on this repo: the export step costs `4.2s` and `3.5s` of runs that took 21.8s and 14.1s wall — 20-30% of every lint. The cacheonly variant of the same build finished in `10.4s` with the lint stage still executing (`0 issues.` after 8.3s of real linting) and no `exporting to image` step at all. - It introduces no false-green path, verified rather than assumed: a deliberately failing `RUN` under `--output=type=cacheonly --no-cache-filter=lint` exited **1** and named the failing step. The stage still executes; only the export is skipped. - **Tagging does not solve it.** Retagging leaves the previous image untagged, i.e. dangling. Skipping the export is the only fix. - Scale: this host currently carries 626 dangling images. Per-run marginal disk is modest since layers are shared, but the count grows without bound across concurrent sessions and pruning is prohibited here. - Divergence cost is near zero: it is the same class of divergence already taken and documented in the same file (`--no-cache-filter`), for the same reason — the reference shape assumes a build with a product, and this build has none. It requires BuildKit, which `--no-cache-filter` already makes a hard requirement. Recommendation: fold `--output=type=cacheonly` and `--target lint` into `script/lint` together — one line, both justified by the same argument — and push both back into `sneak/homoicon` before the shape is copied further. ## Could not verify Gitea Actions runs are not queryable as `clawbot` (403, not repo owner). There is no CI to be green: the repo carries no workflow files by standing exemption, and this change correctly adds none.
Author
Collaborator

Manager: review above is a PASS, but I am reworking before merge rather than banking the findings.

Finding 1 is the one that matters and it is worth stating plainly, because it is the same failure class this whole change exists to kill: --no-cache-filter=lint is silently ignored if no stage by that name exists. The reviewer proved it — a bogus stage name built CACHED and exited 0. So a rename or a typo turns script/lint into a green no-op, which is exactly the false green that started #41 , just relocated. Adding --target lint closes it: a bad target fails loudly instead. One flag, so it goes in now.

Also taking the reviewer's --output=type=cacheonly ruling. Nothing consumes the image — the deliverable is an exit code — and this host already carries 626 dangling images with pruning prohibited.

Both flags need porting back into sneak/homoicon before the shape is cloned into further repos; I will open that separately so it is not buried in this repo's cycle.

Manager: review above is a PASS, but I am reworking before merge rather than banking the findings. Finding 1 is the one that matters and it is worth stating plainly, because it is the same failure class this whole change exists to kill: `--no-cache-filter=lint` is **silently ignored** if no stage by that name exists. The reviewer proved it — a bogus stage name built `CACHED` and exited 0. So a rename or a typo turns `script/lint` into a green no-op, which is exactly the false green that started https://git.eeqj.de/sneak/rgoue/issues/41 , just relocated. Adding `--target lint` closes it: a bad target fails loudly instead. One flag, so it goes in now. Also taking the reviewer's `--output=type=cacheonly` ruling. Nothing consumes the image — the deliverable is an exit code — and this host already carries 626 dangling images with pruning prohibited. Both flags need porting back into `sneak/homoicon` before the shape is cloned into further repos; I will open that separately so it is not buried in this repo's cycle.
clawbot added 1 commit 2026-08-10 14:56:43 +02:00
`--no-cache-filter=lint` is silently ignored by BuildKit when no stage
matches the name, so the entire anti-false-green mechanism hung on one
unvalidated magic string: renaming or mistyping the `lint` stage would
have left the lint layer served from cache and `script/lint` reporting
green having linted nothing. Reproduced here — with the filter pointed at
a nonexistent stage and no `--target`, an unchanged tree built with
`RUN golangci-lint run ... CACHED` and exited 0.

`--target lint` closes it: a stage name that does not exist now fails
loudly (`target stage "nosuchstage" could not be found`, exit 1) instead
of passing. The two flags name the same stage from the same string and
validate each other; both the script and the stage definition in
Dockerfile.lint carry a comment saying they must be kept in sync.

`--output=type=cacheonly` drops the image export. Nothing consumes the
image — the deliverable of this build is an exit code — and the export
cost seconds per run and left one dangling image behind every time, on a
host where pruning is prohibited. The lint stage still executes and a
lint failure still exits non-zero, both verified rather than assumed.

TODO.md: the 2026-08-07 entry's claim that the repo has no linter pin and
lints on the host is marked superseded in place rather than rewritten;
the narrowed scaffold exemption now names `.dockerignore` alongside
`Dockerfile.lint` and `script/lint`; and the specific wall-clock timings
are replaced by the durable property they were evidence for, since they
vary per host and per run.
Author
Collaborator

Reworked in 329c03f on next (new commit; 599286a untouched, not amended). Addressing finding 1, the --output=type=cacheonly ruling, finding 2, and the two minor items, point by point.

1. Finding 1 (medium) — --target lint added. Accepted in full.

script/lint now passes --target lint, --no-cache-filter=lint and --output=type=cacheonly.

I reproduced the silent no-op in this repo, not just on a scratch Dockerfile. With the filter pointed at a nonexistent stage and no --target — i.e. the shape this PR shipped, one typo away — on a genuinely unchanged tree:

#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 CACHED
EXIT=0

Green, exit 0, linted nothing. Confirmed exactly as the review described.

With --target in place, the same bad name now fails loudly:

#1 [internal] load build definition from Dockerfile.lint
#1 transferring dockerfile: 1.44kB done
#1 DONE 0.0s
ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found
EXIT_BADTARGET=1

Reverted after each experiment. The comment in script/lint states why both flags are present, that they name the same stage from the same string, and that neither may be "simplified" away. I also added a two-line comment above FROM deps AS lint in Dockerfile.lint — that is where a rename would actually be typed, so the warning belongs at that end too.

2. --output=type=cacheonly adopted, with the failure path verified first

The catastrophic outcome here would be the flag suppressing a lint failure, so that was checked before anything else. Negative control of my own choosing (not the author's indent-error-flow, not your errcheck — a new game/zzneg_rework_check.go with an unnecessary fmt.Sprintf), under all three flags:

#12 9.050 game/zzneg_rework_check.go:7:2: return with no blank line before (nlreturn)
#12 9.050 game/zzneg_rework_check.go:6:7: integer-format: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
#12 9.050 game/zzneg_rework_check.go:5:6: func reworkNegativeControl is unused (unused)
#12 9.050 3 issues:
#12 9.050 * nlreturn: 1
#12 9.050 * perfsprint: 1
#12 9.050 * unused: 1
EXIT_WITH_VIOLATION=1

make check with the same violation exits 2 and stops before the tests (zero ok lines). After deleting the file, EXIT_AFTER_REVERT=0, 0 issues.

Dangling images. Raw before/after counts are useless on this host — it gained images from other sessions during every window I measured — so I diffed the image IDs and inspected each new one. Across a script/lint run: 4 new dangling images appeared, all cmd=[python3] or cmd=[node], none from this build. Across the full make check: 2 new, both cmd=[python3].

To prove the measurement could actually detect one, I ran a positive control — same script with --output=type=cacheonly removed:

02513b304259 cmd=[golangci-lint] gover=GOTOOLCHAIN=auto
#13 exporting to image
#13 exporting layers
#13 exporting layers 5.3s done

Exactly one new dangling image with the golangci-lint signature, plus 5.3s of layer export. With the flag, zero exporting to image steps in any run. I deleted that one control image by ID (docker rmi 02513b304259, targeted, its own image only). No prune of any kind was run at any point.

3. Finding 2 (low) — TODO.md line 736 marked superseded in place

The 2026-08-07 entry keeps its original text; a superseded note is appended pointing at the 2026-08-10 entry and #41 . History not rewritten.

4. Minor — .dockerignore named in the exemption note

Future Steps note 2 now reads "Dockerfile.lint and script/lint ... along with the .dockerignore that scopes their build context", and "those three files only".

5. Minor — the three-way timing disagreement

Resolved by deleting the numbers from TODO.md prose rather than picking a set. All three measurements were honest; wall clock varies per host and per run, so a specific figure there is a hostage to fortune in exactly the way hard-coded test counts were in #22 . The entry now states the durable property instead: the lint stage executes on every run and is never served from cache. The commit message and PR body history are left alone.

Two consecutive runs, all three flags, unchanged tree

Run 1:

#9 [lint 1/2] COPY . .
#9 DONE 0.5s
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 7.297 0 issues.
#10 DONE 10.9s
real    0m11.630s

Run 2, nothing touched in between:

#9 [deps 4/4] RUN go mod download
#9 CACHED
#10 [lint 1/2] COPY . .
#10 DONE 2.3s
#11 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#11 7.869 0 issues.
#11 DONE 9.9s
real    0m12.517s

DONE both times, never CACHED, deps cached both times, and no export step in either.

One methodology correction worth recording, because it would have quietly weakened this evidence: my first attempt kept scratch logs in a directory inside the repo, which put them in the build context and invalidated COPY . . on every run. The lint stage then re-executes because the context changed, not because --no-cache-filter did its job — a passing result that proves nothing. Scratch moved outside the tree; the runs above have COPY . . and everything above it stable, so the lint layer re-executing is attributable to the filter alone.

make check green in full

Go's test cache served (cached) on a first attempt, which is not evidence, so this is a forced uncached run through the make target (GOFLAGS=-count=1 make check) — real durations:

#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 7.188 0 issues.
ok      git.eeqj.de/sneak/rgoue/cmd/rogue       1.039s  coverage: 29.7% of statements
ok      git.eeqj.de/sneak/rgoue/game            2.882s  coverage: 64.0% of statements
        git.eeqj.de/sneak/rgoue/term            coverage: 0.0% of statements
MAKE_CHECK_EXIT=0

Lint layer executed, not CACHED. make fmt run and committed. .golangci.yml untouched; no CI config, no REPO_POLICIES.md, no further script/ entrypoint. sh -n and dash -n clean, mode 100755 preserved.

The port of both flags back into sneak/homoicon before the shape is cloned further is the manager's to open and is not in this commit.

Reworked in `329c03f` on `next` (new commit; `599286a` untouched, not amended). Addressing finding 1, the `--output=type=cacheonly` ruling, finding 2, and the two minor items, point by point. ## 1. Finding 1 (medium) — `--target lint` added. Accepted in full. `script/lint` now passes `--target lint`, `--no-cache-filter=lint` and `--output=type=cacheonly`. **I reproduced the silent no-op in this repo, not just on a scratch Dockerfile.** With the filter pointed at a nonexistent stage and no `--target` — i.e. the shape this PR shipped, one typo away — on a genuinely unchanged tree: ``` #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 CACHED EXIT=0 ``` Green, exit 0, linted nothing. Confirmed exactly as the review described. With `--target` in place, the same bad name now fails loudly: ``` #1 [internal] load build definition from Dockerfile.lint #1 transferring dockerfile: 1.44kB done #1 DONE 0.0s ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found EXIT_BADTARGET=1 ``` Reverted after each experiment. The comment in `script/lint` states why both flags are present, that they name the same stage from the same string, and that neither may be "simplified" away. I also added a two-line comment above `FROM deps AS lint` in `Dockerfile.lint` — that is where a rename would actually be typed, so the warning belongs at that end too. ## 2. `--output=type=cacheonly` adopted, with the failure path verified first The catastrophic outcome here would be the flag suppressing a lint failure, so that was checked before anything else. Negative control of my own choosing (not the author's `indent-error-flow`, not your `errcheck` — a new `game/zzneg_rework_check.go` with an unnecessary `fmt.Sprintf`), under all three flags: ``` #12 9.050 game/zzneg_rework_check.go:7:2: return with no blank line before (nlreturn) #12 9.050 game/zzneg_rework_check.go:6:7: integer-format: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint) #12 9.050 game/zzneg_rework_check.go:5:6: func reworkNegativeControl is unused (unused) #12 9.050 3 issues: #12 9.050 * nlreturn: 1 #12 9.050 * perfsprint: 1 #12 9.050 * unused: 1 EXIT_WITH_VIOLATION=1 ``` `make check` with the same violation exits 2 and stops before the tests (zero `ok` lines). After deleting the file, `EXIT_AFTER_REVERT=0`, `0 issues.` **Dangling images.** Raw before/after counts are useless on this host — it gained images from other sessions during every window I measured — so I diffed the image IDs and inspected each new one. Across a `script/lint` run: 4 new dangling images appeared, all `cmd=[python3]` or `cmd=[node]`, none from this build. Across the full `make check`: 2 new, both `cmd=[python3]`. To prove the measurement could actually detect one, I ran a positive control — same script with `--output=type=cacheonly` removed: ``` 02513b304259 cmd=[golangci-lint] gover=GOTOOLCHAIN=auto #13 exporting to image #13 exporting layers #13 exporting layers 5.3s done ``` Exactly one new dangling image with the `golangci-lint` signature, plus 5.3s of layer export. With the flag, zero `exporting to image` steps in any run. I deleted that one control image by ID (`docker rmi 02513b304259`, targeted, its own image only). **No prune of any kind was run at any point.** ## 3. Finding 2 (low) — `TODO.md` line 736 marked superseded in place The 2026-08-07 entry keeps its original text; a superseded note is appended pointing at the 2026-08-10 entry and https://git.eeqj.de/sneak/rgoue/issues/41 . History not rewritten. ## 4. Minor — `.dockerignore` named in the exemption note Future Steps note 2 now reads "`Dockerfile.lint` and `script/lint` ... along with the `.dockerignore` that scopes their build context", and "those three files only". ## 5. Minor — the three-way timing disagreement Resolved by deleting the numbers from `TODO.md` prose rather than picking a set. All three measurements were honest; wall clock varies per host and per run, so a specific figure there is a hostage to fortune in exactly the way hard-coded test counts were in https://git.eeqj.de/sneak/rgoue/issues/22 . The entry now states the durable property instead: the lint stage executes on every run and is never served from cache. The commit message and PR body history are left alone. ## Two consecutive runs, all three flags, unchanged tree **Run 1:** ``` #9 [lint 1/2] COPY . . #9 DONE 0.5s #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 7.297 0 issues. #10 DONE 10.9s real 0m11.630s ``` **Run 2, nothing touched in between:** ``` #9 [deps 4/4] RUN go mod download #9 CACHED #10 [lint 1/2] COPY . . #10 DONE 2.3s #11 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #11 7.869 0 issues. #11 DONE 9.9s real 0m12.517s ``` `DONE` both times, never `CACHED`, `deps` cached both times, and no export step in either. **One methodology correction worth recording**, because it would have quietly weakened this evidence: my first attempt kept scratch logs in a directory *inside* the repo, which put them in the build context and invalidated `COPY . .` on every run. The lint stage then re-executes because the context changed, not because `--no-cache-filter` did its job — a passing result that proves nothing. Scratch moved outside the tree; the runs above have `COPY . .` and everything above it stable, so the lint layer re-executing is attributable to the filter alone. ## `make check` green in full Go's test cache served `(cached)` on a first attempt, which is not evidence, so this is a forced uncached run through the make target (`GOFLAGS=-count=1 make check`) — real durations: ``` #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 7.188 0 issues. ok git.eeqj.de/sneak/rgoue/cmd/rogue 1.039s coverage: 29.7% of statements ok git.eeqj.de/sneak/rgoue/game 2.882s coverage: 64.0% of statements git.eeqj.de/sneak/rgoue/term coverage: 0.0% of statements MAKE_CHECK_EXIT=0 ``` Lint layer executed, not `CACHED`. `make fmt` run and committed. `.golangci.yml` untouched; no CI config, no `REPO_POLICIES.md`, no further `script/` entrypoint. `sh -n` and `dash -n` clean, mode `100755` preserved. The port of both flags back into `sneak/homoicon` before the shape is cloned further is the manager's to open and is not in this commit.
Author
Collaborator

Independent adversarial review of the rework commit 329c03f, with the verdict owned for the whole next branch (599286a + 329c03f). Fresh clone of my own; every pasted result on this PR was treated as an unverified claim and re-run.

Verdict: PASS. One low finding below (an inaccurate claim in a comment, plus the residual hazard it papers over). Nothing here is a present false green.

Findings

1 (low, accuracy + residual hazard). script/lint lines 18-19 and TODO.md claim the two flags "validate each other's magic string". They do not — only --target's string is validated.

--target checks its own argument against the stage list. --no-cache-filter's argument is still unchecked, so the guard covers three of the four edit paths and misses one:

  • stage renamed, neither flag updated -> --target fails loudly. Caught.
  • stage renamed, only the filter updated -> --target fails loudly. Caught.
  • stage renamed, both updated -> correct.
  • stage renamed or the filter mistyped, --target still correct -> silent no-op, unchanged.

Reproduced in this repo on a genuinely unchanged tree, one transposed character in the filter only:

docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint .
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 CACHED
EXIT_CASE_C=0

Green, exit 0, linted nothing — the original defect class, surviving in the narrow case. Why it matters beyond pedantry: the prose instruction two lines above ("both must keep naming the stage that Dockerfile.lint calls lint") is the thing actually holding this together, and the "validate each other" sentence tells a future editor the tooling has their back when it does not. This shape is about to be ported into sneak/homoicon and cloned onward, so the overclaim propagates with it.

What acceptable looks like: state the property truthfully — --target validates the stage name and converts a rename into a hard error; the filter's copy of the name is guarded only by keeping the two strings identical, which is the editor's job. Same correction in the TODO.md "Hardened 2026-08-10" paragraph, which repeats it. Dockerfile.lint lines 22-23 are already correct as written.

Reproduced, not trusted

  • Silent no-op the --target flag guards against — real. Bogus filter name, no --target: RUN golangci-lint run ... CACHED, exit 0. With --target nosuchstage: ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found, exit 1. Both on the committed tree with no file changed; tree clean after.
  • Two consecutive script/lint runs, unchanged tree, all three flags. Lint layer DONE 15.4s / DONE 15.4s, 0 issues. after ~15s of real linting each. Never CACHED. deps layers CACHED both times.
  • Attribution of that re-execution — methodology note. COPY . . cannot report CACHED here and its doing so would not be the evidence: it lives inside the cache-busted lint stage, so --no-cache-filter=lint re-runs it by construction (the rework's own paste shows DONE 0.5s / DONE 2.3s for the same reason). I substituted a stronger control — the identical build with --no-cache-filter removed, tree untouched, immediately after: COPY . . CACHED and RUN golangci-lint run ... CACHED. So the context was genuinely stable and the cache would have served the lint result; the re-execution under the flag is attributable to the flag alone. Build context transferred 2.79kB identically across both runs. My scratch lived outside the repo throughout and git status was empty before and after every run.
  • Negative control of my own (goconst, not indent-error-flow / errcheck / fmt.Sprintf): three repeated string literals in a new game/ file, under all three flags -> exit 1 naming it exactly — game/zz_w41rev2_probe.go:5:10: string ... has 3 occurrences, make it a constant (goconst) plus unused. make check with it present exits 2 with zero ok lines. File deleted -> 0 issues., exit 0. --output=type=cacheonly masks nothing, and zero exporting to image steps in the failing run.
  • Dangling images. Raw counts are worthless here — a concurrent session on this host is producing a cmd=[golangci-lint] image roughly every 58s, and two of them landed 0.5s and 2s before my snapshot windows opened. Diffed image IDs and inspected each: no image created inside any script/lint window, and zero exporting to image steps in any run under the flag. Positive control, same build with cacheonly removed: exporting to image / exporting layers 2.8s and naming to moby-dangling@sha256:5d1e3e3d… — exactly one dangling image, so the measurement can detect one. I deleted that one image by ID (docker rmi sha256:5d1e3e3d…), mine only, and left the concurrent session's alone. No prune of any kind at any point.
  • make check green in full, forced uncached through the make target (GOFLAGS=-count=1): lint layer executed (0 issues. after 7.2s), ok cmd/rogue 1.015s / ok game 3.340s — real durations, no (cached).
  • DOCKER_BUILDKIT=0 ./script/lint still exits 125 and fails loudly with the added flags.
  • Scope exactly Dockerfile.lint, script/lint, TODO.md (rework) — script/ contains lint and nothing else; no CI config, no REPO_POLICIES.md. .golangci.yml untouched by both commits. No host golangci-lint path anywhere. dash -n / sh -n clean, mode 100755. fmt-check clean (it gates make check). Titles: 599286a ends exactly (closes #41), 329c03f correctly carries none. No trailers, no vendor references anywhere in either commit, tree, or PR body. next is 2 ahead / 0 behind origin/main, git merge-tree conflict-free.
  • Every remaining TODO.md statement checks out, including the superseded-in-place note at line 749 (original text intact, pointer appended) and the exemption note now naming .dockerignore as the third file.

Anomalies that pass anyway

  • The Dockerfile.lint comment warns against renaming the stage but not against appending one. With --target lint, any stage added after lint is silently skipped. Speculative today (there are only the two stages and no reason to add a third) — noted, not a defect.
  • #5 transferring context: 2.79kB on repeat runs vs 835.32kB on the cold one confirms .dockerignore and the context are behaving; the prior review's cosmetic point about build artifacts not being excluded still stands and is still cosmetic.

Could not fully verify

  • The sneak/homoicon byte-identity claim for the tag/digest pair — no access to that repo from here. Indirect support only: the registry accepted the v2.12.2@sha256:5cce… pair on every pull, and the pulled image's labels report image.version: 2.12.2.
  • The absence of a prune during the rework rests on the PR record and the reworker's statement; host-wide docker history is not auditable from here. I ran none myself.
  • Gitea Actions is not queryable as clawbot (403). There is no CI to be green — the repo carries no workflow files by standing exemption and this branch correctly adds none.
Independent adversarial review of the rework commit `329c03f`, with the verdict owned for the whole `next` branch (`599286a` + `329c03f`). Fresh clone of my own; every pasted result on this PR was treated as an unverified claim and re-run. **Verdict: PASS.** One low finding below (an inaccurate claim in a comment, plus the residual hazard it papers over). Nothing here is a present false green. ## Findings **1 (low, accuracy + residual hazard). `script/lint` lines 18-19 and `TODO.md` claim the two flags "validate each other's magic string". They do not — only `--target`'s string is validated.** `--target` checks its own argument against the stage list. `--no-cache-filter`'s argument is still unchecked, so the guard covers three of the four edit paths and misses one: - stage renamed, neither flag updated -> `--target` fails loudly. Caught. - stage renamed, only the filter updated -> `--target` fails loudly. Caught. - stage renamed, both updated -> correct. - **stage renamed or the filter mistyped, `--target` still correct -> silent no-op, unchanged.** Reproduced in this repo on a genuinely unchanged tree, one transposed character in the filter only: ``` docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint . #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 CACHED EXIT_CASE_C=0 ``` Green, exit 0, linted nothing — the original defect class, surviving in the narrow case. Why it matters beyond pedantry: the prose instruction two lines above ("both must keep naming the stage that `Dockerfile.lint` calls `lint`") is the thing actually holding this together, and the "validate each other" sentence tells a future editor the tooling has their back when it does not. This shape is about to be ported into `sneak/homoicon` and cloned onward, so the overclaim propagates with it. What acceptable looks like: state the property truthfully — `--target` validates the stage name and converts a rename into a hard error; the filter's copy of the name is guarded only by keeping the two strings identical, which is the editor's job. Same correction in the `TODO.md` "Hardened 2026-08-10" paragraph, which repeats it. `Dockerfile.lint` lines 22-23 are already correct as written. ## Reproduced, not trusted - **Silent no-op the `--target` flag guards against — real.** Bogus filter name, no `--target`: `RUN golangci-lint run ... CACHED`, exit 0. With `--target nosuchstage`: `ERROR: failed to build: failed to solve: target stage "nosuchstage" could not be found`, exit 1. Both on the committed tree with no file changed; tree clean after. - **Two consecutive `script/lint` runs, unchanged tree, all three flags.** Lint layer `DONE 15.4s` / `DONE 15.4s`, `0 issues.` after ~15s of real linting each. Never `CACHED`. `deps` layers `CACHED` both times. - **Attribution of that re-execution — methodology note.** `COPY . .` cannot report `CACHED` here and its doing so would not be the evidence: it lives *inside* the cache-busted `lint` stage, so `--no-cache-filter=lint` re-runs it by construction (the rework's own paste shows `DONE 0.5s` / `DONE 2.3s` for the same reason). I substituted a stronger control — the identical build with `--no-cache-filter` **removed**, tree untouched, immediately after: `COPY . . CACHED` and `RUN golangci-lint run ... CACHED`. So the context was genuinely stable and the cache *would* have served the lint result; the re-execution under the flag is attributable to the flag alone. Build context transferred 2.79kB identically across both runs. My scratch lived outside the repo throughout and `git status` was empty before and after every run. - **Negative control of my own** (`goconst`, not `indent-error-flow` / `errcheck` / `fmt.Sprintf`): three repeated string literals in a new `game/` file, under all three flags -> exit 1 naming it exactly — `game/zz_w41rev2_probe.go:5:10: string ... has 3 occurrences, make it a constant (goconst)` plus `unused`. `make check` with it present exits 2 with zero `ok` lines. File deleted -> `0 issues.`, exit 0. **`--output=type=cacheonly` masks nothing**, and zero `exporting to image` steps in the failing run. - **Dangling images.** Raw counts are worthless here — a concurrent session on this host is producing a `cmd=[golangci-lint]` image roughly every 58s, and two of them landed 0.5s and 2s *before* my snapshot windows opened. Diffed image IDs and inspected each: no image created inside any `script/lint` window, and zero `exporting to image` steps in any run under the flag. Positive control, same build with `cacheonly` removed: `exporting to image` / `exporting layers 2.8s` and `naming to moby-dangling@sha256:5d1e3e3d…` — exactly one dangling image, so the measurement can detect one. I deleted that one image by ID (`docker rmi sha256:5d1e3e3d…`), mine only, and left the concurrent session's alone. **No prune of any kind at any point.** - **`make check` green in full**, forced uncached through the make target (`GOFLAGS=-count=1`): lint layer executed (`0 issues.` after 7.2s), `ok cmd/rogue 1.015s` / `ok game 3.340s` — real durations, no `(cached)`. - `DOCKER_BUILDKIT=0 ./script/lint` still exits 125 and fails loudly with the added flags. - Scope exactly `Dockerfile.lint`, `script/lint`, `TODO.md` (rework) — `script/` contains `lint` and nothing else; no CI config, no `REPO_POLICIES.md`. `.golangci.yml` untouched by both commits. No host `golangci-lint` path anywhere. `dash -n` / `sh -n` clean, mode `100755`. `fmt-check` clean (it gates `make check`). Titles: `599286a` ends exactly ` (closes #41)`, `329c03f` correctly carries none. No trailers, no vendor references anywhere in either commit, tree, or PR body. `next` is 2 ahead / 0 behind `origin/main`, `git merge-tree` conflict-free. - Every remaining `TODO.md` statement checks out, including the superseded-in-place note at line 749 (original text intact, pointer appended) and the exemption note now naming `.dockerignore` as the third file. ## Anomalies that pass anyway - The `Dockerfile.lint` comment warns against *renaming* the stage but not against *appending* one. With `--target lint`, any stage added after `lint` is silently skipped. Speculative today (there are only the two stages and no reason to add a third) — noted, not a defect. - `#5 transferring context: 2.79kB` on repeat runs vs `835.32kB` on the cold one confirms `.dockerignore` and the context are behaving; the prior review's cosmetic point about build artifacts not being excluded still stands and is still cosmetic. ## Could not fully verify - **The `sneak/homoicon` byte-identity claim** for the tag/digest pair — no access to that repo from here. Indirect support only: the registry accepted the `v2.12.2@sha256:5cce…` pair on every pull, and the pulled image's labels report `image.version: 2.12.2`. - **The absence of a prune during the rework** rests on the PR record and the reworker's statement; host-wide docker history is not auditable from here. I ran none myself. - Gitea Actions is not queryable as `clawbot` (403). There is no CI to be green — the repo carries no workflow files by standing exemption and this branch correctly adds none.
Author
Collaborator

Manager: second review is a PASS, and its one finding is correct — I wrote the claim it is knocking down, in the manager comment above, and it is wrong. --target validates its own argument only; a typo confined to --no-cache-filter still silently no-ops, which the reviewer reproduced with --no-cache-filter=lnit building CACHED at exit 0.

Taking the fix a step further than the reviewer proposed. Correcting the prose would leave a hazard documented but live; the two flags carry the same stage name twice, and duplication is the defect. Hoisting it into one shell variable used by both makes divergence unrepresentable rather than merely warned about, and it is the same one-line class of change. Prose still gets corrected to describe the real property.

Reworking once more, then a third independent review before this merges. Nothing about this shape gets ported to sneak/homoicon until it is settled here.

Manager: second review is a PASS, and its one finding is correct — I wrote the claim it is knocking down, in the manager comment above, and it is wrong. `--target` validates its own argument only; a typo confined to `--no-cache-filter` still silently no-ops, which the reviewer reproduced with `--no-cache-filter=lnit` building `CACHED` at exit 0. Taking the fix a step further than the reviewer proposed. Correcting the prose would leave a hazard documented but live; the two flags carry the same stage name twice, and duplication is the defect. Hoisting it into one shell variable used by both makes divergence unrepresentable rather than merely warned about, and it is the same one-line class of change. Prose still gets corrected to describe the real property. Reworking once more, then a third independent review before this merges. Nothing about this shape gets ported to `sneak/homoicon` until it is settled here.
clawbot added 1 commit 2026-08-10 15:15:10 +02:00
The previous commit claimed --target and --no-cache-filter "validate each
other's magic string". They do not. --target validates only its own
argument; a typo confined to --no-cache-filter left the build green and
linting nothing:

    docker build --target lint --no-cache-filter=lnit ...
    #10 [lint 2/2] RUN golangci-lint run ... CACHED   exit 0

Three of the four edit paths were caught and one was not, so the original
false green survived in the narrow case.

The duplication was the defect: the stage name appeared twice on one
command line and nothing tied the copies together. Correcting only the
prose would have left the hazard live and merely warned about, so the name
is now written once, as `stage=lint`, and passed to both flags. Divergence
is unrepresentable rather than documented — there is a single name to get
wrong, and --target rejects it loudly when it is not a stage in
Dockerfile.lint, which now covers the filter too because it is the same
string.

The comments in script/lint and Dockerfile.lint and the TODO.md entry drop
the false "validate each other" claim and state the real property, along
with the residual hazard that is genuinely unguarded: --target checks that
the name exists, not that it names the stage which actually runs
golangci-lint, and it stops the build there, so relocating the lint step
or appending a stage after it would go unnoticed.
Author
Collaborator

Second rework, 20cfb47 on next (new commit; 599286a and 329c03f untouched, not amended, not force-pushed).

The finding is correct, and the wrong claim was mine

I wrote "the two flags validate each other's magic string" in script/lint and repeated it in TODO.md. It is false. --target validates its own argument against the stage list and nothing else; --no-cache-filter's argument stays unchecked, so the case where the filter alone is mistyped was never covered. The reviewer's --no-cache-filter=lnit reproduction stands — I re-ran it before changing anything, and it built CACHED at exit 0.

That is worse than a documentation error. The sentence told a future editor the tooling had their back in a case where it did not, and this shape is queued to be ported into sneak/homoicon and cloned onward.

What changed: the duplication is gone, not annotated

The stage name appeared twice on one command line with nothing tying the copies together. script/lint now writes it once:

# Must match the stage name in Dockerfile.lint.
stage=lint

main() {
    cd "$ROOT"
    docker build \
        --target "$stage" \
        --no-cache-filter="$stage" \
        --output=type=cacheonly \
        -f Dockerfile.lint .
}

Divergence between the two flags is now unrepresentable rather than warned about. POSIX sh; sh -n and dash -n clean; expansions quoted; mode 100755 preserved. No buildx parsing or quoting trouble — --no-cache-filter="$stage" expands exactly as the literal did.

Proof 1 — the exact case from the finding is closed

The whole point of the commit, so here it is directly. One typo in the single stage name (stage=lnit), unchanged tree otherwise:

ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?)
EXIT_TYPO=1

Non-zero, loud, and zero CACHED lines in the entire build. Previously this exact typo — confined to the filter — produced RUN golangci-lint run ... CACHED at exit 0. Reverted immediately; git status empty after.

Note what actually fixed it: not a new check, but the fact that there is now only one name to get wrong, so the name that reaches --no-cache-filter is the same one --target rejects.

Proof 2 — two consecutive runs, unchanged tree

git status empty before and after both runs; all scratch outside the clone.

RUN 1:  #9  [lint 1/2] COPY . .   DONE 1.6s
        #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
        #10 16.92 0 issues.
        #10 DONE 17.7s     EXIT_RUN1=0

RUN 2:  #9  [lint 1/2] COPY . .   DONE 0.1s
        #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
        #10 13.71 0 issues.
        #10 DONE 13.9s     EXIT_RUN2=0

deps layers CACHED both times, lint layer never CACHED, zero exporting to image steps in either.

Attribution, using the control the second review introduced rather than my own weaker one. The reviewer is right that COPY . . not reporting CACHED proves nothing — it lives inside the cache-busted stage, so the filter re-runs it by construction, and I cited it as evidence in my previous comment when it was not. The valid control is the identical build with --no-cache-filter removed, tree untouched:

#6 transferring context: 2.79kB done
#7 [lint 1/2] COPY . .
#7 CACHED
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 CACHED
EXIT_CONTROL=0

The cache would have served the lint result, so the context was genuinely stable and the re-execution above is attributable to the flag alone. This control had to be a direct docker build invocation: running it through script/lint would require editing script/lint, which is itself in the build context and would invalidate COPY . ., destroying the thing being measured. It is a measurement, not a lint gate — every lint result on this PR came from script/lint or make check.

Proof 3 — negative control, wastedassign (new to this PR)

Not indent-error-flow, errcheck, fmt.Sprintf/perfsprint, or goconst — all used already. A new game/zz_w41_rework3_probe.go with a value assigned and then overwritten unused, under the final flag set:

#10 20.22 game/zz_w41_rework3_probe.go:4:2: assigned to x, but reassigned without using the value (wastedassign)
#10 20.22 game/zz_w41_rework3_probe.go:5:10: Magic number: 2, in <operation> detected (mnd)
#10 20.22 game/zz_w41_rework3_probe.go:3:6: func w41Rework3Probe is unused (unused)
#10 20.22 3 issues:
#10 20.22 * mnd: 1
#10 20.22 * unused: 1
#10 20.22 * wastedassign: 1
EXIT_WITH_VIOLATION=1

make check with it present exits 2 with zero ok lines. Zero exporting to image steps in the failing run — --output=type=cacheonly still masks nothing. File deleted, git status empty, 0 issues. at exit 0.

Proof 4 — make check green in full

Forced uncached through the make target (GOFLAGS=-count=1 make check), git status empty before and after:

#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 14.92 0 issues.
ok      git.eeqj.de/sneak/rgoue/cmd/rogue       1.046s  coverage: 29.7% of statements
ok      git.eeqj.de/sneak/rgoue/game            3.505s  coverage: 64.0% of statements
        git.eeqj.de/sneak/rgoue/term            coverage: 0.0% of statements
MAKE_CHECK_EXIT=0

Lint layer executed, not CACHED. Real durations, no (cached). No prune of any kind at any point.

Prose corrected, and one place where I did not follow the brief literally

script/lint, Dockerfile.lint and the TODO.md entry now state the property that is true — the name is defined once and shared, so --target rejecting a bad name covers the filter too because it is the same string — and the "validate each other" claim is gone rather than softened. The TODO.md correction is a new dated paragraph recording that the earlier claim was wrong and why, not a rewrite of it.

The brief asked me to record the cross-file link between $stage and Dockerfile.lint as "a real, unguarded seam". I did not write that, because after the change it is not accurate and I am not willing to replace one overclaim with an underclaim: if the stage in Dockerfile.lint is renamed and $stage is not, --target fails loudly, so that mismatch is guarded. The genuinely unguarded residual is different, and is what the files now say: --target verifies the name exists, not that it names the stage that actually runs golangci-lint, and it stops the build at that stage — so relocating the lint step into another stage, or adding a stage after lint, would pass unnoticed. That last case is the second reviewer's "appends a stage" anomaly, which this makes explicit in both files.

Scope unchanged: script/lint, Dockerfile.lint, TODO.md. .golangci.yml untouched, no CI config, no REPO_POLICIES.md, no further script/ entrypoint. make fmt run and committed.

Second rework, `20cfb47` on `next` (new commit; `599286a` and `329c03f` untouched, not amended, not force-pushed). ## The finding is correct, and the wrong claim was mine I wrote "the two flags validate each other's magic string" in `script/lint` and repeated it in `TODO.md`. It is false. `--target` validates its own argument against the stage list and nothing else; `--no-cache-filter`'s argument stays unchecked, so the case where the filter alone is mistyped was never covered. The reviewer's `--no-cache-filter=lnit` reproduction stands — I re-ran it before changing anything, and it built `CACHED` at exit 0. That is worse than a documentation error. The sentence told a future editor the tooling had their back in a case where it did not, and this shape is queued to be ported into `sneak/homoicon` and cloned onward. ## What changed: the duplication is gone, not annotated The stage name appeared twice on one command line with nothing tying the copies together. `script/lint` now writes it once: ``` # Must match the stage name in Dockerfile.lint. stage=lint main() { cd "$ROOT" docker build \ --target "$stage" \ --no-cache-filter="$stage" \ --output=type=cacheonly \ -f Dockerfile.lint . } ``` Divergence between the two flags is now unrepresentable rather than warned about. POSIX `sh`; `sh -n` and `dash -n` clean; expansions quoted; mode `100755` preserved. No buildx parsing or quoting trouble — `--no-cache-filter="$stage"` expands exactly as the literal did. ## Proof 1 — the exact case from the finding is closed The whole point of the commit, so here it is directly. One typo in the single stage name (`stage=lnit`), unchanged tree otherwise: ``` ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?) EXIT_TYPO=1 ``` Non-zero, loud, and zero `CACHED` lines in the entire build. Previously this exact typo — confined to the filter — produced `RUN golangci-lint run ... CACHED` at exit 0. Reverted immediately; `git status` empty after. Note what actually fixed it: not a new check, but the fact that there is now only one name to get wrong, so the name that reaches `--no-cache-filter` is the same one `--target` rejects. ## Proof 2 — two consecutive runs, unchanged tree `git status` empty before and after both runs; all scratch outside the clone. ``` RUN 1: #9 [lint 1/2] COPY . . DONE 1.6s #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 16.92 0 issues. #10 DONE 17.7s EXIT_RUN1=0 RUN 2: #9 [lint 1/2] COPY . . DONE 0.1s #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 13.71 0 issues. #10 DONE 13.9s EXIT_RUN2=0 ``` `deps` layers `CACHED` both times, lint layer never `CACHED`, zero `exporting to image` steps in either. **Attribution, using the control the second review introduced rather than my own weaker one.** The reviewer is right that `COPY . .` not reporting `CACHED` proves nothing — it lives *inside* the cache-busted stage, so the filter re-runs it by construction, and I cited it as evidence in my previous comment when it was not. The valid control is the identical build with `--no-cache-filter` removed, tree untouched: ``` #6 transferring context: 2.79kB done #7 [lint 1/2] COPY . . #7 CACHED #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 CACHED EXIT_CONTROL=0 ``` The cache *would* have served the lint result, so the context was genuinely stable and the re-execution above is attributable to the flag alone. This control had to be a direct `docker build` invocation: running it through `script/lint` would require editing `script/lint`, which is itself in the build context and would invalidate `COPY . .`, destroying the thing being measured. It is a measurement, not a lint gate — every lint result on this PR came from `script/lint` or `make check`. ## Proof 3 — negative control, `wastedassign` (new to this PR) Not `indent-error-flow`, `errcheck`, `fmt.Sprintf`/`perfsprint`, or `goconst` — all used already. A new `game/zz_w41_rework3_probe.go` with a value assigned and then overwritten unused, under the final flag set: ``` #10 20.22 game/zz_w41_rework3_probe.go:4:2: assigned to x, but reassigned without using the value (wastedassign) #10 20.22 game/zz_w41_rework3_probe.go:5:10: Magic number: 2, in <operation> detected (mnd) #10 20.22 game/zz_w41_rework3_probe.go:3:6: func w41Rework3Probe is unused (unused) #10 20.22 3 issues: #10 20.22 * mnd: 1 #10 20.22 * unused: 1 #10 20.22 * wastedassign: 1 EXIT_WITH_VIOLATION=1 ``` `make check` with it present exits 2 with zero `ok` lines. Zero `exporting to image` steps in the failing run — `--output=type=cacheonly` still masks nothing. File deleted, `git status` empty, `0 issues.` at exit 0. ## Proof 4 — `make check` green in full Forced uncached through the make target (`GOFLAGS=-count=1 make check`), `git status` empty before and after: ``` #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 14.92 0 issues. ok git.eeqj.de/sneak/rgoue/cmd/rogue 1.046s coverage: 29.7% of statements ok git.eeqj.de/sneak/rgoue/game 3.505s coverage: 64.0% of statements git.eeqj.de/sneak/rgoue/term coverage: 0.0% of statements MAKE_CHECK_EXIT=0 ``` Lint layer executed, not `CACHED`. Real durations, no `(cached)`. **No prune of any kind at any point.** ## Prose corrected, and one place where I did not follow the brief literally `script/lint`, `Dockerfile.lint` and the `TODO.md` entry now state the property that is true — the name is defined once and shared, so `--target` rejecting a bad name covers the filter too because it is the same string — and the "validate each other" claim is gone rather than softened. The `TODO.md` correction is a new dated paragraph recording that the earlier claim was wrong and why, not a rewrite of it. The brief asked me to record the cross-file link between `$stage` and `Dockerfile.lint` as "a real, unguarded seam". I did not write that, because after the change it is not accurate and I am not willing to replace one overclaim with an underclaim: if the stage in `Dockerfile.lint` is renamed and `$stage` is not, `--target` fails loudly, so that mismatch **is** guarded. The genuinely unguarded residual is different, and is what the files now say: `--target` verifies the name exists, not that it names the stage that actually runs `golangci-lint`, and it stops the build at that stage — so relocating the lint step into another stage, or adding a stage after `lint`, would pass unnoticed. That last case is the second reviewer's "appends a stage" anomaly, which this makes explicit in both files. Scope unchanged: `script/lint`, `Dockerfile.lint`, `TODO.md`. `.golangci.yml` untouched, no CI config, no `REPO_POLICIES.md`, no further `script/` entrypoint. `make fmt` run and committed.
Author
Collaborator

Third independent adversarial review, of 20cfb47, with the verdict owned for the whole next branch (599286a + 329c03f + 20cfb47). Fresh clone of my own; every pasted result on this PR and on #41 was treated as an unverified claim and re-run. git status empty around every evidence run; all scratch outside the clone.

Verdict: PASS. Two low findings, neither a present false green.

The central claim holds

stage=lnit, tree otherwise unchanged, through script/lint:

ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?)
EXIT_TYPO=1

Exit 1, zero CACHED lines in the entire build. Reverted, clean.

I also confirmed the premise independently rather than taking it from the previous review — the pre-fix shape, via a direct docker build, is genuinely silent:

docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint .
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 CACHED
EXIT=0

No warning, no diagnostic. The defect was real and it is closed.

Adjudication: the reworker was right to refuse the requested wording

The brief asked for the $stage <-> Dockerfile.lint link to be recorded as "a real, unguarded seam"; the reworker declined, arguing that would be an underclaim. I tested it. Renaming FROM deps AS lint to FROM deps AS lintstage while leaving stage=lint:

ERROR: failed to build: failed to solve: target stage "lint" could not be found
EXIT=1

Exit 1, zero CACHED. That mismatch is guarded. The refusal was correct and the files say the accurate thing.

The residual the files do claim is unguarded is accurate, and I reproduced both halves of it:

  • stage=deps (a name that exists but is not the lint stage): exit 0, golangci-lint never invoked, zero occurrences in the build log.
  • appending FROM lint AS verify / RUN ... && false after lint: exit 0, the appended stage silently skipped by --target, lint still ran.

So the characterisation is accurate. On completeness, see finding 2.

Attempts to defeat it that failed

  • .dockerignore += game/: fails loudly, exit 1, the build errors on the missing package rather than passing green.
  • DOCKER_BUILDKIT / flag-order / quoting variants: --no-cache-filter="$stage" expands identically to the literal; dash -n and sh -n clean.
  • Deleting --output=type=cacheonly masks nothing (see negative control).

Reproduced, not trusted

  • Two consecutive runs, unchanged tree: lint layer DONE 7.5s then DONE 7.8s, 0 issues. each, never CACHED; deps layers CACHED both times.
  • Attribution control, run by me on the identical tree, --no-cache-filter removed: COPY . . CACHED and RUN golangci-lint ... CACHED, exit 0, context 2.79kB both ways. Cache would have served the lint, so re-execution under the flag is attributable to the flag alone. The reworker's use of a direct docker build for this measurement is sound: script/lint is itself in the build context (.dockerignore excludes only .git), so editing it to drop the flag would invalidate COPY . . and destroy the thing being measured. Every lint gate on this branch still runs through script/lint.
  • Negative control, mine (misspell + unconvert — not indent-error-flow, errcheck, fmt.Sprintf, goconst or wastedassign): exit 1 naming them exactly (game/zz_w41rev3_probe.go:3:54: 'recieve' is a misspelling of 'receive' (misspell), :5:15: unnecessary conversion (unconvert), plus unused), zero exporting to image steps in the failing run. File removed, 0 issues., exit 0.
  • make check green in full, forced uncached (GOFLAGS=-count=1): lint layer executed (0 issues. after 11.2s), ok cmd/rogue 1.017s / ok game 3.266s, real 0m25.892s. No (cached).
  • Dangling images: one new image inside a tight window around a script/lint run, cmd=[node], from a concurrent session — not this build; zero exporting to image steps. Positive control with the export enabled produced exactly one cmd=[golangci-lint] image, proving the measurement can detect one; deleted by ID, mine only. No prune of any kind at any point.
  • Static: 599286a title ends exactly (closes #41), the other two correctly carry none; mode 100755, expansions quoted, POSIX clean; make fmt idempotent and make fmt-check exit 0; next 3 ahead / 0 behind origin/main, git merge-tree conflict-free; scope is Dockerfile.lint, script/lint, .dockerignore, Makefile, README.md, TODO.md only, script/ holds lint and nothing else, no CI config, no REPO_POLICIES.md; .golangci.yml untouched by all three commits; no host golangci-lint path anywhere; no vendor references and no attribution trailers in any commit, the tree, or the PR body; TODO.md superseded note intact with a pointer appended, exemption note naming all three files, no volatile timings, prettier-clean, all issue references full URLs. PR body accurate for all three commits with nothing previously disclosed dropped.

Every statement in script/lint, Dockerfile.lint and the TODO.md entry was checked line by line against observed behaviour; all are literally true, except as below.

Findings

1 (low, prose — ambiguous in a file about to be ported verbatim). script/lint line ~10: "Both flags below must stay: do not 'simplify' either one away."

The sentence now sits at the end of the paragraph that names only --no-cache-filter; --target is not introduced until the following paragraph, and there are three flags in the command below. In 329c03f this instruction was anchored by "--target lint and --no-cache-filter=lint must BOTH be present"; 20cfb47 removed that anchor, leaving "both" as an unresolved forward reference. A reader could take it as --no-cache-filter + --output=type=cacheonly, which is the wrong pair — only the first two are load-bearing for correctness. Not wrong, but under-specified in the one file whose value is that it is exactly true. Acceptable: name the two flags in the sentence.

2 (low, disclosure completeness). The residual-hazard note is accurate but scoped to the $stage / Dockerfile.lint seam; .dockerignore is a third file in the same trust boundary and is not named.

.dockerignore decides what the linter can see, and narrowing it produces a silent partial lint with no diagnostic. Demonstrated with the violating probe file still present in the working tree and only its path appended to .dockerignore:

#10 11.21 0 issues.
EXIT=0

Green, exit 0, with a real misspell/unconvert violation sitting in the tree. This is not introduced by 20cfb47 — it is inherent to the shape from 599286a and has passed two prior reviews — and it needs a deliberate edit. It is worth naming anyway for two reasons: the first review recommended adding build artifacts (/rogue, *.test, *.out) to .dockerignore, so this file is expected to be edited; and the shape is queued for porting. Acceptable: one clause in the same "what the tooling does NOT check" block noting that .dockerignore can narrow what is linted and must not exclude anything golangci-lint should see.

Anomaly that passes

TODO.md's new "Corrected 2026-08-10" paragraph says "the claim first made here that the two flags 'validate each other'", but that sentence was deleted from the "Hardened" paragraph in the same commit, so "here" points at text a reader can no longer find. Defensible as a historical reference, and removing a false claim from live guidance rather than leaving it standing is the right call — noted, not a defect.

Could not verify

  • The sneak/homoicon byte-identity claim for the tag/digest pair — no access to that repo from here.
  • Absence of a prune during earlier reworks rests on the PR record; host-wide docker history is not auditable from here. I ran none.
  • Gitea Actions is not queryable as clawbot (403). There is no CI to be green: the repo carries no workflow files by standing exemption and this branch correctly adds none.
Third independent adversarial review, of `20cfb47`, with the verdict owned for the whole `next` branch (`599286a` + `329c03f` + `20cfb47`). Fresh clone of my own; every pasted result on this PR and on https://git.eeqj.de/sneak/rgoue/issues/41 was treated as an unverified claim and re-run. `git status` empty around every evidence run; all scratch outside the clone. **Verdict: PASS.** Two low findings, neither a present false green. ## The central claim holds `stage=lnit`, tree otherwise unchanged, through `script/lint`: ``` ERROR: failed to build: failed to solve: target stage "lnit" could not be found (did you mean lint?) EXIT_TYPO=1 ``` Exit 1, zero `CACHED` lines in the entire build. Reverted, clean. I also confirmed the premise independently rather than taking it from the previous review — the pre-fix shape, via a direct `docker build`, is genuinely silent: ``` docker build --target lint --no-cache-filter=lnit --output=type=cacheonly -f Dockerfile.lint . #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 CACHED EXIT=0 ``` No warning, no diagnostic. The defect was real and it is closed. ## Adjudication: the reworker was right to refuse the requested wording The brief asked for the `$stage` &lt;-&gt; `Dockerfile.lint` link to be recorded as "a real, unguarded seam"; the reworker declined, arguing that would be an underclaim. I tested it. Renaming `FROM deps AS lint` to `FROM deps AS lintstage` while leaving `stage=lint`: ``` ERROR: failed to build: failed to solve: target stage "lint" could not be found EXIT=1 ``` Exit 1, zero `CACHED`. That mismatch **is** guarded. The refusal was correct and the files say the accurate thing. The residual the files *do* claim is unguarded is accurate, and I reproduced both halves of it: - `stage=deps` (a name that exists but is not the lint stage): exit 0, `golangci-lint` never invoked, zero occurrences in the build log. - appending `FROM lint AS verify` / `RUN ... && false` after `lint`: exit 0, the appended stage silently skipped by `--target`, lint still ran. So the characterisation is accurate. On completeness, see finding 2. ## Attempts to defeat it that failed - `.dockerignore` += `game/`: fails loudly, exit 1, the build errors on the missing package rather than passing green. - `DOCKER_BUILDKIT` / flag-order / quoting variants: `--no-cache-filter="$stage"` expands identically to the literal; `dash -n` and `sh -n` clean. - Deleting `--output=type=cacheonly` masks nothing (see negative control). ## Reproduced, not trusted - **Two consecutive runs, unchanged tree:** lint layer `DONE 7.5s` then `DONE 7.8s`, `0 issues.` each, never `CACHED`; `deps` layers `CACHED` both times. - **Attribution control**, run by me on the identical tree, `--no-cache-filter` removed: `COPY . . CACHED` and `RUN golangci-lint ... CACHED`, exit 0, context `2.79kB` both ways. Cache *would* have served the lint, so re-execution under the flag is attributable to the flag alone. The reworker's use of a direct `docker build` for this measurement is **sound**: `script/lint` is itself in the build context (`.dockerignore` excludes only `.git`), so editing it to drop the flag would invalidate `COPY . .` and destroy the thing being measured. Every lint gate on this branch still runs through `script/lint`. - **Negative control, mine** (`misspell` + `unconvert` — not `indent-error-flow`, `errcheck`, `fmt.Sprintf`, `goconst` or `wastedassign`): exit 1 naming them exactly (`game/zz_w41rev3_probe.go:3:54: 'recieve' is a misspelling of 'receive' (misspell)`, `:5:15: unnecessary conversion (unconvert)`, plus `unused`), zero `exporting to image` steps in the failing run. File removed, `0 issues.`, exit 0. - **`make check` green in full**, forced uncached (`GOFLAGS=-count=1`): lint layer executed (`0 issues.` after 11.2s), `ok cmd/rogue 1.017s` / `ok game 3.266s`, `real 0m25.892s`. No `(cached)`. - **Dangling images:** one new image inside a tight window around a `script/lint` run, `cmd=[node]`, from a concurrent session — not this build; zero `exporting to image` steps. Positive control with the export enabled produced exactly one `cmd=[golangci-lint]` image, proving the measurement can detect one; deleted by ID, mine only. **No prune of any kind at any point.** - Static: `599286a` title ends exactly ` (closes #41)`, the other two correctly carry none; mode `100755`, expansions quoted, POSIX clean; `make fmt` idempotent and `make fmt-check` exit 0; `next` 3 ahead / 0 behind `origin/main`, `git merge-tree` conflict-free; scope is `Dockerfile.lint`, `script/lint`, `.dockerignore`, `Makefile`, `README.md`, `TODO.md` only, `script/` holds `lint` and nothing else, no CI config, no `REPO_POLICIES.md`; `.golangci.yml` untouched by all three commits; no host `golangci-lint` path anywhere; no vendor references and no attribution trailers in any commit, the tree, or the PR body; `TODO.md` superseded note intact with a pointer appended, exemption note naming all three files, no volatile timings, prettier-clean, all issue references full URLs. PR body accurate for all three commits with nothing previously disclosed dropped. Every statement in `script/lint`, `Dockerfile.lint` and the `TODO.md` entry was checked line by line against observed behaviour; all are literally true, except as below. ## Findings **1 (low, prose — ambiguous in a file about to be ported verbatim). `script/lint` line ~10: "Both flags below must stay: do not 'simplify' either one away."** The sentence now sits at the end of the paragraph that names only `--no-cache-filter`; `--target` is not introduced until the following paragraph, and there are *three* flags in the command below. In `329c03f` this instruction was anchored by "`--target lint` and `--no-cache-filter=lint` must BOTH be present"; `20cfb47` removed that anchor, leaving "both" as an unresolved forward reference. A reader could take it as `--no-cache-filter` + `--output=type=cacheonly`, which is the wrong pair — only the first two are load-bearing for correctness. Not wrong, but under-specified in the one file whose value is that it is exactly true. Acceptable: name the two flags in the sentence. **2 (low, disclosure completeness). The residual-hazard note is accurate but scoped to the `$stage` / `Dockerfile.lint` seam; `.dockerignore` is a third file in the same trust boundary and is not named.** `.dockerignore` decides what the linter can see, and narrowing it produces a silent partial lint with no diagnostic. Demonstrated with the violating probe file still present in the working tree and only its path appended to `.dockerignore`: ``` #10 11.21 0 issues. EXIT=0 ``` Green, exit 0, with a real `misspell`/`unconvert` violation sitting in the tree. This is not introduced by `20cfb47` — it is inherent to the shape from `599286a` and has passed two prior reviews — and it needs a deliberate edit. It is worth naming anyway for two reasons: the first review recommended adding build artifacts (`/rogue`, `*.test`, `*.out`) to `.dockerignore`, so this file is expected to be edited; and the shape is queued for porting. Acceptable: one clause in the same "what the tooling does NOT check" block noting that `.dockerignore` can narrow what is linted and must not exclude anything `golangci-lint` should see. ## Anomaly that passes `TODO.md`'s new "Corrected 2026-08-10" paragraph says "the claim first made here that the two flags 'validate each other'", but that sentence was deleted from the "Hardened" paragraph in the same commit, so "here" points at text a reader can no longer find. Defensible as a historical reference, and removing a false claim from live guidance rather than leaving it standing is the right call — noted, not a defect. ## Could not verify - The `sneak/homoicon` byte-identity claim for the tag/digest pair — no access to that repo from here. - Absence of a prune during earlier reworks rests on the PR record; host-wide docker history is not auditable from here. I ran none. - Gitea Actions is not queryable as `clawbot` (403). There is no CI to be green: the repo carries no workflow files by standing exemption and this branch correctly adds none.
clawbot added 1 commit 2026-08-10 15:30:45 +02:00
Comments and documentation only; the docker build invocation and its flags
are untouched, and .dockerignore's effective rules are unchanged.

Two prose gaps from review. First, "both flags below must stay" had lost
its anchor: it ended a paragraph naming only --no-cache-filter, --target
was not introduced until the next one, and three flags follow on the
command, so a reader could pick the wrong pair. It now names --target and
--no-cache-filter explicitly.

Second, the list of things the tooling does not check covered the $stage
seam but not .dockerignore, which sits in the same trust boundary and is
the more likely thing to be edited — the first review on this change
actively suggested extending it for build artifacts. Only what reaches the
container is linted, so excluding a Go source there removes it from the
lint with no warning. Verified rather than asserted: a planted violation
plus that one path in .dockerignore yields `0 issues.` at exit 0 with the
violation still in the working tree, while excluding a file other code
still references fails loudly on `undefined:` typecheck errors instead.
The warning is recorded in script/lint alongside the $stage seam and in
.dockerignore itself, where the edit would actually be made.
Author
Collaborator

Third rework, 3eb9f81 on next (new commit; 599286a, 329c03f and 20cfb47 untouched, not amended, not force-pushed). Both low findings accepted; I checked each before writing, and both are real.

Comments and documentation only. The docker build invocation and its flags are byte-identical, and .dockerignore's effective rules are unchanged — proof below.

Finding 1 — "both flags" had lost its anchor

Correct as reported. The sentence ended a paragraph that named only --no-cache-filter, --target was not introduced until the paragraph after, and three flags follow on the command, so "both" pointed at nothing a reader could resolve. Now:

> --no-cache-filter forces the lint stage to re-execute every run ... It and --target must both stay, for the reason in the next paragraph: do not "simplify" either of those two away.

--output=type=cacheonly is deliberately not in that pair — it is an efficiency/hygiene flag, and removing it would not create a false green.

Finding 2 — .dockerignore is in the same trust boundary

Reproduced before writing anything, and the hazard is real. A planted violation in a new game/ file is caught normally (exit 1, wastedassign + mnd + unused). Adding only that file's path to .dockerignore, with the violation still sitting in the working tree:

violation still in tree: game/zz_w41_di_probe.go
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 8.930 0 issues.
EXIT_B_excluded=0

0 issues. at exit 0 — a silent partial lint over an incomplete tree.

I also checked the other half so the comment states the boundary accurately rather than implying every exclusion is silent. Excluding a file the rest of the package still references fails loudly instead:

#10 7.298 game/types.go:355:13: undefined: DiceSpec
#10 7.298 game/object.go:146:13: undefined: DiceSpec
#10 7.298 game/weapons.go:154:9: undefined: DiceSpec
EXIT_C_depended_on_file_excluded=1

So the accurate statement, which is what the files now say: only what reaches the container is linted; excluding a Go source removes it from the lint with no warning, and it surfaces only if other code still references it — a self-contained file drops out silently. Both experiments reverted; git status empty after each.

The warning is recorded in two places: in script/lint, where the "what the tooling does NOT check" block is now a numbered list of two items ($stage, then .dockerignore), and in .dockerignore itself. The second location is beyond the literal brief — you asked for a clause in script/lint — but .dockerignore is where the edit actually gets made, and the first review on this PR recommended extending that file for build artifacts, so a reader who never opens script/lint is exactly the person who needs the warning. It is comment text only; the .git rule is unchanged.

Proof — diff scope

Non-comment changed lines in the two script/config files: none.

$ git diff script/lint | grep -E '^[+-]' | grep -vE '^[+-]#|^[+-][+-]'
(no output)

$ git diff .dockerignore | grep -E '^[+-]' | grep -vE '^[+-]#|^[+-][+-]'
(no output)

Effective ignore rules, before and after — identical:

$ git show HEAD~1:.dockerignore | grep -v '^#' | grep -v '^$'
.git
$ grep -v '^#' .dockerignore | grep -v '^$'
.git

Files touched: script/lint, .dockerignore, TODO.md. make fmt run and committed; sh -n and dash -n clean; mode 100755 preserved.

Proof — script/lint run, lint layer executing

git status empty before and after:

#9  [lint 1/2] COPY . .
#9  DONE 0.5s
#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 13.74 0 issues.
#10 DONE 16.2s
EXIT_LINT=0

Never CACHED.

Proof — make check green in full

Forced uncached through the make target (GOFLAGS=-count=1 make check), git status empty before and after:

#10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./...
#10 11.46 0 issues.
ok      git.eeqj.de/sneak/rgoue/cmd/rogue       1.016s  coverage: 29.7% of statements
ok      git.eeqj.de/sneak/rgoue/game            2.678s  coverage: 64.0% of statements
        git.eeqj.de/sneak/rgoue/term            coverage: 0.0% of statements
MAKE_CHECK_EXIT=0

Lint layer executed, not CACHED; real test durations, no (cached).

TODO.md

No new dated paragraph. The existing "Corrected 2026-08-10" entry already enumerates what is left to the editor and was incomplete without .dockerignore, so the clause was added to that sentence. Nothing else in TODO.md changed.

All scratch stayed outside the clone throughout. No prune of any kind at any point.

Third rework, `3eb9f81` on `next` (new commit; `599286a`, `329c03f` and `20cfb47` untouched, not amended, not force-pushed). Both low findings accepted; I checked each before writing, and both are real. **Comments and documentation only.** The `docker build` invocation and its flags are byte-identical, and `.dockerignore`'s effective rules are unchanged — proof below. ## Finding 1 — "both flags" had lost its anchor Correct as reported. The sentence ended a paragraph that named only `--no-cache-filter`, `--target` was not introduced until the paragraph after, and three flags follow on the command, so "both" pointed at nothing a reader could resolve. Now: &gt; `--no-cache-filter` forces the lint stage to re-execute every run ... It and `--target` must both stay, for the reason in the next paragraph: do not "simplify" either of those two away. `--output=type=cacheonly` is deliberately not in that pair — it is an efficiency/hygiene flag, and removing it would not create a false green. ## Finding 2 — `.dockerignore` is in the same trust boundary Reproduced before writing anything, and the hazard is real. A planted violation in a new `game/` file is caught normally (exit 1, `wastedassign` + `mnd` + `unused`). Adding **only that file's path** to `.dockerignore`, with the violation still sitting in the working tree: ``` violation still in tree: game/zz_w41_di_probe.go #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 8.930 0 issues. EXIT_B_excluded=0 ``` `0 issues.` at exit 0 — a silent partial lint over an incomplete tree. I also checked the other half so the comment states the boundary accurately rather than implying every exclusion is silent. Excluding a file the rest of the package still references fails loudly instead: ``` #10 7.298 game/types.go:355:13: undefined: DiceSpec #10 7.298 game/object.go:146:13: undefined: DiceSpec #10 7.298 game/weapons.go:154:9: undefined: DiceSpec EXIT_C_depended_on_file_excluded=1 ``` So the accurate statement, which is what the files now say: only what reaches the container is linted; excluding a Go source removes it from the lint with no warning, and it surfaces only if other code still references it — a self-contained file drops out silently. Both experiments reverted; `git status` empty after each. The warning is recorded in two places: in `script/lint`, where the "what the tooling does NOT check" block is now a numbered list of two items (`$stage`, then `.dockerignore`), and in `.dockerignore` itself. **The second location is beyond the literal brief** — you asked for a clause in `script/lint` — but `.dockerignore` is where the edit actually gets made, and the first review on this PR recommended extending that file for build artifacts, so a reader who never opens `script/lint` is exactly the person who needs the warning. It is comment text only; the `.git` rule is unchanged. ## Proof — diff scope Non-comment changed lines in the two script/config files: none. ``` $ git diff script/lint | grep -E '^[+-]' | grep -vE '^[+-]#|^[+-][+-]' (no output) $ git diff .dockerignore | grep -E '^[+-]' | grep -vE '^[+-]#|^[+-][+-]' (no output) ``` Effective ignore rules, before and after — identical: ``` $ git show HEAD~1:.dockerignore | grep -v '^#' | grep -v '^$' .git $ grep -v '^#' .dockerignore | grep -v '^$' .git ``` Files touched: `script/lint`, `.dockerignore`, `TODO.md`. `make fmt` run and committed; `sh -n` and `dash -n` clean; mode `100755` preserved. ## Proof — `script/lint` run, lint layer executing `git status` empty before and after: ``` #9 [lint 1/2] COPY . . #9 DONE 0.5s #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 13.74 0 issues. #10 DONE 16.2s EXIT_LINT=0 ``` Never `CACHED`. ## Proof — `make check` green in full Forced uncached through the make target (`GOFLAGS=-count=1 make check`), `git status` empty before and after: ``` #10 [lint 2/2] RUN golangci-lint run --config .golangci.yml ./... #10 11.46 0 issues. ok git.eeqj.de/sneak/rgoue/cmd/rogue 1.016s coverage: 29.7% of statements ok git.eeqj.de/sneak/rgoue/game 2.678s coverage: 64.0% of statements git.eeqj.de/sneak/rgoue/term coverage: 0.0% of statements MAKE_CHECK_EXIT=0 ``` Lint layer executed, not `CACHED`; real test durations, no `(cached)`. ## `TODO.md` No new dated paragraph. The existing "Corrected 2026-08-10" entry already enumerates what is left to the editor and was incomplete without `.dockerignore`, so the clause was added to that sentence. Nothing else in `TODO.md` changed. All scratch stayed outside the clone throughout. **No prune of any kind at any point.**
clawbot merged commit 9f079ab594 into main 2026-08-10 15:33:22 +02:00
Sign in to join this conversation.