Repository navigation
feat(spec)!: the ADR-0087 migration chain leaves the root entry for @objectstack/spec/migrations (#20646) - #20695
Conversation
…objectstack/spec/migrations (wip: entry + exports) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ve the migration importers to @objectstack/spec/migrations Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ations entry Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ec-registries-subpath
…y as an entry, not a protocol namespace Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ec-registries-subpath
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 24c1006eaa73c1df2ed353cf2a0a9c8dfd208bca && git checkout 24c1006eaa73c1df2ed353cf2a0a9c8dfd208bca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d2820876f7452e44620db6a1810a847613a46f67 6ce9c2b349ef86efb0cebdb50eb9bd04ce54be94 && git checkout -B drift-repro d2820876f7452e44620db6a1810a847613a46f67 && git merge --no-ff 6ce9c2b349ef86efb0cebdb50eb9bd04ce54be94
node scripts/docs-audit/affected-docs.mjs --json d2820876f7452e44620db6a1810a847613a46f67
|
Contract reviewServed-tier: Inputs: card #20646 (body, claim ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ec-registries-subpath # Conflicts: # packages/cli/src/commands/migrate/meta.ts
…undle in view (objectstack-ai#20646) (objectstack-ai#20743) Fixes objectstack-ai#20646 Clause-②: no `Console Pin Gate` is red on `main` since `fbec216e2d` (PR objectstack-ai#20695, the `@objectstack/spec/migrations` entry split). Its build step exits 2: "Neither spec appears in the built console — no @objectstack/spec content matched." The injection works. The check was reading text the console never bundles. This PR makes the build-time probe derivation choose with the bundle in view. The fresh leg stops reading entries the console never imports, and the stale leg becomes strictly stronger. ## Root cause, measured `scripts/console-spec-probes.mjs` builds each spec's blob from every JS file the package's `exports` map resolves to. The assertion then took the alphabetically first unique `.describe()` text on each side. A console bundles only the entries it imports, so either probe could come from an entry the bundle can never carry. The readings below compare the published `@objectstack/spec` 17.4.0 that objectui locks at pin `dd3f7e1be356` with the framework spec. | leg | framework | fresh witness (carried by) | in bundle | stale detector (carried by) | in bundle | old check | | --- | --- | --- | --- | --- | --- | --- | | dark | `f927864ea0` (main before the split) | "A public export added or removed by one release." (`.`) | yes | "Accepted developer agreement version" (`./cloud`) | no | exit 0 | | lit | `fbec216e2d` (`origin/main`) | the same text, now (`./migrations`) | no | the same (`./cloud`) | no | exit 2 | - The witness moved with the split from the root to `./migrations`, which the console never imports. Meanwhile 102 of the 142 injected-only descriptions ARE in that bundle; the first sorts from `./ui`. - The stale leg was already blind. Its detector sits in the published `./cloud` entry, which objectui's shipped source never imports: 116 `/ui`, 62 `/data`, 17 `/kernel` and so on, and no `/cloud`. A console built WITHOUT the injection carries 56 of the 160 published-only descriptions, and not that one. - The spec side is not the cause. The console never imported the change manifest, and the injected spec is in the bundle. `packages/spec` is untouched here. ## The fix (`scripts/console-spec-probes.mjs`, `chooseProbes`) - **Fresh witness:** the first injected-only description the bundle DOES carry. With none carried, "neither spec appears" is still exit 2. - **Stale leg:** judged over EVERY published-only description. It fails when ANY of them is in the bundle, and reports the first one found with the count. A bundle the old single pick flagged is still flagged, so this is strictly stronger. - **Stamp:** keeps its shape and version. With nothing published in the bundle, the stamped detector is the same one `pickProbe` chose, so `check:console-injection`'s cache-hit replay is unchanged. - **Assertion script:** `scripts/assert-console-spec-injection.mjs` calls `chooseProbes` and prints the counts. The exit ladder is unchanged: 0 verified, 1 published spec bundled, 2 unverified. - **Self-test:** `scripts/check-console-injection.mjs` gains battery 13 (roster floor 10 to 11). It runs the real assertion script on fixtures whose first unique candidate is not in the bundle: - a working injection verifies, and its stamp replays green; - a published-only description the old pick skipped fails the build and writes no stamp; - neither side present stays exit 2. I checked that the battery can fail: reverting `chooseProbes` to first-pick semantics made it red with 5 failures, and restoring the fix turned it green again. ## Proof All legs ran against real `vite build`s of the console, with the old and the new assertion on the same bundle. | console build | objectui pin | new check | old check | | --- | --- | --- | --- | | injected spec from `fbec216e2d` | `dd3f7e1be356` | exit 0 (witness from `./ui`; 102/142 present; 0/160 published) | exit 2 | | NOT injected (published spec) | `dd3f7e1be356` | exit 1 (56/160 published-only descriptions present) | exit 2 | | a bundle with no spec text | — | exit 2 ("Neither spec appears") | — | | injected spec from `f927864ea0` (dark) | `dd3f7e1be356` | exit 0 (111/142 present) | exit 0 | | injected spec from `fbec216e2d` | `db11afd49670` (PR objectstack-ai#20706's pin) | exit 0 (102/142; 0/160) | exit 2 | | NOT injected (published spec) | `db11afd49670` | exit 1 (56/160) | exit 2 | On this branch's head, the Console Pin Gate's steps run locally all pass: `bash scripts/build-console.sh` with the injected spec exits 0; the dist presence assert passes; `pnpm check:console-sha` exits 0; and `pnpm check:console-injection --require-stamp` exits 0 and replays the new stamp. ## Verification record - `dispatch-gates --commands` for the 3 changed paths derives 31 families. `--ran` reconciles them as 30 run, all exit 0, and 1 NOT MEASURED: `check:pm-dispatch-gates`, whose self-test alone outruns the 590-second foreground cap here. It does not read these files' behaviour. - `node scripts/check-console-injection.mjs --self-test` passes: 44 assertions, battery floor met. - Lint, narrowed and proven: `eslint --no-inline-config` on the 3 changed files reports 0 errors and 0 warnings. The config enables no type-aware linting, so untouched files' verdicts cannot move. - No changeset: the diff touches only root `scripts/`, which ship in no published package (`@objectstack/spec-monorepo` is private). ## Acceptance notes - **The cache-hit replay's stale leg is still a single stamped detector.** At this pin that detector is `./cloud` text no console bundles, so on a dist-cache HIT the replay cannot catch a published spec. The build-time leg now can. Fixing the replay needs a multi-detector stamp (a `stampVersion` bump that invalidates every cached dist under the unchanged cache key), which is outside this claim. Carrier: the `domain:spec` seat. Noted, not filed. - **CI on this head:** `Console Pin Gate` (job 109696257740) concluded success. The dist cache missed, so step 11 built the console and ran the changed assertion (105 of 145 injected-only descriptions present, all 160 published-only absent), and steps 12 to 14 passed on that fresh dist. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ Co-authored-by: Claude <noreply@anthropic.com>
… — no queued PR left the queue on a draft conversion (objectstack-ai#20845) Fixes objectstack-ai#20764 Clause-②: no ## What changes `.claude/skills/pm-dispatch/references/platform-readings.md`, lines 41-43 only. The rewrite is net 0 lines, and the three new lines are 120 / 118 / 120 bytes (cap 120). | line | before | after | |:--|:--|:--| | `:41` | 转 draft 不是可靠的踢队手段:两向相反读数并存,处置按最坏走。 | 转 draft 不是可靠的踢队手段:本仓与姊妹仓均见已入队转 draft 仍保位照合,处置按最坏走。 | | `:42` | 本仓转 draft 同时掉 auto-merge 与队列成员资格,不自动恢复,转正后重挂;姊妹仓曾保位照合。 | 本仓转 draft 同秒掉 auto-merge 仅见于未入队时,转正后重挂;已入队者 2026-08-08、09-29 照合。 | | `:43` | 补救:转 draft 与卸载 auto-merge 都做 —— 本仓卸载 auto-merge 单独不踢队。 | 补救:转 draft 与卸载 auto-merge 都做,本仓卸载单独不踢队;两手齐做亦未见踢队(08-08、09-28)。 | The remedy on `:43` is unchanged: do both acts. No rule is added. The in-file citations are dates, because `pnpm check:pm-skill-id-lint` goes red on a `#`-number anywhere under `.claude/skills/pm-dispatch/`, and this file cites no PR number today. The PR numbers and timeline events are in this body. That is where the id-lint header puts a reading's provenance. ## The readings (re-readable: `GET /repos/objectstack-ai/objectstack/issues/N/timeline`) I ran a read-only timeline scan over objectstack-ai#4700-objectstack-ai#4900, objectstack-ai#6650-objectstack-ai#6850 and objectstack-ai#20400-objectstack-ai#20840. It looked for two patterns: a `convert_to_draft` between `added_to_merge_queue` and the next `removed_from_merge_queue`, and a `convert_to_draft` on a PR that was armed but not queued. The scan found every row below. The table is complete for those windows only, not for the whole repo. **Queued, then converted to draft.** None of the four left the queue because of the conversion. | PR | queued | draft | disable sent? | what followed | |:--|:--|:--|:--|:--| | objectstack-ai#6732 | 2026-08-08T14:01:50Z | 14:04:32Z | claimed in objectstack-ai#6799's text; no `auto_merge_disabled` event | the queue merged it at 14:38:56Z, 34 min later, still draft | | objectstack-ai#20420 | 2026-09-28T13:43:01Z | 14:02:28Z | claimed in the seat's 14:04Z comment; no event | its own group `pr-20420-3cf6449` had `Lint & Type Check` = failure at 14:01:49Z, before the draft; `github-merge-queue[bot]` removed it at 14:11:24Z, unmerged | | objectstack-ai#20442 | 2026-09-28T13:45:42Z | 14:02:50Z | claimed in the seat's 14:04Z comment; no event | the queue built a new group `pr-20442-b285508` for it at 14:11:29Z, 9 min after the draft; that group's `CI` = failure at 14:30:14Z; removed at 14:35:44Z, unmerged | | objectstack-ai#20695 | 2026-09-29T23:32:51Z | 23:57:02Z | no (the card discloses: draft alone) | the queue merged it at 00:04:21Z, 7 min later, still draft | **Armed, not queued, then converted to draft.** Each one dropped auto-merge. | PR | armed | draft | `auto_merge_disabled` | after | |:--|:--|:--|:--|:--| | objectstack-ai#4745 | 2026-08-03T01:28:46Z | 01:29:30Z | 01:29:30Z (same second) | ready, re-armed 01:30:52Z, queued, merged | | objectstack-ai#6727 | 2026-08-08T13:42:58Z | 13:44:36Z | 13:44:36Z (same second) | ready, re-armed 13:46:02Z | | objectstack-ai#6829 | 2026-08-08T23:37:05Z | 23:40:25Z | 23:40:26Z (1 s) | ready and enqueued 6 h later | Each disable event carries the converter as its actor. A timeline cannot tell whether the platform wrote it or a scripted follow-up did. The line records what was observed ("同秒"). ## Where the old reading came from - The "drops queue membership" reading first appears in `db6581a5` (objectstack-ai#4893, card objectstack-ai#4892, 2026-08-03). It cites no PR. That is the same day objectstack-ai#4745 dropped auto-merge on an armed PR that was not queued. The queue-membership half has no instance behind it. - `cd704cc4` (objectstack-ai#6799) later cited objectstack-ai#6732 for "only draft evicts; disable alone does not". In objectstack-ai#6732's own timeline the PR stays queued after the draft and is merged by the queue 34 minutes later. - The sister-repo half ("姊妹仓曾保位照合", objectui 2026-08-25) is carried over as it was. I did not re-read it. ## How this departs from the triage direction (5903760674) I did not choose these quietly. Each one is a place where a measurement changed what the direction assumed: 1. **`:42` is split by queue state, not "mixed" in the sense of opposite readings.** No queued PR in the scanned windows left the queue because of a draft conversion. The direction assumed the old half had its own measurement. It has none, and the one PR later cited for it reads the other way. What does go both ways in this repo is the effect by state: unqueued, auto-merge drops; queued, the PR keeps its place. 2. **`:43` says "not seen to dequeue", not "not measured".** Three PRs (objectstack-ai#6732, objectstack-ai#20420, objectstack-ai#20442) have both acts claimed in writing, and none was removed by them. The caveat: on a queued PR, the disable leaves no timeline event, so whether it was sent rests on the seats' own comments. 3. **`:41` loses "两向相反读数并存".** No opposite reading survived the re-read. ## Acceptance notes - **Out of scope, same family, Tier H.** `AGENTS.md` states the falsified reading as a premise in three places. Prime Directive objectstack-ai#14 says "draft is what removes queue membership, disabling alone drops only the arming". Multi-agent §7 says "flipping back to draft drops auto-merge and queue membership at once". The "State on your PR" paragraph says the draft flag "flipped back destroys auto-merge and queue membership at once". I did not touch them. They are reported to the seat. - **Eviction.** The only dequeue acts measured in this repo are these two. First, the queue's own ejection after a red group build (objectstack-ai#20420, objectstack-ai#20442). Second, a manual removal by a maintainer with no draft conversion (objectstack-ai#20797, `removed_from_merge_queue` 2026-09-30T07:33:36Z). Whether the remedy should name a different act is a question for the seat. It is not a rule this PR adds. - **Changeset.** `.claude/**` is not in any package's `files[]`, so this PR publishes nothing. `skip-changeset` is the seat's to apply. - **Governed surface, Tier S (`.claude/**`).** This PR stays a draft until a `## Contract review` record at `CONTRACT_REVIEW_TIER` exists for its head. ## Verification (head `02ea176a1`) - The 20 gates from `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` all exit 0. They are `check-closing-keyword-parity` (plus `--self-test`), `check-comment-mask-corpus`, `check-governed-queue-guard --self-test`, `check-harness-current --self-test`, `lint check:doc-formula-expressions` (after building `@objectstack/lint` and its dependency closure under `os-verify-lock.sh`), `check:agent-test-spelling`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:driver-memory-census`, `check:gitlink-declared`, `check:nul-bytes`, `check:pm-governed-merges`, `check:pm-half-states`, `check:pm-skill-id-lint`, `check:pm-skill-ratchet`, `check:refd-timer-probe`, `check:required-contexts`, `check:skill-frame-sync` and `check:watch-hint-literal`. - `pnpm check:pm-settings-deny-roster` was also run, because its roster lives under `.claude`. Exit 0. - `--ran` reconciliation: 20 derived, 20 run, 0 NOT-MEASURED. All 20 recorded an exit code. - `check:pm-skill-ratchet` printed: `platform-readings.md is 469 lines (ceiling 469; headroom 0)`, widest table row 0 bytes (pin 0). - Not measured locally: `check-required-contexts --verify-required-set` and `check-half-states --provenance`, which read the workflow event, and the four CI type-check lanes. This diff touches no TypeScript. ## 维护者速读(草稿) - **改了什么**:PM 协议参考文件 `platform-readings.md` 第 41-43 行,关于「已入队 PR 转 draft 能否踢出合并队列」的三条平台读数。行数不变。 - **为什么改**:旧文说本仓转 draft 会掉出队列。回查时间线后发现,本仓 4 个已入队后转 draft 的 PR 都没有因此出队。其中 2 个带着 draft 状态被队列直接合入,另外 2 个是在自身队列构建变红之后才被移出。旧说法唯一引用的实例,时间线恰好反向。转 draft 会掉 auto-merge,这一点只在「未入队」时成立。 - **风险与代价(含回滚)**:只改文档读数,不改任何规则或代码。补救仍是两手都做。风险在于读者以为有可靠的踢队手段,而实际没有。回滚就是 revert 这一个提交。 - **席位意见**: - **你要做的**:无需操作,由席位按 Tier S 复核后落地。若要让 `AGENTS.md` 里的同一旧说法一并修正(Tier H),需要你点头另开 PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ Co-authored-by: Claude <noreply@anthropic.com>
…le Pin Gate and misses its dist cache (objectstack-ai#20945) Fixes objectstack-ai#20765 Clause-②: no A head that moves the spec's entry layout now runs `Console Pin Gate` on the PR, and misses the dist cache so the gate judges it. Before this, such a head skipped the job on every push, and the first run was the merge-queue build, where a check outside the required set cannot stop a merge (objectstack-ai#20695: `main` red on this check for about 2 h 17 min). ## What changes - **`ci.yml` `filter` job, `console:` list** gains `packages/spec/package.json` and `packages/spec/tsup.config.ts`. The comment above the list now says which entries are build inputs and which are guards, and why the rest of `packages/spec` stays out. - **`ci.yml` `console-pin` job, dist-cache key** (all three spellings: restore, the `CONSOLE_DIST_CACHE_KEY` env, save) now hashes every build input the list names: the pin, `scripts/build-console.sh`, the two probe scripts (`scripts/assert-console-spec-injection.mjs`, `scripts/console-spec-probes.mjs`), and the two spec files. Comments that described the old key are corrected. The restore step name now says what it is keyed on. - **`scripts/check-ci-filter-parity.mjs`** gains a second subject, `judgeConsole`, run by `main()` in both lint.yml legs. It holds five things of the checked-in ci.yml: every `console` entry is a literal path; the key is spelled one way everywhere the job uses it; every hashed path is a `console` entry; every `console` entry is hashed or a declared guard (`CONSOLE_GUARDS`: ci.yml and the two check scripts, which run on a hit and a miss alike); and every declared guard is still listed and not hashed. There is a new self-test battery (8) with 20 cases, and the battery floor goes from 7 to 8. - **`scripts/pm/check-expected-skips.mjs`**: the `Console Pin Gate` row's reason text names the two spec files. Only the prose changes. The row's gate, the job's `if:` and the roster shape are unchanged. - **`scripts/check-console-injection.mjs`** (comments and the `remedy()` fallback text only; seat ruling 5920200169, Q2): the header no longer restates the dist key. It names the two restore steps that each spell one, and says the keys now differ. Its section is renamed "Why the rest of packages/spec is NOT in ci.yml's console filter" and says the spec's entry layout is in both the filter and ci.yml's key. The `remedy()` fallback, used only when `CONSOLE_DIST_CACHE_KEY` is unset, prints `gh cache delete "KEY"` plus a line naming where each workflow spells its key, instead of a stale copy of the old key. - **`.github/workflows/release.yml`** (comment only; no key, step or env change; seat ruling 5920200169, Q1 = B): the comment above the dist restore step says its key hashes the pin and the build script, ci.yml's hashes a superset, the two workflows no longer share dist entries, and the narrower key is deliberate. ## Why the key had to move too (H1, measured) On a cache hit the job skips `Build the Console SPA`, and `assert-console-spec-injection.mjs` runs inside that step. What runs on a hit is `check:console-injection`, which replays the stamp the earlier build wrote. The only thing it reads from the tree is the probe-expiry check over the union of all exports entries, and moving text between entries does not change that union. - **The queue build that went red was a cache MISS.** Run 36645820047, job 109668493655: `Restore vendored Console dist` took 0 s, `Build the Console SPA…` ran 23:37:45 to 23:41:37 and failed, and the only failure annotation is "Process completed with exit code 2" (the assert script's inconclusive exit, "Neither spec appears in the built console"). The job log itself could not be read from this container because the log blob host is refused by the egress policy. - **Fixture replay of the objectstack-ai#20695 shape** (scratch fixture: a root entry whose first injected-only description moves to a new `./migrations` entry the console does not import; the assert and probe scripts as they stood at `9c8f113c~1`): - leg 0: the pre-move build stamps the dist. Exit 0; witness "A public export added or removed by one release." - leg 1, **cache hit** on the post-move head: `node scripts/check-console-injection.mjs --dist dist-pre --spec tree-post --require-stamp` prints "✓ @objectstack/spec: the dist carries this tree's copy, and not the published one." with **exit 0**. The hit is green without judging the change. - leg 2, **cache miss** on the same head: the rebuild prints "✗ Neither spec appears in the built console" with **exit 2**. This is the queue's red. - **How often the cache is cold anyway.** On `merge_group`, 32 of the 38 finished `Console Pin Gate` runs from 2026-09-30T13:46Z to 20:23Z rebuilt, and 6 hit. On the hourly `schedule`, 25 of 30 runs from 2026-09-24 to 09-27 rebuilt. So widening the filter alone would have judged this shape most of the time, but not every time. Moving the key makes the rebuild certain for such a head. ## Which spec paths (H2, measured) What the console build and the probe derivation read from `packages/spec`: - `package.json` `exports`: objectui's `OBJECTSTACK_SPEC_DIST` hook derives one alias per exports entry and refuses a missing target (`apps/console/vite.config.ts`, `scripts/vite-objectstack-spec-dist.ts`), and `readSpecBlob` in `scripts/console-spec-probes.mjs` resolves every entry. - `tsup.config.ts`: its `entries` list decides which built file each exports entry points at, and `build-console.sh` builds the spec through it. - `dist/index.mjs` and `json-schema/openapi.json` are build sentinels. They are generated, not authored. Over the first-parent commits on `main` from 2026-08-31 to `9905e61ca2` (3219 commits, window proven by `scripts/pm/git-history.mjs`), 4 commits moved the entry layout: `fbec216e2` (objectstack-ai#20695), `c23cfb346`, `9165d5cd4`, `776d64cd3`. Every one of them touched both `package.json` and `tsup.config.ts`. `src/index.ts` changed in 5 other commits, all of them content (a new metadata kind, a cycle fix, schema edits, citation re-anchoring), so it is not in the list. `browser-reachable-entries.json` (touched by objectstack-ai#20695) is read only by a lint gate, not by the console build, so it is not in the list either. The triage control holds: no `console` entry reaches `packages/spec/src`. ## Where the pins live (H3) Before this PR, nothing asserted the `console` list. The only readers were `dispatch-gates.mjs` fixtures, and `check-expected-skips.mjs`, which reads outputs and `if:` but never paths. The pins go in `check-ci-filter-parity.mjs`, the gate that already reads and pins the filter job's lists, through the same YAML reader. Its battery (8) pins the following: - A head touching only `packages/spec/package.json` selects the job and moves the key. The same holds for `tsup.config.ts`. - The control: a head touching only `packages/spec/src/ui/view.zod.ts` (a tracked file) does not select the job. - Each way the filter and key can drift turns red on a synthetic tree: a filter entry the key does not hash, a hashed path the filter does not name, a pattern entry, a split key, a stale guard and a hashed guard. There are four refusals, and the report path turns red over the checked-in ci.yml with one hashed path dropped from the filter. ## A bounded in-place fix, declared The two probe scripts were already `console` entries but not in the key, so a head that changed the derivation could also get a green hit without the new derivation ever running. The new rule, applied to the base `ci.yml`, turns red on exactly those two (leg B below). It is the same defect class and the same parameter, a mechanical edit to the same file, and it adds no new verification surface, so it is fixed here. Cost: 1 commit in the 30-day window touched either script. ## Cost - Selection of the job on a pushed head: 37 → 53 of the 3219 commits. The key moves on 13 → 32 of them. - The Version Packages PR bumps `packages/spec/package.json`, so its runs now select the job (93 CI runs on that branch in the same 30 days). The first push of each version rebuilds; later pushes with the same file content hit that PR's own entry. - The queue already runs this job on every `merge_group` build and rebuilds most of the time (above), so the queue's cost barely moves. ## Reverse verification (from the committed state, `1bd002357`) - Leg A, **the hazard itself**: filter widened, key not moved. `node scripts/ablation-replace.mjs --file .github/workflows/ci.yml --anchor NEW_HASHFILES_ARGS --replacement OLD_HASHFILES_ARGS --expect 3 -- node scripts/check-ci-filter-parity.mjs` reported anchor 3 → 0 and blob `24aac400cf2d` → `14c292d64529`. The gate printed "FAIL: ci.yml's `console` filter and the console dist key are out of step", naming the four unhashed entries, and exited 1. The restore brought the blob back to `24aac400cf2d`, equal to HEAD, and `git diff HEAD` was empty. - Leg B, the new rule over the base `ci.yml` (`bee75cebe6`): exit 1, naming exactly `scripts/console-spec-probes.mjs` and `scripts/assert-console-spec-injection.mjs`. That is the in-place fix above. - Direction observed: red, as expected. ## Gates Derived from this diff with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `1bd002357` (57 commands). Exit codes and verdict lines are in the report on objectstack-ai#20765. On-point results: - `node scripts/check-ci-filter-parity.mjs`: exit 0, "OK: all 9 `console` entr(ies) are literal paths; 6 are build inputs the console dist key hashes and 3 are declared guards; the key is spelled one way in all 3 place(s)…" - `node scripts/check-ci-filter-parity.mjs --self-test`: exit 0, 68 assertions. - `pnpm check:pm-expected-skips`, `pnpm check:console-injection`, `pnpm check:console-sha`, `pnpm check:workflow-status-functions`, `pnpm check:workflow-step-name-quoting`, `node scripts/check-self-test-workflow-commands.mjs` (both legs) and `pnpm check:nul-bytes`: all exit 0. - NOT MEASURED, prerequisite not met: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:sourcemap-no-sources-content` need built package `dist/` trees. This diff touches no package source; CI builds before them. - ESLint, narrowed and proven: the population is `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block, which includes both edited `.mjs` files. `--format json` reports 2 files, 0 errors and 0 warnings. The config enables no type-aware linting, so this diff cannot move an untouched file's verdict, and `ci.yml` is not an ESLint input. - `Console Pin Gate` runs on this PR's own head, because `ci.yml` and the key's inputs moved. Its verdict is read from CI. - Patch round at `f57066dd5` (comment and fallback text in `check-console-injection.mjs` and `release.yml`, one `ci.yml` comment): `dispatch-gates.mjs --commands` names the same 57 commands; reconciled with `--ran` (53 run, 4 NOT MEASURED as above). `node scripts/check-console-injection.mjs --self-test` exit 0 (44 assertions); both `remedy()` branches exercised on a scratch dist. ## release.yml's key (seat ruling 5920200169, Q1 = B) `release.yml` keeps its narrower key, and only its comment changes. The two workflows no longer share dist entries. Following ci.yml would rebuild the console on every Version Packages merge, and publishing a cached console whose spec content lags is the existing cache design. ## Acceptance notes - The prose this PR made stale in `scripts/check-console-injection.mjs` and `release.yml` is corrected in the patch round (`f57066dd5`). Neither the header nor the remedy fallback restates the key any more, so `check-ci-filter-parity.mjs`'s hold on ci.yml's spellings is the only copy that matters. - Observation, not filed (a read-only inference with no measured reach): `build-console.sh` copies the tracked `sdui.manifest.json` into the dist, and that file is in neither the key nor the list. `a093ce3e2` changed it without a pin move, so a cached dist would carry the older copy until the pin moves. Carrier: none. - objectstack-ai#20764 (the seat protocol watching non-required queue checks) and making this job a required context are out of scope here, as triage ruled. Body edited by the `domain:spec` seat 2 PM (`session_017VaLJnYwhPsanVCe9dMCJU`) after the patch round, from the dev's wording in its report on objectstack-ai#20765. --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20646
Clause-②: yes (narrowing — the ADR-0087 migration chain and change-manifest names, 17 values and 16 types, leave the package root
@objectstack/specfor the new@objectstack/spec/migrationsentry; the conversion layer stays on the root)This is the source-side payback of the maintainer's ruling on objectui#11088 (decision 1, letter A: raise the console first-screen ceiling now, pay it back at the source). It builds the shape the
domain:specseat ruled on the card (comment 5891704646, option A), under the claim amendment that allows exactly one change tomigrations/registry.ts: the generated D3 block for this narrowing's own ADR-0087 entry.Why the root carried the text
sideEffects: falsewas already declared, so it was not the cause. Each entry ships as one flat module (tsupsplitting: false). Inside it, a consumer's bundler has to keep every top-level call it cannot prove pure, and the migration registry computes things when its module loads: the list of majors, each step's rationale and step 18's conversion ids. So the whole registry (mostly theos migrate metaguidance text) rode in every bundle of the root, whatever the consumer imported. Rolldown (Vite 8's bundler, which the console uses), esbuild and rollup all agree.The conversion layer cannot leave the root chunk:
defineStackandnormalizeStackInputcallapplyConversions, which readsALL_CONVERSIONSat run time. Measured on the round-1 base 0cb72cf, dropping the conversions re-export as well moved the root by 1,828 bytes. It stays.Byte table
Before is the merge base
1a75e39d4a, built in a separate worktree; after is this head. Both are tsup JS builds, gzip -9.dist/index.js(CommonJS root)dist/index.mjsdist/browser/index.mjs(the ESM root a browser bundler pulls).objectui-shafindClosestMatchesindex.mjs:migrations/registry.tswent from 1,761,987 bytes to 0.conversions/registry.tsstays at 321,050.dist/migrations/index.jsis 2,280,591 raw / 616,800 gzip. It carries the chain plus the conversion registry it reads.What changed
packages/spec/src/index.tsstops re-exporting./migrations/index.js.packages/spec/package.jsongains the./migrationsexport (import/require/types, nobrowsercondition), andtsup.config.tsgains the entry.check:browser-reachable-entriesmeasured the entry as linking no Node builtin and no server-only package, so no browser condition is needed.browser-reachable-entries.jsonlists it underunjudged.entries/semantic/18.migrations-entry-split.ts(form D, no tracker number), concatenated bygen:migration-registry. The registry diff against the merge base is that block only: 52 insertions, 0 deletions.conversions/registry.tsis byte-identical.gen:spec-changesandgen:upgrade-guideproduce no diff, because major 18 is not yet in their window.packages/cli/src/commands/migrate/meta.ts;packages/cli/test/migrate-meta-default-range.test.ts,packages/cli/test/migrate-meta-engine-guidance.test.ts,packages/metadata-protocol/src/protocol.stored-migration.test.ts, andpackages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts(onlyapplyMetaMigrationsmoves;ALL_CONVERSIONSstays on the root import).index.tsfiles, covering static, dynamic, namespace and type imports. Repo gates import the registry modules by relative path, so none moves. objectui at the pin imports none of the moved names;cloudwas not measured.api-surface/andexport-origins/have a newmigrations.jsonshard, and 33 names leaveroot.jsonin each.packages/spec/src/root-entry-migrations-split.pin.test.tschecks four things, each negative with a positive control:./migrationsexports all of them, and nothing it exports is on the root;migrationsshard lists all of them;migrations/at all (the edge a bundler follows), and the exports map publishes./migrationsto the built files.No new gate, no byte ceiling.
content/docs/deployment/troubleshooting.mdx's ordered subpath sentence gainsmigrations(check:docs-spec-enumerations).CATEGORY_TITLES.migrationsbecomesMigrations Entry, following theapi-assembledprecedent, so the migrations subpath is counted as a subpath and not as a 16th protocol namespace; theschema-closurepin and its docblock follow.scripts/export-origins.test.ts's entry list gains./migrations.build-migration-registry.tsheader and thetsup.config.tsentry-count note.@objectstack/specminorwith a BREAKING banner, the FROM to TO table and theregistered migrations-entry-splitdisposition;@objectstack/clipatch.Verification record
Heads named per reading. The final head is
8558334ab5, merged withorigin/main1a75e39d4a.Reverse verification of the pin, committed first, mutated through
scripts/ablation-replace.mjs, atc4fdf1105e:./migrationsexports is on the root; the root graph does not reach the registry.Tests:
@objectstack/speclocal project, fullc4fdf1105e@objectstack/specrepo project, 42 of 43 filesc4fdf1105e@objectstack/specbuild-schemas-check-mode.test.ts, the four registry-copying blocksc4fdf1105esrc/migrations,export-originsandschema-closuretests8558334ab5@objectstack/cliunit project, fullc4fdf1105e@objectstack/cliintegration: the two touchedmigrate-metafilesc4fdf1105e@objectstack/metadata-protocolstored-migration testc4fdf1105e@objectstack/service-automationdecision pinc4fdf1105e@objectstack/runtimespec-subpath alias-coverage pinc4fdf1105eNOT MEASURED locally: the other 63 tests of
build-schemas-check-mode.test.ts(the file alone runs about 14 minutes, past the foreground cap); CI runs it.Typecheck:
@objectstack/specpassed atc1dcebf255.@objectstack/cli,@objectstack/metadata-protocoland@objectstack/service-automationpassed atc4fdf1105e.Gates:
node scripts/pm/dispatch-gates.mjs --commandsderives 132 families on the final head.--ranreconciles them as 131 run and 1 NOT MEASURED, with exit codes recorded.5936236fb7, 73 to 110 atc1dcebf255, and on8558334ab5the ratchet and generated-artifact set was re-run together with the 22 docs families the docs rider added.check:api-surface,check:published-files,check:dual-build-cjs-loads(105 require entry points across 66 packages load) andcheck:entry-nameability(494 call probes across 19 public entries).check:generated(all 15 artifacts current),check:adr-0087-registration('registered migrations-entry-split (new here)') andcheck:browser-reachable-entries.check:docs-spec-enumerations(fixed by the docs rider) andcheck:dual-build-cjs-loads(PREREQUISITE NOT MET, cleared by a full build).check:pm-dispatch-gates. Its self-test alone outran the 590-second foreground cap twice on the shared box, and every case it printed passed. The diff does not touchdispatch-gates.mjs.Lint: narrowed and proven, at
8558334ab5:eslint --no-inline-config --format jsonover the 16 changed.ts/.mjsfiles, which is every changed file in eslint's population (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED).eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict. The full-treepnpm lintis CI's.Acceptance notes
migrations: 'Migrations Protocol'would have required listing Migrations as a 16th protocol namespace in the glossary,plugins/packages.mdxand four count claims. I followed theapi-assembledprecedent instead: a published entry that is not a metadata protocol domain is titledEntry.schema-closure.test.tshad pinnedCATEGORY_TITLES.migrationsto containProtocolas corroboration that migrations is NOT declared schema-free. That assertion now pinstoBe('Migrations Entry')andnot.toContain('Vocabulary'). The substantive half,schemaClosureAbsenceIsDeclared('migrations') === false, is untouched.ALL_CONVERSIONSis computed at import time (conversions/registry.ts), so the conversion registry (321,050 bytes of the root) stays in every root bundle, even for a consumer that never callsdefineStackornormalizeStackInput. On the light-consumer probe that is about 40 KB gzip. That would need an edit to the conversion registry, which this card excluded. Carrier: objectui#11101's seat. Noted, not filed.semanticlist.spec-changes.jsonand the upgrade guide do not project major 18 yet, so they are unchanged; they pick the entry up when the protocol major moves.Generated by Claude Code