fix(819): derive the SPA page-size guard population from the git index #875

Merged
timothy merged 17 commits from fix/819-pagesize-guard-git-index into main 2026-08-29 11:14:16 +02:00
Owner

Closes the #806 residual that PR #818 explicitly deferred.

The defect

web/src/api/pageSizeCallSites.guard.test.ts asserts EXACT completeness (a multiset, both
directions) over the pageSize call sites it discovers — but discovered the files with
import.meta.glob over web/src. A directory walk is not an authoritative population, so an
untracked .ts/.tsx entered it and failed as unregistered on that checkout, while CI — which only
ever checks out tracked files — stayed green. Developer-red / CI-green, the #778 shape #806 removed
everywhere else.

Witnessed before the fix: an untracked Scratch819Untracked.tsx holding pageSize: 25 gave
UNREGISTERED (1) — screens/Scratch819Untracked.tsx:literal:25.

The fix

The glob stays, as the source of file content. The population is the git index, read by
web/vite-plugins/trackedSourceFiles.ts in Vite's own Node context and handed to the app project as
a virtual module — which is how the index is reached without admitting @types/node to
tsconfig.app.json, the obstacle that deferred this.

It fails rather than degrades (a failed or empty git ls-files throws) and reads the index
lazily, so vite build never shells out to git.

Three mechanisms carry the proof, each added because the previous one was measured insufficient:

  1. A closed-form restatement proves the shared scope predicate. It may share no helper, at any
    depth
    , with the predicate it checks — anything shared sits on both sides of the comparison and
    cancels.
  2. A second, independent git query (ls-files --others) cross-checks the population: every
    in-scope walked path must appear in tracked ∪ others. Narrowing tracked cannot suppress that,
    because it adds nothing to others.
  3. Real-git tests execute the derivation against a temp repository rather than only injecting a
    fake runner.

Why it took sixteen review rounds

Every early round found one mechanism: the proof shared something with its subject, so a
narrowing cancelled and went green. Each fix reproduced it one layer down — the scope, then an
example table (4 of 8 directories), then a delegated sub-predicate, then a basename helper, then
the population array itself. Two of those were blind: they hid a planted call site with the whole
suite green.

What terminated it was stating the criterion as a checkable property rather than an instruction,
and — where a restatement cannot police the population it reads — cross-checking against a genuinely
different query.

Verification

Claim How it was measured
The guard is not blind A planted untracked file is ignored; the same file git added reddens
Every clause is load-bearing Full mutation battery; each disarm reddens a named test
Holes are named, not dropped A tracked dotfile, dot-directory, or core.ignorecase spelling divergence each redden the shortfall test
No spurious red Unstaged deletion, staged rename, merge conflict, git rm --cached all stay green
The Docker stage works Replicated with both .git absent and git off PATH: rc=0, 117 files / 1226 tests

npx vitest run 119 files / 1272 tests · tsc -b --force · eslint . · npm run build ·
pytest scripts/tests 1228 passed · decisions_validate OK · catalog zero-diff.

Residuals — six, each with its direction MEASURED

Stated rather than implied, because fail-direction is the argument for tolerating a residual:
(2), (4), (5) are fail-noisy; (1) (a three-site coordinated edit) and (3) (a
union-preserving mispartition) are blind; (6) (watch-mode staleness) is each in turn. The
obvious fix for (6) — configureServer invalidation — was implemented, measured, and rejected:
git add fires no watcher event, so it blinds the create-then-stage sequence.

Docs

docs/guard-inventory.md (audit row + scope item), docs/testing.md, docs/spa-conventions.md
(the new web/vite-plugins/ seam), and testing.guard-derives-population-from-source — which
gains a bounded exception and the closed-form criterion, with the catalog regenerated.

fixes #819

Closes the #806 residual that PR #818 explicitly deferred. ## The defect `web/src/api/pageSizeCallSites.guard.test.ts` asserts EXACT completeness (a multiset, both directions) over the `pageSize` call sites it discovers — but discovered the *files* with `import.meta.glob` over `web/src`. A directory walk is not an authoritative population, so an untracked `.ts`/`.tsx` entered it and failed as unregistered on that checkout, while CI — which only ever checks out tracked files — stayed green. Developer-red / CI-green, the #778 shape #806 removed everywhere else. **Witnessed before the fix:** an untracked `Scratch819Untracked.tsx` holding `pageSize: 25` gave `UNREGISTERED (1) — screens/Scratch819Untracked.tsx:literal:25`. ## The fix The glob stays, as the source of file **content**. The **population** is the git index, read by `web/vite-plugins/trackedSourceFiles.ts` in Vite's own Node context and handed to the app project as a virtual module — which is how the index is reached without admitting `@types/node` to `tsconfig.app.json`, the obstacle that deferred this. It **fails rather than degrades** (a failed or empty `git ls-files` throws) and reads the index **lazily**, so `vite build` never shells out to git. Three mechanisms carry the proof, each added because the previous one was measured insufficient: 1. **A closed-form restatement** proves the shared scope predicate. It may share **no helper, at any depth**, with the predicate it checks — anything shared sits on both sides of the comparison and cancels. 2. **A second, independent git query** (`ls-files --others`) cross-checks the population: every in-scope walked path must appear in `tracked ∪ others`. Narrowing `tracked` cannot suppress that, because it adds nothing to `others`. 3. **Real-git tests** execute the derivation against a temp repository rather than only injecting a fake runner. ## Why it took sixteen review rounds Every early round found one mechanism: **the proof shared something with its subject**, so a narrowing cancelled and went green. Each fix reproduced it one layer down — the scope, then an example table (4 of 8 directories), then a delegated sub-predicate, then a `basename` helper, then the population array itself. Two of those were blind: they hid a planted call site with the whole suite green. What terminated it was stating the criterion as a **checkable property** rather than an instruction, and — where a restatement cannot police the population it reads — cross-checking against a genuinely different query. ## Verification | Claim | How it was measured | |---|---| | The guard is not blind | A planted untracked file is ignored; the same file `git add`ed reddens | | Every clause is load-bearing | Full mutation battery; each disarm reddens a **named** test | | Holes are named, not dropped | A tracked dotfile, dot-directory, or `core.ignorecase` spelling divergence each redden the shortfall test | | No spurious red | Unstaged deletion, staged rename, merge conflict, `git rm --cached` all stay green | | The Docker stage works | Replicated with **both** `.git` absent **and** `git` off `PATH`: rc=0, 117 files / 1226 tests | `npx vitest run` 119 files / 1272 tests · `tsc -b --force` · `eslint .` · `npm run build` · `pytest scripts/tests` 1228 passed · `decisions_validate` OK · catalog zero-diff. ## Residuals — six, each with its direction MEASURED Stated rather than implied, because fail-direction is the argument for tolerating a residual: **(2), (4), (5)** are fail-noisy; **(1)** (a three-site coordinated edit) and **(3)** (a union-preserving mispartition) are **blind**; **(6)** (watch-mode staleness) is each in turn. The obvious fix for (6) — `configureServer` invalidation — was implemented, measured, and **rejected**: `git add` fires no watcher event, so it blinds the create-then-stage sequence. ## Docs `docs/guard-inventory.md` (audit row + scope item), `docs/testing.md`, `docs/spa-conventions.md` (the new `web/vite-plugins/` seam), and `testing.guard-derives-population-from-source` — which gains a bounded exception and the closed-form criterion, with the catalog regenerated. fixes #819
timothy added 17 commits 2026-08-29 10:44:14 +02:00
`pageSizeCallSites.guard.test.ts` asserts EXACT completeness over the files it
discovers, but discovered them with `import.meta.glob` over `web/src`. A
directory walk is not an authoritative source: an untracked `.ts`/`.tsx` — a
scratch file, a half-finished component, an editor dropping — entered the
population and failed as unregistered on that checkout, while CI, which only
ever checks out tracked files, stayed green. Developer-red / CI-green, the #778
shape #806 removed from every other guard here.

Witnessed before the fix: planting an untracked `Scratch819Untracked.tsx` with a
`pageSize: 25` literal reddened the guard with
`UNREGISTERED (1) — screens/Scratch819Untracked.tsx:literal:25`.

The glob stays, as the source of file CONTENT. The POPULATION is now
`git ls-files`, read by `web/vite-plugins/trackedSourceFiles.ts` in Vite's own
Node context and handed to the app project as the virtual module
`virtual:etv-tracked-source-files`. That is what reaches the index without
admitting `@types/node` to `tsconfig.app.json` — the obstacle that deferred this
in #818, since wiring those types in was tried and reverted (it leaked Node's
ambient `setTimeout` into the app project and broke three unrelated
`window.setTimeout` mocks).

