fix(gate): read a heading that denies a migration prescription as a denial - #17862
Merged
Conversation
…enial `check-adr-0087-registration`'s branch-1 detector short-circuited on any markdown heading -- a heading is a label by construction -- so a heading DENYING that the changeset carries a FROM/TO mapping evidenced the very prescription it denied, and `not-required (no-migration-prescription)` was refused on it. Measured through the real CLI on a real temp repo: the denial spelled with the token is exit 1 with `Evidence (from-to-label)` naming the denial itself; the identical denial reworded off the token is exit 0. The gate was keying on the spelling rather than the meaning, and the only passing form was to avoid a word. The heading short-circuit now asks whether the word GOVERNING the placeholder is a negator -- a bounded class, adjacent, with a framing word between the two read as transparent. A denying heading is DEMOTED to the status of a governed mention rather than exempted: it falls through to `carriesConcreteRewrite`, so a body that denies and then ships the prescription is refused exactly as before, and every other branch of the detector still reads the same body afterwards. Both directions are pinned in the gate's own `--self-test`: nine unit pins with three positive controls, and three end-to-end cases through `scan()` where the plain-words denial is admitted while a shipped prescription and a silent omission are both still refused. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
os-bill
marked this pull request as ready for review
September 12, 2026 15:52
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.
Fixes #17357
Authored by the
os-devround of thedomain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH— https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH(durable attribution in prose: the trailing footer block on this body is written by the
platform on every edit, so it carries no session).
Clause-②: no — a gate's evidence rule is corrected. No schema key, no closed-set member and no
published export moves;
CATEGORIESis untouched and so is ADR-0087. The set of changesets thegate ACCEPTS grows by exactly the ones that were being refused for denying a prescription in
plain words.
The defect, reproduced two-legged before anything changed
check-adr-0087-registration's branch-1 detector short-circuits on any markdown heading — aheading is a label by construction — so a heading that denies the changeset carries a
FROM/TO mapping evidenced the very prescription it denied, and
not-required (no-migration-prescription)was refused on it. The remedy that refusal offers(
registeredplus an entry id) asserts the opposite of what the changeset says.Measured on
7f625364b0through the real CLI on a real temp git repo — the two legsdiffer only in the wording of one heading, and nothing else in the changeset:
## No FROM → TO mapping, and why this section is not oneEvidence (from-to-label)naming the denial itself## No old-to-new mapping, and why this section is not one⇒ the gate was keying on the spelling rather than the meaning, and the only passing form was
to avoid a word. A one-legged reproduction would have proved nothing here; leg B is what shows
the refusal is about the token and not about the claim.
The reproduction driver is in the round's scratchpad, not in the tree — it copies the gate under
test plus its two sibling modules into a temp repo, builds the base/head commits, and runs
node scripts/check-adr-0087-registration.mjs --base ...from inside it, capturing$?beforeany pipe. Its first run failed both legs on a 32-character justification (the 40-character
floor), which is why the exit codes above are quoted with the hits that produced them rather
than on their own.
The repair
labelPositioned's heading short-circuit now asks whether the word governing theplaceholder is a negator:
GOVERNING_WORD_REis. "A negator appears somewhere inthe heading" is a whole clause and a different claim —
## 升级:不再支持的键的 FROM → TOis areal prescription heading whose negator modifies a noun three words away.
WRAPPED_GOVERNOR_RE's is: an open read of"negative-sounding words" cannot be measured, and a false negative is the one direction this
gate must not buy.
in-line:
## No migration FROM → TOdenies as plainly as## No FROM → TO.This cannot weaken the gate, and the mechanism is why. A denying heading is not granted
an exemption — it is demoted to exactly the status of a governed MENTION, so it falls through to
carriesConcreteRewrite. A body that denies in its heading and then SHIPS the prescription isrefused precisely as before, every other branch of
findMigrationPrescriptionstill reads thesame body afterwards, and a silent omission is untouched.
Both directions are pinned, in the gate's own
--self-testExtended, not bolted beside. Nine unit pins (
D1-D9) and three end-to-end cases throughscan()(D-E2E-*), each registered inSELF_TEST_BATTERIESunder its own floor:contradicts the changeset's own bodyno adr-0087: disposition markerSelf-test after:
exit 0, 353 assertions over real temp git repos.Ablation
The fix reverted to
return truewith the pins left in place, restored afterwards:1 → 0, the injectedablation marker
0 → 1. The script refuses to run the self-test unless both counts move.exit 1, 5 failures —D1,D5,D7,D8,D-E2E-G. The sevencontrol pins stayed green under ablation, which is what makes them controls: they pin
behaviour the repair must not change.
git hash-objectagainst the HEAD blob(
d78b2ddd94284f16a95ecbb77cfe75d758aebed6both sides), never from an exit code, withgit diff HEADclean afterwards.traponEXIT INT TERM, absolute paths throughout, and anempty hash treated as failure rather than as nothing to compare.
Corpus reading — the narrowing removes no true positive
findMigrationPrescriptionrun over the real changeset stock atd87a6936ef(
objectstack-ai/objectstack, this worktree), before and after:diffof the two per-file verdicttables is empty. No row changes branch, evidence line or verdict.
Verification
Gate families derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(exit 0; change set derived by the tool itself from the merge base
7f625364b0, not from ahand-written diff):
(
cmd > file 2>&1; EXIT=$?). No family returned 3.--rancarrying the exit codes: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN— a derived zero, not a claimed one.
node scripts/check-adr-0087-registration.mjs --base origin/main— exit 0 on this PR's own diff.pnpm lintequivalent run whole-tree, not narrowed:eslint . --no-inline-config --format jsonover 6655 files that eslint itself selects, 0 errors, exit 0, at
d87a6936ef. Thenarrowed run on the changed file alone is also 0/0/0. This repo never enables type-aware
linting for any file, so neither reading can move a verdict on an untouched file.
is owed; the file's own test surface is its
--self-test, which is in the derived set above.Changeset — not owed, measured rather than asserted
@objectstack/specbuilt first and confirmed finished (34/34 declared declaration file(s) present, lock wrapperVERDICT command-exit 0), thennpm pack --dry-run --json:dist/*entries, the built surface that does shipsrc/**/*.test.ts, present on disk, never shippedpackage.jsonAcross the workspace: 70 published packages, 0 declaring a
files[]entry that escapes itsown directory — so a repo-root
scripts/file cannot appear in any tarball. No package sourceimports the changed file (the only hits outside
scripts/are CHANGELOG prose and one comment).⇒ nothing published moves;
skip-changesetapplied on this PR.验收备注
whenever a framing word sits between the governor and the placeholder:
the Migration FROM → TO is documented elsewherereturnsfrom-to-label, while the identicalsentence without
MigrationreturnsnullandMigration FROM → TO: delete the block(nogovernor) returns
from-to-label. Cause:prefix.replace(FRAMING_TAIL_RE, '')leaves thespace the framing word sat behind, and
GOVERNING_WORD_REis end-anchored, so the word thestrip exists to expose is never examined. It is the same false-positive direction this card is
about but a different arm with its own measured history — the in-line rule's first draft
took five real prescriptions with it — so repairing it needs its own stock measurement and is
out of this card's declared file face.
labelPositionedkeeps its own inline copy of the ATX-heading pattern whileHEADING_REis hoisted a few hundred lines above for the framed-region arm. Carrier: the nextauthor of this gate, who is the only reader either spelling has.
path:linecitations were re-derived by symbol rather than trusted: every one ofthem had moved, which is the expected state.
Generated by Claude Code