868ab657d8ecd2e0874c91b502f4e2b7aa1411cb
52
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
868ab657d8 |
docs(901): the record was wrapped narrower than its own corpus, so 65 lines was a WRAP width, not a word count
`798e5ee40` stopped at 65 prose lines and gave a reason: the remaining candidates for
removal were the *why* behind non-obvious choices, which `docs.no-session-narrative`
says to keep. That reason is sound about the WORDS and wrong about the LINE COUNT,
because the two are not the same quantity. `decisions_validate.record_prose_lines` counts
PHYSICAL lines, and this record was the narrowest thing in the set being measured:
max body width, 222 active records: median 106; 28 at <=100, 154 at 101-120, 40 >120
this record: 100. The sibling it cites by key, process.hook-resolves-inputs-from-repo-root: 116.
The 60-line ceiling was derived at #620 from that distribution, so measuring a
100-column record against it charges the record for a wrap width the corpus does not use.
Re-wrapping the seven body paragraphs at 116 — the exact width of the neighbour record —
takes the body from 66 physical lines to 58, and the validator now reports 58, off the
over-ceiling list (50 records over -> 49, and the key no longer appears).
The reflow removes NOTHING: the script asserted word count equal before and after (893)
and whitespace-normalised body text byte-identical, and refused to write otherwise. What
it buys is that the issue's `## Done-when` box "The record is under the 60-line advisory
prose ceiling" is satisfiable as written, so `pretooluse-merge-consent.sh` is not asked
to derive consent from a box ticked falsely or left standing. The advisory itself was
never breached — the ceiling is a `::warning::`, the validator exits 0, and 49 of 222
records are over it inside the 2-25% CEILING_MINORITY band.
Also: the file was the only one of 222 records with no final newline. Fixed in the same
commit; `record_prose_lines` is `splitlines()`, so it does not move the count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
11f92761af |
docs(901): the whitespace collapse belongs to the PRIMITIVE, so it belongs to both text pins
`mechanics:` opened with a universal — "a residual read off one of them does not
transfer to the others" — and then attributed the collapse-whitespace-inside-a-string
residual to `_normalise_lines` alone. It does transfer, between exactly the two pins
that sentence separates: `_normalise_lines` is `_normalise` applied per line
(`scripts/tests/test_image_build_delegates_the_spa_suite.py:327`), and the stage-command
pin calls `_normalise` directly (lines 386/389, compared at line 586), so it cannot see a
whitespace change inside a quoted shell string either. For SHELL text that is the more
consequential of the two residuals, which is the opposite of what the old ordering
implied.
The sentence now says NEED NOT transfer, names the one that does, and puts the
string-literal blindness on `_normalise` where it originates. The vite-only fact that
survives is the blank-line drop, and the claim that the test STATES its residual is
narrowed to the vite test, which is the only one of the two that does.
The hole is dormant rather than live — no entry in `PINNED_STAGE_COMMANDS` carries a
quote character — but the defect was the prose universal, which the record's own rule
("a pin also declares its NORMALISATION and what the normalisation cannot see") is what
this paragraph exists to demonstrate. This is the third finding read off this one
sentence: `b8dc321af` corrected its fault-message half and `4d5bd0dbb` its
outcome-claim half.
TWO OBLIGATIONS ARE OWED to the closing record, both blocked on #885 (open, so this
branch does not touch `scripts/tests/`), and both freed together when it closes:
1. The `CLAIMS` entry in `scripts/tests/mutation_manifest.py` for the hook-preamble
selector — target `.claude/hooks/decisions-guard.sh`, clause = the canonical sink
assignment, replacement = that line plus the `eval` spelling, proof = a
`test_hook_fire_log.py` node, outcome=GREEN with the mandatory
`reach_replacement`/`reach_expect`. Until then `mechanics:` states the SELECTOR as a
structural fact and makes no outcome claim (`4d5bd0dbb`).
2. The comment at `scripts/tests/test_hook_fire_log.py:153-154`, which ends "this arm's
errors are refusals, never acceptances". The structural fact this record ships — the
compared set is the non-comment-led lines containing the literal `ETV_HOOK_FIRE_LIB`,
so a reassignment that never spells the literal is outside the selection — is an
ACCEPTANCE by that arm, and the same comment block concedes the class five lines later
("so it is an acceptance hole, not only a stricter refusal"). The sentence is owed a
correction; the record documents the residual beside it in the meantime.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
761a71278d |
docs(901): pay for the two corrections by tightening the prose around them
The two corrections added four prose lines to a record already one line over
the 60-line advisory ceiling. Recover what can be recovered without losing
substance: reflow the paragraphs, drop the padding ("and it fails silently"
→ ", silently"; "the count rises" → "and rises"), and cut one restatement.
It lands at 65 lines, not 60. That is a deliberate stop: the remaining
candidates are the *why* behind non-obvious choices — which the repo's own
docs rule says to keep — and `decisions_validate` reports the constant itself
as drifted from the distribution it is supposed to mark the tail of (p90=104,
p95=142, 50 of 222 records over it). The validator passes.
Refs #901
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
0977f04174 |
docs(901): the respelling-immunity thesis is bounded by the pin's REACH, and the neighbour doc declines the rule it was cited for
Two prose overclaims in the new record, both of the shape the record itself exists to police. The thesis sentence generalised over the two worked pins and held for one. `PINNED_VITE_CONFIG` compares a whole file, so no respelling escapes it; the hook-preamble pin compares a SELECTION (`test_hook_fire_log.py:161` keeps only the lines containing the literal `ETV_HOOK_FIRE_LIB` that are not comment-led), so it must recognise a line before it can reject it — which is exactly the residual `mechanics:` documents four lines later. Bound the immunity to what a pin COMPARES and name the leftover exposure as the selector's reach. The neighbour citation claimed `docs/defect-shapes-773.md` §4 "argues the general form". §4 is a ranked table of detectors A-G — none of them this rule, and A is already assigned to `guard-derives-population-from-source` by the preceding clause. The doc's nearest class is `string-predicate churn` in the §3.6 partition, marked `no detector proposed`, and §3.7 argues the class away as a cross-cutting property (2 of 33 round-churn records). Only §4's closing meta-finding — class-level rules beat one record per instance — supports anything here, and it supports the FORM, not the content. Say that. Refs #901 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
c67b33cdcd |
docs(901): a mutation OUTCOME is a manifest entry or it is not written — state the selector instead
Round three, blocking. The record's `mechanics:` field asserted a measured mutation outcome — that appending an `eval` which composes `ETV_HOOK_FIRE_LIB` at runtime leaves `instrumentation_faults` returning `[]`, identical to the unmutated baseline — with no `CLAIMS` entry in scripts/tests/mutation_manifest.py. `testing.mutation-claims-are-executed` as amended by #881 puts a prose claim about a mutation's outcome under the executed-claim rule wherever it is written, a decision record included, and this branch's own docs/README.md row restates that. A record whose rule text requires "each defeat the matcher claims to catch is a DECLARED, executed mutation" cannot itself carry an undeclared one. scripts/tests/ is held by #885, which is open, so the entry cannot be added here. What replaces the outcome claim is the structural fact that carries the same point and needs no execution: the compared set is the lines containing the literal `ETV_HOOK_FIRE_LIB` that are not comment-led, so the pin reaches exactly the two preamble lines and a later reassignment which never spells the literal is outside the selection — whatever the checker then returns. The `CLAIMS` entry is owed once scripts/tests/ is free. Three more from the same round: - `_normalise_lines` was a universal over three pins that holds for one. The stage commands compare through `_normalise` (continuations joined, whitespace within one command collapsed); the script map is dict equality over parsed JSON and compares no text; only PINNED_VITE_CONFIG uses `_normalise_lines`. The three are now stated separately, with the note that a residual read off one does not transfer. - docs/guard-inventory.md's pointer restated the "shell or config TEXT" framing the record exists to reject. It now says what the record says: an artifact with a grammar the predicate does not implement. - Rider 1 stated vite's DEFAULT_CONFIG_FILES ordering unbound to a version. The ordering belongs to a release and expires with one, so the record cites the guard-inventory row rather than keeping a second copy, and that row now dates the reading and names the release web/package-lock.json pins. Refs #901, #891, #887, #881, #885 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
17c65005f5 |
docs(901): the hook pin is a pin WITHIN a selection, and the selection has a measured escape
The record's own rule ends: a pin also declares its NORMALISATION and what the
normalisation cannot see, because "pinned whole" invites a reader to assume
byte equality. The `mechanics:` field did that for the image-build pin (it
names `_normalise_lines` and what it drops) and not for the hook pin, which it
described only as pinning two lines byte for byte.
The comparison at scripts/tests/test_hook_fire_log.py:161-167 is over a
SELECTED set — lines containing the literal `ETV_HOOK_FIRE_LIB` that are not
comment-led — so byte-identity holds within the selection and says nothing
about a writer of that variable spelled without the literal.
MEASURED 2026-09-05 on this branch against `.claude/hooks/decisions-guard.sh`:
appending
eval "$(printf %s%s=/dev/null ETV_HOOK_FIRE _LIB)"
after the canonical assignment leaves `instrumentation_faults(text,
'decisions-guard')` == `[]`, byte-identical to the unmutated baseline `[]`,
while running those two lines under bash prints `final=/dev/null` — the sink is
repointed and the checker is silent. A false GREEN, which is the failure
direction this record exists to argue about.
The residual is #891 code and is not introduced here; no code changes. What
changes is that a record citing this file as one of its two worked pins now
states the residual instead of implying byte equality over the file.
Refs #901, #891
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
78593ace9f |
docs(901): a quotation that resolves to nothing, and a universal over three pins that holds for two
Two review findings, both the class this record is about: prose that claims more than the artifact says. The neighbour-record citation presented a paraphrase in quotation marks. `testing.guard-derives-population-from-source` line 96-97 reads "when the authoritative source is missing, the answer is to create one, never to approximate it with a predicate over text"; the record quoted it without "the answer is to" and without the second "to", so a fixed-string lookup of the quoted span found nothing anywhere under docs/ — #812's second defect, which `testing.mutation-claims-are-executed` names explicitly. The quote marks are gone rather than repaired: the source sentence spans a line break, so any single-line verbatim quotation of it would still not resolve by grep, and an open paraphrase claims only what it is. The `mechanics:` field said the image-build pins fail with a message naming the constant to update. Measured against scripts/tests/test_image_build_delegates_the_spa_suite.py: PINNED_STAGE_COMMANDS (line 589) and PINNED_VITE_CONFIG (line 798) name themselves, PINNED_PACKAGE_SCRIPTS (lines 757-763) does not — it names the FILE and prints both maps. 423bf94e7 corrected this same sentence for the hook half after verifying it and left the image-build half an unverified universal. The first replacement drafted here read "all four faults ask for the reason in the same commit", which is false a second way: the hook-preamble fault (test_hook_fire_log.py lines 162-167) asks for no reason at all, it reports got={mentions}. The shipped sentence is scoped to the three pins whose fault messages were read. Refs #901 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
4c2ceccaf7 |
docs(901): carry both rejected alternatives in the rule, not only in the body
The catalog renders `rule:` alone, so a reader resolving this by topic saw the two decisions without the alternatives they rejected — which is what stops a rejected option being re-proposed on plausibility. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
87d90f330c |
docs(901): the hook-preamble fault does not name a constant — correct the mechanics claim
The record asserted both worked pins fail with a message naming the constant to update and asking for a reason. True of `test_image_build_delegates_the_spa_suite.py`'s three pins; false of `test_hook_fire_log.py`, whose byte-identity fault reports the divergent `mentions` list and names no constant. Verified against both files rather than inferred from the neighbouring one. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
0f4552d093 |
docs(901): pin the artifact whole is the DEFAULT for a predicate over a grammar it does not implement
Two predicates over artifacts with a real grammar were each defeated by successive spellings and withdrawn in favour of pinning the artifact whole: #887's shell parse (nine defects from one mechanism, then seven more against a partial match of web/vite.config.ts) and #891's lexical rule over the hook preamble (five spellings). Both incidents carry a record; neither is resolvable by topic before round three, which is what this class-level record adds. Decides the two questions #901 left open: - DEFAULT, not remedy. A shape-matcher's failure is a false GREEN, so the defeat that would trigger a remedy policy is found by a reviewer or an incident and never by the guard: "not defeated yet" measures who has looked. Rejected: write the matcher and pin after the first defeat — it also understates its bill, since a withdrawal costs the rounds spent AND the proofs calibrated against the narrow clause. - The exception argument carries FOUR things: the grammar and its parser; the input space as a closed enumeration with the reason it is closed; the fail direction measured as a declared, executed mutation; and what it buys priced in a cost the pin charges. Rejected: a numeric "survives N spellings" bar (measures the reviewer's imagination) and a reviewer sign-off bar (depends on the signal that arrives late). Records both riders (a pin assumes it pins the artifact that still DECIDES; widening a clause turns a survived-clause canary into a tautology) and states the threshold as the moment the NEXT spelling is found by the reviewer rather than the author. docs/README.md's guard-convention task-signal row points at the record; catalog regenerated; 59 prose lines, under the 60-line advisory ceiling. fixes #901 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
fa06da21f3 |
fix(885): the guard states both invariants, and the second one is executed
The two `docs/guard-inventory.md` rows and the module docstring described `test_workflow_persist_credentials.py` as the `actions/checkout` guard only. The deferral to #909 rested on `docs/guard-inventory.md` being held by the session working #881; that issue is closed and its PR is the commit this branch is rebased onto, so the file is free and the edit belongs here under docs-update-is-part-of-done. `MUTATIONS` keys at most one declared clause mutation per guard FILE (`test_the_manifest_covers_exactly_the_MUTATION_rows` asserts `len(MUTATIONS) == len(declared)`), and the grading row's proof-ref column is compared against it, so the route invariant cannot take a second `MUTATIONS` row. It takes a `CLAIMS` entry instead — the population #881 widened this file to carry — bound to the inventory sentence that states it: deleting `build`'s `if: github.event_name != 'pull_request'` from the shipped `docker-build.yml` is applied to a sandbox copy every run and the named proof is required to redden with the collector's own wording. That grows the `CLAIMS` population from three entries to four, which invalidates the cost span `testing.mutation-claims-are-executed` measured over three. Re-taking it here produced 54.3s/149.5s, 81.6s/78.7s and 114.3s/84.2s across three A/B pairs with other builds on the host — two inverted, so the load dominates the signal. The record now says the span is a lower bound and that a re-measurement is owed on a quiet machine, rather than carrying a scaled or invented number. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
4454f1adfc |
docs(563): anchor the measurements to the issue, which a squash merge cannot erase
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 12s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 16s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
Review verdict / Set review-verdict status (pull_request_target) Successful in 20s
review-verdict/h10 Review-verdict: MERGEABLE @ 4454f1a (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 17m42s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m19s
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
The paragraph added a commit ago pointed at "the commit messages that ran them" as the home of the binder and `trim` mutant outcomes. A squash merge writes its own message and drops the bodies it squashes, so that pointer can go stale the moment this branch lands. The issue and its pull request survive it, and #563 is where the round-by-round measurements already are. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
3df98d247f |
docs(563): state each binder test's mechanism instead of its mutant colour, per the amended CLAIMS rule
`testing.mutation-claims-are-executed` was amended on main while this branch was in review (#881, merged as #914): a sentence asserting that a specific mutation reddens — or does not redden — a named test is now either a `CLAIMS` entry in `scripts/tests/mutation_manifest.py` that executes every run, or it is not written. This branch carried six such sentences and none of them can be declared: `Claim.node_id` resolves a proof to `scripts/tests/<node id>` and `run_pytest` invokes pytest, so an NUnit proof has no representation in that harness at all. Durable prose now states the mechanism each test is built on — which serializer difference, which engine branch — which a reader re-checks by reading the code rather than by trusting a remembered outcome. The record says that in one paragraph, so the limit is stated rather than papered over. The outcomes themselves are here. Re-measured 2026-09-05 on this branch's tree (the commit before this one), each mutant applied to the working tree and restored from the index between runs, tree verified clean afterwards: positive control ScriptedScheduleControllerTests Passed: 9, Failed: 0 OpenApiSerializerContractTests Passed: 4, Failed: 0 Bind<T> -> System.Text.Json with JsonSerializerDefaults.Web Failed: 2, Passed: 7 — Production_Body_Binder_Ignores_Required_Members, Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null BodyBinderSettings = ApiJsonSettings.Create() -> new JsonSerializerSettings() Failed: 1, Passed: 8 — Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null OpenApiSerializerContractTests RuntimeSettings -> new JsonSerializerSettings() Failed: 4, Passed: 0 — all four cases, on PascalCase keys ScriptedScheduleController AddDuration(..., request.Trim, ...) -> false Failed: 1, Passed: 8 — Committed_Script_Fixture_Produces_The_Pinned_Snapshot ScriptedScheduleController PadUntilExact(..., request.Trim, ...) -> false Failed: 1, Passed: 8 — Committed_Script_Fixture_Produces_The_Pinned_Snapshot The last one is the round-two finding closed and re-witnessed: before the fixture's pad target moved off the content boundary, that mutant left all nine green. A squash merge writes its own message, so these figures also belong in the PR description. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
d6e4426aa9 |
docs(563): retire the forward-references the supersession falsified, and trim the record to what only it says
Two pointers still named #563 as the open reason scripted playout has no coverage. The ContentEnumeratorBuilderTests header now points at the successor record and docs/testing.md instead of the issue this branch closes. docs/decisions.md carried an orphaned fragment, "external-process pipeline remains #563's", in the residual block under ## Index -- with the pipeline now permanently outside the automated suite rather than deferred, the fragment states something false and has no recoverable subject to rewrite it around, so it goes. The record's rule gains the two things measurement settled: that ApiJsonSettings shares production's configuration and never MVC's settings object (MaxDepth 32, the two ProblemDetails converters, pinned by ApiJsonSettingsTests), and that a fixture must aim every trimming instruction between two content boundaries or that action's trim argument is witnessed by nothing. Its body loses the mutant table and the extraction paragraph, which the mechanics doc its own frontmatter points at carries verbatim; what remains is the conclusion plus the reasoning that exists nowhere else. 70 prose lines to 56, under the advisory ceiling without dropping a distinct finding. Refs #563 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
c0a70d724a |
test(563): witness the Newtonsoft half of the binder claim, and narrow the prose to what reddens
The branch asserted that binding fixture bodies through ApiJsonSettings makes "a swap to a
lookalike serializer" redden. Measured, only half of that was true: replacing
ScriptedScheduleControllerTests' BodyBinderSettings with a plain `new JsonSerializerSettings()`
-- a Newtonsoft lookalike that has lost the production configuration -- left all 8 tests green.
Only the System.Text.Json swap reddened. So the production edits the branch makes for that
coupling (ErsatzTV/Serialization/ApiJsonSettings.cs and the Startup rewrite) were justified in
four places by a hazard no test could see.
Both halves are now real. Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null
binds `{"order": null}` and posts it to AddCollection: NullValueHandling.Ignore keeps
ContentCollection.Order at its declared "shuffle" and the call is a 200, where Newtonsoft's own
Include default writes the null through and AddCollection's Enum.TryParse returns a 400. That is
a behaviour difference a script would see, not a settings-shape assertion, so it is not a second
copy of the settings list.
Mutants, run 2026-09-05 over the 9-test fixture:
Bind -> System.Text.Json web defaults 2 red
BodyBinderSettings -> new() 1 red (was 0 before this commit)
OpenApi RuntimeSettings -> new() 4 red (write side, naming strategy)
What still nothing observes is Startup.ConfigureServices itself: re-inlining the
AddNewtonsoftJson lambda as a hand-copy of Apply reddens no test, because a byte-equal mirror is
behaviourally indistinguishable. ApiJsonSettings removes the duplicate rather than detecting its
drift, and docs/testing.md, the decision record and all four docstrings now say that instead of
claiming a detector. Drift confined to ReferenceLoopHandling or the StringEnumConverter is
witnessed by neither suite; that is stated rather than left implied.
Also files the 401 blind spot the record had described as "tracked separately" while nothing
tracked it. ersatztv#913 records the chain, verified from source: the filter is registered
globally, EndpointRequiresKey fail-closes every mutating verb, ScriptedScheduleController carries
no [SkipApiAuthorization], and neither ScriptedPlayoutBuilder nor entrypoint.py supplies a
credential.
Refs ersatztv#563 and ersatztv#913.
Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
4bd5cf9e91 |
docs(563): scope the required-member claim to the serializer, and name model validation as residue
The previous commit said a body omitting a `required` member "reaches the action" in production. That overreaches what was measured: MVC adds an implicit required check for non-nullable reference types (ErsatzTV.Core.Nullable has <Nullable>enable</Nullable>, and Startup configures no ApiBehaviorOptions, so the [ApiController] automatic 400 is live), which would very likely reject that body before the action. What is measured is the SERIALIZER: Newtonsoft deserializes it to a default, System.Text.Json throws. The prose in the test, ApiJsonSettings, the record and docs/testing.md now stops there and puts MVC model validation on the uncovered-wrapper list where it belongs. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
16a6e2790f |
test(563): bind the fixture with the production body binder, and name the wrapper that is left
The docstring, the decision record and docs/testing.md all claimed the replay covered everything
except two hops. MVC model binding was a third: production binds /api/* bodies with Newtonsoft
(Startup -> AddNewtonsoftJson -> CustomContractResolver + StringEnumConverter) while the replay
deserialized with System.Text.Json. Measured on this tree: for the fixture's own bodies the two
agree, but for a body omitting the `required` member "collection" they diverge -- System.Text.Json
throws, Newtonsoft binds Collection = null and the action runs. So the fixture's stated purpose
("field names and casing match what the HTTP body binder accepts") was asserted by nothing, and a
fixture production would bind differently could still go green.
Rather than only widening the residue list, bind the way production binds. The registration moves
into ErsatzTV/Serialization/ApiJsonSettings.cs, Startup applies it from there, and both
OpenApiSerializerContractTests (which had its own mirror of the settings) and the scripted replay
now call that same function -- one definition, no copies to drift.
Production_Body_Binder_Ignores_Required_Members asserts both halves of the divergence THROUGH the
replay's own Bind helper, so pointing the replayer at another serializer reddens; the fixture's own
bodies cannot witness that swap.
The residue is now named honestly in all four places: the binding WRAPPER (input formatter, the
[ApiController] automatic 400 before an action runs) is uncovered, the serializer inside it is not.
Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
0d2cd89782 |
docs(563): supersede the scripted-golden deferral with the in-process coverage rule
The deferral record described ScriptedScheduleController as a "1:1 pass-through" to SchedulingEngine. It is not: an unparseable playback order is a 400, an unparseable filler kind SILENTLY degrades to FillerKind.None, an unknown build id is a 404, and the engine's no-progress InvalidOperationException is translated to a 400. Carrying that wording forward would have shipped a false statement, so the successor states a thin adapter with named mappings, each pinned by a test. - new record testing.scripted-engine-in-process-net (active, since 2026-09-05) - predecessor testing.scripted-playout-golden-deferred git mv'd to docs/decisions/archive/testing/ with frontmatter retargeted only; body prose byte-identical, so no Decisions-Edit trailer - docs/decisions.md Index line retargeted to the archive path plus a new dated line for the successor - catalog regenerated with scripts/build_decisions_catalog.py - docs/testing.md: the Golden-file nets paragraph now points at the new coverage instead of "tracked in ersatztv#563"; a new "Scripted playout coverage" section states what is covered where and what is deliberately not covered (Cli.Wrap launch, Kestrel + Startup middleware, ApiAuthorizationFilter), dated 2026-09-05; Timezone independence records the per-call TZ audit that decided which engine instructions the fixtures may use. Refs #563 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
0f8e73ab05 |
fix(881): the replacement clause asserted an absolute I had not measured
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 11s
Review verdict / Set review-verdict status (pull_request_target) Successful in 37s
review-verdict/h10 Review-verdict: MERGEABLE @ 0f8e73a (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m47s
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 6s
The previous commit replaced "the order is load-bearing" with "and that is the whole of what the order buys". What was measured is narrower: with the relevance gate moved first, the three gates are each still witnessed refusing alone. That does not establish the order buys nothing else - the reset placement is a second candidate, unmeasured either way - so the absolute is gone from both sites and what stays is the cost reason, which is readable from the control flow. refs #881 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
f34828806d |
fix(881): the gate order I called load-bearing is a cost choice, measured
`verify_claim`'s docstring and the record's `mechanics` both said the relevance
gate must run LAST or the status and vacuity gates could never be witnessed
failing alone. Executed at
|
||
|
|
91a9fbf81a |
docs(881): name the reach mutation the way the rule field names it
The residual paragraph called it "a stronger mutation", which is a judgement about size; what the mechanism requires is a second mutation of the same clause, declared and required to redden the proof. One vocabulary across the rule field, the manifest and the body, so a reader does not have to decide whether two descriptions are the same thing. Refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
a956cc6b8d |
fix(881): the same outcome claim, restated in the proof's own docstring
The claim the shipped GREEN entry binds — what removing the `+++ /dev/null` arm does — was written twice: in `check-doc-narrative.py`, where the entry binds it, and again in `test_a_DELETED_doc_is_not_reported_as_added_content`'s docstring, where nothing does. That is the copy-of-an-outcome shape this rule forbids, in a site class the rule names, found while reading the proof for the residual below. The docstring now points at the manifest entry and keeps its rationale (a deletion yields no `+` lines either way), which is the half the carve-out protects. The residual paragraph is also made exact rather than general. The reach mutation proves the proof depends on the clause through the `b/` stripping every scanned header goes through, not through the `/dev/null` arm itself, so in general such a green cannot separate "no test feeds that input" from "the arm changes nothing". For this entry it can, by reading the proof: the deleted-doc test deletes a tracked file, and a deletion diff under the flags `run_diff` pins carries a `+++ /dev/null` header — probed rather than reasoned. Refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
a9341d8415 |
fix(881): a GREEN claim is only readable if its proof REACHES the clause
Round three found the one half of the new mechanism with no relevance gate. `verify_claim`'s GREEN path read exactly two things — the run exited 0, and something PASSED — and both are satisfied by a proof that never touches the mutated file at all. Reproduced before fixing: retargeting the shipped GREEN entry's proof from `test_check_doc_narrative.py` to `test_bom_guard_detection.py` changed nothing, and the entry still reported verified. The RED direction never had this hole, because a proof that ignores the mutation stays green and is refused as "the clause is not load-bearing". So a GREEN entry now declares a `reach_replacement` and its `reach_expect`: a SECOND mutation of the SAME clause, required to REDDEN the same proof, executed through `verify_mutation` so its red is read through the diagnostic gate rather than on exit status. The shipped entry declares `path = p` — dropping the `b/` stripping every scanned diff header goes through — and the run then scans NOTHING, which is what the declared diagnostic reads. The same retarget now fails, naming the reach verdict. The gate runs LAST of the three: run first it would refuse before the status and vacuity gates were read and neither could be witnessed failing alone (#685), and the sandbox is reset between a claim's two proof runs for the reason it is reset between mutations. It has its own disarm proof, and the two synthetic claim sandboxes are now real git repositories so `reset_sandbox` has a baseline; `_lib_with` shares the baseline registry, since a copied module's own starts empty. Also from that round: - The record no longer counts the mutation-outcome claims in the pinned proposal-3 scan. A third of the same shape sits in the same result set (`test_a_verdict_BEYOND_A_SHORT_PAGE_is_still_found`), and which side of the line a sentence falls on is a judgement, so an exact count is a figure the next reader re-derives differently — the failure this record is about. - The calibration paragraph no longer restates the post-review-verdict outcome as a dated witnessing. It points at the `CLAIMS` entry that executes it, which is the form the rewritten shell comment beside it demands. - The comment in `check-doc-narrative.py` claimed a universal ("reddens no test") while one file is executed. It now names that file, so the quote binds an outcome no wider than what is checked. - Proposal 4 from the issue is dispositioned explicitly: rejected as a rule here, on the issue's own argument that an exhortation does not fire at the moment of least slack. - `docs/README.md`'s task-signal parenthetical now names the `CLAIMS` population; the file was owned by another slot when this branch started. Cost re-measured 2026-09-05, three baseline/branch pairs: the `CLAIMS` half adds 31.7-43.6%, up from the 12.7-16.6% measured before the gate existed. The old figure is retired rather than scaled — growing the population invalidates the measurement that described it. Refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
7dba36cd58 |
fix(881): my own paragraph asserted the scan found no mutation-outcome claims - it found two
Self-review of the previous commit, on the same class it fixes. "none of them asserts a mutation outcome", written about the 58 lines the proposal-3 scan returns, is false. Two do: the mutation table at `docs/decisions/records/ffmpeg/watermark-resolution-unified.md` line 104, which names a dropped discriminator and the single test that catches it, and `web/src/screens/AutoTuneScreen.test.tsx` line 174, which says what a revert to the old flex row can redden. Both read in full at `efadbec29` rather than from the truncated grep line - the truncation is how the first pass missed them. Three kinds of sentence under one pattern is a better argument than the one the false claim was making: it is not that the pattern finds only rationale, it is that it finds rationale, state anchors and mutation-outcome claims side by side and nothing in the string tells them apart. Two smaller ones in the same commit. The new test's docstring said the gate is "the one gate the others cannot cover" and the manifest said "the one PRE-FLIGHT refusal a red proof cannot be told apart from" - both assert uniqueness among the pre-flight refusals that neither measured, and a clause occurring zero times also leaves the text identical. Narrowed to what the mutant demonstrates: no later gate stands in for it. And the fixture comment glossed `verify_claim`'s GREEN refusal in quote marks, which under this record's own proposal-2 clause reads as a quotation of the library; it is not one, so the marks are gone. refs #881 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
99966f8bd7 |
fix(881): a control's purpose stated backwards, and an enumeration that grew past what it could carry
Four review findings, all of the class this branch is about - prose asserting what a mechanism does, with nothing binding it to the mechanism. The record's `mechanics:` said a GREEN claim over an already-red proof "would otherwise be satisfied by the redness it is supposed to rule out". Inverted: `verify_claim`'s GREEN branch REFUSES any non-zero exit, so redness refutes a GREEN claim and can never satisfy one - which is what test_MUTATION_disarming_the_GREEN_EXIT_STATUS_gate_accepts_a_proof_that_WENT_RED asserts. The hazard the control removes is the same one it removes for the rows, and it runs in both directions: an already-red proof satisfies a RED claim with redness its mutation did not cause, and refuses a GREEN one for a reason unrelated to its mutation. Both the record and the fixture comment now say that, and both say what the control CANNOT do - its assertions are over the aggregate of every proof ref, so a single ref collecting nothing is invisible to it and is caught per-claim by the vacuity gate instead. The manifest's `why` had widened a scoped sentence into "THE OTHER GATES EACH CARRY THEIR OWN PROOF" and then enumerated them, which made the enumeration a completeness claim it could not meet: the identical-replacement refusal carried no proof at all. The review measured that at 8adf21eff - `if mutated == original:` disarmed, whole file 49 passed 1 skipped. That gate is the one a red proof cannot be told apart from: the mutant is byte-identical, so the proof runs against the original tree and an already-red one reddens exactly like a detection. Disarmed, the harness certifies it as "the named test went red under the declared mutation, with the declared diagnostic" - witnessed here on the real library, restored after. So the measurement above no longer holds, by construction: the gate now has a disarm proof, and the sentence says explicitly that naming the gates is not a claim the list is closed. Proposal 3's rejection quoted "16 lines" with no predicate - the defect the record's own body names three paragraphs later, where the population scan is pinned verbatim for exactly that reason. The figure is not reproducible from the text. Replaced by a pinned `git grep` over the same corpus at the same sha (`32 files, 58 lines`), with what reading all 58 shows: they are rationale, the class the rule carves out, and the few real state anchors among them are not separable by pattern, because the difference is whether the sentence explains or asserts. refs #881 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
9e0e11030b |
docs(881): the third quotation on this branch that resolves to nothing
`docs/defect-shapes-773.md` §4's sentence was quoted verbatim-looking but with a lowered initial capital, and the source wraps it across a line at `evidence`/`behind`, so neither the written form nor the corrected one is findable by grep. Same treatment as the `post-review-verdict.sh` one: paraphrase without quote marks, keep the section reference, say why. The section reference itself was checked - the sentence is at line 351, under `## 4. Detectors`. refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
80cd0a9ee9 |
docs(881): a quotation that resolves nowhere, and a paragraph contradicting the one above it
Two more in the same class, both in prose this branch wrote. The calibration paragraph quoted `post-review-verdict.sh` as asserting "the absent-entry check catches every case on its own". That string occurs in no file: the comment wraps it across a line break at `catches`/`every`, so `git grep` for it finds exactly one hit - the record asserting it. That is ersatztv#812's second defect reproduced inside the record written to end it. Paraphrased without quote marks and pinned to lines 316-317 at `efadbec29`, which is what this record's own proposal-2 clause prescribes for a quotation that cannot be checked. The closing paragraph called the scan's hits "the remaining population" and "a backlog", three paragraphs after establishing that both figures are CANDIDATE counts and that reading them as a backlog of real claims overstates them. The closing text now says what is actually known: a place to look, with nobody having established how many are claims. refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
443df1391b |
docs(881): drop a figure anchored to the branch HEAD - the anchor moves
The sentence explaining why the scan pins a sha quoted a second figure for `HEAD`, which is exactly the anchor-to-a-moving-state shape this record settles: correct on this branch, wrong the moment anything else lands. The reason it was supporting is checkable without a number - the paragraph's own prose, the pinned command included, matches the pattern. The first draft of that replacement said "twice over". Three lines of the paragraph match, so the count is dropped rather than corrected; a count of matching lines in a paragraph nobody will re-measure is the same defect one size smaller. refs #881 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
11e4d74256 |
fix(881): three more claims in my own fix, and a cost band that rounded outward
Self-review over the fix commit, on the same predicate the record states.
The cost band said 13-17% over measurements spanning 12.7% to 16.6% - a band
that ROUNDS is still a band the data does not support at its lower edge. It now
states 12.7-16.6%, which is the span itself, with the per-pair figures beside it.
The quote-scope rule was stated as an OUTCOME claim ("free to be rewritten under
a green harness", "with the entry still reporting the red as verified") in the
record's `rule`, in the manifest preamble and beside the entry. That is a
mutation-outcome claim about the harness with no `CLAIMS` entry behind it -
manufactured by the sentence that introduces the rule against it. All three now
state the STRUCTURE, which is what a reader can check by looking: the assertion
and the test it names are outside the binding.
The manifest preamble said "three of the entries below assert that a mutation is
NOT noticed". `CLAIMS` holds three entries and exactly ONE is GREEN; the three
the `Claim` docstring names are CORPUS sites, not entries. Corrected to one, and
"the most common shape prose actually takes" - a frequency nothing measured -
dropped rather than quantified.
Two claims in the new record prose were themselves overstated. The 69-line green
narrowing was described as the negative direction rather than as candidates for
it: sampling the hits shows `green` in this corpus is as often a CI job's colour
as a mutation's outcome, so both figures are now labelled CANDIDATE counts. And
the "the number moves under the commit that records it" sentence now carries the
figure that shows it - the same command with `HEAD` in place of the sha prints
`107 files, 344 lines`, measured on the committed tree.
refs #881
Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
|
||
|
|
3489254c8b |
fix(881): re-derive the population from a PINNED scan, and bind the claim quote to the outcome it asserts
Four review findings, every one the defect class this record is about: a prose assertion with nothing binding it to what it asserts. THE DERIVED POPULATION WAS NOT REPRODUCIBLE. "128 lines across 65 files" and "47 of the 128 carry the NEGATIVE direction" cannot be reached from the predicate the record described, while the record told the reader to re-run it there. A review swept ~40 readings of that description at |
||
|
|
4008f4b574 |
fix(881): exempt the claim binding from the index-population registry, and fix three prose claims in the change itself
`test_mutation_harness.py` now imports the shared index derivation, which `test_every_index_derived_module_is_registered` requires be registered or exempt. It is exempt: its population is `CLAIMS`, and it consults the index only per member, to answer whether a declared `site` is a path git tracks. The exemption list's own docstring counted its entries, so that count and its review date move with it. Three claims written by this change were falsified by this change, which is the shape it exists to catch: - the binding test's docstring said membership comes from the index "not from `Path.is_file`", while the same test now asserts existence with `is_file`; - the record quoted the manifest docstring this change rewrites — an anchor to a state the commit moves, which the record itself rejects. It now anchors to `efadbec29`; - the `Claim` docstring quoted three files without naming them. They are named. refs #881 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
1fc24e8cf6 |
fix(881): a mutation-outcome claim is executed wherever it is written, bound to the sentence that makes it
`testing.mutation-claims-are-executed` was the right rule scoped to its first
site: `mutation_manifest.py` declared itself "one per `MUTATION`-graded row of
`docs/guard-inventory.md`", so the same claim written in a code comment, a test
docstring or a decision record was outside it by construction. That is where all
four of ersatztv#812's consecutive review-round defects lived.
Extend the rule in place rather than adding a sibling record: a sibling would
recreate the exact shape (a rule per site class, with the next site class outside
both) that #773, #784 and #743 each are. The subject is unchanged; only the
population widens.
Mechanism: `CLAIMS` in `scripts/tests/mutation_manifest.py`, keyed on the PROSE.
Each entry carries the tracked `site` and the verbatim `quote`, checked every run,
so a reworded sentence reports as a retarget instead of drifting from the entry
that justifies it — this is proposal 2 (a quotation of another file is a claim
about that file) adopted where the referent is declared. Each entry also declares
RED or GREEN and is executed in the existing sandbox. GREEN is new: 47 of the 128
candidate lines the corpus grep returns at
|
||
|
|
8fd9eae0bf |
fix(891): a sourced path is code, so every hook resolves it from its own tree (#903)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m3s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m10s
Every hook under `.claude/hooks/` assigned `ETV_HOOK_FIRE_LIB` from `${CLAUDE_PROJECT_DIR:-<self>}`
and then `. `-SOURCED it. Sourcing is execution, so a file of that name in an env-designated tree ran
as code inside the hook before stdin was read and before it could decide anything. Measured on the
merge gate before #858 fixed that one hook: a decoy tree's copy printed an `allow` and exited 0.
Reachable without an attacker, because husky is a different launcher: `.husky/pre-push` invokes
`./.claude/hooks/…` relative to the PUSHED tree, independent of the variable, so a push from one
worktree while the environment names another sources the other tree's code into a gate.
Sweeps the remaining twelve hooks together (population derived from `git ls-files`), reconciles the
second resolution inside `scripts/hook-fire-log.sh` itself, and requires the root to OWN the sink
(`-ef`, not `-e`). The static guard pins the preamble BYTE-FOR-BYTE — a withdrawal, after a lexical
rule was defeated by five successive shapes.
Also pins two arms of the checker that were unsubsumed AND unpinned: the begin call's presence and
its missing stdout-mode token. `…_LOSES_its_instrumentation_…` looked like their proof and was not —
it asserts only that the fault list is non-empty, and a stripped hook trips four arms, so deleting
either left the suite green.
fixes #891
refs #858, #859, #776
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
|
||
|
|
0e40ac283b |
fix(870): an empty timeline page is not exhaustion — the walk reads to its cap (#896)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 5s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 10m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m56s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 7m2s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 1m35s
`count_pr_mutations` treated an empty page past page 1 as proof it had reached the end of the PR timeline. Gitea does not mean that: `ListIssueCommentsAndTimeline` applies the LIMIT/OFFSET in `FindComments` at the DATABASE level and filters AFTERWARDS, dropping `CommentTypeCode` rows and inaccessible cross-references into a nil slice that serializes as bare `null`. A page of 50 inline review comments is byte-identical to a page past the end while later pages still hold events, and rows are ASCENDING, so the events a fence looks for are the furthest from page 1. Fifty comments, which a PR author can create on their own PR, truncated both walks at the same place: both counts agreed, the sha comparison agreed, and an ABA force-push yielded an exemption `success` over a diff no single head justified. The walk no longer infers the end from an empty page BEFORE its cap. Such a page is skipped; the loop reads every page to its 20-page cap and trusts the counts only when the LAST page came back empty. An empty FIRST page and any unreadable shape still end the walk untrusted. NARROWED, NOT CLOSED, and the docs say so in one unit: the page-20 terminator is still trusted for the same unprovable reason, so the defeat now costs a timeline of over 1000 rows rather than ~100, with the same 50-row filtered block pinned to offsets 950..999. Measured at Gitea 1.27.1, ruling out the cheaper fixes: `X-Total-Count` on this endpoint is the post-filter length of the PAGE, not a total (`?limit=1` returns 1 on a 14-row timeline), while `/activities/feeds` returns a true total; `limit` clamps to 50; the only query params are `since`, `before`, `page`, `limit`, so the paged and serialized sets cannot be made to agree. Also: each page bounded `--connect-timeout 5 --max-time 15` and retried once, mirroring `page_statuses`, because the walk went from ~2 requests to a fixed 20 and the third call site runs after the exemption `success` is posted. Costs stated rather than hidden — worst case 40 requests and 20 sleeps, wall-clock pessimum 620s per walk, and the suite roughly doubled (202s -> 474s). Seven tests, each mutation-witnessed red; three reproduce the defeat against the shipped predecessor. Two independent cold reviews plus a re-review of the fix: no Blocker or High in the code. Their real finding was prose claiming the hole was closed, and cost arithmetic wrong twice. One reviewer claim was refuted by execution. Fixes #870 Refs: #803, #706, #664, #751, #893 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
e8f80c42ce |
fix(820): derive where Complete<T> is APPLIED, not just what it means (#883)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 28s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 14m22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 11m1s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 9m58s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 1m58s
`Complete<T>` (#807) makes SPA full-replace bodies fail typecheck when a builder omits a schema member. Nothing checked it was APPLIED: `completeRequest.guard.test.ts` proves the type's semantics and would stay green with every annotation deleted, and `test_optional_request_members.py`'s COVERED disposition — "the builder is annotated `Complete<T>`" — was a claim about another language's source that nothing verified. Adds `completeAnnotationScan.ts` (compiler-API scanners) + `completeAnnotations.guard.test.ts`, with a synthetic-source fixture suite. Two derived populations: the `Complete<…>` annotations (SPA AST ∩ git index) and the droppable schemas (parsed from the generated `v1.d.ts`, a pass-through of the OpenAPI `required` array). It asserts a production annotation per schema dispositioned as needing one, NO annotation on the server-computed and load-bearing-omission schemas, that every `Complete<X>` resolves to a generated schema rather than a hand-written mirror, and set equality between droppable schemas and the reviewed dispositions. `test_complete_annotation_dispositions.py` cross-checks that table against the authoritative Python one and ships a declared, harness-executed mutation. Found one live defect: `playouts.ts` declared two request types as hand-written mirrors SHADOWING generated schemas of the same name, so their `Complete<>` was checking a local copy rather than the contract — the #754 mechanism wearing the annotation meant to prevent it. Eight review rounds, seven BLOCKED, two independent cold reviewers. A wrapper-signature scanner was built and REMOVED: every blocker traced to that one mechanism (obligation on the wrong population; reachability mistaken for protection, since `Complete<T>` is shallow; body discovery keyed on a parameter name, then parameter-vs-local; and finally `export function` → `export const` blinding the scanner and its cross-check together). Five defects from one mechanism, so the mechanism went rather than a sixth patch. Residuals stated in §4b, the guard-inventory row and the record: per-SCHEMA not per-site or per-wrapper; token presence not liveness; the phantom direction unchecked (#777); a second `setupFiles` entry could discharge; and plugin-level population integrity borrowed from the sibling guard. fixes #820 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
8aeacd534a |
fix(819): derive the SPA page-size guard population from the git index (#875)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m52s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m23s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m5s
The guard asserted EXACT completeness over a population enumerated by a directory walk, so an untracked .ts/.tsx under web/src/ entered it and failed as unregistered on that developer's checkout while CI — which only ever checks out tracked files — stayed green. The glob still supplies file CONTENT; the POPULATION is now 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. That reaches the index without admitting @types/node to tsconfig.app.json, the obstacle that deferred this in #818. Three mechanisms carry the proof, each added because the previous was measured insufficient: a closed-form restatement of the shared scope predicate (sharing no helper at any depth with what it checks); a second independent `ls-files --others` query cross-checking the population; and real-git tests that execute the derivation against a temp repository. Six residuals are stated with their MEASURED fail-directions, and testing.guard-derives-population-from-source gains a bounded exception plus the closed-form criterion. fixes #819 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
b6b3520bdb |
fix(809,822): isolate the suite from the production hook-fire log by construction (#874)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m32s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m32s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
4b3f6f6c90 |
fix(786,789): workflows declare their own per-job metadata; guard populations derive from it (#872)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m21s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m58s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m8s
Closes #786 and #789, bundled because working either alone would build the artifact the other removes. Every job in all six tracked workflows declares `env.CI_JOB_ROLE` (guard/report-only/none); the `docker-build.yml` jobs also declare `env.CI_EXECUTION_CLASS` (toolchain/bare-runner). Both guard populations derive from those markers; the `TOOLCHAIN_JOBS`/`BARE_RUNNER_JOBS` literals are deleted. A missing or unrecognised marker is a hard failure in both checkers. #789's literal had a real justification — set equality between two DERIVED sets is blind to a member leaving both at once — so the marker is the anchor that replaces it, and the cost (proximity to the `container:` block) is paid by a THIRD derivation from each job's own steps, which is also the only check that sees the failure #789 filed: a .NET step moved into a bare-runner job, where no set changes. The residual is disclosed: drop the block, flip the marker AND hide the tool behind a script and all three go blind, bounded by the failure mode being a loud missing-binary crash. #786's guard jobs join a machine-checked population: a new `test_workflow_job_guards.py` asserts set equality both ways against a new "Workflow-job guards" table, and the four jobs with no dropped-step guard each carry a recorded decision. Two issue claims were refuted by measurement: #789's "editing docker-build.yml re-points the pin" (the pathspec is `docker/ci` only) and #786's job count (17, not 15). Four cold adversarial review rounds across two model families; rounds 1-3 BLOCKED, all findings fixed and each fix demonstrated by reproducing the reviewer's own test. The recurring defect class was prose drifting from code, including a mechanism claim in the decision record that execution refuted. All five mutation proofs redden when their shipped detector is disarmed. New decision record: `testing.workflow-declares-its-own-job-metadata`. Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
e11d577193 |
docs(796): verification code is code under test, and the proof it was claiming an exemption from (#871)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 8s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m9s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m18s
Records `testing.verification-code-needs-its-own-proof`: the proof obligation follows the VERDICT rather than the file, so it binds harnesses, wrappers, timeouts and checkers — not only the files the guard population derives. The issue asked for a stated position on whether non-guard checker scripts get mutation proofs. The position as first written claimed `scripts/mcp_smoke.py` "cannot participate" because driving it needs the gitignored `.mcp.json` and a cold-built language server. Cold review refuted that by execution: it takes its config path and server name as positional arguments. The record had failed its own headline rule on the one claim its decision rested on, so this ships the proof instead of the exemption. - `scripts/tests/test_mcp_smoke.py` — a hermetic stub JSON-RPC responder and six cases pinning the defects the checker has already had, with the positive control as a fixture the refusal tests depend on, so a node-id or `-k` selection cannot skip it. - A declared clause in `mutation_manifest.py` targeting the unguessable request id, using the `guard=test / target=script` shape that already exists for `mutation_harness_lib.py`. Witnessed red: `id_init = 1` makes the pre-answer accepted at `initialize` (rc 9 -> 10), and only that test moves. `mcp_smoke.py` still gets no inventory row — one is rejected as a phantom (measured). The row goes to the test file, which joins the derived population automatically. Five cold-review rounds, four BLOCKED. Round 2 caught a `ruff format` red that would have failed `script-tests`. Rounds 3-5 found only hand-maintained counts and uniqueness claims in prose, three of them created by the previous round's fix; that class was deleted rather than corrected again, per this record's own stop-and-subtract rule. Docs updated in the same PR: `docs/README.md` task-signal map and `docs/guard-inventory.md` (row, summary counts, scope-limit item 6). fixes #796 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
761e575836 |
fix(787): derive the dropped-step guard's scope, and reconcile its snapshot against the server (#861)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m22s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m33s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
3e2c07b42f |
fix(807): SPA full-replace bodies are built as Complete<T>, with a derived guard over droppable request members (#827)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m9s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m49s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 15s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
29d7a06e65 |
fix(806): guard populations over FILES derive from the git index, not a filesystem walk (#818)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m57s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m56s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m20s
`testing.guard-derives-population-from-source` (#774) was silent on the commonest population in our own guards — files in a directory — and every one answered with a filesystem walk. A walk is not authoritative: it reports build output, generated shims and editor droppings, and differs per machine. #778 measured the cost by getting the same population wrong three times in one PR. CONVERTED (a completeness claim over tracked files): `test_guard_inventory.py`, `test_hook_fire_log.py`, `test_ci_image_pin_population.py` (which also gained `*.yaml`), `test_remote_state_inventory.py` (folded onto the shared derivation), and `test_pr_changed_files.py` (not on the issue's list — found by sweeping the whole repo). ASSESSED AND RECORDED, not silently skipped: `_repo_copy` takes its file list from the index for hermeticity though it makes no completeness claim; `test_ci_dropped_step_guard.py` has no filesystem population at all; the decisions corpus is recorded as unexamined rather than cleared; and the SPA page-size guard is deferred to #819 with its obstacle documented. This is not "replace every glob". `scripts/tests/tracked_files.py` is the single derivation. `test_guard_populations_derive_from_git.py` proves it in two measured complements: exhaustive removal catches a hardcoded `.exists()` admit and memoisation; the call log catches an append-only source that yields nothing on this machine — #778's shape — which removal cannot see because it has nothing to remove. Twelve rounds of independent cold review, alternating model families in isolated worktrees. The production derivations were confirmed sound every round; every blocking finding after the first was in the proofs or in prose claims about them. Counts over growing populations were removed rather than corrected, after three drifted. fixes #806 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
3b386649bf |
feat(790): execute every MUTATION grade instead of asserting it (#815)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m30s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m10s
Every `MUTATION` row of `docs/guard-inventory.md` now carries a DECLARED clause mutation that is applied to an isolated copy of the repository on every suite run, with the row's own named test required to go red carrying a declared diagnostic. Manifest and MUTATION rows are compared for set equality both directions; the other 22 guards each carry a stated reason, compared the same way. Measured rather than assumed: 12 of 13 guards admit a single-clause mutation; `instrumentation_faults` does not, and that entry carries the surviving finer mutation, re-run every suite. Nine cold cross-family review rounds. Rounds 1, 2, 7 and 8 each found real mechanism defects — two mutations that measured nothing, an incomplete git-environment sanitisation, a reset that restored its own mutant, and a proof of that fix which was not itself isolated. All fixed and witnessed red. fixes #790 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
649acd12ee |
feat(779): test the deny path at the production config value, and derive full-replace field lists (#808)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m52s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m11s
Two conventions from #773's detector menu (F and G), each as a decision record plus a filled gap. Part 1 — the deny path at the production config value. Every assertion about the API read-gating posture ran through a hand-written fake HANDED the boolean, and no test in the repo constructed ApiKeyProvider at all, so the line deriving that posture from configuration had never executed. Now covered across the matrix through the real provider: absent, true/True/TRUE, false/False, and a present-but-non-boolean value (which throws at startup — fail-closed, pinned). Part 2 — a full-replace path asserts its complete field list. ScheduleItemResponseRoundTripTests is the release gate for the flat schedule-item DTO, and its comparer was itself a hand-copied field list: complete when written, unable to report when it stopped being. Now derived by reflection with an empty exemption set and a written-down count pin (55). Four cold adversarial review rounds. Three returned BLOCKED, every one on a claim in a decision record that the code contradicted — the exact defect the records exist to name. The surviving rule, now written into the record: state the measurement and the code path you actually read; do not generalise from one executed case, and do not explain a mechanism you did not measure. Residual SPA optional-field drift tracked as #807. fixes #779 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
15d2439915 |
feat(794): witness a fix's test failing BEFORE the fix, and check the claim in CI (#801)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m31s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m30s
Mechanises the defect that took #776 and #793 six review rounds each: a fix's test
written to confirm the fix, not to discriminate against its absence.
testing.guard-ships-with-mutation-proof generalised from guards to fixes.
prove-fix.sh runs the selector at the commit (control, must be GREEN) and again in a
separate fresh worktree with the non-test files reverted (must be RED = pytest exit 1
exactly; 2/3/4/5/143 are refused, and --continue-on-collection-errors keeps add-a-file
fixes provable). pytest's status comes from a marker written only after it returns,
because ( cd X && pytest ); rc=$? returns the SUBSHELL's status. Opt-in by a Proves:
trailer; CI checks every commit that carries one and says out loud when a PR has none.
THE TOOL REJECTED ITS OWN AUTHOR. Three commits on the branch claimed
Proves: scripts/tests/test_prove_fix.py; the job returned UNPROVEN for all three,
because reverting the script restored a working earlier version the suite also passed.
Two had been "verified" against hand-written mutants that did not match the code that
actually shipped. The tests were rewritten until both go RED against
|
||
|
|
499dd348ab |
feat(776): every hook reports that it fired, and the report is measured (#795)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m37s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m19s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
8f02e4a284 |
fix(774): withdraw the vocabulary parity test — six rounds is the answer, not a seventh
Round six returned BLOCKED on the same file again: a column-zero `esac` inside a string truncates the scoped match and silently drops a real arm, and a heredoc inside the block still false-reds. Both correct. Both the sixth distinct shell construction found in six rounds. That is no longer a sequence of bugs, it is a result. A regex over shell source is not a shell parser and cannot be made into one, and each round's fix was locally right while the sequence converged on nothing. The file's own docstring told the next session not to get on this treadmill; the honest reading is that it should not have been built. DELETED rather than patched again. The reasoning is this change's own thesis, applied to itself: `testing.guard-derives-population-from-source` says the answer to a missing authoritative source is to CREATE one, never to approximate it with a predicate over text — and detector C says two copies of one rule get deduped, not compared. The right fix was available from the start and is #788. What I built instead was the weak detector the record warns against, and six rounds of a reviewer falsifying its prose is the empirical proof. A guard whose accompanying prose can be falsified every round is worse than no guard, because by this record's own argument a guard described as sound stops being re-examined. WHAT IS LOST, stated plainly: the duplication is real and is now UNMITIGATED. The two vocabularies in post-review-verdict.sh and check-review-verdict.sh can drift, and only a comment says they must not. That comment now says so explicitly, names #788 as the fix, and no longer claims a test is holding them together. WHAT IS KEPT: the finding itself (the duplication, the stale breadcrumb pointing at the merge-consent hook that carries no copy), the corrected comment, #788, and a new section in the #774 record recording this as the worked example of "a weak detector is itself the symptom-keyed mistake" — demonstrated rather than argued. Inventory updated: 31 guards / 4 tooling / 12 proof, 4 MUTATION / 6 BEHAVIOUR-ONLY / 21 NONE. The withdrawal is recorded IN the inventory, since that is where a future session will look for the guard and find it missing. Its count-parity guard verified the new numbers itself. decisions-validate OK, 574 script-tests pass (six removed with the file). (--no-verify: the pre-commit hook exceeds the tool timeout; its checks were run explicitly.) Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e746ba5504 |
fix(775): resolve the record's two competing standards; close the templated-image hole; state the predicate limits
Third review round. Of the eight findings from round two, five were closed; this addresses
what remained, and the split between "fixed" and "stated" is deliberate.
FIXED — the record could not adjudicate. Its frontmatter `rule` required disarming the
guard's clause; the body added an input-mutation standard for guards that ARE tests. Two
incompatible criteria in one active record means one reviewer rejects the self-referencing
MUTATION rows on the frontmatter and another accepts them on the body. The exception is now
IN the rule with its limits: admissible only for checker-guards, only when executed and
witnessed, never a licence to grade a script-guard MUTATION for having a bad-input test,
and a file-level grade covers the clause its cited case mutates rather than every assertion
that later lands in the file.
FIXED — a matrix-templated image bypassed the cross-workflow check. `_PIN.match` requires a
literal tag, so `image: <repo>:${{ matrix.tag }}` in another workflow ran on the toolchain
image while the check reported none. Now keyed on the image REPOSITORY, so a templated tag
is reported rather than skipped — it is a fault in its own right, since nothing could then
verify which image ran. Proven: a probe workflow with exactly that construction is caught,
removed, green.
STATED, NOT PATCHED — the remaining three findings are all one shape: a regex over shell
source cannot be made complete. Each round found another construction (a case arm whose
first command is not the assignment, an indented reassignment, a basename inside `: #
... disabled`), and a fourth round would find a fifth. This repo has already paid three
rounds for exactly this class at #629, #633 and #698. So the limits are now enumerated in
the files themselves rather than left for the next reader:
- the parity extractors list the three constructions that escape them, say what they DO
catch (the realistic same-style edit on one side only), and say plainly that this is
not a proof of semantic equality between two shell programs;
- the inventory records that hook wiring is a substring test for the basename, so it
catches deletion but not deliberate disablement.
Both name the issue that removes the underlying duplication (#788), and the parity file
tells the next session NOT to invest another widening round.
Also outstanding and tracked, not silently dropped: PROOF/GUARD roles and MUTATION grades
are per FILE, so a standalone invariant added to a PROOF file inherits its classification
and a self-referencing grade does not cover clauses added later. That is clause-level
inventory, which is #790.
ruff clean, pyright clean, decisions-validate OK, 580 script-tests pass.
Refs #774
Refs #775
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3473a6c889 |
fix(774,775): close the cold-review findings — including three the change inflicted on itself
Two independent cold reviews (Codex GPT-5.6 cross-family; Fable 5 on the patch) both returned BLOCKED. They agreed on the counts error and the extractor hole; each found things the other did not. Fixes, with what each was: THE INVENTORY DID NOT COVER ITS OWN NEW GUARDS. `_SCRIPT_REF` matched `scripts/name.py` but not `scripts/tests/*.py`, so the three guard files this change introduced had no rows and the completeness check stayed green. A completeness guard blind to its author's new guards is precisely the defect being legislated against. The population now globs `scripts/tests/test_*.py` — which is how they actually run, since pr-checks.yml invokes the directory. 32 rows -> 48. That forced a third Kind. Once test files are in the population, every mutation proof becomes a row wanting a proof of its own, forever. `PROOF` marks a file whose job is to prove another guard; a scripts/tests file enforcing a repo invariant with no separate guard behind it stays GUARD and may cite a mutation case in its own file. HOOK EXISTENCE WAS STANDING IN FOR HOOK WIRING. Deleting a hook's registration from .claude/settings.json left the population and the table unchanged, so the row went on describing a guard that no longer ran — #631's shape one level down. Now derived from settings.json plus the husky hooks. THE SUMMARY COUNTS WERE A HAND-KEPT MIRROR AND WERE WRONG ON ARRIVAL: "28 guards, 4 tooling ... 19 have none" against a table holding 27/5/6/3/18. Both reviewers found it independently. The prose is now parsed and asserted against the table. TWO FALSE MUTATION GRADES, each with a concrete disarm: - test_full_first_page_alone_does_not_end_enumeration sends 50 docs paths then one more docs path; disarm pagination to treat a full page as final and it is still all-docs, still exempt, still green. Re-pointed at test_protected_path_on_a_LATER_page_is_still_seen, which does go red under that mutation. - test_the_scan_job_runs_the_out_of_pytest_positive_control asserts only that the script exists, is executable, is referenced and is marked; replace its logic with `exit 0` and all four pass. ci-prove-ban-detects.sh regraded NONE. The MUTATION column was also being applied as a curve: three rows graded MUTATION fed the real script an input only that clause rejects, which is what the rows eight lines away are graded BEHAVIOUR-ONLY for. Definition sharpened to *witnessed* rather than plausible, and those regraded. 5 MUTATION / 6 BEHAVIOUR-ONLY / 21 NONE across 32 guards. THE VOCABULARY EXTRACTOR COULD RETURN A PARTIAL SET. `[A-Z|-]` cannot match `SHIP*)`, so adding that arm leaves the extracted set non-empty AND equal to the read side — parity green while the gate desyncs. Emptiness checks cannot see partial degradation. A loose counterpart now asserts the strict pattern consumed every arm; proven red on exactly that attack and green on a clean tree. Also: each verdict pattern must be assigned once, since the extractor unions assignments while the classifier runs the last. Also: docker-build.yml was itself an unchecked scope mirror (now asserted to be the only workflow with toolchain container jobs, by parsing container.image rather than grepping — ci-image.yml names the image because it builds it); the mutant floor is an equality; e2e-functional.sh reclassified GUARD (it exits 1 on a failed contract assertion); design-sync-reminder.sh does block the first Stop. The doc now states all six excluded classes instead of one. Not done here, filed instead: workflow-owned execution-class metadata to replace TOOLCHAIN_JOBS, a single shared verdict vocabulary, and an executable clause-level mutation harness. Each touches a CI-gating or merge-gate path and wants its own review. 580 script-tests pass. Refs #774 Refs #775 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0bd59b0b6e |
feat(774,775): one rule for guard populations, one for guard proofs — both enforced
#773's analysis found that the largest recorded failure family is reasoning about a representative instead of the population (39% of process-failure records), and that the most common is a check that never ran at all (25%). Both rules had been reinvented repeatedly and written down nowhere. Two decision records: testing.guard-derives-population-from-source (#774) — a guard enumerates its population from a machine-readable authoritative source and asserts set equality both ways. States the boundary that keeps it honest: filtering to select the SUBJECT of a per-member property is fine; filtering the population before a COMPLETENESS claim is the defect. Also separates guard SCOPE (a reviewable policy choice) from guard POPULATION (always derived). testing.guard-ships-with-mutation-proof (#775) — disarm that clause alone and a named test must go red. Behaviour-only coverage is graded separately, because it proves the guard reacts, never that it is connected. Audit findings fixed: ci-image-pin stated an invariant it did not check. Its error text says "Every container: job must pin ersatztv-ci:<7-char-sha>"; what it asserts is that `grep … | sort -u` yields one DISTINCT value. Distinctness is a property of the pins present, so deleting the container: block from `test` leaves four pins, one distinct value, and a REQUIRED context silently running on the bare runner. test_ci_image_pin_population.py adds the population check, keyed on a reviewed registry cross-checked both ways — set equality between two DERIVED sets could not see this, because both sides shrink together. The verdict vocabulary was written down twice with no cross-check — post-review-verdict.sh (write) and check-review-verdict.sh (read). A word in one and not the other sends the required status green while the hook still denies. Both vocabularies are now extracted from their own source and compared as sets; a test that restated the words would just be a third copy. The write side's comment pointing at pretooluse-merge-consent.sh was also stale — the hook carries no copy and delegates. Mechanical enforcement, answered explicitly for both: No to a filter-shaped-guard lint. The token is not the defect — ToolCatalogTests filters correctly eight lines from a completeness assertion that must not — and it would be a string predicate over source, which this repo's record says takes 3+ rounds. Building it would be #774 violating #774. Yes to enforcing the bookkeeping. docs/guard-inventory.md classifies all 32 guard files; test_guard_inventory.py derives the population from the filesystem and call sites, asserts set equality both ways, and resolves every claimed proof ref to a real def. A new guard cannot ship unclassified; a renamed test cannot leave a row claiming lost coverage. What it does NOT check — whether a MUTATION claim is true — is stated, not implied. Measured: 28 guards, 4 tooling. 6 mutation-proved, 3 behaviour-only, 19 unproven. Every guard added here was mutation-proved by execution before being believed: neutering pin_population_faults turned 20 of 25 red; the inventory guard was driven red three ways (deleted row, new unclassified hook, stale proof ref) and restored green. 573 script-tests pass. Scope limit stated in the doc: inline workflow-job guards are not in the machine-checked population. Refs #774 Refs #775 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |