Mutation proofs for the 19 unproven guards, worst-consequence first #785
Closed
opened 2026-08-13 21:59:10 +02:00 by timothy
·
4 comments
No Branch/Tag Specified
main
901-pin-the-artifact-whole
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Defect-shape hardening
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#785
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Spawned by #775's audit.
docs/guard-inventory.mdmeasures 28 guards: 6 mutation-proved, 3 behaviour-only, 19 with no test at all. The inventory now makes the gap visible and machine-checked; it does not close it.Ranked by what a silent failure would let through, not by test count:
.claude/hooks/pretooluse-bom-guard.sh— guards a defect that has recurred three times (#311, #402, #405)..claude/hooks/pretooluse-worktree-guard.sh+.claude/hooks/posttooluse-worktree-marker.sh— a two-file mechanism guarding #289. A regression in either half is invisible, and the halves have never been tested together. (This one bit us during #774 itself: we routed a review agent around the live worktree specifically because this guard is unproven.).husky/pre-push:11'sunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE— a one-line fix for a real bug (a nestedgit diff --exit-codesilently reporting no diff). Reordering it after the nested git calls reintroduces the bug silently.scripts/build_decisions_catalog.py—--checkis what CI runs and no test callsmain(); the tests exerciserender_catalog()directly.Do these as separate commits, not one sweep. A batch of mutation proofs written to clear rows is exactly the test that passes for the wrong reason — and each proof must be demonstrated red before it is believed (
testing.guard-ships-with-mutation-proof).Done-when
docs/guard-inventory.mdrows updated to MUTATION with real refs🔗 Related: #796 — five false greens measured in one PR's own verification code (#793/#777), each introduced by the fix for the previous one. Two points bear directly on this issue: (a) the mutation-proof rule applies to harness/wrapper code that is not in the guard population —
scripts/mcp_smoke.pyis a checker wired to no CI and outsidedocs/guard-inventory.mdby design; (b)perl -e 'alarm shift; exec @ARGV'exits 0 when exec fails, so a timeout wrapper can report a clean pass having run nothing. Same family as #776's shellcheck-caught-0 finding.Claiming — Claude Code session, 2026-08-21. Checked: no open PR references #785, no remote branch names it, no prior claiming comment, fresh
git fetch origin mainatad0bed458.Done
What was done: Independent adversarial review of
ad0bed458..7bd7b4c8againsttesting.guard-ships-with-mutation-proofandtesting.guard-derives-population-from-source.Root cause: Two coverage claims still substitute source-text presence for active wiring / actual protected clauses:
scripts/tests/test_build_catalog_check_path.py:147-153:_ci_check_command()accepts a commented-out# run: PYTHONPATH=. python3 scripts/build_decisions_catalog.py --checkline (and does not bind it to thedecisions-guardjob). The subprocess test therefore stays green after CI stops running the guard.docs/guard-inventory.md:258-262: the new NONE rationale calls both Husky files thin dispatchers and saystest_every_hook_file_is_actually_WIREDcovers their dispatch. In fact.husky/commit-msgimplements itsgrepguard inline,.husky/pre-commitimplements root-PNG and format guards inline, and the named inventory test only checks.claude/hooks/*.shbasenames.scripts/tests/test_worktree_ownership_guard.py:23-24says the three mutations cover every clause the pair hangs on, but removing themergealternative from the command-detection clause leaves this file green because every mutating payload usescommit. The cited marker-read/write grades remain valid; the prose is broader than the proof.Files changed: None; review was read-only.
Verification:
git diff --check ad0bed458..HEADpassed; all 35 focused tests collect; inventory functions independently report 57 derived/listed rows and the documented 34 GUARD / 5 TOOLING / 18 PROOF, with 12 MUTATION / 6 BEHAVIOUR-ONLY / 16 NONE; current catalog--checkpasses; an in-memory stale/live comparison returned 1 and the comparison mutant returned 0. Full pytest execution was not possible in this read-only session because pytest's autouse fixture requires a writable temporary directory; both attempts stopped before test bodies withNo usable temporary directory.Deferred: Correct the two MEDIUM findings and rerun the focused pytest files on a writable Linux runner.
Follow-up issues: None created.
Docs updated: None; the false inventory rationale is itself a blocking finding.
Reviewer verdict: BLOCKED. Issue left open; no commit, push, or closure performed.
Closing record
Outcome: Shipped in PR #810, merged as
706674272. The three still-unproven guards from #785's ranked list now carryclause-level mutation proofs, each witnessed red against the real subject in place (not only against
the isolated copies the tests build): the
pretooluse-worktree-guard.sh+posttooluse-worktree-marker.shPAIR (4 clauses, including the cross-file seam),
.husky/pre-push:11'sunset GIT_DIR ...(deletionand relocation), and
scripts/build_decisions_catalog.py --check(the stale comparison and the__main__wiring). Item 1 of the list, the BOM guard, was already proven before this session.docs/guard-inventory.mdregraded: 12 MUTATION / 6 BEHAVIOUR-ONLY / 16 NONE across 34 guards, withthree new PROOF rows — all machine-checked against the table.
Root cause: Not a bug — a measured coverage gap #775's audit created and #785 tracked. Worth
recording is why two of the four ranked entries were worse than the ranking predicted. The
.husky/pre-pushunsetguards the NORMAL case, not an edge case: git exportsGIT_DIRtopre-pushonly when the push comes from a worktree, andprocess.shared-tree-readonlymakes theworktree the mandated way to work here. And
build_decisions_catalog.py's__main__wiring can dropa failure while still printing it — replacing
raise SystemExit(main())with a baremain()leavesthe script printing "is stale" on stderr and exiting 0, which the workflow step cannot see.
Decisions/conventions changed: none.
testing.guard-ships-with-mutation-proofandtesting.guard-derives-population-from-sourcewere applied, not amended. #790 (clause-level grading)and #809 (new) carry the follow-ups.
Reusable knowledge:
asserted on
_env()'s return value; leaving the helper correct and reverting oneenv=at thecall site gave 13 passing tests and 58 leaked records. Assert the EFFECT. Witnessing a mutation
against a hand-written revert is not witnessing it against the code a future tidy-up produces.
echoargument are indistinguishable from a command by any line-matching rule. Executing thestep's WHOLE
runscript resolves every such case by execution and costs nothing — it also fixedthree false REDS (backslash continuation,
set -euo pipefail, anifwrapper) that theline-matcher would have needed teaching one at a time.
on:as the booleanTrue, not the string"on"(YAML 1.1). A lookup of"on"alone yields an empty trigger set and fires on a correct workflow.
isolation: "worktree"bounds the FILES, not shared git state. Sibling worktrees share.git;my index came back holding a pre-fix blob while HEAD and the working tree were both correct. A
git add -A && git commitin that window would have committed a silent partial revert with everytest still passing.
then reported the discarded edit as landed. Apply, write and re-read each edit individually.
Verification:
scripts/tests771 passed / 2 skipped locally; CIScript tests (pytest)green in4m12s with 773 collected — identical to local, so nothing silently skipped. The three new files run
on the Linux runner host (git 2.47.3): 41 passed, and the pre-push deletion mutation reddens there
too. Every commit green in isolation. No flakiness across 5 sequential + 3 concurrent runs. All 18
proof refs resolve to a real
def, checked independently of the inventory guard. Productionhook-fire log: head adds 0 records, verified at full-suite scale on both hosts.
Deferred: #809 (generalise the production-log isolation guard beyond one file — includes why the
obvious per-test log diff is racy against a live session's hooks). #790 already tracks clause-level
grading. Two workflow channels deliberately not covered because they are undecidable outside the
runner, and named as such in
_falsey's docstring: a jobif:false only at run time, andcontinue-on-erroras a non-literal expression.Docs updated:
docs/guard-inventory.md(rows, counts, the ranked list with what each mutationestablished, what a file-level MUTATION grade does NOT claim, and the three groups every remaining
NONErow falls into — verified member-for-member against the derived set).Process note: five cold review rounds, each finding a defect introduced by the previous round's
fix, every one of them a green test over a dead check — the exact class this issue exists to close,
reproduced inside the work meant to close it. The pattern broke at round 4 by WITHDRAWING the
line-level predicate rather than patching it a fourth time. Limitation stated rather than hidden:
Codex failed twice to produce a cross-family review, so all five rounds came from one model family.