Nothing couples ci-image.yml's push.paths to ci-image-pin's expected pathspec #855

Closed
opened 2026-08-27 20:47:14 +02:00 by timothy · 7 comments
Owner

Split out of #744, raised by its second cold review.

The gap

Two lists must name the same set of image sources, and only prose says so:

  • .gitea/workflows/ci-image.ymlon.push.paths — what causes a publish.
  • .gitea/workflows/pr-checks.ymlci-image-pin's expected="$(git log -1 --format=%H -- docker/ci)" — what the pin is required to name.

If they diverge, the failure is silent and green in the dangerous direction: a path that publishes but is not in expected produces images the pin never tracks; a path in expected that does not publish produces a permanent red on a blocking job.

Why this PR made it load-bearing

Before #744 the two were kept identical by the self-reference — .gitea/workflows/ci-image.yml appeared in both, so an edit to either was felt immediately. #744 removed it from both as a decided tradeoff (ci.toolchain-image-publish-is-a-dispatch), which is the right call for its own reasons but leaves the agreement enforced by nothing but three prose comments.

This is the exact shape docs/defect-shapes-773.md §4 catalogues: a hand-maintained mirror of a population, with no derivation.

Proposed

A scripts/tests/ case that parses both workflows and asserts set equality between ci-image.yml's on.push.paths and the pathspec ci-image-pin passes to git log. Both are in YAML, and the expected= line is a single run: string, so the pathspec has to be extracted from it — that extraction is the part to get right, and per testing.fixing-a-parser-bug-introduces-the-next-one budget more than one round for it.

It must ship with a declared clause mutation (testing.mutation-claims-are-executed) and a docs/guard-inventory.md row, like every guard here. The obvious mutation: change one list and not the other.

Note the population subtlety — expected's pathspec is docker/ci, while paths: is docker/ci/**. Those denote the same set to their respective consumers but are not string-equal, so the comparison has to normalise rather than compare literally. A guard that demanded literal equality would be red on day one.

Done-when

  • A guard asserts the two lists agree, deriving both from the workflow files rather than restating either
  • It ships a declared clause mutation re-run every suite, and a docs/guard-inventory.md row
  • The three prose comments that currently carry this invariant point at the guard
  • Adversarial review passed
Split out of #744, raised by its second cold review. ## The gap Two lists must name the same set of image sources, and only prose says so: - `.gitea/workflows/ci-image.yml` → `on.push.paths` — what causes a publish. - `.gitea/workflows/pr-checks.yml` → `ci-image-pin`'s `expected="$(git log -1 --format=%H -- docker/ci)"` — what the pin is required to name. If they diverge, the failure is **silent and green in the dangerous direction**: a path that publishes but is not in `expected` produces images the pin never tracks; a path in `expected` that does not publish produces a permanent red on a blocking job. ## Why this PR made it load-bearing Before #744 the two were kept identical by the self-reference — `.gitea/workflows/ci-image.yml` appeared in both, so an edit to either was felt immediately. #744 removed it from both **as a decided tradeoff** (`ci.toolchain-image-publish-is-a-dispatch`), which is the right call for its own reasons but leaves the agreement enforced by nothing but three prose comments. This is the exact shape `docs/defect-shapes-773.md` §4 catalogues: a hand-maintained mirror of a population, with no derivation. ## Proposed A `scripts/tests/` case that parses both workflows and asserts set equality between `ci-image.yml`'s `on.push.paths` and the pathspec `ci-image-pin` passes to `git log`. Both are in YAML, and the `expected=` line is a single `run:` string, so the pathspec has to be extracted from it — that extraction is the part to get right, and per `testing.fixing-a-parser-bug-introduces-the-next-one` budget more than one round for it. It must ship with a declared clause mutation (`testing.mutation-claims-are-executed`) and a `docs/guard-inventory.md` row, like every guard here. The obvious mutation: change one list and not the other. Note the population subtlety — `expected`'s pathspec is `docker/ci`, while `paths:` is `docker/ci/**`. Those denote the same set to their respective consumers but are not string-equal, so the comparison has to normalise rather than compare literally. A guard that demanded literal equality would be red on day one. ## Done-when - [x] A guard asserts the two lists agree, deriving both from the workflow files rather than restating either - [x] It ships a declared clause mutation re-run every suite, and a `docs/guard-inventory.md` row - [x] The three prose comments that currently carry this invariant point at the guard - [x] Adversarial review passed
timothy added the ci-cdpriority: medium labels 2026-08-27 20:47:26 +02:00
Author
Owner

Unowned — explicitly declined, so it stops circulating. Recording this on the issue rather than only in session traffic, because four parallel sessions ran on 2026-08-30 and #855 was offered to each of us in turn: it was left for "whichever session is not doing #887", then declined by the #858/#859 session and the #887 session on disjointness grounds, then offered to me. The argument that assigned it to somebody was never completed by anybody, which is how an issue silently looks claimed while nobody is on it.

