fix(887): a pin assumes it is pinning the thing that still DECIDES

Round 6 found three more false greens and named the class they share, which is worth
more than any of the three fixes:

  * `web/vitest.config.ts` OUTRANKS the pinned `vite.config.ts` — closed in the previous
    commit, found by probing vitest rather than reading about it.
  * A DECOY first `test: {` block. The comparison took `text.index("test: {")`, so a copy
    of the pin placed above `defineConfig` satisfied it while the real block was narrowed.
    Exactly one is now required — the same assertion this file already made about the
    gating step's NAME, for the same reason, not carried across.
  * A `needs:` edge matched by bare job id. `needs:` resolves within its own workflow, so
    a SECOND workflow publishing this Dockerfile while needing its own unrelated job
    called `test` satisfied it. Now bound to `GATING_WORKFLOW`. (The reviewer downgraded
    this to MEDIUM on measuring that `test_remote_state_inventory.py` forces a human to
    classify any new workflow — so the hole is "the guard is blind", not "silent". The
    forced review asks about remote state, not about whether the image is gated, so the
    one-line fix stands.)
  * A vite PLUGIN can shell out to the suite from `buildStart()`. The plugin ARRAY is
    pinned; the plugin BODIES are a stated residual, mitigated because
    `trackedSourceFilesPlugin` is deliberately lazy — a fact its own comment now marks as
    LOAD-BEARING for the image build rather than leaving as an optimisation note.

THE CLASS: **a pin assumes it is pinning the artifact that still decides.** Every route
found so far is authority moving where the pin is not looking — to another FILE, another
OCCURRENCE in the same file, another WORKFLOW, or a HOOK the pinned command invokes. That
question is now written down for the next person adding a pin, because a list of four
instances is not what generalises.

