Skip to content

fix(scripts): close an ADR-0087 framed region at the same or a shallower heading, not at any heading - #13142

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-12996-migration-framing-depth
Aug 29, 2026
Merged

fix(scripts): close an ADR-0087 framed region at the same or a shallower heading, not at any heading#13142
os-elon merged 2 commits into
mainfrom
claude/issue-12996-migration-framing-depth

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#12996

findMigrationPrescription's framed-table arm opened a framed region at a heading matching MIGRATION_FRAMING_RE and closed it at the next heading of any level. A ## Migration section that organises its rewrite tables under ### sub-headings — the ordinary way to write a two-part rename — therefore lost its framing before the first table row was read.

Reproduced on today's main (4d94823a36), through the gate's own exported function

A '## Migration' + table branch: framed-table
B '## Migration' + '### Method namespace' + table null
C '## Migration' + '| Wrote | Write instead |' table branch: framed-table

A and B differ by one line. Isolated: holding the table byte-identical and adding only the ### Method namespace line flips framed-table to null.

The fix

Framing is tracked per heading depth. A heading closes every region nested at or outside its own level and opens one of its own if it carries framing; regions shallower than it survive.

The superset property is structural, one level up from where #6497 and #6559 established it: the predicate became "some heading on the current path carries framing", and the most recent heading — the whole of the old predicate — is always on that path. No body the old scan framed can stop being framed.

Per-depth rather than one remembered depth: a ### Response keys sibling closes only its own level, and the ## Migration region above it survives (pinned as P67).

Stock measurement — one row moves, and it is the live instance

Measured over the stock at 4d94823a36: 419 changesets, 53 declared-breaking.

hitsdeclared-breaking hits
before (origin/main)3423
after (this branch)3524

One changeset newly flagged, zero no longer flagged, zero changed branch or evidence line:

  • .changeset/adr0006-d2-client-environments-namespace.md — declared breaking, holds not-required (runtime-interface-only ...), evidence | client.projects.list(…) | client.environments.list(…) | on branch framed-table.

Is the new refusal correct? Yes. That changeset ships sixteen rows of client.projects.X becoming client.environments.X plus a response-key table — a real consumer-rewrite prescription. Its own disposition marker says the author declined no-migration-prescription because it "would mechanically pass only through a detector blind spot while contradicting the migration table below it". The fix makes the detector agree with the author's own reading; runtime-interface-only inherits the prescription refusal, which is what makes it a narrowing rather than a fifth way around it.

Verdict impact on any future PR: none. The gate is forward-only (git diff --diff-filter=AMR base..head -- .changeset/*.md), and an M row already breaking at base is skipped. That changeset is on main and already breaking, so it is never re-judged. What changes is that the next changeset written this way is asked the question instead of being waved through.

The card's smaller alternative was measured and rejected

Widening OLD_COLUMN_RE to admit before:

  • on its own it buys nothing — plus zero changesets.after is not a NEW column word, so | before | after | still does not frame. The alternative as literally stated cannot rescue probe B.
  • admitting after as well takes it to plus three, two declared-breaking, and two of the three are behaviour comparisons rather than rewrites: compound-meta-door-mode-draft.md (| Request | Before | After |, one request's response before and after the change) and oauth-resource-identifier-sourced-255.md (| | before (1024) | after (255) |, two information_schema readings). The second declares breaking and holds not-required (no-migration-prescription), so the false positive would hard-block an author entitled to their exemption.

That is exactly the trade #6559 measured and refused for was/nowbefore/after is named in the same paragraph as this repo's behaviour-comparison header vocabulary — and P33 pins the refusal. It also addresses nothing about nesting.

No earlier repair is subsumed or removed.#6419 and #6497 added arms, #6559 added an arm that needs no heading at all; this changes only where a heading-opened region ends.

A guard whose success condition equals its total-failure condition

If the framing scan ever stops seeing headings, every region is empty, no prescription is ever found, and both inheriting exemptions become free for everyone — reported as a clean run. "Zero framed sections" is also what a body with no migration section legitimately produces, so the live stock cannot tell the two apart.

assertInputs gains a FRAMING-SCAN ROT control in both directions, synthetic for the #8658 reason the existing CONVENTION ROT control gives, and it runs on every invocation. Demonstrated on the real shipping path by blinding the heading pattern:

legordinary gate run
unmutated HEAD (dual control)EXIT=0, clean
this branch, framing scan blindedEXIT=1, 3 FRAMING-SCAN ROT input problems, "refusing to report a verdict"
origin/main, same blindingEXIT=0, "no declared-breaking changeset" — clean, with the scan totally dead

The inverted half of the control pins that a framed region still closes: a region that never closes frames the rest of the document, which is the over-matching #6559 rejected the label-framed arrow arm for.

Pinning the ruling, not just the code

P62-P68. Predicted before running, then run — the map matched exactly:

pinwith the fixfix reverted
P62 nested rewrite tableGREENRED
P63 nested arrow rewriteGREENRED
P64 three levels deepGREENRED
P67 second ### sibling, outer region survivesGREENRED
P65 FLOOR — sibling heading still closesGREENGREEN
P66 FLOOR — shallower heading still closesGREENGREEN
P68 FLOOR — unframed outer heading frames nothingGREENGREEN

Plus the FRAMING-SCAN ROT control's nested case (RED when reverted), and --self-test exit 1. The floors are green with the fix and green without it, said out loud rather than counted as coverage, per this file's own convention. Every restore leg was verified by observation — worktree blob equal to the HEAD blob, git diff HEAD empty — never by an exit code.

Verification

All at 11c00cc038, under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0):

  • the 14 path-derived families from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after syncing (the first derivation printed STALE TREE and still exited 0)
  • the convention pair for a gate-script edit: scripts/pm/bare-root-worklist.mjs --self-test, pnpm check:pm-dispatch-gates
  • pnpm check:objectui-changesetby hand — this gate is staged and executed by scripts/objectui-changeset-digest.mjs, which the path derivation cannot see; that script's own self-test is the first half of it
  • pnpm check:changeset-gate-self-tests — 235 assertions over real temp git repos
  • pnpm check:published-files, pnpm check:nul-bytes

node scripts/check-test-completeness.mjs answers a standalone run with EXIT=1 and a usage line. That is an invocation error, not a red gate — the unmodified script does the same, and it is #13110.

No changeset

Derived, not asserted: the diff's only path is scripts/check-adr-0087-registration.mjs, which lies outside every workspace package root, and files whitelists are package-relative — pnpm check:published-files reports all 69 publishable packages whitelisting dist/ plus README and CHANGELOG. Nothing publishes, so the PR carries the skip-changeset label instead. The gate this PR changes ran on its own diff and reported no declared-breaking changeset.

Generated by Claude Code


Generated by Claude Code

…wer heading, not at any heading
`findMigrationPrescription`'s framed region opened at a heading matching
`MIGRATION_FRAMING_RE` and closed at the next heading of ANY level, so a
`## Migration` section organising its rewrite tables under `###` sub-headings lost
its framing before the first table row was read. Framing is now tracked per heading
depth: a heading closes every region nested at or outside its own level and opens
one of its own if it carries framing, so shallower regions survive.
The superset property is structural -- the predicate became "some heading on the
current path frames", and the most recent heading is always on that path.
Adds a FRAMING-SCAN ROT control to `assertInputs`, in both directions: the framing
scan's total-failure condition (no section found anywhere) is indistinguishable
from its success condition on the live stock, so synthetic controls run on every
invocation and refuse rather than report clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 29, 2026 — with Claude
@os-elon
os-elon marked this pull request as ready for review August 29, 2026 06:41
@os-elon
os-elon enabled auto-merge August 29, 2026 06:41
@os-elon
os-elon added this pull request to the merge queueAug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33240069522 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL test/serve-publishes-bound-port.e2e.test.ts > #13062 `os serve --port 0` — the request that can never be the answer > announces the BOUND port on all three channels, and ↳ 失败原因: @objectstack/cli:test: Error: ENOENT: no such file or directory, open '/tmp/os-bound-port-home-UOUgwz/runtime.env_local.json'
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 596b20aAug 29, 2026
34 checks passed
@os-elon
os-elon deleted the claude/issue-12996-migration-framing-depth branch August 29, 2026 07:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-adr-0087-registration: a ## Migration section loses its framing at the first ### sub-heading, so its rewrite tables read as no prescription

2 participants

@os-elon@claude