I am declining it too, with a reason rather than a hand-off: it is not the small guard it looks like. This issue's own body says the expected= pathspec has to be extracted from a single run: string, that paths: is docker/ci/** while the pathspec is docker/ci so the comparison must NORMALISE rather than compare literally, and — citing testing.fixing-a-parser-bug-introduces-the-next-one — to budget more than one round for that extraction. On top of that it needs a declared clause mutation and a docs/guard-inventory.md row. Starting it late in a session would leave a half-built guard, which is worse than leaving it clean.

No work has been done on it, no branch exists, nothing is reserved. It remains priority: medium and is the top-ranked remaining candidate for a session with the runway to finish it in one go.

**Unowned — explicitly declined, so it stops circulating.** Recording this on the issue rather than only in session traffic, because four parallel sessions ran on 2026-08-30 and #855 was offered to each of us in turn: it was left for "whichever session is not doing #887", then declined by the #858/#859 session and the #887 session on disjointness grounds, then offered to me. The argument that assigned it to somebody was never completed by anybody, which is how an issue silently looks claimed while nobody is on it. I am declining it too, with a reason rather than a hand-off: it is not the small guard it looks like. This issue's own body says the `expected=` pathspec has to be extracted from a single `run:` string, that `paths:` is `docker/ci/**` while the pathspec is `docker/ci` so the comparison must NORMALISE rather than compare literally, and — citing `testing.fixing-a-parser-bug-introduces-the-next-one` — to budget more than one round for that extraction. On top of that it needs a declared clause mutation and a `docs/guard-inventory.md` row. Starting it late in a session would leave a half-built guard, which is worse than leaving it clean. No work has been done on it, no branch exists, nothing is reserved. It remains `priority: medium` and is the top-ranked remaining candidate for a session with the runway to finish it in one go.
timothy added the in-progress label 2026-08-30 15:40:36 +02:00
Author
Owner

Claiming — Claude Code session, 2026-08-30 13:40 CEST. Taking this with the full runway the previous comment said it needs; nothing about the scope changed, only the session's remaining budget.

Pre-claim checks all clear at cf5f42edf: no open PR references #855, git ls-remote --heads origin '*855*' returns nothing, the only prior comment is an explicit decline, and origin/main was re-fetched immediately before this comment.

Plan follows the issue body rather than reinterpreting it: parse on.push.paths from .gitea/workflows/ci-image.yml and the git log pathspec out of ci-image-pin's run: string in .gitea/workflows/pr-checks.yml, normalise the docker/ci vs docker/ci/** difference rather than comparing literally, and assert set equality. Ships with a declared clause mutation in scripts/tests/mutation_manifest.py, a docs/guard-inventory.md row, and the three prose comments repointed at the guard.

Will release this claim explicitly if I stop short.

**Claiming — Claude Code session, 2026-08-30 13:40 CEST.** Taking this with the full runway the previous comment said it needs; nothing about the scope changed, only the session's remaining budget. Pre-claim checks all clear at `cf5f42edf`: no open PR references #855, `git ls-remote --heads origin '*855*'` returns nothing, the only prior comment is an explicit decline, and `origin/main` was re-fetched immediately before this comment. Plan follows the issue body rather than reinterpreting it: parse `on.push.paths` from `.gitea/workflows/ci-image.yml` and the `git log` pathspec out of `ci-image-pin`'s `run:` string in `.gitea/workflows/pr-checks.yml`, normalise the `docker/ci` vs `docker/ci/**` difference rather than comparing literally, and assert set equality. Ships with a declared clause mutation in `scripts/tests/mutation_manifest.py`, a `docs/guard-inventory.md` row, and the three prose comments repointed at the guard. Will release this claim explicitly if I stop short.
Author
Owner

Progress — claim still live, branch fix/855-ci-image-paths-pin-agreement (not yet pushed). Recording this because the issue has been open under an in-progress label for several hours and a parallel session should not read that as a stalled claim.

The guard is built and its two comparison clauses are each witnessed load-bearing. What has taken the time is the part this issue's body flagged: normalising docker/ci/** against docker/ci rather than comparing literally. Three cold reviews have returned BLOCKED, and every finding was one shape — two spellings a canonicaliser mapped together that the two consumers treat DIFFERENTLY:

  • a bare docker/ci in paths: is an anchored match selecting none of the directory's contents, while the git pathspec docker/ci is recursive;
  • {a,b} is alternation to Gitea and a literal to git; a leading ./ is literal to Gitea and normalised away by git; <file>/** matches nothing while the pathspec <file> tracks the file.

Three of round 2's five findings were created by round 1's fix, which is the trigger testing.verification-code-needs-its-own-proof names for STOP-AND-SUBTRACT. So the canonicaliser was deleted rather than extended. The guard now models exactly one pair of spellings — <dir>/** against <dir>, segments restricted to [A-Za-z0-9._-] — refuses everything else, and separately asserts from the git index that each named path really is a directory. Refusal is the safe direction here: a false red asks a question, a false green is the divergence this issue is about.

Amendment to this issue's own guidance, from measurement. The body says to "normalise rather than compare literally". That is right about the goal and wrong about the method: a normaliser over these two dialects has an open-ended input space and cannot be made sound by adding cases. Anyone picking up similar work should model the one shape they need and refuse the rest.

Round 4 is running. If it finds another false green, I will withdraw the guard rather than patch a fifth time, and say so here.

**Progress — claim still live, branch `fix/855-ci-image-paths-pin-agreement` (not yet pushed).** Recording this because the issue has been open under an `in-progress` label for several hours and a parallel session should not read that as a stalled claim. The guard is built and its two comparison clauses are each witnessed load-bearing. What has taken the time is the part this issue's body flagged: **normalising `docker/ci/**` against `docker/ci` rather than comparing literally.** Three cold reviews have returned BLOCKED, and every finding was one shape — two spellings a canonicaliser mapped together that the two consumers treat DIFFERENTLY: - a bare `docker/ci` in `paths:` is an *anchored* match selecting none of the directory's contents, while the git pathspec `docker/ci` is recursive; - `{a,b}` is alternation to Gitea and a literal to git; a leading `./` is literal to Gitea and normalised away by git; `<file>/**` matches nothing while the pathspec `<file>` tracks the file. Three of round 2's five findings were *created by* round 1's fix, which is the trigger `testing.verification-code-needs-its-own-proof` names for STOP-AND-SUBTRACT. So the canonicaliser was **deleted** rather than extended. The guard now models exactly one pair of spellings — `<dir>/**` against `<dir>`, segments restricted to `[A-Za-z0-9._-]` — refuses everything else, and separately asserts from the git index that each named path really is a directory. Refusal is the safe direction here: a false red asks a question, a false green is the divergence this issue is about. **Amendment to this issue's own guidance, from measurement.** The body says to "normalise rather than compare literally". That is right about the goal and wrong about the method: a normaliser over these two dialects has an open-ended input space and cannot be made sound by adding cases. Anyone picking up similar work should model the one shape they need and refuse the rest. Round 4 is running. If it finds another *false green*, I will withdraw the guard rather than patch a fifth time, and say so here.
Author
Owner

Correction to my progress comment above — one of the examples I gave is wrong, and wrong in the direction that matters.

I wrote that "{a,b} is alternation to Gitea and a literal to git". A later review measured that and it is inverted, or at least unestablished in both halves. What I have now measured myself: bash expands braces before git is ever executed (printf '%s' docker/{ci,extra}/x yields two paths), so on the pathspec side the shell is the thing that alternates; and shlex.split, which the guard uses to read the line as text, does not expand them. How Gitea's own matcher treats braces I have not measured, so the shipped guard now claims nothing about it in either direction.

The refusal of a brace form is unchanged and still correct — it rests on the shlex fact, which is measured — but the reason I gave for it was not established. Recording it here because that comment is the durable artifact a future session would read, and an unverified mechanism stated as fact is worse than no explanation.

The examples that ARE measured against Gitea's compiler and real git, and that the design rests on: a bare docker/ci in paths: is an anchored match selecting none of the directory's contents while the pathspec docker/ci selects all of them; <file>/** matches nothing while the pathspec <file> tracks the file; a leading / is literal to Gitea while git refuses it outright (fatal: Invalid path).

Five review rounds have now each found a false prose claim — the shape #881 is open about. No round has found a false green.

**Correction to my progress comment above — one of the examples I gave is wrong, and wrong in the direction that matters.** I wrote that "`{a,b}` is alternation to Gitea and a literal to git". A later review measured that and it is inverted, or at least unestablished in both halves. What I have now measured myself: **bash expands braces** before git is ever executed (`printf '%s' docker/{ci,extra}/x` yields two paths), so on the pathspec side the shell is the thing that alternates; and `shlex.split`, which the guard uses to read the line as text, does **not** expand them. How Gitea's own matcher treats braces I have not measured, so the shipped guard now claims nothing about it in either direction. The refusal of a brace form is unchanged and still correct — it rests on the `shlex` fact, which is measured — but the *reason* I gave for it was not established. Recording it here because that comment is the durable artifact a future session would read, and an unverified mechanism stated as fact is worse than no explanation. The examples that ARE measured against Gitea's compiler and real git, and that the design rests on: a bare `docker/ci` in `paths:` is an anchored match selecting none of the directory's contents while the pathspec `docker/ci` selects all of them; `<file>/**` matches nothing while the pathspec `<file>` tracks the file; a leading `/` is literal to Gitea while git refuses it outright (`fatal: Invalid path`). Five review rounds have now each found a false prose claim — the shape #881 is open about. No round has found a false green.
Author
Owner

Closing record — WITHDRAWN, not merged. Releasing the claim.

Outcome: the guard is built, green, and pushed as fix/855-ci-image-paths-pin-agreement (head cc555659f, based on 4cd692973). No PR was opened and nothing merged. Seven independent cold-review rounds found no false green and repeatedly confirmed the mechanism, but every round found at least one prose claim untrue of the shipped code, and rounds 4-7 each found one created by the previous round's fix. Rounds 6 and 7 both recommended withdrawing rather than correcting it an eighth time, and I had committed to withdrawing if round 7 was not clean. This issue stays OPEN.

Root cause of the gap itself: #744 removed .gitea/workflows/ci-image.yml from both on.push.paths and ci-image-pin's expected as a decided tradeoff (ci.toolchain-image-publish-is-a-dispatch). That self-reference was what kept the two lists in step, and nothing replaced it.

What is MEASURED and settled — do not re-derive this

Against Gitea 1.27.1's actual glob compiler (gitea.com/gitea/act pkg/workflowpattern, ported independently by three reviewers and diffed against nektos/act) and real git ls-files:

spelling Gitea compiles to selects git pathspec docker/ci selects
docker/ci/** ^docker/ci/.*\Z the subtree, except a descendant whose name contains a newline (Go . excludes \n) the whole subtree
docker/ci ^docker/ci\Z nothing under the directory the whole subtree
docker/ci/ ^docker/ci/\Z nothing
docker/ci/**/* ^docker/ci/(.+/)?[^/]*\Z the subtree including newline-named files ([^/] does match \n)
./docker/ci/**, /docker/ci/**, docker/{ci,extra}/**, docker/ci/** anchored literals nothing (-- /docker/cifatal: Invalid path)

The key negative result: canonicalising these two dialects into one string form cannot be made sound by adding cases. Two successive canonicalisers were built and both were defeated; three of one round's five findings were created by the previous round's fix. That is the testing.verification-code-needs-its-own-proof stop-and-subtract trigger, and the design that survived deletes the canonicaliser: accept exactly <dir>/** against <dir> with segments [A-Za-z0-9._-], refuse everything else, and separately assert from the git index that each named path really is a directory (<file>/** matches nothing while the pathspec <file> tracks the file).

Amendment to this issue's own guidance. The body says to "normalise rather than compare literally". Right about the goal, wrong about the method — a normaliser over these dialects has an open-ended input space. Model the one shape you need and refuse the rest; refusal is the safe direction.

Why it was withdrawn, precisely

Not the comparison — the ~78-line module docstring. Every round found a false claim in it, and the corrections kept generating new ones: process.unknown-must-fail (a decision key I fabricated, cited twice, absent from the catalog); "{a,b} is alternation to Gitea and a literal to git" (inverted); "refused because this reader uses shlex — MEASURED, and it is the whole reason" (shlex is never called on that path; _SAFE_SEGMENT does the refusing); "a leading / is normalised by git" (git aborts); "each a false RED against some legitimate edit" (6 of 7 enumerated spellings are TRUE reds catching a broken trigger). Two reviewers independently concluded the narration is the mechanism producing the defects.

What the next session should do — this is cheap from here

  1. Start from the pushed branch. The mechanism, the tests (21), the declared clause mutation, the docs/guard-inventory.md rows and the three repointed prose sites are all done and green (full scripts/tests passes).
  2. Delete the docstring narration. Keep only: what the two lists are, which shapes are accepted, the two stated boundaries, and the newline residual. Delete every "because", every "false RED" characterisation, and every claim about what a refused spelling selects. The refusal messages already carry everything operative.
  3. Round 7's remaining unfixed findings, all in that narration: H-2 (lines 51-58, the "false RED" list — measurably wrong for 6 of 7, and in the unsafe direction: it invites widening _publish_directory to admit a bare <dir>, which would report agreement for a trigger that publishes nothing); H-4 (lines 253-255 still call **/* a pair the consumers treat differently — false); H-5 (lines 39-42 claim the assignment binding covers the heredoc case; the << clause refuses first); L-2 (the "complete refused set" omits >1 git log assignment, no path after --, and the structural refusals); L-3 (a message says "ambiguity is refused" in the found-zero case, where there is none).
  4. Round 7's L-1, the only non-narration residual: a heredoc opener split by a line continuation at column 0 evades the << clause. Adversarial-only, and NOT a regression — the pre-fix check missed it identically.

Verification of what is on the branch: full scripts/tests green (1433 passed, 2 skipped); every clause witnessed red by individual disarm under python3 -B -p no:cacheprovider; the declared mutation re-run by test_mutation_harness.py reddens only publish-path-added; the co-ordinated-rename property executed (git mv docker/ci docker/toolchain + both lists, no guard edit → 21 green).

Reusable knowledge: (a) two glob dialects cannot be canonicalised into a common string form — model one shape, refuse the rest; (b) stop-and-subtract applies to PROSE as much as to code, and a subtraction that replaces per-item explanations with one blanket explanation is not a subtraction; (c) a mutation probe that restores with git checkout -- <file> restores from the INDEX and silently reverts unstaged work — it cost me a full round's fixes here; back up to a scratch path instead; (d) two mutants of the same clause can produce same-size files within one mtime tick, and CPython's (mtime, size) invalidation then reuses the stale bytecode and attributes a red to the wrong mutant — use python3 -B -p no:cacheprovider.

Deferred: everything above. Docs updated: none merged.

## Closing record — WITHDRAWN, not merged. Releasing the claim. **Outcome:** the guard is built, green, and pushed as `fix/855-ci-image-paths-pin-agreement` (head `cc555659f`, based on `4cd692973`). **No PR was opened and nothing merged.** Seven independent cold-review rounds found **no false green** and repeatedly confirmed the mechanism, but every round found at least one prose claim untrue of the shipped code, and rounds 4-7 each found one *created by the previous round's fix*. Rounds 6 and 7 both recommended withdrawing rather than correcting it an eighth time, and I had committed to withdrawing if round 7 was not clean. This issue stays OPEN. **Root cause of the gap itself:** #744 removed `.gitea/workflows/ci-image.yml` from both `on.push.paths` and `ci-image-pin`'s `expected` as a decided tradeoff (`ci.toolchain-image-publish-is-a-dispatch`). That self-reference was what kept the two lists in step, and nothing replaced it. ### What is MEASURED and settled — do not re-derive this Against Gitea 1.27.1's actual glob compiler (`gitea.com/gitea/act` `pkg/workflowpattern`, ported independently by three reviewers and diffed against `nektos/act`) and real `git ls-files`: | spelling | Gitea compiles to | selects | git pathspec `docker/ci` selects | |---|---|---|---| | `docker/ci/**` | `^docker/ci/.*\Z` | the subtree, **except a descendant whose name contains a newline** (Go `.` excludes `\n`) | the whole subtree | | `docker/ci` | `^docker/ci\Z` | **nothing under the directory** | the whole subtree | | `docker/ci/` | `^docker/ci/\Z` | nothing | — | | `docker/ci/**/*` | `^docker/ci/(.+/)?[^/]*\Z` | the subtree **including** newline-named files (`[^/]` does match `\n`) | — | | `./docker/ci/**`, `/docker/ci/**`, `docker/{ci,extra}/**`, ` docker/ci/**` | anchored literals | nothing | (`-- /docker/ci` → `fatal: Invalid path`) | **The key negative result: canonicalising these two dialects into one string form cannot be made sound by adding cases.** Two successive canonicalisers were built and both were defeated; three of one round's five findings were created by the previous round's fix. That is the `testing.verification-code-needs-its-own-proof` stop-and-subtract trigger, and the design that survived deletes the canonicaliser: accept exactly `<dir>/**` against `<dir>` with segments `[A-Za-z0-9._-]`, refuse everything else, and separately assert from the git index that each named path really is a directory (`<file>/**` matches nothing while the pathspec `<file>` tracks the file). **Amendment to this issue's own guidance.** The body says to "normalise rather than compare literally". Right about the goal, wrong about the method — a normaliser over these dialects has an open-ended input space. Model the one shape you need and refuse the rest; refusal is the safe direction. ### Why it was withdrawn, precisely Not the comparison — the ~78-line module docstring. Every round found a false claim in it, and the corrections kept generating new ones: `process.unknown-must-fail` (a decision key I fabricated, cited twice, absent from the catalog); "`{a,b}` is alternation to Gitea and a literal to git" (inverted); "refused because this reader uses `shlex` — MEASURED, and it is the whole reason" (`shlex` is never called on that path; `_SAFE_SEGMENT` does the refusing); "a leading `/` is normalised by git" (git aborts); "each a false RED against some legitimate edit" (6 of 7 enumerated spellings are TRUE reds catching a broken trigger). Two reviewers independently concluded the narration is the mechanism producing the defects. ### What the next session should do — this is cheap from here 1. Start from the pushed branch. The mechanism, the tests (21), the declared clause mutation, the `docs/guard-inventory.md` rows and the three repointed prose sites are all done and green (full `scripts/tests` passes). 2. **Delete the docstring narration.** Keep only: what the two lists are, which shapes are accepted, the two stated boundaries, and the newline residual. Delete every "because", every "false RED" characterisation, and every claim about what a *refused* spelling selects. The refusal messages already carry everything operative. 3. Round 7's remaining unfixed findings, all in that narration: **H-2** (lines 51-58, the "false RED" list — measurably wrong for 6 of 7, and in the unsafe direction: it invites widening `_publish_directory` to admit a bare `<dir>`, which would report agreement for a trigger that publishes nothing); **H-4** (lines 253-255 still call `**/*` a pair the consumers treat differently — false); **H-5** (lines 39-42 claim the assignment binding covers the heredoc case; the `<<` clause refuses first); **L-2** (the "complete refused set" omits >1 `git log` assignment, no path after `--`, and the structural refusals); **L-3** (a message says "ambiguity is refused" in the found-zero case, where there is none). 4. Round 7's **L-1**, the only non-narration residual: a heredoc opener split by a line continuation at column 0 evades the `<<` clause. Adversarial-only, and NOT a regression — the pre-fix check missed it identically. **Verification of what is on the branch:** full `scripts/tests` green (1433 passed, 2 skipped); every clause witnessed red by individual disarm under `python3 -B -p no:cacheprovider`; the declared mutation re-run by `test_mutation_harness.py` reddens only `publish-path-added`; the co-ordinated-rename property executed (`git mv docker/ci docker/toolchain` + both lists, no guard edit → 21 green). **Reusable knowledge:** (a) two glob dialects cannot be canonicalised into a common string form — model one shape, refuse the rest; (b) stop-and-subtract applies to PROSE as much as to code, and a subtraction that replaces per-item explanations with one blanket explanation is not a subtraction; (c) a mutation probe that restores with `git checkout -- <file>` restores from the INDEX and silently reverts unstaged work — it cost me a full round's fixes here; back up to a scratch path instead; (d) two mutants of the same clause can produce same-size files within one mtime tick, and CPython's `(mtime, size)` invalidation then reuses the stale bytecode and attributes a red to the wrong mutant — use `python3 -B -p no:cacheprovider`. **Deferred:** everything above. **Docs updated:** none merged.
timothy removed the in-progress label 2026-08-30 19:29:17 +02:00
timothy added the in-progress label 2026-08-30 19:47:56 +02:00
Author
Owner

Re-claiming — the withdrawal above is retracted, and the reason is worth stating rather than quietly reversing.

I withdrew because seven rounds each found a prose defect and I had committed to stopping. That was the right call about the narration and the wrong call about the guard: round 7's own diagnosis was that the ~78-line docstring is the mechanism producing the defects and that the honest subtraction is that narration, not the comparison — which is a deletion, not another correction, and deletions do not manufacture new claims.

So the docstring is cut from ~78 lines to 42 of operative fact. Deleted outright: the "each a false RED against some legitimate edit" list (measurably wrong for 6 of 7 spellings, and in the unsafe direction — it invited widening the check to admit a bare <dir>, which is exactly the false green this guard exists to stop); the "complete refused set" claim, which was not complete; the assertion that the assignment binding covers the heredoc case, which the << clause refuses first; and the inline comment calling **/* a pair the consumers treat differently, which measurement contradicts. Round 7's one functional finding is fixed too — a hard-coded ["docker/ci"] that broke the co-ordinated-rename property the file advertises and failed with a message describing a refusal that never happened; the expectation is now derived, and the rename property is verified by executing it.

Branch fix/855-ci-image-paths-pin-agreement, now at 3dbdff550. Full scripts/tests green, every clause witnessed red by individual disarm. One more independent review round is running; if it is clean this merges, and if it is not I will say so here rather than patch a ninth time.

**Re-claiming — the withdrawal above is retracted, and the reason is worth stating rather than quietly reversing.** I withdrew because seven rounds each found a prose defect and I had committed to stopping. That was the right call about the *narration* and the wrong call about the *guard*: round 7's own diagnosis was that the ~78-line docstring is the mechanism producing the defects and that the honest subtraction is **that narration, not the comparison** — which is a deletion, not another correction, and deletions do not manufacture new claims. So the docstring is cut from ~78 lines to 42 of operative fact. Deleted outright: the "each a false RED against some legitimate edit" list (measurably wrong for 6 of 7 spellings, and in the unsafe direction — it invited widening the check to admit a bare `<dir>`, which is exactly the false green this guard exists to stop); the "complete refused set" claim, which was not complete; the assertion that the assignment binding covers the heredoc case, which the `<<` clause refuses first; and the inline comment calling `**/*` a pair the consumers treat differently, which measurement contradicts. Round 7's one functional finding is fixed too — a hard-coded `["docker/ci"]` that broke the co-ordinated-rename property the file advertises and failed with a message describing a refusal that never happened; the expectation is now derived, and the rename property is verified by executing it. Branch `fix/855-ci-image-paths-pin-agreement`, now at `3dbdff550`. Full `scripts/tests` green, every clause witnessed red by individual disarm. One more independent review round is running; if it is clean this merges, and if it is not I will say so here rather than patch a ninth time.
timothy removed the in-progress label 2026-08-30 21:26:14 +02:00
Author
Owner

Closing record

Outcome: scripts/tests/test_ci_image_paths_pin_agreement.py shipped in PR #902, squashed to dd0f75f1b. It derives ci-image.yml's on.push.paths and ci-image-pin's git log pathspec from the two workflow documents and asserts set equality in both directions, with a docs/guard-inventory.md row, a declared clause mutation in scripts/tests/mutation_manifest.py, and the three prose sites repointed at it. The earlier withdrawal comment on this issue is superseded.

Root cause: #744 removed .gitea/workflows/ci-image.yml from both on.push.paths and ci-image-pin's expected as a decided tradeoff (ci.toolchain-image-publish-is-a-dispatch). That self-reference was what kept the two lists in step, and nothing replaced it — the hand-maintained-mirror shape docs/defect-shapes-773.md §4 catalogues.

Decisions/conventions changed: none added. This applies testing.guard-derives-population-from-source, testing.guard-ships-with-mutation-proof, testing.mutation-claims-are-executed, and the STOP-AND-SUBTRACT threshold in testing.verification-code-needs-its-own-proof, which is what decided the final design.

Reusable knowledge — the negative result is the valuable part. Two glob dialects cannot be canonicalised into a common string form. Three designs were tried. Symmetric stripping mapped docker/ci onto docker/ci/** — but a bare docker/ci in a Gitea paths: filter compiles to an anchored ^docker/ci and selects NONE of the directory's contents, while the git pathspec docker/ci selects all of it. Making the canonicaliser asymmetric fixed that pair and surfaced four more of the same shape ({a,b}, a leading ./, a trailing /, <file>/** vs <file>), three of them created by the previous round's fix. What holds is a guard that models exactly ONE pair of spellings and refuses every other, because the refusal direction is safe: a red asks a question, a green over a divergence is the defect. Amendment to this issue's own guidance: the body said to "normalise rather than compare literally" — right about the goal, wrong about the method. Model the one shape you need and refuse the rest.

Four more traps worth carrying:

  • Read the compiler your instance actually runs. Gitea 1.27.1 uses its in-tree modules/actions/workflowpattern -> modules/glob.CompileWorkflow, NOT nektos/act's PatternToRegex. They agree on the three patterns this guard rests on and DISAGREE on brace, **/x and character-class forms — exactly the spellings someone extending it would look up.
  • paths: [] is not "publishes nothing". Skip returns false on an empty sequence, so an empty list filters nothing and every push publishes. Diagnosing it as an absence of publishing names the safe failure for the dangerous one.
  • A mutation probe restored with git checkout -- <file> restores from the INDEX and silently reverts unstaged work. It cost a full round's fixes here; back up to a scratch path instead, and commit before probing.
  • Two mutants of one clause can produce same-size files within one mtime tick, and CPython's (mtime, size) invalidation then reuses stale bytecode and attributes a red to the wrong mutant. Use python3 -B -p no:cacheprovider.

Verification: full scripts/tests green (1435 passed, 2 skipped) and green in CI, including PR Gates / Script lint and tests, which is the job that actually runs the guard. Every clause witnessed red by individual disarm; the declared mutation is re-run by test_mutation_harness.py every suite and reddens only publish-path-added, its counterpart only pathspec-extra-entry, so CLAUSE is the honest grade. The advertised co-ordinated-rename property was executed (git mv docker/ci docker/toolchain plus both lists, no guard edit -> green). Nine independent cold-review rounds, alternating Codex and a separate Claude line, each from a review-only brief in an isolated worktree; no round found a false green, and the last fuzzed 27,720 publish/pathspec pairs against a port of the deployed compiler and real git ls-files.

Deferred: (a) the guard compares the pathspec the pin job WRITES and does not establish that the staleness comparison consumes it; (b) the git-history half of ci-image-pin still has no test; (c) a descendant whose path below <dir> contains a newline is selected by the pathspec and not by the publish pattern; (d) restoring ci-image.yml to both lists is a file-against-file pair the guard refuses and would need a file arm — docs/ci-cd.md now forewarns about this next to the paragraph that calls the reversal workable; (e) one known nit shipped deliberately rather than polished a tenth time: the Unnormalisable class docstring names _directory while the class has two raise sites. (a)-(d) are stated in the module docstring and the inventory rows rather than left implied.

Docs updated: docs/guard-inventory.md (new row, bounded ci-image-pin workflow-job row, summary counts 47/27), docs/ci-cd.md, .gitea/workflows/ci-image.yml and .gitea/workflows/pr-checks.yml comment sites.

## Closing record **Outcome:** `scripts/tests/test_ci_image_paths_pin_agreement.py` shipped in PR #902, squashed to `dd0f75f1b`. It derives `ci-image.yml`'s `on.push.paths` and `ci-image-pin`'s `git log` pathspec from the two workflow documents and asserts set equality in both directions, with a `docs/guard-inventory.md` row, a declared clause mutation in `scripts/tests/mutation_manifest.py`, and the three prose sites repointed at it. The earlier withdrawal comment on this issue is superseded. **Root cause:** #744 removed `.gitea/workflows/ci-image.yml` from both `on.push.paths` and `ci-image-pin`'s `expected` as a decided tradeoff (`ci.toolchain-image-publish-is-a-dispatch`). That self-reference was what kept the two lists in step, and nothing replaced it — the hand-maintained-mirror shape `docs/defect-shapes-773.md` §4 catalogues. **Decisions/conventions changed:** none added. This applies `testing.guard-derives-population-from-source`, `testing.guard-ships-with-mutation-proof`, `testing.mutation-claims-are-executed`, and the STOP-AND-SUBTRACT threshold in `testing.verification-code-needs-its-own-proof`, which is what decided the final design. **Reusable knowledge — the negative result is the valuable part.** *Two glob dialects cannot be canonicalised into a common string form.* Three designs were tried. Symmetric stripping mapped `docker/ci` onto `docker/ci/**` — but a bare `docker/ci` in a Gitea `paths:` filter compiles to an anchored `^docker/ci` and selects NONE of the directory's contents, while the git pathspec `docker/ci` selects all of it. Making the canonicaliser asymmetric fixed that pair and surfaced four more of the same shape (`{a,b}`, a leading `./`, a trailing `/`, `<file>/**` vs `<file>`), three of them created by the previous round's fix. What holds is a guard that models exactly ONE pair of spellings and refuses every other, because the refusal direction is safe: a red asks a question, a green over a divergence is the defect. **Amendment to this issue's own guidance:** the body said to "normalise rather than compare literally" — right about the goal, wrong about the method. Model the one shape you need and refuse the rest. Four more traps worth carrying: - *Read the compiler your instance actually runs.* Gitea 1.27.1 uses its in-tree `modules/actions/workflowpattern` -> `modules/glob.CompileWorkflow`, NOT `nektos/act`'s `PatternToRegex`. They agree on the three patterns this guard rests on and DISAGREE on brace, `**/x` and character-class forms — exactly the spellings someone extending it would look up. - *`paths: []` is not "publishes nothing".* `Skip` returns false on an empty sequence, so an empty list filters nothing and every push publishes. Diagnosing it as an absence of publishing names the safe failure for the dangerous one. - *A mutation probe restored with `git checkout -- <file>` restores from the INDEX* and silently reverts unstaged work. It cost a full round's fixes here; back up to a scratch path instead, and commit before probing. - *Two mutants of one clause can produce same-size files within one mtime tick*, and CPython's `(mtime, size)` invalidation then reuses stale bytecode and attributes a red to the wrong mutant. Use `python3 -B -p no:cacheprovider`. **Verification:** full `scripts/tests` green (1435 passed, 2 skipped) and green in CI, including `PR Gates / Script lint and tests`, which is the job that actually runs the guard. Every clause witnessed red by individual disarm; the declared mutation is re-run by `test_mutation_harness.py` every suite and reddens only `publish-path-added`, its counterpart only `pathspec-extra-entry`, so `CLAUSE` is the honest grade. The advertised co-ordinated-rename property was executed (`git mv docker/ci docker/toolchain` plus both lists, no guard edit -> green). Nine independent cold-review rounds, alternating Codex and a separate Claude line, each from a review-only brief in an isolated worktree; **no round found a false green**, and the last fuzzed 27,720 publish/pathspec pairs against a port of the deployed compiler and real `git ls-files`. **Deferred:** (a) the guard compares the pathspec the pin job WRITES and does not establish that the staleness comparison consumes it; (b) the git-history half of `ci-image-pin` still has no test; (c) a descendant whose path below `<dir>` contains a newline is selected by the pathspec and not by the publish pattern; (d) restoring `ci-image.yml` to both lists is a file-against-file pair the guard refuses and would need a file arm — `docs/ci-cd.md` now forewarns about this next to the paragraph that calls the reversal workable; (e) one known nit shipped deliberately rather than polished a tenth time: the `Unnormalisable` class docstring names `_directory` while the class has two raise sites. (a)-(d) are stated in the module docstring and the inventory rows rather than left implied. **Docs updated:** `docs/guard-inventory.md` (new row, bounded `ci-image-pin` workflow-job row, summary counts 47/27), `docs/ci-cd.md`, `.gitea/workflows/ci-image.yml` and `.gitea/workflows/pr-checks.yml` comment sites.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#855