80cd0a9ee9f582eec8dcc1fd2f7faa73e0707dca
3810
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
1b5dd80681 |
fix(881): regenerate the catalog the previous commit's rule edit staled
The generated catalog embeds each record's `rule`, so rewording the quote-scope clause in 38bdf7bd0 left `docs/decisions/README.md` behind the record. Nine tests red on it - the four `build_catalog_check_path` reformat cases, its stale-catalog CLI proof, two `decisions_validate` main() cases, and the two mutation-harness entries whose positive control runs that validator. 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
|
||
|
|
4a56d54283 |
fix(911): a should-fix finding sends the round back, a failed-lens round is an error, and a harness executes the loop (#912)
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 28s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m46s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m37s
fixes #911 Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
fd58b1df33 |
fix(554): the channel preview says why it is not playing, and only the preview opts into muted autoplay (#910)
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 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 12s
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 4m46s
fixes #554 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
14e075174b |
fix(911): in a resume, the existing PR's stale head is the expected state until the push, not a finding
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ 14e0751 (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 14s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 18s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 15m39s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m41s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
Three consecutive #554 resume rounds graded "PR #910's head is still the first commit" as blocking; the push deliberately follows the loop. The reviewer brief now says so when args.pr is set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
692affca97 |
fix(911): a round with no reviews is an error, the history binds each fix to the round it answered, and a harness executes the loop
A round in which every lens returned null read as a clean pass and would now have been quoted verbatim into the PR body; it is an error before the push, in the review loop and in the post-rebase round, which also gains the same blocking-or-should-fix filter; a fixer that dies or stops (no done) is an error too, the same test the implementer already gets. The history entry now carries the fix that answered that round and only the commits that fix added (a line-set difference against the previous branch log — a fixer that reformats or rebases mid-loop defeats it, which is why the finisher is told to read git show, not the list). An empty fix-commit set is described as "answered without a new commit" when a fix round ran, and as "round one was clean" only when none did. web/scripts/orchestration-workflow-loop.test.mjs compiles the committed script bodies with stubbed agent/parallel and pins eleven paths per script (22 tests). Measured: reverting the loop condition to blocking-only reddens six cases per script (every case that needs a should-fix round to reach the fixer); deleting any of the three zero-lens guards, the fixer guard or its done half, or the empty-fix sentence branch reddens its own case, in both scripts. web/vite.config.ts is untouched: it is pinned whole by test_image_build_delegates_the_spa_suite.py, comments included. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
350509f8bb |
docs(554): the opt-in's unreachability is about started, not about mount order
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 22s
PR Gates / decisions lifecycle (pull_request) Successful in 26s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 14s
Review verdict / Set review-verdict status (pull_request_target) Successful in 35s
review-verdict/h10 Review-verdict: MERGEABLE @ 350509f (base: main)
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 16m49s
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 33s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m20s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m1s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 45s
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 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
"its button renders only before the player has ever mounted" is loose: a channel switch to a forced channel re-renders the button after a player has mounted for the previous channel. The load-bearing fact is the one the code comment states — the button renders only while `started` is false, and the only thing that un-starts the panel is the channel reset that clears the flag in the same batch. Refs #554 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
f119bfcb29 |
docs(554): the opt-in is a third path back to starting, and the invariant must say so
The hint's single-guard invariant was stated as "every path back to `starting` clears the flag itself", enumerating Retry and a channel change. `onOptIn` is a third such path and clears nothing, so the sentence was false as written — in the code comment, in the Retry test's comment, and in docs/spa-conventions.md §5b. Adding a clear to `onOptIn` would be dead code no test could distinguish, which is the exact shape this branch removed from `onPlaying`. The omission is correct for a reason none of the three places stated: the opt-in button renders only while `started` is false, `started` only goes false in the render-phase reset that clears the flag two lines later, and no player exists to set the flag while `started` is false. State that exception, and pin the reachability premise it rests on with a test that fails if the opt-in button outlives the mounted player. Refs #554 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
b6b4fb6619 |
test(554): pin both clears the hint's single guard depends on
Dropping the onPlaying clear made the `state === 'starting'` render guard the hint's only guard, which moves the burden onto the two paths back to `starting`: each has to clear `autoplayBlocked` itself. Both were assertable but unasserted — either `setAutoplayBlocked(false)` could be deleted with the whole panel suite green, so the invariant the code comment and docs/spa-conventions.md §5b both state was unpinned in both of its named paths. Add one test per path (Retry; a channel switch), each asserting the hint is gone while the panel is back at `starting` — so the render guard cannot be what hid it. Each also asserts the player really re-mounted (loadSource count / last URL, plus a non-null <video>), so the hint cannot be absent merely because the `resolvedSrc` block is unrendered. Measured on this tree, each mutation caught by exactly one test: deleting the onRetry clear reds only 'clicking Retry clears the autoplay-blocked hint' (Tests 1 failed | 25 passed); deleting the render-phase reset clear reds only 'clears the autoplay-blocked hint when switching to a different channel' (1 failed | 25 passed); replacing `autoplayBlocked && state === 'starting' &&` with `autoplayBlocked &&` reds one test too. Unmutated: 26 passed. Refs #554 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
4e042fb7de |
fix(554): the hint's cause must be measured, and muting is the consumer's call
Round one measured three defects in the first commit. onAutoplayBlocked fired on ANY rejected video.play(), so the panel could say "autoplay was blocked" when it was not. A play() interrupted by teardown rejects with AbortError — which is exactly what the panel's own Retry produces while the MANIFEST_PARSED play() is still pending — and because the element was muted, a genuine NotAllowedError is the rare case, so the realistic firings were the mislabelled ones. Report only a DOMException named NotAllowedError, on both the MSE and native paths. `muted` was applied to the shared player unconditionally, which silently muted the playback-troubleshooting screen — the tool whose job includes verifying the audio side of an FFmpeg profile, and which the legacy Blazor player never muted. Make it an opt-in `muted` prop defaulting to false; the channel preview passes it, troubleshooting does not, and a test on each side pins its own value. The two clauses hiding the hint once playback starts masked each other: removing either alone left the panel suite green. Every path back to 'starting' (Retry, a channel change) already clears the flag itself, so the clear in onPlaying could never be the load-bearing guard — drop it and let the `state === 'starting'` render guard be the single pinned one. Also cover the native-HLS (Safari) branch, which no test had ever executed: its play() kick, its playing/error wiring, and both autoplay rejection names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
ade14a7521 |
fix(911): a should-fix finding sends the round back, and the finisher reports the recorded review history
The fixer brief already said "fix every blocking and should-fix one"; the loop condition alone disagreed, so a merge-worded round with real defects skipped the fixer and the finisher attested to fixes it never saw (#554 / PR #910). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
02e1c583e9 |
fix(554): mute the preview player so autoplay is never blocked
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 6s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 21s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 24s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 13s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m24s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 17m7s
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 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
review-verdict/h10 Awaiting review verdict for 02e1c58
Review verdict / Set review-verdict status (pull_request_target) Successful in 33s
HlsPlayer's manifest GET can block until segments exist (unbounded maxTimeToFirstByteMs), so MANIFEST_PARSED can arrive past the browser's transient user-activation window and video.play() gets rejected as blocked autoplay — the channel preview panel then sat at "starting" over a black frame with no hint the operator just needed to press play. Render the <video> element muted (browsers permit autoplay of muted media without user activation) so the common case starts on its own, and add an optional onAutoplayBlocked callback for the residual case (stricter policy/extension) that the channel preview panel wires to a "press play" hint shown only while still starting. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
e9de5dcb1b |
fix(830): a write failure reports to a surface that outlives the dialog that started it (#878)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 17s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 13s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
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 4m54s
fixes #830 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
e1d211cd1b |
test(830): pin which ARM the call site reaches, not only that the hook has one
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 6s
PR Gates / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 10s
Review verdict / Set review-verdict status (pull_request_target) Successful in 22s
review-verdict/h10 Review-verdict: MERGEABLE @ e1d211c (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m34s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m5s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Review measured M6: swapping `reportFailure(...)` for `onAddFailed(...)` in `AddItemsDialog`'s catch left all 490 tests across the 42 `src/screens` files green. Under that mutant a failure that lands while the dialog is STILL OPEN renders into `ManualItemsView`'s screen banner, which sits behind the dialog's `createPortal` panel with `aria-modal="true"` — covered for sighted users, hidden from AT, and the surface the user is actually looking at stays blank. That is the exact shape the decision record calls "its own defect", and the whole gap was the call-site wiring: the hook's inline branch is pinned at unit level in `hooks.test.tsx`, but a unit test of the hook cannot see which arm a consumer reaches. Adds the integration assertion: fail the POST with the dialog still up, assert the message is inside `[role="dialog"]` and appears exactly once in the tree. Re-executed the mutation with it in place — 1 failed / 490 passed, and the red is this test alone. Records the new pin as mechanics (5) on `spa.dismissible-write-failure-reporting` and the general form in `spa-conventions.md` §3c, so the next site wired to the hook pins both arms at its call site rather than inheriting the hook's unit coverage. Refs #830 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
e4c346e649 |
test(830): pin the ORDINARY close too — the negative test did not cover it
Round 6 verdict was MERGEABLE with two follow-ups; both are one-liners on lines this branch just touched, so they are in rather than deferred. L11. I believed the suite covered "a normal add still closes the dialog". Review MEASURED that it did not: deleting the `onClose()` call entirely -- so a successful add leaves the picker open forever -- kept the whole suite green, 1277/1277. The new #830 test only pins the NEGATIVE direction (do not close when unmounted), so a future edit dropping the call, believing the guard had made it dead, would have shipped silently. The Song add test now asserts the dialog closes; with that line, the same deletion reddens. Both directions of the report/dismiss split are pinned. Worth naming the shape: I asserted coverage from plausibility rather than from a mutation, in the same PR whose whole subject is claims that were written down before they were measured. N12. The guards test's "exactly ONE post-await write to state THIS component owns" is still true, but it now reads as a census of `mountedRef` reads, and `submit` has two -- the success path's guarded `onClose()` is the other, which that failure-path test never reaches. Added the clause so nobody derives the guard population from that number. 1277 tests green, tsc/eslint/build clean, pytest 1228 passed, validator OK, catalog no drift. The two red CI contexts on the previous head are runner flakes, not this branch: both failed inside `Post Checkout` with `Cannot find module '/var/run/act/actions/<hash>/dist/index.js'`, their logs are timestamped 19:18 (before that head existed), this branch touches no CI or docker/ci file, and both contexts were green on its earlier heads. refs #830, #877 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY |
||
|
|
86e9ad41a6 |
fix(830): gate onClose at the exemplar — the convention contradicted its own reference
Round 5 found that §3c and the `rule:` field now instruct readers to gate the DISMISS request, while `AddItemsDialog` -- the one site this record names as "the shape fixed here" -- called `onClose()` unguarded, with a comment arguing that was correct. So a reader following the convention wrote the gate and a reader copying the reference implementation did not. The defect is pre-existing; the CONTRADICTION is mine, introduced when round 3 withdrew H1's code but kept the convention it produced. I checked the docs against the withdrawn addTo code and did not re-check them against the exemplar that stayed. Measured at this site: submit, Escape mid-request, reopen the picker to retry, first POST returns 204 -> the stale instance's `onClose()` (`() => setPickerOpen(false)`) closes the dialog the user just reopened, discarding the selection they rebuilt. Identical mechanism to the addTo clobber. Unlike the addTo layer, the one-line gate IS sufficient here, and that difference is the point: `AddItemsDialog`'s parent has no competing closer (`onAdded` is `load`, which never touches `pickerOpen`), whereas `AddToMenu.handleAdded` closes its dialog itself. That is now stated in the record as the concrete reason one half shipped and the other went to #877. - `onAdded()` stays unguarded -- it REPORTS, and the parent's list reload must survive dismissal - `onClose()` is guarded -- it REQUESTS A DISMISSAL, and after dismissal it aims at whatever the user opened next - comment rewritten to say which is which and why, instead of defending both as "belong to the still-mounted PARENT" Pinned, and nothing pinned it before: "a late SUCCESS does not close the dialog the user reopened after dismissing (#830)". It carries an anti-vacuity check that the late response was actually processed -- `onAdded` is `load`, so a second GET of the items endpoint must have happened -- because otherwise "the dialog is still open" holds trivially. Executed: deleting the `if (mountedRef.current)` around `onClose()` reddens it alone. Also rewrapped five record body lines left ragged by earlier splices. 1277 tests green, tsc/eslint/build clean, pytest 1228 passed, validator OK, catalog no drift. refs #830, #877 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY |
||
|
|
4b12125f4d |
docs(830): drop a coverage claim this record's own mechanics field contradicts
Round 4 found one clause left, and it is a good example of the thing this record is about. The one-consumer paragraph said `hooks.test.tsx` "is the ONLY thing pinning the diverted branch". Measured at the previous head, disarming `reportRef.current(message)` reddens THREE tests -- both hooks.test.tsx divert tests AND the CollectionsScreen integration test -- which is exactly what the `mechanics:` field of the same record says 58 lines earlier. So the record asserted a coverage fact and then contradicted itself. The concrete harm is not the inconsistency: a future session pruning tests reads "hooks.test.tsx is the only pin", concludes the CollectionsScreen #830 test is redundant, and deletes the only end-to-end pin of the whole path -- the one that actually drives Escape-dismissal through the real dialog. Clause dropped; the argument the paragraph needed (the hook's shape earns its own unit tests) survives without it. The clause originated in the reviewer's round-3 wording and I transcribed it without checking it against a field I had written myself two rounds earlier. Worth recording: a review finding is not exempt from verification just because it came from the reviewer. Also: - the `AddToMenu` clobber sentence now splits what was MEASURED (a late success closes a reopened dialog) from what was READ (both parents call `clearSelection()` unconditionally, so the wipe follows). On a record whose subject is over-attributing measurements, that distinction has to hold in its own prose. - §3c now carries the same "nothing diverts to those screens today" disclaimer the record's limit (2) has, so the two artifacts say the same thing - rewrapped one 141-char comment line left ragged by the previous round's splice Docs only, plus one comment rewrap. 1276 tests green, tsc/eslint/build/validator/catalog clean. refs #830, #877 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY |
||
|
|
626cbfbf9d |
docs(830): three sentences survived the revert of the code they described
Round 3 verified the withdrawal itself is clean -- the eight reverted files are byte-identical to origin/main, no orphans, and the mutation gives the stated three reds -- but found docs still asserting the withdrawn change shipped. That is the stale-comment failure in its usual form: after a retraction, the retracted WORDING has to be swept, not just the code. - `hooks.ts` said "#830 removed that gate", flatly false at this head, in the hook's own doc comment right above the export -- the first thing a maintainer reads. It also carried round 2's framing ("both halves of the outcome") as the hook's purpose, when what ships carries only the failure half. Rewritten to the present tense of the shipped tree. - The `rule:` field still said the surviving surface "differs per screen", naming MediaBrowseScreen and SearchScreen as wired. They wire nothing. This one matters beyond an ordinary sentence: `rule:` is the canonical summary, it is what the catalog row shows, and it is what gets mirrored per-key into MemPalace -- so it is the version a future session retrieves WITHOUT opening the file. Now: exactly one wired screen, the Toast pair named as a CANDIDATE. - The "two limits" bullet described a failure being diverted to those same screens and announced politely. Nothing can divert there -- they receive no reporting callback. Restated as the limit the second surface will have when it is wired. - `onFailed` in a hooks.ts comment was a dangling identifier; the real prop is `onAddFailed`. Also added the caveat the reviewer asked for rather than leaving it to be discovered: this is a shared hook with exactly ONE consumer. It earns that shape (directly unit-tested, and those tests are the only thing pinning the diverted branch; prescribed by §3c; #877 queued as a second consumer) -- but #877 may land a shared reporting SURFACE instead of a per-site prop, in which case the second consumer never arrives. Accepted risk, now written down. Docs only. No code change, 1276 tests still green, tsc/eslint/validator/catalog clean. refs #830, #877 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY |
||
|
|
de9432ff49 |
fix(830): withdraw the media/addTo extension — two rounds, two defects, one coupled gate
Round 2 of adversarial review found that my round-1 fix introduced an adjacent defect, and it is the same mechanism both times: `onAdded?.()` and `onClose()` sat behind ONE unmount gate in the four `media/addTo/` dialogs, and those two callbacks do not mean the same thing. - Gate both (origin/main): a write that SUCCEEDS after dismissal reports nothing. Measured on `AddToCollectionDialog` -- `onAdded` called 0 times after dismissal. That was round 1's finding. - Un-gate both (my round-1 fix): a late success closes a dialog the user REOPENED to retry, and on SearchScreen/MediaBrowseScreen `clearSelection()` wipes a multi-select they rebuilt. Measured against the real `AddToMenu`. That was round 2's finding, and I introduced it. - Gate only `onClose`: still wrong on its own, because `AddToMenu.handleAdded` nulls the dialog itself. Needs three coupled edits across five files -- plus a genuine product question nobody has answered: should `clearSelection()` fire for a write the user walked away from? That is a design change, not a bug fix, and #830 never asked for it -- the issue is about `AddItemsDialog`. Two defects from one mechanism in two rounds is the signal to stop widening, so the `media/addTo/` extension is REVERTED here and moves to #877 with every measurement attached (#877 comment). What ships is the thing the issue asked for, proved: - `useDismissSafeError` + `AddItemsDialog` + `CollectionsScreen` wiring - the witnessed red is unchanged: disarming `reportRef.current(message)` reddens the integration test on `Unable to find an element with the text: Request failed with status 500` Docs now describe what is actually true rather than what I hoped: - the record says ONE A1 site is fixed and explains why the other four were withdrawn, keeping the wrong first claim visible because "one site read, four assumed" is the lesson - §3c splits the rule the round-2 defect came from: report the OUTCOME unguarded, gate the DISMISS request separately -- the earlier text lumped `onClose` in with `onAdded` and would have propagated the clobber to the next screen that adopted it - §5c no longer tells authors to wire an `onFailed` that the addTo layer does not have; it says the layer has no failure channel at all and points at #877 - the reporting prop is REQUIRED where the host has a surface (`AddItemsDialog.onAddFailed`), which is what the docs now say instead of calling it optional - `mechanics:` no longer implies the shared clause reddens one test; it reddens three, so re-running the mutation should expect three refs #830, #877 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY |
||
|
|
f61764d051 |
fix(830): report SUCCESS past dismissal too — the review measured my claim false
Adversarial review (cold, worktree-isolated) blocked the first commit on its central prose claim,
correctly. I wrote "at every one of these sites SUCCESS already outlives dismissal" into three
durable artifacts -- the decision record, spa-conventions §3c and the hooks.ts header -- after
reading ONE site. `AddItemsDialog` does report success past dismissal and says so in a comment; I
generalised from it. The review probed the other four instead and MEASURED `onAdded` called 0 times
after dismissal: all four `media/addTo/` dialogs gated `onAdded?.()`/`onSaved?.()` behind their own
`activeRef`, exactly like the failure path.
So after the first commit those four were still asymmetric, just inverted: dismiss-then-fail loud,
dismiss-then-succeed silent -- and additionally leaving the caller's selection state stale, because
SearchScreen's `onAddedToSelectionTarget` never ran to clear it. The record's own advice ("add the
failure counterpart") followed literally would have reproduced it.
Fixes, each proved by execution:
- the `activeRef` gate above `onAdded?.()`/`onSaved?.()` is removed in all four dialogs; those two
statements belong to the still-mounted PARENT, which is the reasoning AddItemsDialog already had
- `AddToCollectionDialog.test.tsx` covers the media/addTo half in BOTH directions. It had NO
coverage before: reverting `reportFailure` to `setInlineError` in all four left the whole suite
green. Restoring the success gate reddens the SUCCESS test alone; disarming
`reportRef.current(message)` reddens the FAILURE test alone
- the three prose sites now say what was measured, and the record keeps the wrong first version
visible, because "one site read, four assumed, written down before measuring" is the finding
Also from the review:
- hooks.ts said "React 18"; package.json pins 19.2.7. Now "React 18+"
- the "nothing better to do" comment overclaimed: diversion reaches ONE level, so Back out of a
collection mid-add still drops the message. Stated, with where it would be fixed
- recorded two limits rather than leaving them to be rediscovered: useIsMountedRef clears in a
PASSIVE effect cleanup, leaving a narrow window where the message renders inline into a detached
tree (useLayoutEffect would close it, but that hook is shared by every async caller -- #877, not a
bug fix); and Toast is role="status" with one last-writer-wins slot, so it is not equivalent to
CollectionsScreen's role="alert"
- §5c now cross-links §3c, since that is the section a screen author reads before wiring AddToMenu
- the sweep count is 67 caller-owned + ConfirmDialog's own internal <Dialog>
Three other media/addTo dialogs remain unpinned; they are identical in shape to the covered one,
which is a reason to expect the same behaviour, not evidence of it. Said so in the record.
refs #830, #877
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY
|
||
|
|
9ba397e1dd |
fix(830): a write failure reports to a surface that outlives the dialog that started it
`AddItemsDialog.submit` POSTed to `/api/v1/collections/{id}/items` and reported failure into a
banner rendered from its OWN state. The dialog is dismissible mid-request through three paths that
never consult `adding` -- Escape and a backdrop click (both `useOverlayBehavior`) and the header
close button -- and the caller remounts it on `key={`add-${pickerOpen}`}`, so dismissal genuinely
unmounts it. Select 12 items, Add, press Escape, the request fails: nothing surfaces, the list
reloads unchanged, and the user believes 12 items were added.
drop deliberate rather than accidental. A deliberate drop is still a user who is told nothing.
The asymmetry is the finding: SUCCESS already outlived dismissal everywhere here, because it is
reported through a parent callback (`onAdded`/`onDone`, which the screens turn into a `Toast`).
Only failure died with the surface. So this is not a new notification system -- it routes failure
through the channel success already uses. `AddToMenu` had `onDone` and no counterpart at all.
`useDismissSafeError` (`web/src/hooks.ts`) renders the message INLINE while the surface is mounted
-- the better surface, since it keeps the user's selections and context -- and diverts to a
caller-supplied `onFailed` once it is gone. The surviving surface belongs to the parent and differs
per screen (a `role="alert"` banner on CollectionsScreen, `notice`+`Toast` on MediaBrowse/Search),
so it is a prop contract rather than a rendering decision. Gating dismissal on the busy flag was
considered and rejected: it traps the user behind an in-flight request with no cancel path, and
would not cancel the write anyway.
Applied to the A1 shape -- where the surface owns the error state and is really unmounted:
AddItemsDialog plus the four `web/src/media/addTo/` dialogs, whose failures previously could not
reach the screen Toast that already showed their successes.
Proofs, executed rather than described:
- deleting `reportRef.current(message)` alone reddens the new CollectionsScreen test on
`Unable to find an element with the text: Request failed with status 500`
- `hooks.test.tsx` pins both branches directly, plus that the report goes through the LATEST
callback rather than the one captured on first render
- `CollectionsScreen.guards.test.tsx`'s is-mounted read count moves 2 -> 1 because the catch's
guard migrated into the hook (its `...actual` module mock cannot see the hook's internal
`useIsMountedRef()`); removing the surviving `finally` guard takes it to 0 and reddens, so the
anti-masking property that count was added for is intact
Scope is stated rather than implied. A sweep of all 68 Dialog/ConfirmDialog/SlideOver call sites
found three shapes; only A1 is fixed here. A2 -- error state that survives but whose render site is
gated by the same condition dismissal clears, mostly delete-confirm flows -- is left open in #877
because its right answer is probably a shared surface, not twenty prop threads.
fixes #830
refs #877, #740, #685
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XE2tF2aUasK2hWPmBRsrMY
|
||
|
|
92db6dc81e |
feat(907): orchestrated sessions — mechanics page, standing prompt, pick/build/resume workflows (#908)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 13s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 22s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 32s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 12s
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 4m23s
fixes #907 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
208ae00953 |
fix(907): round four — a resume that updated its existing PR is a success, and a post-push error says so
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 41s
review-verdict/h10 Review-verdict: MERGEABLE @ 208ae00 (base: main)
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 50s
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m35s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 33s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 19s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m33s
PR Gates / decisions lifecycle (pull_request) Successful in 13s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
181847f70a |
fix(907): round three — the gate is a function of port and worktree, the port guard demands an integer
Embedding the slot's gate text in the reviewer brief carried the slot worktree into the one prompt that forbids it, and a substring port substitution could rewrite a path containing the same digits; gateFor(port, where) renders each brief for its own tree and port. The port guard accepted "", null and false through Number(); it now requires a JS integer in (1024, 65000). The finisher schema requires only patch_changed, and a done report without a PR URL or head sha is an error rather than a placeholder. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
a0203f2863 |
fix(907): round two — a guard denial means a foreign worktree, required keys under every branch, a gated post-rebase round
The marker-overwrite instruction asserted an antecedent no agent can verify and, since same-session worktrees carry no marker, could only fire in another session's worktree; the scripts now stop and report. `ran` and `patch_changed` move into required keys of their own schemas so a missing field cannot read as a successful cross-family review or an unchanged patch. A blocking finding in the post-rebase round now returns an error like every other failure path. Each reviewer lens gets its own E2E port; the gate text travels with the reviewer brief. The standing prompt no longer contradicts the substitution the scripts perform; the record names patch-id, the mechanism the scripts use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
63256afd96 |
fix(907): answer the cold review — drop the E2E lock, scope force-with-lease in a record, referee ticks the boxes
The mkdir lock around scripts/e2e-local.sh serialised the launch, not the run (the launcher returns with the server up), and its stale-holder path double- acquired in 4 of 91 measured races; the launcher's documented conflict is its per-worktree wwwroot, so slots now run on their own port and the lock is gone with its inventory row. process.orchestrated-session records the two scopings the harness needed: a rebase pushed with --force-with-lease as the one sanctioned rewrite, and the referee as the only agent that ticks Done-when boxes. Scripts: required-arg guard, per-issue claim probe, reviewer fetch recipe, codex fallback to a cold review-only agent with the substitution stated in the PR body, rebase before the review loop with a patch-id check at the push, non-interactive squash recipe, Land phase. README bullets re-parented; kickoff bullet keyed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
a380129700 |
feat(907): orchestrated sessions — mechanics page, standing prompt, pick/build/resume workflows, E2E gate
The single-issue kickoff stays as it is; this adds the layer that runs several of those under one referee. docs/handoffs/orchestration.md owns roles and sizing, one worktree per issue under ~/orca/workspaces/ersatztv/, the landing order with the review loop inside the worktree before the single push, and the merge through the consent hook. Three Workflow scripts encode it: a picker over scripts/select-queue.sh with two refuters, an issue-build pipeline (claim, recon, implement, gate, cold review with a cross-family runner for the rubric's risk classes, fix loop, finisher), and a resume pipeline for a paused branch. scripts/e2e-gate.sh serialises live-E2E across worktrees because e2e-local.sh refuses concurrent runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV |
||
|
|
efadbec297 |
fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus (#906)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
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 5m10s
|
||
|
|
a7d91bf15a |
fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus
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 |
||
|
|
fb55929711 |
fix(869,893): re-establish the 1.25.4-dated CI claims on 1.27.1, and settle the page_statuses asymmetry from source (#905)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m24s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m10s
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 4m17s
Finishes the 1.25.4-dated CI claim sweep #747 deliberately left incomplete (#869), and answers #893 from the Gitea v1.27.1 source instead of inferring it from a header. Docs and comments only - zero non-comment changes in scripts/ and .gitea/. Population derived with `git ls-files`, not from the issue's item list: 17 files, 43 occurrences of `1.25.4`, against the 4 items #869 named. Re-established on 1.27.1: the `creator`-attribution claim the H10 allow-list rests on (4 merged heads, both endpoints); the scope enum (no `status` scope); the `reqRepoWriter(unit.TypeCode)` gate; the `write:package` 403 (live probe with a read control 200 and a write control 201, throwaway repo, artifacts deleted); the absence of any REST cancel route (from source, which a 404 alone cannot establish); and `pull_request`/`pull_request_target` definition resolution. #893: `/statuses/{sha}` does NOT drop rows after pagination. `getCommitStatuses` appends unconditionally and its only filter is a SQL WHERE in the same query as the LIMIT/OFFSET, so an empty page really is the end, `page_statuses` terminating on its first empty page is safe, and the asymmetry with `count_pr_mutations` is correct - recorded with its reason and a date so it is not tidied away. Corrected rather than re-dated: the `--depth=1` no-merge-base claim was filed against the wrong axis (a git property, re-probed on git 2.55.0), and `enable_bypass_allowlist` postdating 1.25.4 had an issue body as its only provenance. Five cold review rounds plus a cross-family Codex pass. They caught a wrong MECHANISM for `creator: null` (it is `CreatorID == -2`, not `== 0`), an evidence count that straddled the upgrade, and a reason for not re-probing MCP `cancel_run` that was invented - all fixed, final verdict CLEAN. fixes #869 fixes #893 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Whe75djeAEuZpdNk6KU7No Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
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>
|
||
|
|
dd0f75f1b6 |
fix(855): two glob dialects cannot be canonicalised into one, so model one shape and refuse the rest (#902)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m46s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m4s
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 4m16s
`ci-image.yml`'s `on.push.paths` decides which pushes to `main` publish a toolchain image; `ci-image-pin`'s `git log` pathspec decides what the pin must name. #744 removed the shared self-reference that kept them in step, leaving the agreement carried by three prose comments, and divergence is silent and green in the dangerous direction. The guard derives both lists from the workflow documents and compares them for set equality in both directions. The comparison is deliberately narrow: it accepts a publish entry spelled exactly `<dir>/**` against a pathspec entry spelled exactly `<dir>`, segments restricted to `[A-Za-z0-9._-]`, and raises on every other spelling rather than deciding what that spelling would have selected. That narrowness is the substance. Measured against Gitea 1.27.1's own in-tree compiler (`modules/actions/workflowpattern` -> `modules/glob.CompileWorkflow`) and real git: a bare `docker/ci` in `paths:` compiles to an anchored `^docker/ci` and selects none of the directory's contents while the git pathspec `docker/ci` selects all of them; `<file>/**` matches nothing while the pathspec `<file>` tracks the file; a leading `/` is literal to Gitea while git refuses it outright. A canonicaliser mapping the two dialects onto one string form was built twice and defeated twice, each repair surfacing another spelling, so it was deleted rather than extended per `testing.verification-code-needs-its-own-proof`. The guard also asserts from the git index that each named path really is a directory, since `<file>/**` and the pathspec `<file>` spell the same string; takes the pathspec from the `git log` assignment rather than any `git log` in the job; refuses a `<<` token on a code line (a herestring excluded) and a second bare `--`; and treats an absent and an empty `paths:` alike, because Gitea's `Skip` returns false on an empty sequence, so `paths: []` filters nothing and every push publishes. The docstring states the boundaries rather than implying coverage: the guard compares the pathspec the pin job writes and does not establish that the staleness comparison consumes it, and a descendant whose path below `<dir>` contains a newline is matched by the git pathspec but not by the publish pattern. Verified by nine independent cold-review rounds, none of which found a false green; the last fuzzed 27,720 publish/pathspec pairs against a port of the deployed compiler and real `git ls-files`. fixes #855 |
||
|
|
4cd6929735 |
fix(887): the image build builds the SPA and does not test it (#899)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m14s
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 4m1s
`docker/Dockerfile`'s web-build stage ran the SPA vitest suite with two hand-written `--exclude`d spec paths. The stage is gitless twice over, and the suite has members needing a git checkout or the git binary, so that list was a population nothing derives. #883 added a third member without updating it; because `Build & push image (amd64)` is `if: github.event_name != 'pull_request'`, the red was unreachable on a PR and landed on `main` and the `v*` tag path. Every image build has failed since. The list is removed rather than extended: the stage builds the SPA and does not test it, and the suite runs once, unfiltered, in the `test` job that `build` already `needs:`. `scripts/tests/test_image_build_delegates_the_spa_suite.py` holds the invariant in three parts, because the first two together still certify a publish on which the suite never ran. It PINS command text rather than parsing it: three earlier versions asked what a command MEANS and were wrong nine times, and a partial match of `web/vite.config.ts` was then defeated seven more ways, so both mechanisms were withdrawn rather than respelled. The transferable rule, recorded in the guard and the decision record: a pin assumes it is pinning the artifact that still DECIDES. Every route found was authority moving where the pin was not looking — another file, another occurrence, another workflow, or a hook the pinned command invokes. Nine independent cold-review rounds, eight BLOCKED. 73-mutant development battery, 0 missed; one declared clause mutation harness-executed per suite. fixes #887 |
||
|
|
4b7ede80b8 |
Merge pull request 'docs(release): record the v26.15.0 release notes' (#898) from release/v26.15.0-notes into main
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 15s
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 8s
|
||
|
|
9da0020462 |
docs(887): sweep the withdrawal through the record — it still described the withdrawn pin
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 17s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 16s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 12s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
review-verdict/h10 Review-verdict: MERGEABLE @ 9da0020 (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m53s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m2s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
Round 9 returned MERGEABLE with BLOCKER and HIGH empty. Every remaining item was a sentence, and every one erred by UNDERSTATING the guard — which is the safe direction and still worth fixing, because the decision record is what CLAUDE.md routes convention lookups to. The record's `rule:` still listed "`web/vite.config.ts`'s `test:` block" among the pinned things — the very mechanism the previous commit withdrew — and named only `vitest.config.*` as the outranking family, omitting `vite.config.js`/`.mjs`, which is the MEASURED attack from round 7 (a `web/vite.config.js` ran the suite in the gitless stage with 1411 tests green). That family went short in round 7 and again in round 8. This is `enumerate-CLAUSES-to-close-a-sweep`: the survivors were phrased in a different category (WHAT is pinned) from the retracted claim (HOW it is extracted), so sweeping for the retracted words missed them. Also: "any edit to this file reddens, including a comment" was an absolute and is refutable — a reindent, added blank lines, tabs, and a form feed all stay green, because `_normalise_lines` collapses whitespace. Restated as what is actually true (a line's TOKEN sequence, a comment's words included) plus the reason the tolerance is currently inert: this file has no template literal and no ASI-sensitive token outside a comment. And a YAML single-quote escape had leaked from the frontmatter into the markdown BODY, where `''` renders literally. No code change; the guard is unchanged and still 73/0. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
4c44a13e3d |
docs(release): record the v26.15.0 release notes
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 14s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 23s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 12s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 9s
review-verdict/h10 Exempt: docs-only change (no code, no protected path)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Review verdict / Set review-verdict status (pull_request_target) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m1s
Adds the `v26.15.0` row to the release table in `docs/ci-cd.md`. The tag goes on `736649b3b`, NOT on this commit and not on `main`'s head. Every `Build & push image (amd64)` since `e8f80c42c` fails: that commit added `web/src/api/completeAnnotations.guard.test.ts`, a third importer of `virtual:etv-tracked-source-files`, without adding it to the hand-maintained `--exclude` list in the Dockerfile's `web-build` stage — and that stage has no git index, by construction (#887, claimed and in progress elsewhere). The guard is behaving correctly; it refuses to fall back to a filesystem walk. Measured: run 2459 on `736649b3b` ran the image job for 6m45s and published; run 2515 on `cf5f42edf` died in web-build after 86s. `736649b3b` is therefore the newest commit on `main` that can produce a release image. Consequence recorded in the row itself: #880 (scheduling recurrence) slips to the next release, since it merged after the break. Release-boundary sweep (docs/ci-cd.md -> "Before cutting a release"): - `decisions_validate.py` -> OK; 0 legacy-unmigrated records remain - `build_decisions_catalog.py` -> no drift - record ceiling: 45/218 over 60 lines (fraction 0.21, inside the blocking 0.02-0.25 band). The validator notes the 60 has drifted below the tail boundary (p90=104, p95=142) and asks for re-derivation when convenient — a maintenance signal about the constant, not a blocker for this cut. |
||
|
|
1506e5a545 |
docs(887): 'pinned whole' is not byte equality — say what it cannot see
`_normalise_lines` drops blank lines and collapses whitespace WITHIN a line, so a reflow, an indentation change, and a change to the spacing inside a STRING LITERAL are invisible. The first two carry no meaning; the third could, and does not here. Line order and any token change are caught. All four measured. Stated because the phrase 'pinned whole' invites a reader to assume byte equality, and a reader who assumes that will not check the one case where it matters. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
58def5faa1 |
fix(887): withdraw partial matching too — web/vite.config.ts is pinned WHOLE
Round 8. Three consecutive rounds had each closed one SPELLING of the same marker match,
which is `every-blocker-was-one-mechanism-so-delete-it` at the count where it says
withdraw. Round 6 pinned a block; round 7 fixed `test: {` (two spaces); round 8 defeated
the repaired matcher four more ways — `test : {`, `"test": {`, and the same two for
`plugins:` — plus two that never touched the marker at all:
plugins: [react(), trackedSourceFilesPlugin()].concat([evil])
test: { …pinned… }, ...moreTest
`defineConfig` is the identity function in BOTH vite and vitest (read from the installed
tree), so a spread AFTER the pinned span simply replaces what the pin matched. No
respelling of the marker could ever have caught those: the defect was partial matching,
not the pattern.
So the file is pinned whole. 48 lines, nothing generates it, no marker to respell and
nothing after the span. One assertion replaces a bracket walk, a block extractor and two
uniqueness assertions — and catches all seven measured routes. Stated cost, which is the
same one every other pin here carries: any edit to that file reddens, a comment included.
This also retires a claim I made in a commit message AND in the inventory row: that the
two pins "share one bracket walk and cannot drift apart again". It was false when
written — the block extractor had its own inline copy and never called the shared helper.
Verified by spying on the call: the `test:` pin made zero calls to it.
Also closed: the second exec-form branch in `copies_spa_source` was DEAD code (measured by
poisoning it with a `raise` — every test stayed green), since the round-7 reorder rewrites
`remainder` before it; the exec-form `--from` case that reorder was written for had no
STANDING test, only the development battery, and now has three; and
`OUTRANKING_CONFIG_PREFIXES` matched nested paths, so a `web/e2e/vite.config.ts` — and
`web/e2e/` is a real directory — would have reddened with a message asserting it outranks
the pinned config, which is false. Direct children of `web/` only.
Battery 68 -> 73, 0 missed.
Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
|
||
|
|
69b30d15ec |
docs(887): the shared bracket walk fails RED on a bracket in a string — measured
It counts brackets without understanding string literals, so a `]` inside one ends the span early. The direction is what matters and it was measured: the truncated span does not equal the pin, so the outcome is a false RED, never a false green. Stated rather than fixed — parsing TypeScript to do better is exactly the predicate this file withdrew. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
8141ac7807 |
fix(887): the rule applied to one config family and not its sibling
Round 7. BLOCKER empty; three real holes, two of them introduced by round six's own
fixes, which is this repo's recorded `each-fix-round-introduces-an-adjacent-defect`
happening inside a single commit.
**`web/vite.config.js` outranks the pinned `.ts` for `vite build`.** Read from the pinned
tarball rather than remembered: vite 8.1.3's `DEFAULT_CONFIG_FILES` is
`["vite.config.js", "vite.config.mjs", "vite.config.ts", …]`. So a `vite.config.js` whose
plugin shells out to the suite runs it in the gitless stage — measured with all 1411
tests green. The governing rule was already written down one family over ("pinning one
config is worthless while a second can outrank it") and had been applied to
`vitest.config.*` and not to this one. The refusal now covers both families.
**The decoy fix counted one SPELLING of the marker.** `text.count("test: {")` is defeated
by writing the real block `test: {` with two spaces beside a decoy that matches exactly:
count is 1, the comparison takes the decoy, and the live block filters out precisely the
specs #883 broke on. Now `re.finditer(r"\btest:\s*\{")`.
**The plugin pin added in that same commit shipped the identical decoy hole** it was
written next door to fix — a raw `text.count(PINNED_VITE_PLUGINS) == 1` with a decoy
above `defineConfig`. Both `vite.config.ts` pins now share ONE bracket walk and ONE
whitespace-tolerant uniqueness rule, so they cannot drift apart again.
PROSE, and this one is a false completion claim in my own previous commit message: I said
the `PUBLISH_ACTION`/anti-vacuity sentence and the singular "only an `ENV`" residual were
corrected. They were — in the record and the inventory row, and NOT in the guard
docstring, which is the artifact a code reader hits first. Both are now fixed there too,
the route COUNT is removed from the docstring and the record and kept in ONE place, and
the residual that stated its own false version before retracting it now states the
boundary once.
Also: the `--from=` branch never reached the JSON exec-form parser, so
`COPY --from=web-build ["/source/web", "/dest"]` left the receiving stage unpinned; the
revalidate arm of the gating `if:` is now described as a DEPENDENCY on
`ci-detect-already-validated.sh` (graded `MUTATION: NONE`) rather than as something
asserted here, since only the `docs_only` arm is; and the plugin-bodies residual now says
there are TWO plugins, `react()`'s being third-party and unmitigated.
Battery 64 -> 68, 0 missed. One of those four exists because the battery itself briefly
reported NOTHING and exited 0 after a bad splice deleted its `main()` — it now carries an
anti-vacuity assert on its own mutant count.
Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
|
||
|
|
06b8081664 |
docs(887): two probed non-routes, recorded so they are not re-derived
Hunting a fifth instance of "the pin assumes it is pinning the thing that still decides"
turned up two candidates that look like routes and are not, both measured rather than
argued:
* `setupFiles` is pinned by NAME while its CONTENT is not, which reads like the
package.json hole one level down. It is fail-NOISY: `process.exit(0)` at the top of
`src/setupTests.ts` makes vitest report `121 failed (121)`, not a green.
* `tsconfig*.json` shapes what `tsc -b` compiles, not what vitest collects.
Recorded because a reader who spots either will otherwise spend the same probe to reach
the same answer — and because the honest residual beside them is the one that IS open: a
dependency's own install script, reached through `npm ci` and `web/package-lock.json`.
That is a supply-chain concern wider than this guard, and it is named rather than claimed
covered.
Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
|
||
|
|
3ec73f3769 |
fix(887): a pin assumes it is pinning the thing that still DECIDES
Round 6 found three more false greens and named the class they share, which is worth
more than any of the three fixes:
* `web/vitest.config.ts` OUTRANKS the pinned `vite.config.ts` — closed in the previous
commit, found by probing vitest rather than reading about it.
* A DECOY first `test: {` block. The comparison took `text.index("test: {")`, so a copy
of the pin placed above `defineConfig` satisfied it while the real block was narrowed.
Exactly one is now required — the same assertion this file already made about the
gating step's NAME, for the same reason, not carried across.
* A `needs:` edge matched by bare job id. `needs:` resolves within its own workflow, so
a SECOND workflow publishing this Dockerfile while needing its own unrelated job
called `test` satisfied it. Now bound to `GATING_WORKFLOW`. (The reviewer downgraded
this to MEDIUM on measuring that `test_remote_state_inventory.py` forces a human to
classify any new workflow — so the hole is "the guard is blind", not "silent". The
forced review asks about remote state, not about whether the image is gated, so the
one-line fix stands.)
* A vite PLUGIN can shell out to the suite from `buildStart()`. The plugin ARRAY is
pinned; the plugin BODIES are a stated residual, mitigated because
`trackedSourceFilesPlugin` is deliberately lazy — a fact its own comment now marks as
LOAD-BEARING for the image build rather than leaving as an optimisation note.
THE CLASS: **a pin assumes it is pinning the artifact that still decides.** Every route
found so far is authority moving where the pin is not looking — to another FILE, another
OCCURRENCE in the same file, another WORKFLOW, or a HOOK the pinned command invokes. That
question is now written down for the next person adding a pin, because a list of four
instances is not what generalises.
Prose, all refuted by execution: the residual naming the uncovered COPY shapes was wrong a
THIRD time at the same site (`/source/web /elsewhere` IS recognised — only the destination
is renamed — and the file's own test 700 lines below said so); "only an `ENV` is
unmodelled" was an absolute and is now a list; "Reach: N mutants, 0 missed" is restated as
a DEVELOPMENT BATTERY, since it is not in the repo, nothing re-derives it, and an
independent battery found misses against an earlier head; and `PUBLISH_ACTION` was claimed
covered by anti-vacuity, which proves the selector is non-empty and cannot prove it
complete.
Battery 61 -> 64, 0 missed.
Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
|
||
|
|
848216e277 |
fix(887): a second config file outranks the pinned one — found by probing vitest itself
Pinning `web/vite.config.ts`'s `test:` block is worthless while a file that takes precedence over it can simply be added. Vitest resolves `vitest.config.*` (and `vitest.workspace.*` / `vitest.projects.*`) BEFORE `vite.config.*`. MEASURED, not read: dropping a `web/vitest.config.ts` carrying `include: ['nope/**'], passWithNoTests: true` beside the pinned file made `npx vitest run` report "No test files found, exiting with code 0". The gating step would be green having run NOTHING — worse than the filtered run ersatztv#887 removed, because a filtered suite at least reports on what it ran. The construct is refused rather than modelled: no such file exists, so the guard asserts none appears. Its population is the git INDEX, which is right and worth stating — an untracked config does not exist in a CI checkout either, so the mutant proving this has to STAGE the file. It failed to redden until it did, which is the correct behaviour demonstrating itself. Route count five -> six -> seven -> eight, wrong at every previous count, so it stays a running total with its history attached. Battery 60 -> 61, 0 missed. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
9b99a23835 |
fix(887): a selector where a pin was available — the blocker round 5 measured
Round 5 found a BLOCKER, and it is the sharpest kind: I made the exact mistake I had described one screen earlier. `test_only_the_PINNED_npm_SCRIPTS_run_vitest` SELECTED the scripts to pin by asking whether their body contained the literal `vitest` — a selector, the category this file calls the worst-behaved because going short is silent — and then its docstring claimed "going short is caught by the equality below", which is false: a script the substring misses is absent from the compared map, so the equality still holds. Four one-line `web/package.json` edits, none of which spells `vitest`, each put the suite back into the gitless stage with the whole guard green: `"build": "npm run test -- --run && …"`, the same via `npm t`, and the `prebuild` / `preinstall` LIFECYCLE HOOKS, which npm runs for `npm run build` and `npm ci` without anything naming them. That is #883 verbatim, through the route round 4 identified and the previous commit reported closed. The fix is the one the file's own vocabulary prescribes: pin the WHOLE script map. A script that does not exist cannot be a lifecycle hook, and one that changes is not equal. The category disappears rather than being widened by two entries. ALSO CLOSED, all measured: * `web/vite.config.ts`'s `test:` block is now pinned. `npm test -- --run` collects what that file says, so `test.exclude` is where a filter would now naturally be written — it is the only place left after this change removed the Dockerfile's. Three mutants narrowed the gating suite through it with the step's own command unchanged. * A step-level `shell:` and a job-level `defaults:` each override the pinned workflow default. Both forbidden. * `test_no_run_BODY_builds_or_pushes_an_image` is RESTORED — I dropped it in the parser withdrawal, and a job publishing via `run: docker build … && docker push …` was then outside the action-derived population with anti-vacuity none the wiser. * A leading-slash context copy (`COPY /web/. ./web/`) was not recognised. * The sweep gains `yarn test`, `pnpm test`, `bun test`. The residual naming the uncovered COPY shapes was wrong for the SECOND consecutive round — it named `COPY --from=X /source/web /elsewhere`, which is covered (only the destination is renamed). The real gaps are an ANCESTOR source (`/source` brings `/source/web` along) and `/source/.`. Both measured. Route count: five, then six, now seven. It has been wrong at every count, so it is now stated as a running total with that history attached rather than as an enumeration. Battery 51 -> 60, 0 missed. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
3a88f5dc3d |
fix(887): six meaning-change routes, not five — counted by checking rather than asserting
The previous commit said "five such routes" in three places. Checking rather than restating found six, and the sixth is one this guard must NOT close itself: the gating job runs in a `container:`, whose image decides which `npm` exists at all. That is already pinned by `test_ci_image_pin_population.py`, so it is CITED — two guards on one condition mask each other (ersatztv#685), and the way to find that out is to delete one and look for a red, which nobody does. Also measured rather than assumed: an INDIRECT script chain (`"test": "npm run inner"` with `inner` running vitest) needs no clause of its own. The set-equality against `PINNED_VITEST_SCRIPTS` reddens on it, because `inner` mentions vitest and `test` no longer does — verified across four scenarios, three red and one green. An enumeration is a claim like any other. This one was written from memory of what had been fixed rather than from the code, and it was short by one. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |