fix(asvs): a scorecard cell whose own prose carries a banned glyph was unwritable (BACKLOG #1308) - #584
Merged
Merged
Conversation
…s unwritable (BACKLOG #1308)
LEAD WITH THE UNWRITABILITY, NOT THE CHARACTERS. Read as a tidiness item this defers
forever; the actual symptom is a security record going read-only through its own guard.
`apply.py` scanned the WHOLE residual a payload carried and refused on any banned
character. But every payload must carry the residual forward, so a cell whose existing prose
already held one re-presented it on every pass and was refused every time -- however
mechanical the edit. The only route to touching such a cell was to edit prose the pass was
not about, which is a different act needing a different decision. The existing test says this
fired for real on 13.3.4.
THE FIX IS TO SCAN WHAT THE PAYLOAD INTRODUCES, NOT WHAT THE RECORD ALREADY CARRIES.
`_introduced_banned(payload, live)` returns the first banned codepoint the payload holds MORE
of than the live record does. `live` was already in scope at the check site.
COUNTED PER CODEPOINT, NOT PRESENCE, and that distinction is the whole safety margin.
Presence alone would let a payload add a SECOND warning sign to a cell that already had one
-- new vocabulary, which is precisely what the ban exists to stop. Counting refuses that
while allowing the character to be kept or MOVED, since neither introduces anything a later
reader could copy forward.
FAIL-CLOSED WHERE THERE IS NOTHING TO COMPARE AGAINST: a cell with no live counterpart has a
live count of zero for everything, so any banned character in a NEW cell is introduced and
refused. The comparison relaxes the scan only where a record exists to relax it against.
MUTATION-PROVED WITH THREE MUTANTS, each attacking a different property, each asserting its
anchor unique and the file hash CHANGED before scoring:
A the old whole-payload scan 2 red exactly the two "still applies" arms
B PRESENCE instead of COUNT 1 red ONLY "adding MORE of the same glyph"
C guard removed entirely 4 red both refuse arms, plus the PRE-EXISTING glyph test
Three mutants, three distinct red sets, each matching its prediction exactly. B is the one
worth having: it reds a single test, which is what shows the counting is load-bearing rather
than decoration. C reding the pre-existing test_it_refuses_a_glyph_in_a_residual shows the
original guard's coverage survives untouched.
The five new tests drive the REAL main() against a record whose LIVE residual carries U+26D4,
so the reassuring arm and the alarming arm differ only in what the payload does with it -- a
suite that only ever fed it clean records could not tell the fix from the defect.
Verification, with scope: tests/test_asvs_apply.py, 31 passed (was 26, all 26 still green
including the refuse arm), on .venv/Scripts/python.exe (CPython 3.14.6 non-freethreaded,
seven CI extras). ruff 0.15.22 (== the constraints.lock pin) check and format --check clean;
mypy strict clean on apply.py. NOT the full suite.
NOT DONE, deliberately: I did not open the vault scorecard and did not count how many live
cells this makes writable again. That coverage is vaulted, and the fix is correct
independently of the population -- a number I cannot show anyone is worse in a commit than
no number.
No test was removed or weakened. The ledger row is not mine to author and is not in this
commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wshallwshall
enabled auto-merge (squash)
August 25, 2026 18:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A vault scorecard cell whose existing prose already carried a banned character had become unwritable by the safe writer: every payload had to carry that residual forward, so every payload re-presented the character and got refused, however mechanical the edit -- a security record gone read-only through its own guard. docs/BACKLOG.md untouched (verified, count 0).
The fix compares the payload against the LIVE record and refuses only what the payload introduces, counted per codepoint rather than by presence -- presence would let a payload add a SECOND instance of the banned glyph to a cell that already carried one, which is new vocabulary, the exact thing the ban exists to stop.
Mutation-proved with three mutants: reverting to a whole-payload scan reds the two "still applies" arms; swapping the count for a presence check reds exactly one test (adding more of the same glyph) -- the one that shows counting is load-bearing rather than decorative; removing the guard entirely reds both refuse arms plus the pre-existing glyph-refusal test, confirming original coverage is untouched.
Deliberately not done: did not open the vault scorecard to count how many live cells become writable again -- that population is vaulted, and the fix is correct independently of the count. A number nobody can verify is worse in a commit than none.
Verification, explicitly scoped: tests/test_asvs_apply.py, 31 passed (was 26; the pre-existing 26 ran green before and after, so the change disturbs nothing). ruff/mypy strict clean. Full suite not run.