Repository navigation
check-adr-0087-registration reads a heading that DENIES a migration prescription as evidence of one, so not-required (no-migration-prescription) is unclaimable by the case it names #17357
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 10, 2026 Triage: lands in
scripts/check-adr-0087-registration.mjs;domain:spec— a gate whose subject is spec registration falls to the spec lane under the anchoring rule's gate clause, ⛔ not devx;priority:p2.The gate reads a heading that DENIES having a migration prescription as EVIDENCE of one, and then refuses the
not-required (no-migration-prescription)claim ⇒ ⭐ the one exemption that case exists for is unclaimable by the case it names.⇒ p2: this is a false red that blocks legitimate work, and worse, it blocks it in the direction of the honest answer. A contributor who correctly states "no migration prescription" is refused for saying so. That teaches the wrong lesson quickly — the cheapest way past a gate like this is to stop stating it.
⇒ Fix the evidence predicate so a denial is read as a denial.
⚠️ ⛔ Do not fix it by removing the check or widening the exemption to anything unclaimed — that is gate-weakening and the maintainer floor. The gate should still refuse a silent omission; it must stop refusing an explicit denial.⭐ Acceptance needs both controls: the explicit-denial case must now pass, and a silent omission must still fail. One without the other is how this becomes the opposite defect.
Size/model suggestion:S–M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:37Z · 本评论来自分诊座位
Generated by Claude Code
Claim:
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-12T14:25Z. Assignee set in the same write; theos-devround inherits both and ⛔ posts no second claim.Branch:
claude/issue-17357-adr0087-denial-headingClause-②: no — a gate's evidence rule is corrected. No schema key, no closed-set member, no published export moves; the set of changesets the gate ACCEPTS grows by exactly the ones that were being refused for denying a prescription in plain words.
⚠️ Lane note, declared rather than glossed. The card's own body says "The fix lands inscripts/, which is not this lane's." Triage nonetheless labelled itdomain:spec, and triage is the authority on routing —check-adr-0087-registrationis the ADR-0087 metadata-protocol-upgrade-contract gate, i.e. a gate that turns around the spec contract rather than general engineering. ⇒ dispatched here with the cross-cut declared. ⛔ If the round finds the fix reaches beyond that gate and its self-test into generalscripts/tooling, it stops and reports rather than widening.File face:
scripts/check-adr-0087-registration.mjsand its self-test. ⛔ Declared NOT touched: any changeset the gate judges, and the ADR itself.
Generated by Claude Code
os-dev-report
{ "card": 17357, "status": "done", "pr": "https://github.com/objectstack-ai/objectstack/pull/17862", "branch": "claude/issue-17357-adr0087-denial-heading", "head_sha": "d87a6936ef1bfbea33a3590ea38cfad6bf367799", "files_changed": ["scripts/check-adr-0087-registration.mjs"], "premise_still_valid": true, "premise_controls": { "re_derived_by_symbol": "every path:line on the card was re-derived by SYMBOL, not trusted. All of them had moved: FROM_TO_LABEL_RE is not at :1132, the from-to-label return is not at :1401, the P20 self-test assertion is not at :5337. The mechanism is exactly where the card says it is; only the numbers were stale.", "mechanism": "labelPositioned's first statement short-circuited on any ATX heading with an unconditional `return true` -- so the heading `## No FROM -> TO mapping, and why this section is not one` matched FROM_TO_LABEL_RE and was label-positioned by construction, returning branch from-to-label.", "category_count_checked": "the card calls type-surface-only 'the sixth category'; CATEGORIES holds FIVE not-required categories. NOT a contradiction: ADR-0087's own sentence counts `registered` as the first member of the closed vocabulary, so type-surface-only is the sixth of six dispositions and the fifth not-required category. The V3 self-test pins CATEGORIES against the real ADR in both directions and is green. No finding.", "lane_note": "the card body says the fix 'lands in scripts/, which is not this lane's'. The fix landed in exactly the declared file face -- scripts/check-adr-0087-registration.mjs and its own --self-test -- and reached no general scripts/ tooling, so the dispatch's stop-and-report condition did not trigger." }, "two_legged_reproduction": { "how": "the real CLI on a real temp git repo (the gate under test plus its two sibling modules copied in, base+head commits built, `node scripts/check-adr-0087-registration.mjs --base SHA` run from inside), exit captured before any pipe. Driver kept in the round scratchpad, not in the tree.", "before_at_7f625364b0": { "leg_A_token_present": "heading `## No FROM -> TO mapping, and why this section is not one` => exit 1, hit text `not-required (no-migration-prescription) contradicts the changeset's own body` with `Evidence (from-to-label):` naming the DENIAL ITSELF as the evidence, remedy offered = `registered` plus an entry id", "leg_B_token_absent": "the identical denial reworded `## No old-to-new mapping, and why this section is not one` => exit 0, `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition`" }, "after_at_d87a6936ef": { "leg_A_token_present": "exit 0", "leg_B_token_absent": "exit 0" }, "fixture_defect_caught_and_named": "the FIRST run of the driver failed BOTH legs -- on a 32-character justification against the 40-character floor, not on the detector. That is a defect of my fixture, not of the gate; it is reported rather than quietly re-run, and the exit codes above are quoted together with the hit text that produced them for exactly that reason." }, "both_pin_directions": { "b_denial_accepted": "D1 (the card's heading verbatim), D5 (the Chinese spelling, `wu` governing), D7 (a framing word between the negator and the placeholder), D-E2E-G (end to end through scan(): the plain-words denial is ADMITTED).", "a_genuine_prescription_still_refused": "D3 (a denying heading over a body that SHOWS a concrete rewrite is STILL a from-to-label hit -- the denial demotes the occurrence to a governed MENTION, it does not exempt it), D8 (only the denying occurrence is demoted; a genuine label later in the same body still hits and is the reported evidence line), D-E2E-R1 (end to end: same denial heading + a shipped rewrite => STILL REFUSED on `contradicts the changeset's own body` + `Evidence (from-to-label)`), D-E2E-R2 (a SILENT omission => STILL REFUSED on `no adr-0087: disposition marker`).", "positive_controls": "D2 (an ordinary label heading with NO concrete rewrite in the body to fall back on still matches), D4 (a negator three words away from the placeholder governs a different noun and denies nothing), D6 (the Chinese real-prescription heading `sheng-ji: bu-zai-zhi-chi-de-jian-de FROM -> TO` is untouched), D9 (the class is WORD-anchored, so a word merely beginning with the negator's letters keeps its label reading). The pre-existing P46 pin on that same Chinese heading is also still green.", "harness": "extended the gate's OWN --self-test rather than bolting a harness beside it; two new batteries registered in SELF_TEST_BATTERIES with their own floors (9 and 6). --self-test exit 0, 353 assertions over real temp git repos." }, "ablation": { "method": "the fix alone reverted to `return true`, the pins left in place; restored afterwards. trap on EXIT INT TERM, all paths absolute (REPO_ROOT read from `git rev-parse --show-toplevel` before the mutation).", "on_disk_proof_read_first": "occurrence counts on the mutated text, read BEFORE anything ran: fixed text `!HEADING_DENIAL_RE.test(prefix.replace(FRAMING_TAIL_RE` 1 -> 0; injected marker `return true; // ABLATED-17357` 0 -> 1. The driver exits 92 without running if either count fails to move, so a no-op edit cannot be read as a result.", "result": "ablated self-test exit 1, 5 failures -- D1, 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, so they were green before and after by design.", "restore": "proven by git hash-object against the HEAD blob -- worktree d78b2ddd94284f16a95ecbb77cfe75d758aebed6 == HEAD d78b2ddd94284f16a95ecbb77cfe75d758aebed6 -- never from an exit code; an empty hash is treated as failure. `git status --porcelain` empty and `git diff HEAD` clean afterwards. Restore used `git checkout HEAD -- path`, never a bare `git checkout -- path`." }, "corpus_reading": "findMigrationPrescription run over the real changeset stock of objectstack-ai/objectstack at d87a6936ef (this worktree): 238 changesets, 17 hits, BEFORE and AFTER byte-identical -- the diff of the two per-file verdict tables (file, branch, evidence line) is empty. The narrowing removes no true positive from the stock. Note the checkout is SHALLOW, so this is the PENDING stock, not the 1792-changeset historical stock the script's own comments cite; declared rather than glossed.", "changeset_decision": { "verdict": "not owed; `skip-changeset` applied to the PR", "build_first": "pnpm --filter @objectstack/spec build under the shared lock (OS_VERIFY_LOCK_SLOT=issue-17357). First attempt returned the wrapper's own VERDICT queue-timeout (exit 99) -- NOT ACQUIRED, nothing measured, holder named as the issue-17385 seat; re-taken on the KEPT slot and completed: VERDICT command-exit 0, `34/34 declared declaration file(s) present`.", "measurement": "npm pack --dry-run --json in packages/spec: 2012 tarball entries. NEGATIVE (entries matching this round's file) = 0. POSITIVE CONTROL (dist/* 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.", "workspace_reading": "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.", "label_write": "additive POST /issues/17862/labels => HTTP 200; comparative read-back => ['size/m','skip-changeset'], union(read,target) minus read-back empty, so the size labeler's PUT stripped nothing." }, "verification": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, exit 0. Change set derived by the tool itself from merge base 7f625364b0 (committed 1, working tree 0, untracked 0) -- not from a hand-written git diff. Repo assertion held against this checkout's origin.", "ran": "35 derived families, 35 run, ALL exit 0. Every code captured before any pipe (`cmd > file 2>&1; EXIT=$?`). No family returned 3, so nothing is PREREQUISITE-NOT-MET.", "reconcile": "node scripts/pm/dispatch-gates.mjs --ran RECORD_FILE => exit 0, `35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN` and the tool confirms the zero is DERIVED from the recorded codes, not claimed.", "gate_on_its_own_diff": "node scripts/check-adr-0087-registration.mjs --base origin/main => exit 0", "self_test": "node scripts/check-adr-0087-registration.mjs --self-test => exit 0, 353 assertions", "lint": "NOT narrowed -- the whole-tree run completed: `eslint . --no-inline-config --format json` over 6655 files that eslint itself selects, 0 errors, exit 0, at d87a6936ef. Narrowed run on the changed file alone also 0 errors / 0 warnings. Invariance: this repo enables type-aware linting for NO file (no parserOptions.project, no typed @typescript-eslint rules), so this diff cannot move a verdict on an untouched file.", "clause2": "node scripts/pm/check-clause2-carriers.mjs --pair 17862 => exit 0; both carriers agree on `Clause-②: no`, no widening tell. Declared on the PR body starting the line with the bare token (NOT backticked).", "control_chars": "grep -naP over the changed file for NUL and the other C0/DEL bytes => exit 1, zero bytes of output; lit control on a planted 0x01 => exit 0. pnpm check:nul-bytes is in the 35 above and exited 0.", "packages_spec": "NOT APPLICABLE -- the diff touches no path under packages/spec, so neither `--project local` nor `test:repo` (`--project repo`) is owed. Declared rather than silently skipped.", "not_owed_locally": "the 52 artifact-roster families, the 11 declared-wide-population families, the 9 pending-changeset families and the 1 path-scheduled CI job that dispatch-gates names are outside the derived 35 by its own declaration; they are CI's." }, "commit_hygiene": { "hash_digits": "grep -E '#[0-9]' over the commit message => no match (exit 1); lit control on a planted `closes #999` => 1 match. No card relation can form.", "bare_stems": "swept SEPARATELY: 1 hit, the conventional-commit type `fix(gate):`. With zero `#`+digits anywhere in the message, no closing relation exists. `closed class` was reworded to `bounded class` to keep the stem count at the unavoidable one.", "trailers": "model-free pair -- `Co-authored-by` naming Claude at the noreply address in angle brackets and `Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH`. The push-time check:commit-card-trailers printed `1 commit message(s) on this push carry no card relation and no model identifier in the trailer pair.`" }, "pr_body_readback": { "create": "POST /pulls with draft=true => PR 17862 draft. Body read back and diffed against what was sent: IDENTICAL line-for-line, ONE footer, session-URL form preserved.", "edit_mutation_measured_and_repaired": "a later PATCH (to name the filed finding) caused the platform to APPEND a second, BARE footer -- 2 footers. Repaired by re-sending the body with MY footer stripped (never re-sending a body that already carries an appended one): read-back now shows exactly ONE footer, the platform's bare form. Durable attribution moved into body PROSE naming the session and its URL, per the AGENTS.md rule. Final read-back: draft true, `Fixes #17357` exactly once, the Clause-② line present, nothing removed by the platform.", "angle_sweep": "grep -E '<[A-Za-z!/]' over the PR body and the filed issue body => no match (exit 1); lit control on a planted HTML comment => 1 match. No HTML comments and no angle-bracket fragments anywhere in either." }, "out_of_scope_findings": [ "filed as #17864: 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 (control A) and the framing word with no governor returns from-to-label (control B). 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 reached. Same false-positive direction as this card, DIFFERENT arm with its own measured history (its first draft took five real prescriptions with it), so it needs its own stock measurement and is outside this card's declared file face. Filed unassigned, no domain:* and no pm:* label.", "noted, not filed: labelPositioned keeps its own inline copy of the ATX-heading pattern while HEADING_RE is hoisted for the framed-region arm. Carrier: the next author of this gate -- the only reader either spelling has.", "noted, not filed: the ADR anchor scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json says `never widen the exemptions here without the ADR half in the same PR`. This round widens NO exemption -- CATEGORIES is untouched, ADR-0087 is untouched, and ADR-0087's own sentence `no-migration-prescription is refused by a body that carries a migration prescription` is unchanged in force; what changed is the READING of `carries`. Recorded so the reviewer can check that judgement rather than take it." ], "open_questions": [], "mcp_calls": "0 -- every GitHub read and write went through the container's repo-scoped REST channel (probe: GET /repos/objectstack-ai/objectstack => 200), including the card and comment reads, the PR create/patch/read-back, the additive label write and read-back, the dedup listing and the filed finding.", "summary": "Reproduced the defect two-legged on my own head before changing anything, through the real CLI on a real temp repo: the denial heading spelled with the placeholder is exit 1 with the DENIAL ITSELF quoted as `Evidence (from-to-label)`, and the identical denial reworded off the token is exit 0. The repair gives labelPositioned's heading short-circuit a polarity read -- a bounded class of negators, ADJACENT to the placeholder, with a framing word between them read as transparent -- and, crucially, DEMOTES a denying heading to the status of a governed MENTION rather than exempting it, so it falls through to carriesConcreteRewrite and a body that denies and then ships the prescription is refused exactly as before. Both directions pinned in the gate's own --self-test (9 unit + 3 end-to-end cases, 4 of them positive controls, 4 of them the refusal half); ablation puts D1/D5/D7/D8/D-E2E-G red before the fix and green after, with the on-disk mutation proven first and the restore proven by git hash-object. The narrowing changes no verdict over the 238-changeset stock. 35 derived gate families all exit 0 and reconcile 0 NOT-MEASURED / 0 UNRUN; whole-tree eslint over 6655 files is green. No changeset owed, measured with four controls against a real npm pack." }
Generated by Claude Code
已落地并按内容核实,收口
domain:spec执行席,sessionsession_01MkQhmuuJAVDjmeWNixwDDH,核实取数 2026-09-12T16:45Z。PR #17862 已合并(
Fixes #17357,GitHub 自动关单)。核实读的是origin/main,先git fetch,⛔ 不是共享检出的工作树:scripts/check-adr-0087-registration.mjs上HEADING_DENIAL_RE读到 3 次;D1-D9 (#17357)电池读到 3 次。- 点亮对照:伪 token
HEADING_DENIAL_RE_XYZ读到 0 次(对照是暗的,所以上面的非零是读数,不是结构性的命中)。
本席落地前亲跑的四步门禁:①
Clause-②: no,两载体一致 ⇒ 不触至档;②check-clause2-carriers --pair 17862exit 0;③check-governed-merges --test scripts/check-adr-0087-registration.mjsexit 0,点亮对照(同一命令加一条docs/adr/路径)exit 3;④ 31 个 check 按 newest-run-per-name 全绿,且翻 ready 之后注册的 run 已存在。⇒ 队列 refpr-17862-…是唯一的入队读数。复核采信的要点:否定式标题被降格为 governed mention 而不是豁免,仍落到
carriesConcreteRewrite,所以「标题否定、正文照发处方」照旧拒收(D3 与 D-E2E-R1 两个红向钉子)。D2 / D4 / D6 / D9 是正位对照 —— 其中 D4、D6 钉住邻接规则:否定词隔着三个词修饰的是别的名词,不构成否定。238 份 changeset 存量上零判决变化。pm:dispatched已在关单后同一笔摘掉;domain:spec与priority:p2留下 —— 归属不是状态。
Generated by Claude Code
- added 2 commits that reference this issue
on Sep 17, 2026 - added a commit that references this issue
on Sep 18, 2026 - added a commit that references this issue
on Sep 28, 2026
Filed by the
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T08:39Z, out of the #16320 / PR #17146 rework round. ⛔ Unclaimed. Nodomain:*label and nopm:*state: ⛔ both are the triage seat's to produce. The fix lands inscripts/, which is not this lane's.The shape
check-adr-0087-registrationreads a heading that DENIES having a migration prescription as evidence of one, and then refuses thenot-required (no-migration-prescription)disposition on that evidence.Measured by the
os-devreworking PR #17146, on a real changeset it was writing:## No FROM → TO mapping, and why this section is not one⇒ gate exit 1, evidence linefrom-to-label, demandingregistered <ENTRY_ID>instead.⇒ The author is refused for saying, in the clearest available words, that there is nothing to map. The remedy the gate offers (
registered <ENTRY_ID>) is exactly what the maintainer's 「直接删」 ruling forbids on that PR, and the sixth categorytype-surface-onlyis barred by its ownno-spec-diffpredicate because the diff is insidepackages/spec. ⇒ On that route the requirement and the ruling were jointly unsatisfiable, and the only passing form was to avoid a word.What the seat verified, and what it did NOT
origin/main501959b72:scripts/check-adr-0087-registration.mjs:1132—FROM_TO_LABEL_RE;:1401returns{ branch: 'from-to-label' }.:5337— the script's own self-test asserts that a bare heading matches:findMigrationPrescription('### 迁移:FROM → TO\n')?.branch === 'from-to-label'. ⇒ A heading matching is by design, not an accident, and the detector has no polarity: it cannot tell an assertion from a denial.:841—## #6967 — the first NARROWING, and why the direction reversed;:603-604records that ADR-0087 迁移说明探测器把「指向别处的迁移说明」当成迁移说明本身 —— 这类 changeset 无任何可用处置,PR 会被卡死 #6967 narrowed branch 1 because "a superset-only detector can only ever add false positives".:422— a self-test group named "the floors: labels that are NOT mentions, and mentions that ARE evidenced".⇒ ⭐ The mention-vs-use distinction already exists in this script and was already narrowed once for exactly this class of false positive. #6967 drew the line for INLINE text and stopped there; a heading is still read as a label whatever it says.
Why this is filed rather than shrugged at
The dev explicitly declined to file it, for a reason worth quoting: it could not separate the behaviour from "the gate's deliberate heading conservatism" and could not name a carrier. ⛔ That was the correct call from where it stood.
The seat can name the carrier, which is what changes the answer:⚠️ That is the cost: the gate is currently shaping prose that a human will read, in the direction of not naming the thing being denied.
scripts/check-adr-0087-registration.mjsitself, and the next author of a declared-breaking changeset that genuinely has nothing to map. That author now exists as a concrete precedent — PR #17146 — and the shipped changeset there had to be worded around the gate rather than for the reader.⛔ Not asking to weaken the gate. Its conservatism about labels is load-bearing and #6967's history says a superset detector was the right default. The question is narrower: should a heading that denies a prescription count as one? A polarity check, or an explicit opt-out marker, are both smaller than relaxing the detector.
⛔ Not asking to re-open PR #17146's disposition. That changeset ships the passing form and states its reasoning in the open — a bare deletion on a non-strict schema refuses nothing and converts nothing, so no metadata upgrader has an edit and
os migrate metahas nothing to list.Dedup
Complete enumerations read 2026-09-10T08:39Z: open
domain:devx+domain:spec+domain:skillsunion = 233 cards. Lit control: 4 titles contain "changeset"; dark control: a fabricated token matches 0.#16787 is a sibling, ⛔ not a duplicate — "⚠️ Whoever takes either should read the other: two categories of six are now measured as unclaimable by their own intended class, and that pattern may be the real card.
check-adr-0087-registration'stype-surface-onlycategory is unclaimable by the class it was built for". Same gate, and the same shape (an ADR-0087 category unreachable by a case it should cover), but a different category (type-surface-onlyvsno-migration-prescription) and a different mechanism (two path predicates firing onpackages/specvs a polarity-blind label detector).Source
#16320 · PR #17146 · the
os-devround report of 2026-09-10, open question 2 ·scripts/check-adr-0087-registration.mjsonorigin/main501959b72Generated by Claude Code