Skip to content

feat(cli): os migrate duplicates reports the rows blocking the kernel:ready index tightenings - #11031

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-8725-kernel-ready-migrations-plan-visibility
Aug 22, 2026
Merged

feat(cli): os migrate duplicates reports the rows blocking the kernel:ready index tightenings#11031
os-elon merged 2 commits into
mainfrom
claude/issue-8725-kernel-ready-migrations-plan-visibility

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#8725

Three migrations replace a declared UNIQUE index with the NULL-safe — and sometimes active-rows-only — form it was always meant to have, at kernel:ready on a serving boot:

tableindex(es)migration
sys_metadataoverlay active + draftensureMetadataOverlayIndexes
sys_view_definitionidx_sys_view_def_activeensureViewDefinitionActiveIndex
sys_settingthe declared row identityensureSysSettingIdentityIndex

Each is a tightening, so rows an installation already holds can block it. The migration then refuses under ADR-0120 D4 — previous index kept, no row touched, boot continues — and reports at error on the boot channel.

Why that channel was the only one

Not by omission. These indexes are invisible to the drift differ by construction, twice over:

  • after the tightening, isRuntimeManagedIndex excludes the index, because isSyncReproducibleIndex is false for a partial index and for any key part that is a COALESCE over a non-tenant column. That exclusion is correct — without it a boot would propose rebuilding away the guarantee it had just created;
  • before it, there is no drift to see either: each migration deliberately reuses the DECLARED index's name, so the reconciler's name-matched slot reads as filled whichever physical form is really there.

A prior round measured it end to end with a matched control — one database carrying the same duplicate damage under a declared organization-unique index and under sys_view_definition's runtime one. os migrate plan named the declared one in full and said nothing whatsoever about the runtime one.

The ruling

Maintainer ruling, 2026-08-22: the reporting path is os migrate duplicates, which already boots read-only and owns the "inventory, never repair" contract, keeping os migrate plan's drift contract untouched. plan describes work os migrate apply will do; this work is applied by the next SERVING boot, by a different applier, so a plan line an operator reads as "apply will handle it" would be a promise apply cannot keep.

What lands

  • @objectstack/metadata-protocol gains runtime-index-preflight.tsruntimeIndexProbes() and collectRuntimeIndexPreflight(). The descriptors read each migration's OWN exported builders rather than restating the keys, so the pre-flight and the boot report cannot describe different duplicates. On MySQL the sys_setting probe uses the migration's MySQL spelling, where the bare form is ERROR 1064 on the reserved word key. Four probes from three migrations: the overlay migration builds one index per state and either can be blocked on its own.
  • The report gains runtimeIndexPreflight — one entry per index, each blocked (naming every colliding key group and its row count), clear, table-absent (sys_setting arrives with the optional settings service) or unreadable (with the driver's own message) — plus summary.runtimeIndexesBlocked and summary.runtimeIndexBlockingRows. reportVersion moves 1 to 2: every version-1 field keeps its name, shape and meaning, and the bump says there is more in the document for a consumer that validates it strictly.
  • The new option is REQUIRED, not optional. An optional section defaults to an empty array, and an empty array is also what a clean database produces — so a caller that forgot to wire it would ship a clean bill of health from a probe that never ran. That is the migrate duplicates: no_sql_seam loud-refusal is dead code for the memory driver (false all-clear) #10677 failure one section over; required moves the mistake to a compile error.
  • Liveness is keyed on whether the seam returns a RESULT SET, reusing the sibling migration's isResultSet rather than copying it. A no-op seam would otherwise report all four tightenings as table-absent — the same failure wearing a different status.
  • Nine referral sites repointed, not deleted (the ruling is explicit): three conflict-error strings that told the operator to "run os migrate plan", an instruction the measurement proved false, and the six doc comments that state the same referral as part of the D4 disposition. Their pins now assert both that the new command is named and that the false one is gone.
  • Docs: a new "The kernel:ready index pre-flight" section in content/docs/deployment/cli.mdx, and the QA checklist item for this command updated to the new contract.

Nothing about a migration's behaviour changes. No tightening is armed, deferred or altered, and plan is untouched. The pre-flight only makes the refusal's evidence readable one command before the restart.

Verification

Everything below ran at e9a5893416 (this branch's head, after merging origin/main).

Non-vacuity — the matched control, reused.duplicates.integration.test.ts now carries both damage classes in one database: crm_case.case_number held on both sides of the organization partition (the declared class, reported before this change), and two ACTIVE shared views named crm_case.all_open (the runtime class). A probe that surfaced only the declared class would still satisfy "the report names some duplicate", so both are asserted separately over one run. The archived pair seeded beside them is outside the partial index and is asserted NOT to be reported.

Reverse verification, both directions, each with its rebuild:

legpredictedobserved
pre-flight returns nothing (planted in metadata-protocol, rebuilt into dist, marker proved present)integration test red at the runtime-class assertion, declared-class assertions still greenAssertionError: the pre-flight must cover sys_view_definition: expected undefined to be defined — the failure lands there, after the CASE-00001 and liveConditions assertions passed. Contract test red too (expected [] to deeply equal [ …(4) ])
restore (rebuilt, marker proved absent)greenTest Files 2 passed (2) · Tests 6 passed (6)
the three error strings put back to os migrate planthe five conflict-arm pins redTests 5 failed | 77 passed (82), each expected '[metadata-protocol] cannot …' to contain 'os migrate duplicates'
restoregreenTest Files 3 passed (3) · Tests 82 passed (82)

The ablation legs consume metadata-protocol's built dist, so each leg was rebuilt and proved with scripts/ablation-dist-preflight.mjs before its colour was read.

Read-only, proved LOGICALLY. Schema plus every row of every table, ordered, byte-identical across a full run — in the new unit suite and in the CLI integration test. Deliberately not a file hash: a raw hash over a SQLite file moves on any read-write open and would accuse this command of mutating the install it exists to describe.

Suites:@objectstack/metadata-protocol 1838 passed / 10 skipped; @objectstack/cli 1683 passed; pnpm --filter @objectstack/cli typecheck clean.

Gates: the set derived by node scripts/pm/dispatch-gates.mjs with no path arguments — 27 matched families plus the four convention-triggered ones — re-derived after the merge (identical set) and all run again at this head, every one exit 0, each exit code captured before any pipe. With them, pnpm lint (clean) and the two ratchets:

  • check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger
  • check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 257.7s, 1908 raw tsc error(s) total, none above its recorded number
  • check-nul-bytes: OK (scanned 6389 text file(s) … no raw ASCII control bytes)
  • check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/

One finding for the maintainer, not an edit

ADR-0120 D4 decides that "os migrate plan gains a duplicate pre-flight probe per affected index". That sentence was written for the DECLARED class, which the differ can see and plan does report. This ruling routes the runtime-managed class — the one D4's mechanism cannot reach — to os migrate duplicates instead. The three migrations' doc comments cite D4's disposition while now naming a different command, so ADR-0120 D4 may want an amendment recording the split. docs/adr/ is a governed surface and nothing here touches it.


Generated by Claude Code

…el:ready index tightenings
Three migrations replace a declared UNIQUE index with the NULL-safe — and
sometimes active-rows-only — form it was always meant to have, at `kernel:ready`
on a serving boot: `ensureMetadataOverlayIndexes` (`sys_metadata`, one index per
overlay state), `ensureViewDefinitionActiveIndex` (`sys_view_definition`) and
`ensureSysSettingIdentityIndex` (`sys_setting`). Each is a tightening, so rows
an installation already holds can block it; the migration then refuses under
ADR-0120 D4 — previous index kept, no row touched, boot continues — and reports
at `error` on the boot channel.
That channel was the only one, and not by omission. These indexes are invisible
to the drift differ by construction, twice over: after the tightening
`isRuntimeManagedIndex` excludes the index (without that exclusion a boot would
propose rebuilding away the guarantee it had just created), and before it each
migration deliberately reuses the DECLARED index's name, so the reconciler's
name-matched slot reads as filled whichever physical form is really there. A
prior round measured it with a matched control — one database carrying the same
duplicate damage under a declared organization-unique index and under
`sys_view_definition`'s runtime one — and `os migrate plan` named the declared
one in full while saying nothing whatsoever about the runtime one.
Maintainer ruling, 2026-08-22: the reporting path is `os migrate duplicates`,
which already boots read-only and owns the "inventory, never repair" contract,
keeping `os migrate plan`'s drift contract untouched. `plan` describes work
`os migrate apply` will do; this work is applied by the next SERVING boot, by a
different applier.
What lands:
* `@objectstack/metadata-protocol` gains `runtime-index-preflight.ts` —
`runtimeIndexProbes()` and `collectRuntimeIndexPreflight()`. The descriptors
read each migration's OWN exported builders rather than restating the keys,
so the pre-flight and the boot report cannot describe different duplicates,
and the `sys_setting` probe uses the migration's MySQL spelling on MySQL,
where the bare form is ERROR 1064 on the reserved word `key`.
* The report gains `runtimeIndexPreflight` (one entry per index:
blocked/clear/table-absent/unreadable, a blocked one naming every colliding
key group and its row count) plus `summary.runtimeIndexesBlocked` and
`summary.runtimeIndexBlockingRows`. `reportVersion` moves 1 → 2: every
version-1 field keeps its name, shape and meaning, and the bump says there is
more in the document for a consumer that validates it strictly.
* `collectDuplicateIdentifierReport`'s new option is REQUIRED rather than
optional. An optional section defaults to `[]`, and `[]` is also what a clean
database produces, so a caller that forgot to wire it would ship a clean bill
of health from a probe that never ran — the #10677 failure, one section over.
Required moves that mistake to a compile error.
* Liveness is keyed on whether the seam returns a RESULT SET, reusing the
sibling migration's `isResultSet` rather than copying it. A no-op seam would
otherwise report all four tightenings as `table-absent`, which is that same
failure again wearing a different status.
* Nine referral sites repointed, not deleted (the ruling is explicit): three
conflict-error strings that told the operator to "run `os migrate plan`" — an
instruction the measurement proved false — and the six doc comments that state
the same referral as part of the D4 disposition. Their pins now assert both
that the new command is named and that the false one is gone.
Nothing about a migration's behaviour changes: no tightening is armed, deferred
or altered, and `plan` is untouched. The pre-flight only makes the refusal's
evidence readable one command before the restart.
Read-only is pinned LOGICALLY — schema plus every row, ordered — in both the new
unit suite and the CLI integration test, never by a file hash: a raw hash over a
SQLite file moves on any read-write open and would accuse this command of
mutating the install it exists to describe.
Fixes#8725
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/metadata-protocol, touching 30 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via runtimeIndexPreflight (symbol), os migrate duplicates (command))
  • content/docs/deployment/seed-tenancy-repair.mdx(via os migrate duplicates (command))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os migrate duplicates (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cec9d239dd8858b759f2d62b4119b1d09dc90a60packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0dc6d8466d2ad037b77502456789c7c4b2cf2be5 — the merge of head e9a589341679af4e174cab28b9bd28bb41ddb87c into base cec9d239dd8858b759f2d62b4119b1d09dc90a60, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0dc6d8466d2ad037b77502456789c7c4b2cf2be5 && git checkout 0dc6d8466d2ad037b77502456789c7c4b2cf2be5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cec9d239dd8858b759f2d62b4119b1d09dc90a60 e9a589341679af4e174cab28b9bd28bb41ddb87c && git checkout -B drift-repro cec9d239dd8858b759f2d62b4119b1d09dc90a60 && git merge --no-ff e9a589341679af4e174cab28b9bd28bb41ddb87c
node scripts/docs-audit/affected-docs.mjs --json cec9d239dd8858b759f2d62b4119b1d09dc90a60

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cec9d239dd8858b759f2d62b4119b1d09dc90a60 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.30s
    

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

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

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

@os-elon
os-elon deleted the claude/issue-8725-kernel-ready-migrations-plan-visibility branch August 22, 2026 11:51
os-elon pushed a commit that referenced this pull request Aug 22, 2026
…duplicates-inventory
check:platform-checklist was red on main: the item carried revision 3 but its
history array held only two entries, the last at revision 2. #11031 made a
genuine semantic edit to the item's contract (reportVersion 1 -> 2, the
runtimeIndexPreflight jq walk step, the kernel:ready seeding step, and a new
acceptance clause for the kernel:ready pre-flight) and bumped the revision,
but never appended the matching history entry.
The revision bump is correct and stays; this adds the missing revision-3
history entry describing what #11031 actually changed, citing #8725/#11031
as provenance, matching the sibling entries' shape and voice.
Fixes#11037
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
…n cli.migrate-duplicates-inventory (objectstack-ai#11047)
check:platform-checklist was red on main: the item carried revision 3 but its
history array held only two entries, the last at revision 2. objectstack-ai#11031 made a
genuine semantic edit to the item's contract (reportVersion 1 -> 2, the
runtimeIndexPreflight jq walk step, the kernel:ready seeding step, and a new
acceptance clause for the kernel:ready pre-flight) and bumped the revision,
but never appended the matching history entry.
The revision bump is correct and stays; this adds the missing revision-3
history entry describing what objectstack-ai#11031 actually changed, citing objectstack-ai#8725/objectstack-ai#11031
as provenance, matching the sibling entries' shape and voice.
Fixesobjectstack-ai#11037
Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Aug 23, 2026
…dex class
D4 decides that `os migrate plan` gains a duplicate pre-flight probe per
affected index. That was written for the declared class — the `recreate_index`
drift ops the reconciler can see — and it is true there. It cannot reach the
three `kernel:ready` runtime index tightenings in `packages/metadata-protocol`,
which are invisible to the drift differ by construction.
The maintainer's 2026-08-22 ruling on #8725 routes that class to
`os migrate duplicates` instead, explicitly leaving `os migrate plan`'s drift
contract untouched. PR #11031 shipped that behaviour; this records the split in
D4's text, where the three migration modules' doc comments already point.
Documentation only: no behaviour, gate or test changes.
Governed surface (docs/adr/**) — draft PR, maintainer merges by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

2 participants

@os-elon@claude