Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 31s
review-verdict/h10 Awaiting review verdict for 10dba08
Review verdict / Set review-verdict status (pull_request_target) Successful in 39s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 4m38s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m32s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m52s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 1s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
Cold review of the previous commit found that IT had made
`docs/guard-inventory.md`'s "load-bearing for the six modules that import it"
false, by adding a seventh importer — in the file it was editing, inside the sweep
its own message claims to have done by subject. That is the third hand-maintained
count in this change to go stale, after `mutation-claims-are-executed.md`'s three
and `guard-inventory.md`'s "three of the thirteen".
Correcting a number that tracks a growing population buys one commit of accuracy.
So the counts that track populations are gone rather than updated:
* "load-bearing for the six modules that import it" → "for every module that
imports it"
* "this file is not four near-copies" → "not one near-copy per derivation"
* "adding a fifth derivation / a fifth index-derived guard" (twice) → "another"
* "five guard modules would otherwise die" → "every module that imports this one"
* "Exhaustive removal is cheap (~1s for 60 members)" → "about a second at the
population sizes here"
The sweep was then done MECHANICALLY — a regex over every spelled-out and numeric
count in all five artifacts this change touches — rather than by eye for a fourth
time. What survives is verified true rather than assumed: "three of the fourteen"
and "36 guards … 14/6/16" (the latter machine-asserted by
`test_the_summary_counts_match_the_table`), "unions four contributors",
"five modules each derived a file population" (`DERIVATIONS` holds exactly five),
and the re-measured 61/39/48/58. `guard-inventory.md`'s "three of these four rows"
is #790's text about a different row set and is untouched.
The figures that remain are the ones that describe a FIXED structure or carry a
reproduction command; the ones that counted a set expected to grow are the ones
that kept breaking, which is the distinction the sweep now encodes.
Correction to the previous commit message: it called the second converted call site
"the delimiter ban over every workflow". That guard is
`test_every_workflow_expression_names_a_REAL_context_or_function`, an
expression-context check; the delimiter ban does not use `_workflow_files()`. Both
call sites are completeness claims over every workflow and both were demonstrated
red against an untracked `.yaml` at the predecessor sha — only the label was wrong.
refs #806
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 lines
6.4 KiB
Python
121 lines
6.4 KiB
Python
"""The authoritative population for a guard whose members are FILES: the git index (ersatztv#806).
|
|
|
|
`testing.guard-derives-population-from-source` (#774) says a completeness guard derives its
|
|
population from a machine-readable authoritative source, and its worked examples are an enum and the
|
|
generated OpenAPI document. It does not say what to do when the population is *files*, and every
|
|
guard in this repo answered that with a filesystem walk. **A filesystem walk is not an authoritative
|
|
source.** It reports build output, editor droppings and whatever else happens to be on disk, and it
|
|
differs per machine, so a guard derived from it asserts a different population in CI than on the
|
|
laptop of the person it is supposed to stop.
|
|
|
|
#778 got that wrong three times in one PR, each time with an argument for why the traversal
|
|
sufficed, and the third shape is the one that motivates this module:
|
|
|
|
* a **content filter** on outbound-network tokens that omitted `git fetch`, making a hook that
|
|
fetches `origin/main` and derives a push decision structurally invisible;
|
|
* a **non-recursive `Path.glob`**, missing four nested files, one of which calls a live ErsatzTV
|
|
API and acts on the reply;
|
|
* **`Path.rglob`**, which then enumerated `.husky/_/` — 17 husky shims generated by `npm ci` via
|
|
`web/package.json`'s `prepare` script, gitignored (`.husky/_/.gitignore` is `*`) and untracked.
|
|
That made the guard **RED on every developer checkout and GREEN in CI**, whose `script-tests`
|
|
job checks out and pip-installs but never runs `npm ci`. A guard that fails everywhere except
|
|
where it runs teaches its readers to ignore it.
|
|
|
|
The index holds the same set of files every checkout receives from a clone, and excludes untracked
|
|
generated files **by construction** rather than by an exclusion list somebody has to maintain and
|
|
keep correct. It is not immutable and it is per-worktree; the claim is not that it never changes,
|
|
but that it changes only through a deliberate git operation — staging, a checkout, a reset, a
|
|
merge — whereas the disk changes whenever a build runs.
|
|
Note what that buys over `.gitignore`-awareness: `.husky/_/` happens to carry its own `.gitignore`,
|
|
but a stray untracked `foo.sh` in `.claude/hooks/` carries nothing, and only the index knows it is
|
|
not part of the repo.
|
|
|
|
**This is not "replace every glob".** The question per guard is whether it makes a COMPLETENESS
|
|
claim over tracked files. If it does, the index is the authoritative source. If it does not — a
|
|
fixture copying files into a tmp tree, a walk selecting the SUBJECT of a per-member property — say
|
|
so in the guard and leave it, per the boundary #774 already draws between scope and population.
|
|
|
|
`git ls-files` lists INDEX entries, so a file deleted in the working tree but not yet staged is
|
|
still reported. That is deliberate: callers that read member contents assert existence with their
|
|
own message rather than filtering, because filtering is what makes a missing member unrepresentable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def _git_ls_files() -> list[str]:
|
|
"""Every path git tracks, as repo-relative posix strings.
|
|
|
|
The single place this package shells out to git, so a regression proof can narrow the tracked
|
|
set once and have every derivation built on it react.
|
|
|
|
Fails LOUDLY on an empty result rather than returning it: an empty population makes every
|
|
completeness assertion downstream pass vacuously, which is the exact failure these guards exist
|
|
to prevent.
|
|
"""
|
|
proc = subprocess.run(
|
|
["git", "-C", str(REPO_ROOT), "ls-files", "-z"],
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
# `check=True` would raise "returned non-zero exit status 128" and leave git's own diagnostic
|
|
# trapped in `e.stderr`. The two real triggers — a tree that is not a repository, and CI's
|
|
# `detected dubious ownership` — are both diagnosable ONLY from that text, and every module
|
|
# that imports this one would otherwise die with the same inscrutable line.
|
|
assert proc.returncode == 0, (
|
|
f"`git ls-files` failed in {REPO_ROOT} (exit {proc.returncode}). Every file population here "
|
|
f"comes from the index, so this is fatal rather than empty. git said:\n"
|
|
f"{proc.stderr.decode(errors='replace').strip() or '(no stderr)'}"
|
|
)
|
|
paths = [p for p in proc.stdout.decode().split("\0") if p]
|
|
assert paths, (
|
|
f"`git ls-files` reported nothing under {REPO_ROOT} — the derivation is broken, not the "
|
|
"repo. Every file population built on it would be empty and every completeness assertion "
|
|
"would pass vacuously."
|
|
)
|
|
return paths
|
|
|
|
|
|
def tracked_children(directory: str, patterns: tuple[str, ...]) -> set[str]:
|
|
"""Tracked files that are DIRECT children of `directory` and match one of `patterns`.
|
|
|
|
Direct children only, and that is the point rather than a limitation: every population here is
|
|
a flat directory (`.claude/hooks/*.sh`, `.husky/*`, `.gitea/workflows/*.yml`,
|
|
`scripts/tests/test_*.py`), and recursing is what dragged `.husky/_/` in. A guard that genuinely
|
|
needs a nested population should say so and ask for it explicitly.
|
|
|
|
Returns repo-relative posix paths, matching what the guards' inventories and error messages use.
|
|
"""
|
|
found: set[str] = set()
|
|
for path in _git_ls_files():
|
|
parent, _, name = path.rpartition("/")
|
|
if parent != directory:
|
|
continue
|
|
if any(fnmatch.fnmatch(name, pattern) for pattern in patterns):
|
|
found.add(path)
|
|
return found
|
|
|
|
|
|
def tracked_paths(directory: str, patterns: tuple[str, ...]) -> list[Path]:
|
|
"""`tracked_children` as absolute `Path`s, sorted — for guards that read member contents.
|
|
|
|
Existence is ASSERTED, never filtered: a path in the index with no file on disk means the tree
|
|
is mid-edit, and reporting that is strictly better than silently shrinking the population, which
|
|
is the defect this module exists to remove.
|
|
"""
|
|
paths = []
|
|
for rel in sorted(tracked_children(directory, patterns)):
|
|
absolute = REPO_ROOT / rel
|
|
assert absolute.is_file(), (
|
|
f"git tracks {rel} but there is no file there. The population comes from the index, so "
|
|
"a working tree mid-delete is reported rather than silently shrinking the population."
|
|
)
|
|
paths.append(absolute)
|
|
return paths
|