REVIEW OF THE PREVIOUS COMMIT, NOT NEW MACHINERY. The diagnosis it states was re-verified in both directions before anything here was written: `dist plan --tag=v0.1.0-rc2` gets past the tag/version check (stopping at the pinned cargo-dist-version, as expected) and `--tag=v0.1.0-rc3` reproduces the CI error verbatim, naming `--tag=v0.1.0-rc2` as the fix in its own help text. The workflow was never broken. Both claimed fault injections also reproduce exactly: the comparison replaced by `if false;`, and `expected` derived from the tag, each produce four failures and exit 1 -- exit code read unpiped, since a pager's status would mask it. WHAT THE TEST COULD NOT SEE. Disabling the dirty-tree guard outright left all six checks green and the script exiting 0. That guard is not decoration: it is what stops a tag naming a commit that was never tested, and its failure is irreversible in the same way the version mismatch is. The cause is the previous repair. Moving the negative cases to a clean throwaway checkout is what made the tag comparison reachable at all -- and it thereby removed the only circumstance in which the test could observe the tree guard. The guard whose PRECEDENCE caused the original vacuity became the one thing the suite could not see. The vacuity and its repair are not two events; the repair relocated the blind spot rather than closing it. So the new check is aimed the other way: a tag that MATCHES the version, run against a dirtied checkout, so the comparison passes and a refusal can only be the tree. Verified discriminating -- with the guard disabled it fails with exit 1, and the two faults are independent in both directions (comparison disabled leaves the tree check passing, guard disabled leaves the three tag checks passing), so the seventh check is not a restatement of the other six. Also recorded, rather than guarded: the `dist plan` second opinion can be disabled with the suite green, because every input the test feeds it has already been refused by the textual check. That follows from the arm being optional by design -- pinning it would mean installing a dist matching cargo-dist-version, the very mismatch the script tolerates on purpose. Stated so the silence is not mistaken for coverage. Two corrections of fact. The CI step lives in the `kb` job beside pr-watch-test.sh, where this repo keeps its check_the_checkers -- not in `test`; tooling.md now says so. And the awk version parser was probed rather than hardened: `version.workspace = true` yields empty, which the root path refuses loudly and the per-crate loop skips; a non-first [package] block and build metadata both parse correctly; a trailing comment on the version line mis-parses but fails CLOSED and prints the mis-parsed value in the refusal. No manifest in this workspace has one, so nothing was changed for it. No version bumped, no tag cut. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
24 KiB
The repository's own checkers
scripts/ holds the gates this repo runs on itself. They are code, and they are
held to the rule they exist to enforce: a check nobody has seen fail is
indistinguishable from one that always passes, so each of them carries a
self-test whose cases are injected faults
(doctrine(check_the_checker)).
Four docstrings in scripts/check-comment-runs.py point here. This is the file
they mean.
1. The comment cap
check-comment-runs.py fails a .rs file carrying more than two consecutive
comment lines. The rule is context/design-principles.md §6: reasoning lives in
context/ prose, where it can be read, corrected and audited, not in a comment
that nothing checks against the code.
The whole difficulty is deciding what is a comment line. Ten silent successes have been found in this one function, every one of them failing in the direction that looks like passing — a file that should have been flagged and was not:
"test" in linematched#[cfg(test)], so every cfg-gated fixture cleared a gate written to reject exactly those. Anchor the match; an unanchored substring is a filter that over-matches, and a filter that over-matches fails in the direction that looks like success. (The same class as the bd ID audit's exclusion list, and as_JS_TEST's exclusion of.skip/.only.)- A
proptest!flag was set and never cleared, so the firstproptest!in a file made every laterfn— helpers included — count as a test. - An attribute holding a bracket inside a string (
#[cfg(feature = "[")]) left the bracket depth unbalanced and exempted the rest of the file. Every question is therefore asked of the scanner, never of the raw line: the scanner knows Rust strings, raw strings, char literals and nested block comments, and the attribute depth is counted over code text only. - A fence that opens and never closes exempted everything after it.
Two rules that are decisions rather than accidents. A fence **suspends** the run rather than ending it, so two lines above a fence and two below is a run of four — a reader meets it as one uninterrupted block. And only a *runnable* fence earns the exemption: rustdoc executes a bare or rust and does not execute text, ignore or no_run, so the rest is prose wearing a fence.
2. The evidence check
--check-evidence holds context/test-evidence/ and the tests it describes to
each other, in two directions:
- Orphans. Every heading shaped like a test name must name a test that runs.
- Unstated. Every test whose doc comment cites
context/test-evidence/must have an entry there.
Neither direction is "every test has evidence". Most tests have none and should not be forced to have one; what is checkable is a claim, and both directions are claims. The failure they close is the same one a dead ticket ID is: a pointer that reads as a promise the thing exists.
A leaf is not an identity
The names cargo lists are module paths, and the checker keeps them whole.
Six leaf names collide in this workspace — two of them cited in evidence — so
reducing a path to its last segment made a heading satisfiable by a test in
another module answering another question. Renaming
ui::project::tests::a_load_that_fails_is_not_a_rebase, the one its entry
actually describes, left the checker at 579 entries, all name a test that
runs, exit 0: the surviving ui::host::tests namesake answered for it.
So a bare leaf resolves only when it is unique, and an ambiguous one is a refusal naming the candidates — not a warning, because a warning re-creates the problem for anyone who does not read it. The heading is then written with its module path, which is also what makes the entry say which test it is about.
Note the two halves are both needed and neither is sufficient. The refusal forces qualification but cannot survive a rename on its own: rename one of a colliding pair and the leaf becomes unique, silently resolving to the survivor. Qualification is what closes that, and the refusal is what makes somebody write it.
What stays open is collision across binaries: --list prints module paths
with no crate and no binary, so two crates sharing one could not be told apart
(improvise-sxsf). Measured 2026-09-21 and still latent — 1444 names, zero
duplicate full paths, and zero duplicate (crate, path) pairs.
The fix the ticket proposes — --list per package with the crate prefixed —
was measured and is declined twice over. It costs 45.2s against 2.4s for
the one workspace call (11 members, warm), 18× for an unreachable case, and
--check-evidence needs the --ignored twin too, so the real figure is 22
invocations. More to the point it does not close the class it names: a
package is not a binary. improvise alone carries five integration test
binaries beside its lib, and an integration test's top-level fn lists with no
module path at all — 60 of the 1444 names are bare — so -p improvise prefixes
all six identically and tests/a.rs::foo still cannot be told from
tests/b.rs::foo. A fix that ever becomes worth having is per-target
(--lib, --test <name>), not per-package, and the ticket's proposal would
have bought 18× for a partial cure.
The names come from cargo, not from the source
A heading is diffed against cargo test --workspace -- --list, minus the names
that --list --ignored reports. It used to be scraped out of .rs text, and
four shapes defeated that, all of them "evidence for a check that never runs"
(improvise-rm4s):
| shape | why the scrape accepted it |
|---|---|
#[test] fn x inside a raw string literal |
the scanner reads inside strings |
#[cfg(feature = "…")] #[test] |
the feature is never enabled |
#[cfg(target_arch = "wasm32")] #[test] |
cargo test --workspace is native |
#[test] #[ignore] |
it compiles, and never runs |
The third is the realistic one here: this repo has real wasm32-only code and a
wasm CI job that only runs cargo check. Adopting the stricter source cost
nothing — all 539 headings resolved against a real listing on the commit that
made the change — which is the moment to tighten a gate rather than the moment
to argue about it.
Demonstrated before the fix, by injecting all four shapes into src/draw.rs and
context/test-evidence/tui.md at once: the previous checker printed 543
entries, all name a test that exists, exit 0. The current one names all four.
What this costs: --check-evidence now needs a toolchain and a compiled
workspace, about three seconds warm. A missing cargo says so rather than
raising. The comment-cap check is unaffected and needs neither.
--rust-tests FILE takes a saved listing for a caller that has one, and it
requires --rust-tests-ignored FILE: a first version took one file, called
cargo_test_names with one argument, and so skipped the ignored rule entirely
while printing the same success line as the honest path. A flag that is
strictly more permissive and indistinguishable in the log is a bypass, not a
convenience — so the pair is mandatory and the success line names its
source. CI passes neither and runs cargo itself.
A missing context/test-evidence/ is not an early return either. It used to
be, and it exited 0 while 170 tests cited a directory that was not there; the
walk now yields nothing and every citation is reported unstated, which is loud.
Note what the self-test can and cannot carry here. The listing reader is
testable from fixtures — a leaf name taken off a module path, an --ignored
name subtracted, cargo's own prose not mistaken for a test — and those cases
are in SELF_TEST_LISTING. The four source shapes are not: once defined
comes from cargo, a fixture asserting "a heading with an empty listing is an
orphan" is the same case however the Rust beside it is spelled, and writing
four of them would be four copies of one assertion dressed as coverage. The
evidence for those is the injection above, against the real tree.
What it does not close: citing is still scraped, so a citation inside a
string literal would be reported as a test owing an entry. That is a false
positive in the safe direction — loud, not silent — and it has no instance.
A demotion is a reviewer's job, not a gate's
A heading is classified by shape: a bare identifier is a Rust test, a quoted
string a JS test, anything else is prose about the section (Helper — x,
Note — y, a file's own scope). So any orphaned heading can be retired by
rewriting it with a space and a dash, and nothing can check whether the prose is
honest — no mechanism could, because the question is whether a sentence is true
(improvise-9za9).
Two things make that tolerable. The unsafe direction is the one that fails
loudly: prose that looks like an identifier is classified as a Rust claim and
reported as an orphan, so the error you can make by accident is the noisy one.
And the mechanism is git diff context/test-evidence/ — a demotion shows
up there as a ### identifier line becoming prose, in the diff every reviewer
already reads. The live instance, PR #69 demoting a heading while PR #72
restored its coverage under another name, is what says a reviewer rather than a
gate has to catch it.
The printed count of prose headings is a convenience, not a guard: nothing stores the previous count and nobody diffs two CI logs, so a retirement moves a number no one is watching. Saying otherwise would claim a detectability it cannot support, which is the class of defect §7's adjacent-check section is about.
A flag adds a question; it never selects one away
--check-evidence used to early-return, so the flagged run answered the
evidence question and said nothing at all about the comment-run cap. Three
agents hit that independently in one campaign, and in one case a local gate went
green while CI went red on a three-line doc comment (improvise-ijql). It is now
additive: main runs check_cap first, then check_evidence, and returns the
worse of the two.
The ticket offered a cheaper shape — leave the early return and print one line
saying the cap did not run — on the precedent of PR #83 making --rust-tests
name its source. That precedent does not reach this case, and the distinction is
worth keeping. A provenance line and a disclaimer are not the same object.
Naming which listing the names came from qualifies an answer the run actually
gave; saying "the cap did not run" qualifies an answer it did not give, while
still exiting 0. Exit 0 is what CI branches on and what a human reads as
clean, so a disclaimer leaves the gate exactly as skippable and merely documents
the skip. The rule the fix is written to is therefore stated over the exit code:
a tree with a long comment run must not pass any invocation that can exit 0.
Which invocations those are is a decision, not a sweep. --self-test and
--list-doctests are excluded and neither is an exception to the rule: the
first scans no tree, and the second is a listing that renders no verdict. Only
the two that answer "is this tree clean" are in SELF_TEST_DISPATCH.
That table is checked by running the script as a subprocess against a
temporary tree, which is the only level at which this bug is visible — every
unit in this file was correct, and the defect lived in main's dispatch. Both
arms are needed and both were demonstrated: restoring the early return fails
DISPATCH: --check-evidence: exited 0 over a four-line comment run, and forcing
the cap to always report a violation fails the clean arm for both invocations,
which is what stops an always-fail from satisfying the first. The temporary tree
also has no context/test-evidence/ and no JS roots, so the evidence half is
vacuously clean there and the cap is the only thing that can decide the exit
code — the isolation is what makes the assertion name one question.
Two headings, one test
An orphan is a heading whose test nobody can find; the mirror image is a test that two headings answer for, and it is invisible to every rule above — each copy names a test that runs, so each copy passes. PR #92 shipped two entries under one heading where the older described a fixture the test no longer had and repeated a claim an adversarial review had measured false. A reader meeting the older one first has no way to know a successor exists two thousand lines down, so the stale copy reads as current. That is worse than an orphan, which at least announces itself as broken.
It is a refusal and not a warning, on the precedent improvise-rm4s set for
the ambiguous leaf: the two are the same defect seen from opposite ends — one
name answered by two tests, and one test answered by two names — so they are
answered the same way. The refusal names the file and every line, in file order.
The key is the resolved test, not the spelling. ### f beside
### m::tests::f is two headings for one test, and grouping by the string would
miss it precisely because qualification is the repair rm4s asks for; a heading
qualified in one file and bare in another would have been the first way this
came back. Resolution reuses the same leaf table the ambiguity rule uses, so a
leaf that is genuinely ambiguous canonicalises to itself and is reported as
ambiguous rather than as a duplicate — two different tests sharing a leaf are
not two entries for one test, and conflating them would file the wrong repair.
The check is over the whole directory rather than per file, because a heading
keys a test name and a test name is unique workspace-wide once qualified.
What it still cannot see, and neither can any of the others: an entry that exists, names a live test, is the only one that does — and describes it backwards.
3. The others
scripts/kb is the only writer of prolog-sessions/kb.pl; its own reasoning is
in context/code/kb-tool.md. scripts/node-test.sh, scripts/kb-test.sh,
scripts/pr-watch-test.sh and scripts/assemble-stamp-test.sh are the same
shape as this file's subject: a guard, plus a suite that makes the guard refuse
what it claims to refuse. Each is wired into .github/workflows/ci.yml in the
same job as the thing it guards, because a check that only runs locally is a
convention.
scripts/check-ci-graph.py is the workflow's own gate: pages-deploy must
needs every other job in ci.yml. The rule "every check job" was written down
(improvise-x7cq) and a job added during review of PR #83 was still ungated with
nothing noticing, which is what moved it from a sentence to a check. It reads
the job keys and the needs: list off indentation, tracking which job a
needs: belongs to — taking the first one seen would read another job's list as
the deploy's and pass. README.md therefore states the rule and deliberately
does not list the jobs: a second copy with nothing holding it to the first
is what went stale, twice.
Why the Pages deploy fails on "Found 0 artifact(s)" (improvise-864a)
actions/deploy-pages@v5 intermittently fails a push to main with No
artifacts named "github-pages" were found for this workflow run, while the
upload step was green in that same run. The ticket framed this as concurrent
merges racing on one Pages environment, and PR #46 proposed skipping the deploy
unless the commit is the tip of main. Both the framing and the guard are
wrong, and measurement says so.
What was measured. All 100 push-to-main CI runs from 2026-09-19T22:02Z to
2026-09-21T05:02Z, from the run, job, step and log APIs (job metadata and logs
outlive artifacts, which expire in a day and are all 404 now — so this is
reproducible where the ticket's own artifact-count table is not):
| deploys | with another run's SUCCESSFUL deploy completing inside this run's own upload→deploy window |
|---|---|
| 12 artifact-not-found failures | 12 (100%) |
| 56 successes | 2 (3.6%) |
| 4 other failures | 0 — all four are the unrelated Create Pages site failed: Resource not accessible by integration, from before enablement: false (improvise-3gy) |
The direction is settled, and it is neither of the two the ticket offered.
The oldest run of the 17:47Z batch failed at 17:47:23Z — before the next
deploy in the batch had started (17:47:31Z) and before any Pages deployment
existed at all, since deploy-pages fetches the artifact metadata before it
creates a deployment. So the failure cannot have been caused by this run's own
deploy, by supersession, or by a concurrent deploy. The artifact was already
gone. The reaper is another run's successful deployment.
It is scoped by artifact NAME, not by run. In run 35560726456 the build job
finalized three artifacts seconds apart — harness-serve, wasm-bundles and
github-pages, each with an id — and 2m21s later that run's own lookup reported
Found 2 artifact(s) and no github-pages. Same run, same token, same call,
same retention: the two siblings survived and only github-pages was missing.
That one observation rules out the whole family of general explanations —
service flakiness, eventual consistency, retention, a token scope, a v3/v4
backend mismatch — because any of them would have taken the siblings too.
Four things this kills. Queue depth is not the mechanism: the three deploys
of the 17:47Z batch never queued, running 9s and 17s apart with each failure
taking one second, so the pages group serialized nothing. Retention is not it
either — all three shared retention-days: 1, and since the newest-finalized
expires last, expiry can produce 0/1/1 or 0/0/1 but never the ticket's 0/1/0.
Nothing in deploy-pages deletes an artifact (an upstream feature request asks
for exactly that capability, which is only open because nothing does). And the
action-version theory dies on the interleaving: successes and failures alternate
minute by minute on identically-resolved action SHAs.
One clause of the ticket's own correction is falsified, and the conclusion
still holds. The correction argued the tip-of-main guard would have skipped
the one deploy that published "and let 52507a6 deploy and fail anyway" — which
assumed 52507a6's artifact loss was independent of its neighbour. It was not:
5f871f7's deploy completed at 17:47:40Z, four seconds before 52507a6's began.
Fewer successful deploys really is the right lever, so PR #46 aimed at the right
quantity for the wrong reason. It should still not be shipped, because the
association is not a mechanism and the mechanism is not deterministic: 5f871f7
had a foreign deploy land inside its own window too and survived. A guard
measured at nothing, against a probabilistic effect, is
doctrine(measure_the_obvious_fix_before_reporting_it_as_the_fix) exactly.
What the evidence does license. The risk lives entirely in the upload→deploy
window — every victim had a foreign deploy land inside its own — so shortening
that window is the one lever with evidence behind it, and consolidating the
build already did some of that. Closing it properly means no two runs holding a
github-pages artifact across a deployment, which is a real change to the job
graph with real costs and is not attempted here. A retry is not a fix: the
artifact is gone rather than transiently unlistable, so a retry cannot find it.
4. The release preflight, and why a loud failure was not enough
scripts/release-preflight.sh answers, before a tag exists, the question the
Release workflow answers after.
What happened. v0.1.0-rc3 was pushed to all three remotes while every
crate still said 0.1.0-rc2. dist matches a tag against the root package's
version, refused with "This workspace doesn't have anything for dist to
Release!", and the four jobs behind plan skipped. Nothing was wrong with
the workflow, which is worth stating plainly because the instinct on a failed
release run is to edit the YAML. The trigger is correct, the pattern matches,
the workflow is enabled; dist plan --tag=v0.1.0-rc2 succeeds today and
--tag=v0.1.0-rc3 reproduces the CI error exactly, which is the whole
diagnosis and both halves of it.
Why a loud, precise failure still cost something. dist named the right
answer in its own error text — "--tag=v0.1.0-rc2 will Announce: improvise".
The problem is not the message, it is when it arrives: a tag is public and
immutable by the time CI reads it, so the repair is a new version rather than an
edit, and the tag that failed stays on three remotes forever. A check that runs
after the irreversible step is a check in the wrong place, however good its
message. So the preflight runs before tagging and prints the tag command only
once it has passed — the honest path made the easy one, the same shape as
Basis having no public constructor (gotcha 43).
The primary check is textual and the dist plan run is a second opinion.
An absent dist must not read as a pass, so the script says which of the two
ran. It also tolerates a locally-installed dist whose version differs from the
one pinned in dist-workspace.toml: that stops dist before it plans, and
reporting it as a refusal would train the reader to ignore refusals.
Only the root package's version can disagree with a tag.
dist-workspace.toml says members = ["cargo:."], so gp-tree sitting at
0.1.0 while everything else is on 0.1.0-rc2 is not a finding. The script
lists such crates and says so rather than failing, because a deliberate split
should stay deliberate and a silent one should be visible.
The test's first version was vacuous, and the fault injection is what showed
it. scripts/release-preflight-test.sh originally asserted only did it
refuse. Every negative case passed with the tag comparison replaced by
if false; — because the script refuses a dirty working tree first, and
this repo's tree is almost always dirty, bd auto-exporting .beads/issues.jsonl
on nearly every command. Four of five checks were answering is the tree dirty
while claiming to answer does it catch a bad tag. The test now runs the
negative cases against a clean throwaway checkout and asserts the refusal
names the version mismatch, so a refusal for the wrong reason is a failure
rather than a pass. Two rival faults were injected against the repaired
version — the comparison disabled, and expected derived from the tag itself —
and each produces four failures and exit 1.
That is context/design-principles.md §7's "a check that looks like the answer,
answering a neighbouring question", found in a checker written to prevent
exactly that class, within an hour of writing it.
And the repair opened a second blind spot in the same place — which is the
more useful half. Moving the negative cases to a clean checkout is what made
the tag comparison reachable at all, and it thereby removed the only
circumstance in which the test could observe the dirty-tree guard.
Disabling that guard outright left all six checks green and the script exiting
0: the guard whose precedence caused the original vacuity had become the one
thing the suite could not see, and the repair's own shape is what hid it. A
vacuity and its repair are not two events here; the repair relocated the blind
spot rather than closing it.
The check that closes it is therefore aimed the other way — a tag that matches the version, run against a dirtied checkout, so the comparison passes and a refusal can only be the tree. That is §7's assert the direction the default cannot supply applied to a fixture rather than to a default: the clean checkout is the fixture, and the discriminating input is the one that makes it dirty. The two faults are independent in both directions — with the comparison disabled the dirty-tree check still passes, and with the guard disabled the three tag checks still pass — which is what makes the seventh check worth having rather than a restatement of the other six.
What is still unchecked, and why that is a decision. The dist plan second
opinion can be disabled — its refusal arm rewritten never to match — with the
suite green, because every input the test feeds it is one the textual check has
already refused. That follows from the arm's own design rather than being an
oversight: it runs after the primary check, it is allowed to be absent
entirely, and an absent dist already prints that only the textual check ran.
Pinning it would mean installing a dist matching cargo-dist-version, which
is the very mismatch the script tolerates on purpose. Stated here rather than
guarded, so the next reader does not mistake the silence for coverage.
Where it runs. The kb job, beside scripts/pr-watch-test.sh — this
repo keeps its check_the_checkers there, not in test. The step needs no
trealla and no nix of its own; it is sh, git archive and awk.