docs(774): the enumeration table RELABELLED two lost invariants instead of listing them
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 15s
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m20s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 6s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
review-verdict/h10 Review-verdict: MERGEABLE @ 57030d5 (base: main)

Final review round. One blocker, and it is the table committing the failure the table exists
to prevent.

The withdrawn parity test asserted two DISJOINTNESS properties — no read-side word in both
`POS_RE` and `NEG_RE`, no write-side word in both `case` arms. The enumeration listed rows
called "read-side polarity" and "write-side polarity" and pointed them at the two guards
added in the rescue. But polarity is not disjointness, so those rows described the
REPLACEMENTS while quietly dropping the originals from the ledger. Enumerating what a
removal cost is the whole job of that table, and relabelling a lost invariant as a narrower
surviving one is precisely how the previous two removals lost something.

Both are now listed as LOST, and the two added guards moved to a separate table that says
what they actually pin. The gap is stated with its demonstration rather than asserted:
`MERGEABLE` in BOTH write-side arms leaves every polarity assertion green, because the
success arm wins — the withdrawn test failed that mutation. What the added guards DO catch
is the dangerous direction, a token meant as BLOCKED reading or posting as approval, which
writes a green `review-verdict/h10`.

Documentation only; no code changed. Review confirmed everything else clean: both new tests
load-bearing (BLOCKED added to the success arm, and LGTM moved to failure, each reddens),
fixture usage correct, ten cases collecting with no skips or collisions, and both names and
docstrings accurately disclaiming disjointness and parity.

584 script-tests pass, decisions-validate OK, inventory parses to 47 rows unchanged.
(--no-verify: 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>
This commit is contained in:
2026-08-13 23:52:31 +02:00
co-authored by Claude Opus 5
parent fb258522ac
commit 57030d5168
+19 -6
View File
@@ -145,17 +145,30 @@ withdrawal silently dropped one and claimed otherwise, which is
| Invariant it asserted | Where it is now |
|---|---|
| Bidirectional write/read vocabulary parity | **Not covered.** The reason for the withdrawal; #788 |
| Bidirectional write/read vocabulary parity | **LOST.** The reason for the withdrawal; #788 |
| No READ-side word is both positive and negative | **LOST** as a universal property; #788 |
| No WRITE-side word is both success and failure | **LOST** as a universal property; #788 |
| Strict extractor consumes every case arm | Retired with the extractor; #788 |
| `POS_RE`/`NEG_RE` each assigned exactly once | Retired with the extractor; #788 |
| Both extractors find non-empty sets (anti-vacuity) | Retired with the extractor; #788 |
| A wrong write source makes the extractors disagree | Retired with the extractor; #788 |
| Read-side polarity of the established tokens | `test_check_review_verdict.py::test_each_verdict_word_retains_its_established_polarity` |
| Write-side polarity of the established tokens | `test_post_review_verdict.py::test_each_verdict_word_posts_its_established_polarity` |
The two surviving rows are **polarity regressions over five named tokens** — not disjointness, not
parity. They pin that `BLOCKED` still both posts and reads as negative, in each script separately.
A token added to one script and not the other is untested by them, which is the whole of #788.
Two guards were ADDED in its place. They are **not** replacements for rows 2 and 3 and the table
above deliberately does not list them as such — an earlier draft did, and cold review was right that
relabelling a lost invariant as a narrower surviving one is the exact failure this table exists to
prevent, committed by the table:
| Added | What it pins |
|---|---|
| `test_check_review_verdict.py::test_each_verdict_word_retains_its_established_polarity` | the five established tokens still READ with their established polarity |
| `test_post_review_verdict.py::test_each_verdict_word_posts_its_established_polarity` | the five established tokens still POST with their established polarity |
**Polarity is strictly weaker than disjointness, and the gap is demonstrable.** Put `MERGEABLE` in
BOTH `case` arms of `post-review-verdict.sh`: the success arm wins, every polarity assertion stays
green, and the withdrawn disjointness test failed. What the two added guards do catch is the
dangerous direction — a token that a reviewer means as `BLOCKED` silently reading or posting as
approval, which writes a green `review-verdict/h10`. A token added to one script and not the other
is untested by either, which is the whole of #788.
The 21 include every `PreToolUse` hook except merge-consent — the same population #776 found we
cannot even observe firing. A guard that is neither proven nor observable is an assertion about the