The plugin fails rather than degrading: a failed or empty `git ls-files` throws,
because answering "could not tell" with the very population the index replaced
is a permanent no-op that reports green (#772's shape). It reads the index
lazily, inside `load()`, so `vite build` — including the Docker image build,
whose context carries no `.git` — never shells out to git. Verified by building
in a copy of `web/` outside any repository.

Proofs, all executed:
- clause mutation: deleting `if (!isTrackedSourcePath(...))` from
  `listSourceFilesFrom` reddens exactly one named test, `EXCLUDES an untracked
  file that matches the scope in every other way (#819)`, and nothing else. The
  pipeline was extracted to take injected inputs precisely so that clause is
  provable — over real source on a clean checkout it removes nothing, so a
  plant-a-real-file check pins nothing, the same argument
  `isScannableSourceFileName` already records.
- negative control: the same planted file, once `git add`ed, is caught again.
  Untracked is ignored, tracked is caught — the guard did not go blind.
- positive control against a clause mutated to reject everything.
- anti-vacuity on the consuming side: the real index is >50 paths and contains
  named known files, so a plugin that stopped being registered cannot pass an
  empty population off as a satisfied one.

Residual, stated rather than implied: a file tracked but DELETED in the working
tree has no content and drops out. Not asserted — an unstaged deletion is a
normal developer state, and a guard red on one gets ignored (#806) — and a
REGISTERED site in such a file still fails loudly on the STALE direction.

fixes #819
Making the page-size guard index-derived makes it READ as covered by
`scripts/tests/test_guard_populations_derive_from_git.py`, which proves exactly
this property over every guard it knows about. It does not reach this one: that
register detects Python modules importing `scripts/tests/tracked_files.py`, and
its own docstring already names the gap — "a module deriving a file population
some other way — shelling out to `git ls-files` itself — is invisible to it."

An unstated gap here would be worse than the one #819 closed, because a reader
would infer mechanical coverage from the conversion itself.

refs #819
Three defects found by cold review of 9e5c288db, all reproduced here before
fixing and re-measured after.

BLOCKER — the Docker image build failed on the previous commit.
`docker/Dockerfile`'s web-build stage copies only `web/` and `design-system/`,
so it has no `.git`, and `node:22-bookworm-slim` ships no git binary. Its
`RUN` line ran `npm test -- --run`, which loads the virtual module, which shells
out to git and throws by design. Measured in an exact gitless replica of the
stage: `Test Files 1 failed | 117 passed`, against `117 passed` on the parent.
`build` is gated `needs: [test, migrations, scan]` and the `test` job — which
carries no `if:`, so it runs on PR, main and tag alike — already runs the
identical suite on a real checkout, so the Dockerfile's run was duplicated work
and is dropped rather than propped up with a `.git` copy and a git install. The
previous commit message asserted this build was safe on the strength of having
verified `vite build` alone; the test step on the same line was never measured.

HIGH — a tracked file could still leave the population silently, and one way was
a REGRESSION. `import.meta.glob` matches no dotfile or dot-directory, and on
`core.ignorecase=true` a disk spelling that diverges from the index spelling
(`caseprobe.tsx` vs `CaseProbe.tsx`) is tracked, present, and invisible to the
intersection — measured blind here, and measured CAUGHT by the parent commit.
The stated residual named only the deleted-file case, so the prose claimed a
completeness the code did not have. Fixed by asserting the other direction: the
plugin now reports which tracked paths are absent from disk, and the guard
requires every tracked, on-disk, in-scope path to have reached the scanner. A
mid-edit deletion stays tolerated — measured, still green — because that is
transient, while a hole is now named and red. This is the rule
`scripts/tests/tracked_files.py` already states for the Python derivations:
existence is ASSERTED, never filtered.

MEDIUM — the clause proof was fixture-only. Every clause test drove
`listSourceFilesFrom` with injected inputs, so `... && tracked.size < 10` was
live over the 1-2 entry fixtures and dead over the real 260-path index: the fix
fully regressed, suite green. Measured. Added a liveness test that drives the
REAL glob and REAL index with one synthetic key. That mutation now reddens it,
and disarming the clause outright reddens two named tests.

Also: the plugin refuses instead of guessing when `configResolved` never ran
(a wrong root was a silent wrong answer), its doc comment states the `src/`-only
pathspec so a future caller cannot inherit a truncated population, and git's
stderr is piped so the framed error is the only message.

Docs: `docs/spa-conventions.md` §1 gains the `web/vite-plugins/` layout entry;
`docs/guard-inventory.md`'s hand-written TS/C# list gains the fourth file the
review noted it had already gone stale against, and the row's Notes no longer
overstate the population as "`git ls-files`" when it is the walk intersected
with the index.

refs #819
Round 2's cold review found the previous commit had reproduced, one layer up,
the exact defect it was fixing: `expectedTrackedSources` — the function that
closes the dot-directory / case-divergence hole — had no clause proof. Measured:
replacing its body with `return []` left all 1249 tests green while the hole it
exists to catch went undetected, and deleting the `absentFromDisk` subtraction
(the whole point of the plugin's second field) was green too.

Three proofs added, each MEASURED red against the mutation it names:
- a direct fixture test pinning all four terms of `expectedTrackedSources`
  (`return []` and dropping the subtraction now each redden it);
- an anti-vacuity floor INSIDE the shortfall test, so an empty expected set
  cannot pass the comparison it feeds;
- a table pinning `isInScopeSourcePath`. That one closes a hole the review
  measured separately: the walk and the index side applied the SAME scope
  filters, so scope cancelled out of the equality between them — excluding
  `src/components/` dropped 13 real files with the whole suite green. The two
  copies of that predicate are now ONE named function, and the table is what
  reddens a change to it, since no assertion comparing the two sides can.

Also corrected, all found by the same review:
- The comment justifying the `absentFromDisk` subtraction cited
  `scripts/tests/tracked_files.py` as stating the rule it follows. That module
  states the OPPOSITE and enforces it with `assert absolute.is_file()`. It is
  now recorded as a deliberate DEVIATION with its reason (that population is a
  handful of hooks where a missing file is remarkable; this one is ~260 SPA
  sources a developer edits continuously), rather than as an instance of it.
- `absentFromDisk` is named for what it measures — no READABLE file — not for
  its common cause. `existsSync` follows links, so a tracked BROKEN SYMLINK is a
  committed, permanent state landing in that set, which "deleted in the working
  tree" licensed subtracting without saying so.
- The Dockerfile comment claimed the `needs: [test, …]` edge alone guarantees no
  image ships from an untested tree. `Test SPA` is also gated on `docs_only` and
  the #420 revalidate skip, and `build` is not gated on `revalidate`; the
  property survives via #420's byte-identical-tree argument, which the comment
  now names instead of implying the wrong mechanism.
- `docs/testing.md`'s web row was falsified by this PR: `web/vite-plugins/**` is
  a third collection root, and `npm test` now hard-requires a git checkout.
- Two prose slips: guard-inventory counted "the first of those" onto the wrong
  file, and both `docs/spa-conventions.md` and the Dockerfile said "guards"
  plural where exactly one derives a population.

Rebased onto 4b3f6f6c9 (#872), which rewrote `docs/guard-inventory.md`.

refs #819
Round 3's cold review found the same defect class for the third round running,
and this time in the fix for round 2's finding. That fix pinned the shared
`isInScopeSourcePath` with a hand-written table of 8 example paths. The table
names four of `src/`'s eight directories, so it proved only the one mutation the
previous round happened to use. MEASURED: narrowing the predicate to skip
`media|app|mediaSources|schedules` removed 23 real, tracked, on-disk files —
16% of the SPA — with all 35 tests green, and `/media/` alone was green too.

That is precisely what the rule it was meant to satisfy calls out: "a
hand-written literal list of members is the same defect in slower motion — a
filter frozen at authoring time, correct on the day it was written and unable to
report the day it stopped being."

So the mechanism is deleted rather than extended. The scope is now proved by
DECOMPOSITION over the whole tracked population: the predicate must equal its two
documented components — `!includes('/generated/')` and `isScannableSourceFileName`
— each already independently pinned by the 14-case filename table. Any new term,
naming any directory or any filename shape, makes the two sets differ. MEASURED
red against all four mutations above plus a filename-shaped one
(`!endsWith('.d.ts')`) that a path table could never have seen. The table stays
as worked examples and now says so instead of claiming to be the proof.

Also from round 3:
- The anti-vacuity floor's stated reason was falsified by the commit that wrote
  it: with the fixture test present, `expectedTrackedSources -> []` reddens even
  without the floor. Restated as what the floor uniquely covers — vacuity in the
  REAL plugin output, which injected fixtures structurally cannot reach — and the
  floor is now derived from the index size rather than a literal that was true
  once. The stale "1249-test suite" figure is gone with it.
- `testing.guard-derives-population-from-source` states "assert existence rather
  than filtering on it" in its `rule:` field, so the catalog row a reader
  resolves by topic said the opposite of what this guard does. The record now
  carries the bounded exception — filtering on existence is admissible only where
  the OTHER direction reports the on-disk hole, so the absent member stays
  representable — and the catalog is regenerated. Without that pairing it remains
  the original defect.
- Three cross-references the refactor invalidated: the `/generated/` exclusion
  moved to `isInScopeSourcePath`, and one comment said "above" of a predicate
  defined below.
- guard-inventory: the docstring quote is attributed to
  `test_every_index_derived_module_is_registered` rather than the module, with the
  elided `Path.rglob` clause restored, and the over-long line rewrapped.

Refs: #819
Decisions-Edit: yes
Round 4's cold review found the recurring class had moved one level down rather
than terminated. The decomposition restated the `/generated/` half inline but
DELEGATED the filename half to `isScannableSourceFileName` — the same function
`isInScopeSourcePath` calls — putting it on both sides of the comparison, where
it cancels algebraically. Exactly the cancellation that hid the walk-vs-index
narrowing in round 2 and the directory narrowing in round 3.

MEASURED survivors on the previous head, whole suite, no filter: adding
`&& !name.endsWith('.d.ts')` to that predicate dropped a real tracked file
(`src/vite-env.d.ts`) and a term skipping lowercase-initial `.tsx` dropped
eleven — 8% of the in-scope SPA — both with all 1259 tests green. Reproduced
here before fixing. Both now redden `the shared scope predicate is EXACTLY its
two documented components`.

The fix is to restate the filename predicate inline too, so the decomposition
delegates nothing. What that is, said plainly instead of overclaimed: a verbatim
restatement of a FIXED-SIZE predicate checked against itself every run — the
`MARKED_JOBS` shape the record already blesses — which does not decay as the SPA
grows, unlike a list of members or example paths. Two residuals are now stated
rather than implied: an author editing the predicate AND the restatement together
passes deliberately, the two-site edit being the signal; and a term matching zero
tracked paths survives until the day it first matches one. So the claim is "a
term becomes visible the moment it affects a tracked path", not the previous
"regardless of what it excludes", which round 4 measured false.

Also from round 4:
- `docs/guard-inventory.md` still named the table as the proof — the mechanism
  the previous commit retracted in code but did not sweep out of the docs. It now
  describes the decomposition, why both halves are restated, and the residual.
  The same retracted phrase in the walk-side code comment is gone too; grepping
  the subject ("table of cases") found both, which is the sweep that was missed.
- The anti-vacuity floor was bounded by every tracked path under `src/` (260),
  a denominator that grows with test files while the numerator counts sources.
  It is now bounded by the IN-SCOPE count, so at most a tenth of those may be
  absent from disk before the plugin result reads as degenerate.
- The Dockerfile comment named two gates on `Test SPA` and argued only one; the
  `docs_only` arm holds because `ci-detect-docs-only.sh` classifies any `web/**`
  path as not-docs-only, so a tree taking that skip has an SPA subtree already
  tested.
- "~260 SPA sources" read as the guarded population in both the record and the
  guard comment; it is every tracked path under `web/src/`, of which ~141 are in
  scope. A worked example named a file that does not exist.

Refs: #819
Decisions-Edit: yes
Round 5's cold review found the class a fifth time, and it was a BLOCKER rather
than a narrowing. The restatement inlined both documented components but computed
its filename with `basename(path)` — the same helper `isInScopeSourcePath` calls
— so `basename` sat on both sides of the comparison and cancelled, exactly as the
filename predicate did in round 4 and the path table in round 3.

MEASURED on the previous head: teaching `basename` to return '' for paths four
segments deep removed 15 real tracked files, and a `pageSize: 100` planted in one
of them (`src/builder/rules/compile.ts`) went completely unseen — 36/36 green.
Reproduced here before fixing. The guard did not merely see less; it stopped
working and reported success. An `index.ts` variant hid 3 more.

What actually terminates this, and why the previous four attempts did not: each
one restated something and left something else SHARED, and "restate the scope"
gives no way to tell whether you are done. The criterion is now a checkable
property, written where the next author will edit:

  the restatement must be CLOSED FORM over the raw path — it may share NO helper,
  at ANY depth, with `isInScopeSourcePath`.

`basename` is why it is phrased that way: it reads as plumbing rather than as
policy, which is precisely why it got shared. The inlined `lastIndexOf` carries a
"do not tidy this into basename(path)" note for the same reason. Both mutations
above now redden `the shared scope predicate is EXACTLY its two documented
components`, including with the rogue site planted.

The previous head also ASSERTED that its residuals were complete, which this
blocker falsifies — a single-site edit to a shared helper narrowed scope
invisibly. Both the code comment and `docs/guard-inventory.md` now record the
three retracted mechanisms with what each one measured, and state the two
residuals as residuals rather than as closure.

Also from round 5:
- The anti-vacuity floor fired at 15 unstaged in-scope deletions with a bare
  assertion message, inside a test whose own failure text tells the reader to
  rename files or widen the glob. Wrong cause, and the #806 shape. It now carries
  its own message naming deletions first and a degenerate plugin result second.
- The Dockerfile's `docs_only` claim stated an accident as the rule.
  `ci-detect-docs-only.sh` classifies by PATH SHAPE (`docs/` or `*.md`), not by
  directory, so "any `web/**` path is not docs-only" is true only because no
  `web/*.md` is tracked. Restated, with `design-system/` named too.
- `ABSENT_FROM_DISK` emptied is an uncaught mutation. Recorded as a residual with
  its DIRECTION, which is what makes it tolerable: an empty set makes the guard
  red on an unstaged deletion, never blind.
- The decision record's `signals:` line carried neither the bounded exception's
  vocabulary nor #819, so the record's discovery surface did not carry the change
  its `rule:` makes.
- A worked example still named `src/api/generated/v1.ts`; the tracked file is
  `v1.d.ts`. Swept both tables this time rather than the one site.

Refs: #819
Decisions-Edit: yes
Round 6's cold review found the class had moved from the scope predicate into
the POPULATION SOURCE. Every comparison in the guard was between two things
derived from `trackedSources.tracked` — the scope restatement, the shortfall
direction, the floor's denominator — so a filter applied inside the plugin
shrank both sides of all of them and cancelled.

MEASURED on the previous head: adding `&& !path.startsWith('src/components/')`
to the plugin's split dropped 8 real in-scope files, and a `pageSize: 100`
planted in one of them went entirely unseen — 46/46 green. Reproduced here
before fixing, and again on a second subtree (`/rules/`, 7 files).

The fix is not another restatement. The plugin now runs a SECOND, independent
query — `ls-files --others`, deliberately without `--exclude-standard` — and the
guard asserts that every in-scope path the walk found is accounted for by one
list or the other. `tracked ∪ others` is what git says is on disk, so narrowing
`tracked` cannot suppress the signal: it adds nothing to `others`, and the file
lands in neither. An untracked scratch file stays green, which is the #819
property itself. MEASURED red for both narrowings, including with the rogue site
planted; MEASURED green for the untracked plant, an unstaged deletion, and a
staged rename; MEASURED red still for a tracked plant and a tracked dotfile.

The plugin also gains what it never had: tests that EXECUTE the real derivation
against a real repository — a temp repo with one committed file and an identical
untracked sibling, a working-tree deletion, and a non-repo — instead of only
injecting a fake `GitRunner`. That is the point
`test_guard_populations_derive_from_git.py` makes about its own primitive: it
proves the mechanism by running it rather than by recognising its shape.

Also from round 6:
- `expectedTrackedSources` was still proved by a seven-path fixture table — the
  mechanism this PR deleted one function over. MEASURED: a `/media/` term inside
  it left the suite green. Now restated closed-form like the scope predicate.
- The anti-vacuity floor had no clause proof and its failure direction is
  fail-OPEN, unlike `ABSENT_FROM_DISK`'s. Extracted as `populationIsDegenerate`
  so the threshold is a pinnable function, with the tight-threshold cost stated.
- `basename` is deleted, not just labelled. It existed only as the trap the
  comment warned about, and the two-site "tidy" edit still survived. Inlined into
  its one caller, so there is no shared helper to reintroduce.
- The closed-form criterion — the whole termination argument — lived only in the
  record's `signals:` line, which the catalog does not render. It is now in the
  `rule:` field and the record body, so a reader resolving the key by topic sees
  it.
- A subsumed clause is mirrored deliberately and now says why; session
  chronology in durable comments is dated to #819 rather than "the previous
  head".

Refs: #819
Decisions-Edit: yes
Round 7 found no blocker and reported the cancellation class terminated for
every single-site edit it could construct — 16 mutations, all caught. What it
found instead was proof debt on the round-6 fix itself, plus two environment
defects.

- The `others` term of the new cross-check had no clause proof. MEASURED:
  removing it left all 1265 tests green, because `others` is EMPTY on any clean
  checkout — the only tree CI sees — and with it gone an ordinary untracked
  scratch file reddens the developer's checkout while CI stays green. That is
  the #778/#806 shape #819 exists to remove, reintroduced by #819's own fix.
  Extracted as `unaccountedWalkedPaths` and pinned by fixtures, the load-bearing
  case being "in `others` only -> not returned".
- The anti-vacuity floor shipped as TWO identical assertions side by side, so
  deleting either left the suite green — `duplicate guards mask each other`,
  and the comment claimed a clause proof that measurement refuted. One
  assertion now, over `populationIsDegenerate`, with a four-case fixture table.
  MEASURED: `return false` now reddens by name; before, it was green.
- The new real-git tests were not hermetic: MEASURED 2 failures under a global
  `commit.gpgsign = true`, which many people set. The commit was unnecessary —
  `ls-files` reads the INDEX — so the fixture now stages without committing, and
  the non-repo test pins `GIT_CEILING_DIRECTORIES` so it does not depend on
  where TMPDIR points. Green under both configurations.
- The cross-check's message named one cause and MEASURED two more: a disk/index
  spelling divergence (where the sibling test gives the better diagnosis) and an
  untracked NESTED git repository, which `ls-files --others` reports as a
  directory rather than as files. Both named now, and a trailing-slash `others`
  entry covers its subtree so an embedded repo is not a permanent spurious red.
- The Dockerfile threw away 117 test files (~1228 tests) that run fine gitless
  in order to accommodate one that cannot. It now excludes that single file
  instead. MEASURED in an exact gitless replica: 117 files / 1228 tests pass,
  lint, typecheck and build all exit 0.

Residuals, now three rather than two: the surviving hole is a two-site PARTITION
inside the derivation — moving paths out of `tracked` and into `others` preserves
the union the cross-check compares, so it is invisible to any consumer. MEASURED
green with a planted call site. Nothing a consumer can compute closes it; a
derivation that lies consistently in both outputs is answerable only by testing
the derivation, which is what the real-git tests are for. Stated in the guard, in
`docs/guard-inventory.md` and in the record rather than left implied.

Also: the plugin deduplicates `tracked` (a merge conflict emits an unmerged path
once per stage), its error message drops the "guards" plural retracted earlier
elsewhere, `vite-env.d.ts` records that it is a hand-written mirror with no
compile-time link, the record's enumeration was four items for five rounds
(round 2's shared scope was missing), and the last undated "previous head"
references are now issue-referenced.

Refs: #819
Decisions-Edit: yes
Round 8 found a BLOCKER, and it was my verification that was wrong rather than
an argument. The previous commit re-armed `npm test` in the web-build stage and
excluded the one file that needs a git CHECKOUT. It did not exclude the file
added in round 6 that needs the git BINARY: `trackedSourceFiles`' real-repository
tests shell out to `git` to prove the derivation by executing it, and
`node:22-bookworm-slim` ships no git. So `docker build` would have failed on
every push to `main` and every `v*` tag — no image ships.

Why I did not catch it: every "gitless replica" I ran deleted `.git` and left the
git binary on `PATH`. That reproduces one of the two preconditions. Re-measured
with `PATH` stripped, the previous head gives `spawnSync git ENOENT`,
`Test Files 1 failed | 116 passed`. The Dockerfile comment stated the right
premise — "no `.git`, AND node:22-bookworm-slim ships no git binary" — two lines
above the line that only acted on the first half.

The real-git block is now its own file, `trackedSourceFiles.realgit.test.ts`, so
excluding it costs two tests rather than the thirteen injected-runner ones that
run fine with no git at all. Both exclusions are named in the Dockerfile with
their DIFFERENT reasons, and with the instruction to verify any change there with
the binary off `PATH`. MEASURED in a replica with neither `.git` nor `git`:
117 files / 1225 tests pass, lint, typecheck and build all exit 0.

Also from round 8:
- `docs/testing.md`'s row asserted the suite "cannot run against an exported tree
  with no index" and that the Dockerfile "no longer runs it" — both false at the
  previous head. It now distinguishes the checkout requirement from the binary
  requirement, which is the distinction the blocker turned on.
- Two clauses this PR added had no proof and MEASURED green when disarmed: the
  trailing-slash restriction in `unaccountedWalkedPaths` (without it every
  `others` entry becomes a prefix mask) and the plugin's dedupe. Both now have
  fixtures, and both mutations redden by name.
- The dedupe's stated rationale was wrong: duplicates appear on both sides of the
  ratio the floor computes, so they cancel. Re-stated as tidiness, with the
  measurement, and "three times" corrected to two-or-three (a both-added conflict
  has two stages).
- Three JSDoc blocks were stacked above one function, so two of them documented
  functions defined 15 and 50 lines below and attached to the wrong ones. Pure
  reordering, plus the blank lines that were detaching them.
- `vite-env.d.ts` named the wrong mechanism for its loud-failure claim: a renamed
  field yields an empty Set, caught by the anti-vacuity floor rather than by a
  throw at first read.
- The Dockerfile no longer hardcodes a test-file count that nothing reddens on.

Refs: #819
Round 9 found no blocker and no HIGH. These are its mechanical findings, plus one
correction to the fix it proposed.

The proposed fix for its MEDIUM did NOT work, and I measured it rather than
taking it. Round 9 suggested two assertions to pin the population WIRING —
`TRACKED_SOURCE_PATHS` equals `trackedSources.tracked`, and `listSourceFiles()`
equals `listSourceFilesFrom(rawSourceModules, TRACKED_SOURCE_PATHS)` — reported
as reddening the two survivors. Written and measured: both mutations stayed green
with those assertions in place (44/44). The reason is structural. On a clean
checkout the walk set and the index set ARE the same set — every tracked file is
on disk, nothing untracked exists — so no comparison between the two populations
can tell which one is wired in. It is observable only on a tree that HAS an
untracked source file, which is precisely the tree CI never checks out.

So it is recorded as residual (4) with its measurement and its DIRECTION, rather
than papered over with an assertion that cannot fail. Direction is what makes it
a residual and not a defect: reverting the wiring is fail-NOISY — it reddens a
developer's checkout that has a scratch file, the original #819 complaint — and
cannot hide a call site from the registry. Every blind direction runs through the
population SOURCE, and those are pinned by the cross-check.

Also from round 9:
- The checkout-vs-binary distinction was stated as an either/or in
  `docs/testing.md` and the Dockerfile. MEASURED: with `.git` PRESENT and git off
  `PATH`, the guard still fails — it needs both. As written it would license a
  future "just `COPY .git` and re-enable it", which is round 8's blocker in
  reverse. Both now say so explicitly.
- `docs/guard-inventory.md` claimed the partition residual is "checked only by
  the plugin's own real-git tests". They catch an unconditional mispartition, not
  one keyed on a path pattern. Corrected, and the wiring residual added beside it.
- The file split left two comments pointing at the file the real-git tests are no
  longer in, and one saying "every test above" where there is now nothing above.
- A drifting "1259 tests" figure — the class this PR already swept out once.
- The TS/C#-guards scope item had grown into "A, B and C and D and its sibling"
  across four over-long lines; rewritten as a four-item list.

Rebased onto b6b3520bd (#874).

Refs: #819
Round 10 adjudicated the wiring disagreement in the previous commit's favour
(round 9's proposed assertions are green against both mutations; the structural
argument holds), found no blocker and no HIGH, and then found that the previous
commit had introduced a measured falsehood while writing that argument down.

"Both (3) and (4) are fail-noisy" is false for (3). MEASURED, and reproduced here
before fixing: a union-preserving mispartition in the plugin — move
`src/components/` out of `tracked` and append it to `others` — hides a planted
`pageSize: 100` in `src/components/bugPreview.tsx` with the whole suite green
(43/43), where the same plant without the mutation reddens with `UNREGISTERED
(1)`. Round 7 had already measured this and I wrote the opposite anyway.

Why it matters more than a wording slip: fail-DIRECTION is the entire argument
for tolerating a residual rather than closing it. A reader deciding whether (3)
deserves work would have read "cannot hide a call site" — which is exactly the
inference this issue exists to prevent. Two neighbouring sentences contradicted
it in the same file, which is how it survived my own read.

The residual list now states five, with their directions separated: (1), (2), (4)
and (5) are fail-noisy — they redden a checkout and cannot hide a call site —
while (3) is blind, tolerated only because nothing a CONSUMER computes can see it
(the union it compares is unchanged), and answered by testing the derivation
directly. The real-git tests that answer it catch an unconditional mispartition,
not one keyed on a path pattern, and now say so.

Also from round 10:
- The list said "Three residuals" and enumerated four; `ABSENT_FROM_DISK` was a
  fifth, documented only at its declaration. Five, listed.
- "No assertion can tell which is passed" was broader than measured: a
  `listSourceFiles.toString()` assertion DOES redden one of the two mutations. It
  is rejected rather than impossible — a text snapshot of one's own source is the
  hand-written-mirror shape this file spent nine rounds removing, and it misses
  the other mutation. Both places now say "no assertion comparing the two
  populations", with the reason.
- The cross-check message said the shortfall test is "above"; it is below. This
  branch's own sweep of stale directional pointers missed it.
- "Excluded for DIFFERENT reasons" survived above the text retracting exactly
  that framing. The reasons OVERLAP — both files need the binary, one also needs
  a checkout — and reading them as separate is what produced the round-8 blocker.
- `vite-env.d.ts` claimed a renamed field always fails loudly; true for `tracked`
  and `others`, silent for `absentFromDisk`.
- The realgit non-repo test asserts a message the missing-binary path also emits,
  so it passes for the wrong reason where git is absent — recorded, since that is
  the environment the file is excluded from.
- A blank line had made the guard-inventory scope list render loose.

Refs: #819
Round 11 found the same defect class as round 10, one clause over: the previous
head grouped residual (1) with the fail-noisy ones. It is blind.

MEASURED here before fixing, planting `pageSize: 100` in the tracked file
`src/media/HlsPlayer.tsx`:
  * plant alone                        -> UNREGISTERED (1), red
  * plant + TWO-site edit              -> RED. `supplies content for every
    tracked source file on disk` catches it, because `expectedTrackedSources`
    carries a SECOND closed-form restatement the two-site edit does not touch.
  * plant + THREE-site edit            -> 43/43 GREEN, call site hidden.

So two claims were wrong, not one. The direction ("cannot hide a call site") and
the extent ("the two-site edit being the signal") — the scope predicate has three
restatement sites, at `isInScopeSourcePath`, this restatement, and the one
guarding `expectedTrackedSources`, and four when a term also moves a worked
example.

Twice now I have written a fail-direction I had not measured, in a change whose
whole subject is the difference between coverage and the appearance of it. So the
directions are no longer stated as a group: each residual carries its own,
each was measured by planting a real call site, and the block says so. (2), (4)
and (5) are noisy; (1) and (3) are blind, tolerated for different and explicitly
non-"harmless" reasons — (1) because a three-site policy edit is review-visible
where a one-line slip is not, (3) because a mispartition preserves the union any
consumer compares and is answerable only by testing the derivation.

Also from round 11:
- The residual paragraph had grown a verbatim duplicate of its own last two
  claims, eight lines apart — the second-copy-of-a-corrected-statement shape the
  decision record this branch amends warns about, and this branch has already
  lost a round to a claim drifting between copies. `docs/guard-inventory.md`'s
  row had the same duplication; both consolidated.
- The `isInScopeSourcePath` filter on `walked` survives deletion. It only WIDENS
  the set and every extra path is tracked, so it cannot hide anything — recorded
  as a narrowing convenience so a later round does not re-litigate it.

Refs: #819
Round 12 found a BLOCKER, and it is the third instance of one habit of mine in
this change: an affirmative coverage claim I had not measured. The guard said

  "Every mutation that could make it blind runs through `expectedTrackedSources`,
   which the fixture test pins."

MEASURED FALSE, and reproduced here before fixing. Widening the plugin's own
derivation by `|| path.startsWith('src/components/')` does not run through that
function at all — the fixture test INJECTS `absentFromDisk`, so it structurally
cannot police what the plugin derives. Those files are then subtracted from
`expected`, which disarms the hole-detection direction: a tracked
`src/components/.probe819.ts` holding `pageSize: 100` goes from red — named
correctly by the shortfall test — to 43/43 green.

The co-occurring condition is not exotic. It is exactly the case the shortfall
test exists for and that this repo has measured as PERMANENT rather than
transient: a dotfile, a dot-directory, or a disk/index spelling divergence under
`core.ignorecase` (true on this machine) or NFD/NFC.

CLOSED rather than merely stated, since one assertion does it: a path the WALK
found is on disk by construction, so it can never legitimately be reported
absent. MEASURED green on a clean tree, and red by name under the mutation.

What that closes and what it does not, measured both ways and written down: it
catches any fattening overlapping a walked file. A fattening restricted to paths
the walk cannot see anyway stays inside residual (3) — which is widened from "the
plugin's two lists partition faithfully" to "the plugin's THREE outputs describe
the repository faithfully", since `absentFromDisk` is an output the pair-shaped
wording did not cover.

Also from round 12:
- A pointer said "see residual (3)" as though it were the only blind one; (1)
  became blind last round.
- "every extra path is tracked" is inexact on a developer checkout — an untracked
  out-of-scope file lands in `others`, so it is accounted for but not tracked.
  The conclusion holds; the reason is now stated exactly.

Refs: #819
Round 13 found no blocker. Two things, both mine.

WATCH MODE (residual 6), present since the first commit on this branch and
missed by twelve rounds. The virtual module has no backing file, so Vite never
invalidates it and `resolveTrackedSourceFiles` runs once per dev-server lifetime
while the glob refreshes. MEASURED under `npx vitest --watch`: create an
untracked scratch file mid-session and the cross-check reddens; `git add` does
not clear it; `npx vitest run` on the same tree is green. That is the #778/#806
developer-red / CI-green symptom this change exists to remove, appearing under
the invocation `docs/testing.md` documents as `npm test`.

My first probe of this reported all-green and was WRONG — a filtered watcher
never re-ran, so I was reading a stale first result. Re-measured with a forced
re-run, the review's report reproduces exactly.

The obvious fix is rejected on MEASUREMENT rather than taste, which is the part
worth recording: invalidating the module from `configureServer` on the watcher's
`add`/`unlink` does fix the scratch-file red — and then `git add`, which touches
no file and so fires no watcher event, leaves the module holding its pre-staging
answer. Measured with that hook in place, a tracked planted `pageSize: 100` goes
UNSEEN, green. It trades a noisy red for a blind green. So it is named, with both
measurements, in the guard, in `docs/guard-inventory.md` and beside the `npm test`
line in `docs/testing.md`.

Also: widening residual (3) to the plugin's three outputs last round did not
carry to its DIRECTION paragraph, which still said "(3) is invisible to any
CONSUMER by construction". Measured false for the `absentFromDisk` half — the
assertion added last round catches it wherever it overlaps a walked file — and
`docs/guard-inventory.md` already had the correct wording, so the claim had
drifted between two copies again. Corrected, with a note not to delete that
assertion as decoration.

Two smaller ones: "an UNCONDITIONAL mispartition" became the wrong noun once (3)
covered three outputs, and "the walk set and the index set are THE SAME SET" is
imprecise — they agree on every in-scope key, but the index also carries `.css`.

Refs: #819
Round 14 found no blocker in the mechanism — the Docker stage, all six residual
mutations and every gate reproduce exactly as documented — and one more false
direction claim from me. That is the fourth in a row, and the class has been the
only defect for six rounds: a coverage or direction sentence asserted without
measurement, in a change whose subject is the difference between a check and the
appearance of one.

Residual (6) was labelled fail-NOISY in four places. MEASURED here before fixing,
with a file that is already untracked when the watcher starts and `git add`ed
mid-session: watch mode is green across both phases, while `npx vitest run` on
that identical tree reports `UNREGISTERED (1) — screens/Blind819.tsx:literal:100`.
The stale module still answers `others`, so the file never enters the population
and its call sites are never scanned. Blind, not noisy — and a window that did
NOT exist before #819, since the population was then the walk, which always
contained the file.

Worse, the remedy I wrote into `docs/testing.md` — "restart the watcher" —
resolves the noisy case by putting the developer into the blind one, because
after a restart the file is untracked at session start. It now says what is
actually true: a watch-mode green is not authoritative for this guard, confirm
with `npm test -- --run`.

(6) is now stated in both directions with the sequence that produces each, and is
explicitly the one residual that belongs to neither the noisy nor the blind
bullet — previously the taxonomy silently covered five of six.

The `configureServer` rejection stands but was stated too broadly. It is not
"a noisy red traded for a blind green": the hook fixes the created-mid-session
red and ADDITIONALLY blinds the create-then-stage sequence, while the
already-untracked-then-staged sequence is blind either way, because `git add`
fires no watcher event in either design.

Also: `docs/guard-inventory.md` said FIVE residuals and then described a sixth,
and still carried "the walk set and the index set are the same set" — the exact
phrase the previous commit corrected in the guard file and did not mirror here.
Two-copy drift again, in the paragraph that warns about it.

Refs: #819
docs(819): mirror the retracted checkout-vs-binary framing into its last copy
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 10s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
review-verdict/h10 Review-verdict: MERGEABLE @ bd69b4f (base: main)
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
Review verdict / Set review-verdict status (pull_request_target) Successful in 7s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 11s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 7m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m3s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m52s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m58s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
bd69b4f1db
Round 15 returned MERGEABLE — no blocker, no HIGH, no MEDIUM, the mechanism
re-verified end to end and residual (6)'s correction measurably true in every
clause. Two LOW prose inaccuracies, both of the two-copy-drift class that has
blocked six rounds, so they are fixed rather than shipped.

`trackedSourceFiles.realgit.test.ts`'s header was the fourth copy of the
checkout-versus-binary framing that `docker/Dockerfile` and `docs/testing.md`
both explicitly retract — the reading that broke the Docker stage once already.
It is also the header a reader consults when changing that exclusion, which is
exactly the scenario the repair was needed for. Measured again here: with `.git`
present and git off `PATH`, the guard still fails with the plugin's refusal, so
it needs a checkout AND, through it, the binary. The realgit pair is the half
that needs the binary and NO checkout.

`docs/guard-inventory.md`'s scope item said "four files" and then named five —
in the paragraph whose whole subject is a hand-written mirror going stale, and
one table down from the FIVE-vs-SIX count the previous commit fixed.

Refs: #819
Author
Owner

Review-verdict: MERGEABLE @ bd69b4f

16 cold review rounds; final two independent reviews returned MERGEABLE with nothing at any severity. Mechanism re-verified end to end: all six residuals measured by planting real call sites, Docker stage replicated with both .git absent and git off PATH, full mutation battery, all gates green.

Review-verdict: MERGEABLE @ bd69b4f 16 cold review rounds; final two independent reviews returned MERGEABLE with nothing at any severity. Mechanism re-verified end to end: all six residuals measured by planting real call sites, Docker stage replicated with both .git absent and git off PATH, full mutation battery, all gates green.
timothy merged commit 8aeacd534a into main 2026-08-29 11:14:16 +02:00
timothy deleted branch fix/819-pagesize-guard-git-index 2026-08-29 11:14:17 +02:00
Sign in to join this conversation.