Prose, all refuted by execution: the residual naming the uncovered COPY shapes was wrong a
THIRD time at the same site (`/source/web /elsewhere` IS recognised — only the destination
is renamed — and the file's own test 700 lines below said so); "only an `ENV` is
unmodelled" was an absolute and is now a list; "Reach: N mutants, 0 missed" is restated as
a DEVELOPMENT BATTERY, since it is not in the repo, nothing re-derives it, and an
independent battery found misses against an earlier head; and `PUBLISH_ACTION` was claimed
covered by anti-vacuity, which proves the selector is non-empty and cannot prove it
complete.

Battery 61 -> 64, 0 missed.

Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
This commit is contained in:
2026-08-30 14:07:47 +02:00
co-authored by Claude Opus 5
parent 848216e277
commit 3ec73f3769
5 changed files with 66 additions and 13 deletions
@@ -66,6 +66,14 @@ So: the pin removes the SPELLING problem outright and reduces the MEANING proble
be worked through — a much better place than a predicate that was wrong nine times. It is not
"cannot be wrong", and the list's own history says so.
THE SHAPE THOSE ROUTES SHARE, which is the thing to check when adding a pin rather than a list to
memorise: **a pin assumes it is pinning the artifact that still DECIDES.** Every instance found so
far is authority moving somewhere the pin is not looking — to another FILE (`vitest.config.*`
outranks `vite.config.ts`), to another OCCURRENCE in the same file (a decoy first `test: {`), to
another WORKFLOW (a `needs:` edge naming a job called `test` that is not this one), or to a HOOK the
pinned command invokes (a vite plugin, an npm lifecycle script). Ask of any new pin: what else could
decide this, and would the pin still match?
The cost is stated rather than hidden: a legitimate edit to those command lines reddens this guard
and its author must update the pin. That is the same deliberate review trigger `KNOWN_GATE_*` uses,
applied to the two places where a silent change publishes an untested image.
@@ -93,9 +101,12 @@ WHAT IS NOT COVERED, stated rather than implied:
copy whose source names `web` — and NOT the wider "has the suite available to run", which is
what an earlier version of this sentence claimed. A stage copy that RENAMES the tree on the way
in (`COPY --from=web-build /source/web /elsewhere`) has no `web` segment in its source and is
not recognised — nor is a source that is an ANCESTOR of it (`COPY --from=web-build /source /src`
brings `/source/web` along without naming it). Both were MEASURED; the previous two attempts at
this sentence each named a shape that was in fact covered while omitting these. The
not recognised. THAT CLAUSE WAS WRONG THREE TIMES and is now stated from a measurement rather
than from memory: `COPY --from=web-build /source/web /elsewhere` IS recognised (only the
DESTINATION is renamed, and only sources are read). What is not: a source that is an ANCESTOR of
the tree (`COPY --from=web-build /source /src` brings `/source/web` along without naming it) or
names it only as `/source/.`. Both measured, and both backstopped by the substring sweep — an
ancestor-copy stage running `npm t`, `npx vitest` or a new npm script is caught. The
false-NEGATIVE direction is what drops a stage out of the check, so the predicate is pinned in
both directions by its own test, and the substring sweep below is the backstop for whatever it
still misses.
@@ -213,6 +224,12 @@ VITE_CONFIG = "web/vite.config.ts"
# image published. Pinning one file is worthless while a second can outrank it, so the absence of
# every higher-precedence spelling is asserted rather than assumed.
VITEST_CONFIG_PREFIXES = ("vitest.config.", "vitest.workspace.", "vitest.projects.")
# A vite plugin runs inside the same process as the suite and can shell out from a build hook, so a
# `buildStart()` calling `npm test` re-arms ersatztv#887 from the plugin side with every other pin
# matching (measured). The ARRAY is pinned so no plugin joins silently; the plugin BODIES are not,
# and that is a stated residual — `trackedSourceFiles.ts` is deliberately LAZY for this exact
# reason, which its own comment now marks as load-bearing for the image build.
PINNED_VITE_PLUGINS = "plugins: [react(), trackedSourceFilesPlugin()],"
PINNED_VITE_TEST_BLOCK = """test: {
environment: 'jsdom',
environmentOptions: {
@@ -245,6 +262,16 @@ def _vite_test_block() -> list[str]:
explained without the explanation being pinned.
"""
text = _read(VITE_CONFIG)
occurrences = text.count("test: {")
# A decoy `const reviewed = { test: {…} }` copied byte-for-byte from the pin, placed ABOVE
# `defineConfig`, satisfies a comparison that takes the FIRST match while the real block is
# narrowed — measured green. The file already requires exactly ONE step named `Test SPA` for the
# same reason; the reasoning had not been applied here.
assert occurrences == 1, (
f"{VITE_CONFIG} contains {occurrences} `test: {{` blocks, not one. The pin compares the "
f"FIRST, so a second is either a decoy that satisfies it while another block decides, or a "
f"real second config nobody is checking."
)
start = text.index("test: {")
depth = 0
for index in range(start, len(text)):
@@ -560,13 +587,20 @@ def test_every_IMAGE_PUBLISHING_job_is_gated_on_the_job_that_runs_the_suite() ->
if not any(stage_rel == dockerfile for stage_rel, _, _ in spa_carrying_stages()):
continue
gates = _transitive_needs(job_id, jobs)
if GATING_JOB not in gates:
# `needs:` names a job in the SAME workflow, so the edge only reaches the pinned step when
# the publishing job lives in `GATING_WORKFLOW`. Matching a bare job id anywhere was a false
# green: a second workflow publishing this Dockerfile while `needs:`-ing its OWN unrelated
# job called `test` (`run: echo ok`) satisfied it — measured, and it escaped every
# workflow-population guard in the repo, 588 tests green.
if rel != GATING_WORKFLOW or GATING_JOB not in gates:
faults.append(
f"{rel}: job `{job_id}` publishes an image built from `{dockerfile}`, which carries "
f"the SPA source, but does not transitively `needs:` `{GATING_JOB}` — the job "
f"holding the pinned suite step. It needs: {sorted(gates)}. Since ersatztv#887 the "
f"image build deliberately does not run the suite itself, so this `needs:` edge is "
f"the ONLY thing standing between a red suite and a published image."
f"the SPA source, but does not transitively `needs:` `{GATING_JOB}` IN "
f"`{GATING_WORKFLOW}` — the job holding the pinned suite step. It needs: "
f"{sorted(gates)}. A `needs:` edge resolves within its own workflow, so a job named "
f"`{GATING_JOB}` in a DIFFERENT workflow is a different job and gates nothing. Since "
f"ersatztv#887 the image build does not run the suite itself, so this edge is the "
f"ONLY thing between a red suite and a published image."
)
assert not faults, "\n".join(faults)
@@ -698,6 +732,23 @@ def test_the_npm_SCRIPT_MAP_is_exactly_what_was_reviewed() -> None:
)
def test_the_VITE_PLUGIN_LIST_is_exactly_what_was_reviewed() -> None:
"""A plugin can run the suite from a build hook, so the list of them is pinned.
Vite plugins execute in the same process as `vite build` and `vitest`, and a `buildStart()` that
shells out to `npm test` puts the suite back into the gitless stage with every command pin
intact — measured, both as an inline plugin and inside an existing one. The list is pinned so a
new plugin cannot arrive unnoticed; what each plugin DOES is a residual, and the mitigation
there is that `trackedSourceFilesPlugin` is deliberately lazy.
"""
text = _read(VITE_CONFIG)
assert text.count(PINNED_VITE_PLUGINS) == 1, (
f"`{VITE_CONFIG}`'s plugin list is not the pinned one ({PINNED_VITE_PLUGINS!r}). A vite "
f"plugin runs in the same process as the suite and can shell out from a build hook, so a "
f"new one is a decision to state. Update the pin deliberately."
)
def test_NO_OTHER_vitest_CONFIG_outranks_the_pinned_one() -> None:
"""A pinned config is worthless if a second file can take precedence over it.