Skip to content

fix(gate): read a heading that denies a migration prescription as a denial - #17862

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-17357-adr0087-denial-heading
Sep 12, 2026
Merged

fix(gate): read a heading that denies a migration prescription as a denial#17862
os-bill merged 1 commit into
mainfrom
claude/issue-17357-adr0087-denial-heading

Conversation

@os-bill

@os-bill os-bill commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17357

Authored by the os-dev round of the domain:spec execution seat, session
session_01MkQhmuuJAVDjmeWNixwDDHhttps://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; CATEGORIES is untouched and so is ADR-0087. The set of changesets the
gate 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 — a
heading 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
(registered plus an entry id) asserts the opposite of what the changeset says.

Measured on 7f625364b0 through the real CLI on a real temp git repo — the two legs
differ only in the wording of one heading, and nothing else in the changeset:

leg heading gate, before the fix after
A · token present ## No FROM → TO mapping, and why this section is not one exit 1, Evidence (from-to-label) naming the denial itself exit 0
B · token absent ## No old-to-new mapping, and why this section is not one exit 0 exit 0

⇒ 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 $? before
any 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 the
placeholder is a negator:

  • Adjacency is the rule, exactly as GOVERNING_WORD_RE is. "A negator appears somewhere in
    the heading" is a whole clause and a different claim — ## 升级:不再支持的键的 FROM → TO is a
    real prescription heading whose negator modifies a noun three words away.
  • The class is bounded, for the same reason 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.
  • A framing word between the negator and the placeholder is transparent, exactly as it is
    in-line: ## No migration FROM → TO denies 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 is
refused precisely as before, every other branch of findMigrationPrescription still reads the
same body afterwards, and a silent omission is untouched.

Both directions are pinned, in the gate's own --self-test

Extended, not bolted beside. Nine unit pins (D1-D9) and three end-to-end cases through
scan() (D-E2E-*), each registered in SELF_TEST_BATTERIES under its own floor:

pin direction
D1 · D5 · D7 the denial heading is read as a denial (English, Chinese, and with a framing word between)
D2 · D4 · D6 · D9 positive controls — an ordinary label heading with no concrete rewrite to fall back on; a negator three words away; the Chinese real-prescription heading; a word merely beginning with the negator's letters. If any of these reds, the arm has stopped seeing rather than started discriminating
D3 a denying heading over a body that SHOWS the goods is still a hit — demoted to a mention, not exempted
D8 only the DENYING occurrence is demoted; a genuine label later in the same body still hits, and the evidence line is that label
D-E2E-G the plain-words denial is admitted, end to end
D-E2E-R1 the same denial heading over a body that ships the prescription is still refused, on contradicts the changeset's own body
D-E2E-R2 a silent omission is still refused, on no adr-0087: disposition marker

Self-test after: exit 0, 353 assertions over real temp git repos.

Ablation

The fix reverted to return true with the pins left in place, restored afterwards:

  • On-disk proof read FIRST, before anything ran: the fixed text 1 → 0, the injected
    ablation marker 0 → 1. The script refuses to run the self-test unless both counts move.
  • Result: self-test exit 1, 5 failuresD1, D5, D7, D8, D-E2E-G. The seven
    control pins stayed green under ablation, which is what makes them controls: they pin
    behaviour the repair must not change.
  • Restore proven by git hash-object against the HEAD blob
    (d78b2ddd94284f16a95ecbb77cfe75d758aebed6 both sides), never from an exit code, with
    git diff HEAD clean afterwards. trap on EXIT INT TERM, absolute paths throughout, and an
    empty hash treated as failure rather than as nothing to compare.

Corpus reading — the narrowing removes no true positive

findMigrationPrescription run over the real changeset stock at d87a6936ef
(objectstack-ai/objectstack, this worktree), before and after:

  • 238 changesets, 17 hits, byte-identical both sidesdiff of the two per-file verdict
    tables 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 a
hand-written diff):

  • 35 derived families, 35 run, all exit 0, every code captured before any pipe
    (cmd > file 2>&1; EXIT=$?). No family returned 3.
  • Reconciled with --ran carrying 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 lint equivalent run whole-tree, not narrowed: eslint . --no-inline-config --format json
    over 6655 files that eslint itself selects, 0 errors, exit 0, at d87a6936ef. The
    narrowed 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.
  • Diff touches no package, so there is no dependency closure to build and no package test suite
    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/spec built first and confirmed finished (34/34 declared declaration file(s) present, lock wrapper VERDICT command-exit 0), then npm pack --dry-run --json:

reading count
tarball entries 2012
negative — entries matching this round's file 0
positive controldist/* entries, the built surface that does ship 216
second negative control — src/**/*.test.ts, present on disk, never shipped 0
lit control for the matcher itself — entries matching package.json 1

Across the workspace: 70 published packages, 0 declaring a files[] entry that escapes its
own directory — so a repo-root scripts/ file cannot appear in any tarball. No package source
imports the changed file (the only hits outside scripts/ are CHANGELOG prose and one comment).
⇒ nothing published moves; skip-changeset applied on this PR.

验收备注

  • Filed as issue 17864, not fixed here — the same detector reads a governed MENTION as a label
    whenever a framing word sits between the governor and the placeholder:
    the Migration FROM → TO is documented elsewhere returns from-to-label, while the identical
    sentence without Migration returns null and Migration FROM → TO: delete the block (no
    governor) returns from-to-label. Cause: prefix.replace(FRAMING_TAIL_RE, '') leaves the
    space the framing word sat behind, and GOVERNING_WORD_RE is end-anchored, so the word the
    strip 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.
  • Noted, not filed: labelPositioned keeps its own inline copy of the ATX-heading pattern while
    HEADING_RE is hoisted a few hundred lines above for the framed-region arm. Carrier: the next
    author of this gate, who is the only reader either spelling has.
  • The card's path:line citations were re-derived by symbol rather than trusted: every one of
    them had moved, which is the expected state.

Generated by Claude Code

…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 os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026 — with Claude
@os-bill
os-bill marked this pull request as ready for review September 12, 2026 15:52
@os-bill
os-bill added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 57cbb1d Sep 12, 2026
57 of 58 checks passed
@os-bill
os-bill deleted the claude/issue-17357-adr0087-denial-heading branch September 12, 2026 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants