Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only `docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep over every tracked file outside the detector, for the detector's own phrasings plus the attribution and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer proved") added residuals in the same files. Every site was classified with #812's three dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and traps stay; the attribution of who found them and the round in which they were found go. The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus `scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as the records' own exemption, and what keeps the record's depth measurement true), and `--all` lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812 argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff` sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill the withdrawn parity test ran on. The population oracle is restated over segments with the new arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures exemption, list only markdown, drop the symlink filter, test the mode per row instead of per path) each redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++` filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no notice — fixed, with a positive control and its own mutant. Code is unchanged by construction, measured per file type against `origin/main`: Python modules are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs (string literals) of three test modules; workflows differ only in `#` lines inside `run:` block scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated exceptions: the detector and its test, 26 vitest titles that carried review-round or severity labels or a reviewer attribution (call sites whose title changed — every changed title line walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed. Web: lint, typecheck, 1319 tests green. Closes #876. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk
333 lines
18 KiB
Python
333 lines
18 KiB
Python
"""`docs/remote-state-inventory.md` covers exactly the in-scope files, each classified for whether
|
|
it reads live remote state and acts on that read (ersatztv#778).
|
|
|
|
WHAT THIS CANNOT DO, said first because #778's own issue body says it: there is no lint for "this
|
|
code should have pinned a sha." `docs/defect-shapes-773.md` §4 grades detector D as a *fix pattern*
|
|
whose detector is detector A applied to an enumerated inventory. So this file does not try to grade
|
|
pinning. It guarantees that every in-scope file has been CLASSIFIED BY SOMEONE, and that no file
|
|
joined the scope without acquiring a row — which converts "remember to think about this" into "the
|
|
suite is red until you have".
|
|
|
|
The split is deliberate and mirrors `test_guard_inventory.py`:
|
|
|
|
* the POPULATION is derived from `git ls-files` and compared for SET EQUALITY, both directions;
|
|
* the CLASSIFICATION vocabulary is closed, so a typo cannot invent a state;
|
|
* whether a `PINNED` row is TELLING THE TRUTH is not checked here and cannot be. That stays with
|
|
review, and the inventory's prose is what review reads.
|
|
|
|
SCOPE vs POPULATION, per `testing.guard-derives-population-from-source`: the SCOPE — four
|
|
directories, their per-directory file patterns, and one excluded subdirectory — is a hand-written
|
|
policy choice and is reviewable as one. The POPULATION inside that scope is derived on every run
|
|
from the git index, with no content predicate at all: a file that reads no remote state earns an
|
|
explicit `N/A` row rather than staying out, which is why most rows are `N/A`.
|
|
|
|
No count is written here on purpose. An exact "N of M" has now gone stale FOUR times in this change,
|
|
most recently in the same commit that demoted two rows — a hand-maintained number is a second copy of
|
|
the table, and `docs/guard-inventory.md` earns its counts by having a test assert them. Nothing
|
|
asserts one here, so nothing states one.
|
|
|
|
That is the fourth version, and the history is the point — see the `SCOPE` comment below for the
|
|
three that failed and what each one hid.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from scripts.tests import tracked_files
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
INVENTORY = REPO_ROOT / "docs" / "remote-state-inventory.md"
|
|
|
|
CLASSES = {"PINNED", "CAS", "UNSAFE-KNOWN", "N/A"}
|
|
|
|
# NO CONTENT FILTER. The population is every file in the scoped directories, and a file that reads
|
|
# no remote state earns an `N/A` row rather than silently staying out.
|
|
#
|
|
# A token-list filter (`curl`, `wget`, `urllib`, ...) reads as a SCOPE choice and is a population
|
|
# filter; the distinction does not hold. The list omitted `git fetch`, which is this repo's most
|
|
# common remote read, so `.claude/hooks/prepush-rebase-check.sh` — which fetches `origin/main` and
|
|
# derives a PUSH DECISION from it — was structurally invisible to a guard whose stated claim is
|
|
# "every executable that reads live remote state". Three more (`prepush-clean-worktree-check.sh`,
|
|
# `ci-detect-docs-only.sh`, `refresh-shared-checkout.sh`) were missing for the same reason.
|
|
#
|
|
# That is precisely the defect `testing.guard-derives-population-from-source` describes: a filter
|
|
# cannot see the member that is missing, because the absent member is not a row the predicate
|
|
# rejected, it is a row that was never produced. The defence offered for it — "over-inclusion is the
|
|
# safe direction" — was answered by the filter ALSO under-including. Enumerating the directories
|
|
# costs more rows and has no blind spot; that is the trade the rule already made.
|
|
SCOPE = (
|
|
# `*.jq` joined in ersatztv#787, when `scripts/lib/branch-rule-classifier.jq` became the first
|
|
# non-shell program under `scripts/` — a jq file the merge-consent hook loads to make a security
|
|
# decision. Extensions are the SCOPE and are stated deliberately (see the doc), but a scope that
|
|
# silently omits a new executable artifact type is the frozen-filter defect this guard is about:
|
|
# the file would have shipped outside a population whose whole claim is that it has no filter.
|
|
("scripts", ("*.sh", "*.py", "*.jq")),
|
|
(".claude/hooks", ("*.sh",)),
|
|
(".husky", ("*",)),
|
|
(".gitea/workflows", ("*.yml", "*.yaml")),
|
|
)
|
|
|
|
# THE POPULATION IS DERIVED FROM `git ls-files`, NOT FROM THE FILESYSTEM.
|
|
#
|
|
# This is the third time this population has been found incomplete or wrong, each time by a
|
|
# different mechanism, and the third fix is the one that stops patching the traversal:
|
|
#
|
|
# 1. a content filter on an outbound-network token list, which omitted `git fetch` — this repo's
|
|
# commonest remote read — so a hook that fetches `origin/main` and derives a PUSH DECISION was
|
|
# structurally invisible;
|
|
# 2. a non-recursive `Path.glob`, which missed four nested files including one that calls a live
|
|
# ErsatzTV API and acts on the reply;
|
|
# 3. `Path.rglob`, which is recursive and therefore ALSO enumerated `.husky/_/` — 17 husky shims
|
|
# generated by `npm ci` via web/package.json's `prepare` script, gitignored (`.husky/_/.gitignore`
|
|
# is `*`) and untracked. That made this guard RED on every developer checkout while staying 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 is worse than no guard: it trains its readers to
|
|
# ignore it, and it would have done so on the artifact whose entire thesis is population
|
|
# correctness.
|
|
#
|
|
# The lesson each time was the same one this repo already wrote down — derive the population from an
|
|
# AUTHORITATIVE source — and the filesystem is not one. It reports build output, editor droppings and
|
|
# anything else that happens to be on disk, and it varies per machine. The repo's index is
|
|
# authoritative: it holds the same set of files every checkout receives from a clone, and it
|
|
# excludes untracked generated files by construction rather than by an exclusion list that must be
|
|
# maintained.
|
|
#
|
|
# `scripts/tests/` is still excluded explicitly, because those files ARE tracked. That exclusion is a
|
|
# scope decision, reviewable in one line: they run only under pytest and authorize nothing. Their
|
|
# network activity is CONFINED rather than absent — `test_hook_fire_log.py` starts a real
|
|
# `http.server` on 127.0.0.1 and drives it with real `curl`, and several suites create real local git
|
|
# remotes — but all of it is fixture state the test creates and tears down, so there is no live remote
|
|
# to race.
|
|
EXCLUDED_DIRS = ("scripts/tests",)
|
|
|
|
# `| ` + backticked path, optionally followed by ` — <site description>`, then the class cell.
|
|
_ROW = re.compile(r"^\|\s*`([^`]+?)`[^|]*\|\s*`([^`]+)`\s*\|", re.M)
|
|
|
|
|
|
def _tracked_files() -> list[str]:
|
|
"""Every file git tracks, as repo-relative posix paths.
|
|
|
|
A thin wrapper over the SHARED derivation rather than a second copy of it (ersatztv#806): one
|
|
definition of the rule, not two, which is detector C — dedup by construction — applied to the
|
|
file that first stated the rule. The wrapper survives because this module patches
|
|
`_tracked_files` by name in its own proofs, and because its scope is RECURSIVE over `scripts/`
|
|
where `tracked_children` is deliberately flat.
|
|
|
|
It therefore carries BOTH its own proofs and a row in that file's `DERIVATIONS`, which is not
|
|
the duplication that masks: they cut at different seams — the in-file pair patches
|
|
`_tracked_files` (this wrapper), the shared pair patches `_git_ls_files` (the subprocess) — and
|
|
each was witnessed red independently, so neither can hide the other's total failure.
|
|
|
|
Fails LOUDLY rather than returning nothing, now including git's own stderr: an empty population
|
|
would make every completeness assertion below pass vacuously, which is the exact failure this
|
|
guard exists to prevent.
|
|
"""
|
|
return tracked_files._git_ls_files()
|
|
|
|
|
|
def derived_population() -> set[str]:
|
|
"""Every in-scope tracked file, as a repo-relative posix path.
|
|
|
|
No content predicate of any kind: a file that reads no remote state earns an explicit `N/A` row
|
|
rather than silently staying out.
|
|
"""
|
|
found: set[str] = set()
|
|
for path in _tracked_files():
|
|
if any(path == d or path.startswith(d + "/") for d in EXCLUDED_DIRS):
|
|
continue
|
|
for directory, globs in SCOPE:
|
|
if not (path == directory or path.startswith(directory + "/")):
|
|
continue
|
|
name = path.rsplit("/", 1)[-1]
|
|
if any(fnmatch.fnmatch(name, pattern) for pattern in globs):
|
|
found.add(path)
|
|
# First matching scope entry wins. Safe only while no scope directory nests inside
|
|
# another; if one ever does, the inner entry's patterns would be silently skipped.
|
|
break
|
|
return found
|
|
|
|
|
|
def _inventory_section(text: str) -> str:
|
|
"""Only the classification tables, never the surrounding prose.
|
|
|
|
This is the second time this parser has read the document's own explanation as data: the
|
|
UNSAFE-KNOWN justification check once parsed the "Columns" paragraph that DEFINES
|
|
`UNSAFE-KNOWN`, and adding a scope TABLE to the heading made three more prose rows parse as
|
|
sites. A guard that treats its own documentation as input is the failure this whole change is
|
|
about, so the boundary is explicit rather than left to a cleverer regex.
|
|
"""
|
|
for heading in ("## The inventory", "## Limits"):
|
|
if heading not in text:
|
|
raise AssertionError(
|
|
f"{INVENTORY.name} has no {heading!r} heading. Row parsing is bounded by "
|
|
"'## The inventory' and '## Limits'; renaming or reordering either one would "
|
|
"silently change which rows are checked, so it fails here instead."
|
|
)
|
|
start = text.index("## The inventory")
|
|
try:
|
|
end = text.index("## Limits", start)
|
|
except ValueError:
|
|
# Reachable when '## Limits' exists but PRECEDES '## The inventory' — the presence check
|
|
# above passes and the bounded search does not. An `end <= start` guard here instead is
|
|
# unreachable by construction: `str.index(…, start)` either returns an index >= start or
|
|
# raises, and a guard that cannot execute proves nothing.
|
|
raise AssertionError(
|
|
f"{INVENTORY.name}: '## Limits' precedes '## The inventory', so the parsed window "
|
|
"would be empty and every completeness assertion would pass vacuously."
|
|
) from None
|
|
return text[start:end]
|
|
|
|
|
|
def inventory_rows(text: str | None = None) -> list[tuple[str, str]]:
|
|
if text is None:
|
|
text = INVENTORY.read_text(encoding="utf-8")
|
|
return _ROW.findall(_inventory_section(text))
|
|
|
|
|
|
def inventory_sites(text: str | None = None) -> set[str]:
|
|
return {site for site, _ in inventory_rows(text)}
|
|
|
|
|
|
def test_the_inventory_file_exists_and_is_not_empty():
|
|
assert INVENTORY.is_file(), f"{INVENTORY} is missing"
|
|
assert INVENTORY.stat().st_size > 0
|
|
|
|
|
|
def test_anti_vacuity_the_derivation_and_the_table_both_found_something():
|
|
"""The characteristic failure of a completeness check is reporting success over an empty
|
|
population. Both sides get a floor, because either one collapsing to zero would make the set
|
|
comparison below pass trivially."""
|
|
population = derived_population()
|
|
sites = inventory_sites()
|
|
assert len(population) >= 40, (
|
|
f"derived only {len(population)} in-scope files — the globs are broken, not the repo "
|
|
"(the scope held 59 files on 2026-08-16, and it only grows)"
|
|
)
|
|
assert len(sites) >= 40, (
|
|
f"parsed only {len(sites)} rows out of the inventory — the row regex has drifted from the table format"
|
|
)
|
|
|
|
|
|
def test_every_in_scope_file_has_a_row_and_every_row_names_a_real_file():
|
|
"""Set equality in BOTH directions, because the two failures are different defects and a single
|
|
'sets differ' message invites fixing one and re-running.
|
|
|
|
MISSING: a script that talks to a remote service and was never classified — the defect #778
|
|
exists to prevent. PHANTOM: a row for a file that was renamed or deleted, which leaves the table
|
|
claiming coverage it has lost.
|
|
"""
|
|
population = derived_population()
|
|
sites = inventory_sites()
|
|
missing = sorted(population - sites)
|
|
phantom = sorted(sites - population)
|
|
assert not missing, (
|
|
"in scope but absent from docs/remote-state-inventory.md (classify each as "
|
|
f"PINNED / CAS / UNSAFE-KNOWN / N/A): {missing}"
|
|
)
|
|
assert not phantom, f"listed in docs/remote-state-inventory.md but no such in-scope file exists: {phantom}"
|
|
|
|
|
|
def test_MUTATION_PROOF_a_dropped_row_and_a_phantom_row_are_both_detected():
|
|
"""The proof that the set comparison above is load-bearing (`testing.guard-ships-with-mutation-
|
|
proof`). This guard IS a test, so disarming it makes it absent rather than red; the admissible
|
|
proof is therefore the contrapositive — introduce the defect into an isolated copy of the
|
|
GUARDED ARTIFACT and show the comparison reports it.
|
|
|
|
Both directions are mutated, because they are different defects: a dropped row is an
|
|
unclassified script, a phantom row is a table claiming coverage it has lost. This ran for real
|
|
on the day it was written — `dependency-scan.yml` was genuinely absent from the inventory and
|
|
this comparison is what found it.
|
|
"""
|
|
text = INVENTORY.read_text(encoding="utf-8")
|
|
population = derived_population()
|
|
|
|
victim = sorted(population)[0]
|
|
dropped = "\n".join(line for line in text.splitlines() if not line.startswith(f"| `{victim}`"))
|
|
assert victim not in inventory_sites(dropped), (
|
|
f"the mutation did not actually remove {victim}; the proof below would be vacuous"
|
|
)
|
|
assert population - inventory_sites(dropped), (
|
|
"a row was removed from the inventory and the comparison still reported complete coverage"
|
|
)
|
|
|
|
# Inserted INSIDE the inventory section, not appended to the file: rows are parsed only between
|
|
# "## The inventory" and "## Limits", so appending at the end would test nothing.
|
|
phantom = text.replace("## Limits", "| `scripts/does-not-exist.sh` — invented | `PINNED` | n/a |\n\n## Limits", 1)
|
|
assert inventory_sites(phantom) - population == {"scripts/does-not-exist.sh"}, (
|
|
"a row naming a file that does not exist was not reported as phantom"
|
|
)
|
|
|
|
|
|
def test_every_class_cell_comes_from_the_closed_vocabulary():
|
|
bad = sorted({cls for _, cls in inventory_rows() if cls not in CLASSES})
|
|
assert not bad, (
|
|
f"unknown classification(s) {bad}; allowed: {sorted(CLASSES)}. A typo here would silently "
|
|
"create a state nobody reviews."
|
|
)
|
|
|
|
|
|
def test_every_unsafe_row_states_why_the_residual_is_accepted():
|
|
"""`UNSAFE-KNOWN` means 'accepted with a reason', not 'noticed'. A row that records the window
|
|
without the argument for tolerating it is how a deferral becomes permanent by default."""
|
|
text = _inventory_section(INVENTORY.read_text(encoding="utf-8"))
|
|
thin = []
|
|
for line in text.splitlines():
|
|
# TABLE ROWS ONLY. Matching any line containing the token parses the prose in
|
|
# "Columns" that DEFINES `UNSAFE-KNOWN` as a row, and the split blows up. A guard
|
|
# that reads its own documentation as data is the failure this whole change is about.
|
|
if not line.startswith("|") or "`UNSAFE-KNOWN`" not in line:
|
|
continue
|
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
if len(cells) < 3:
|
|
continue
|
|
site, note = cells[0], cells[-1]
|
|
if len(note) < 120:
|
|
thin.append(site[:60])
|
|
assert not thin, f"UNSAFE-KNOWN row(s) with no stated justification: {thin}"
|
|
|
|
|
|
def test_the_population_never_includes_a_file_git_does_not_track(monkeypatch):
|
|
"""The regression for the third population defect, and the reason the source is the index.
|
|
|
|
`Path.rglob` enumerated `.husky/_/` — 17 husky shims generated by `npm ci`, gitignored and
|
|
untracked — so this guard was RED on every developer checkout and GREEN in CI, which never runs
|
|
`npm ci`. A guard that fails everywhere except where it runs trains its readers to ignore it.
|
|
|
|
Asserted through the mechanism rather than against the current disk, so it holds on a machine
|
|
that has never installed husky: the tracked list is narrowed, and anything outside it must
|
|
disappear from the population even though it is still sitting on disk and still matches the
|
|
scope globs.
|
|
"""
|
|
real = derived_population()
|
|
assert real, "empty population — the derivation is broken, not the repo"
|
|
|
|
victim = sorted(real)[0]
|
|
tracked = [p for p in _tracked_files() if p != victim]
|
|
monkeypatch.setattr(
|
|
"scripts.tests.test_remote_state_inventory._tracked_files",
|
|
lambda: tracked,
|
|
raising=False,
|
|
)
|
|
# Patch the module object this test is running inside, whatever name it was imported under.
|
|
import sys
|
|
|
|
mod = sys.modules[__name__]
|
|
monkeypatch.setattr(mod, "_tracked_files", lambda: tracked)
|
|
|
|
assert (REPO_ROOT / victim).is_file(), f"{victim} must still exist on disk for this proof to mean anything"
|
|
assert victim not in derived_population(), (
|
|
f"{victim} is on disk and matches the scope, but git no longer tracks it — it must not enter "
|
|
"the population, or untracked build output can redden this guard again"
|
|
)
|
|
|
|
|
|
def test_every_derived_member_is_tracked():
|
|
"""The same property stated as an invariant over the real tree, so a future refactor that goes
|
|
back to walking the filesystem fails here rather than on someone's laptop."""
|
|
tracked = set(_tracked_files())
|
|
stray = sorted(p for p in derived_population() if p not in tracked)
|
|
assert not stray, f"population contains untracked path(s): {stray}"
|