Repository navigation
feat(runtime,cli)!: os serve refuses a v17-shape database until the organization-ownership ceremony has verified it (ADR-0131 D10 item 5, C7b S5) - #23461
Conversation
…shape database until the ownership ceremony is verified (ADR-0131 D10 item 5, C7b S5) WIP: tests not yet run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…rified marker and a fresh database boot (ADR-0131 D10 item 5, C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…5, C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…e docs for the boot refusal; changeset wording (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…hape (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 33 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 153 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 b6534e3bdec9064867a70ca85dc973ec10ae0f12 && git checkout b6534e3bdec9064867a70ca85dc973ec10ae0f12
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 ba6d9c5de89ffb42a0454b852d3e41838648be8a && git checkout -B drift-repro 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 && git merge --no-ff ba6d9c5de89ffb42a0454b852d3e41838648be8a
node scripts/docs-audit/affected-docs.mjs --json 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02
|
…dges SqliteWasmDriver, the default SQLite driver of a served file database (C7b S5) Measured on the showcase composition: the database is served by SqliteWasmDriver, whose knex client is a dialect class, not a string. The guard read config.client alone, resolved no dialect and skipped every table, so a v17-shape database booted. SqlDriver.dialectName is the public, structural answer; the client string stays the fallback. Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…fused, the ceremony completes with no boot between, then boots (C7b S5) Measures the order the seat asked for: the refusal fires at kernel:ready, after every plugin's start(), where the additive schema sync runs, so the refused boot has already added sys_user_permission_set.permission_set, the column the ceremony waits for. No deadlock. Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…s an operator's next process does (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
…sal pin (check:test-source-alias) (C7b S5) Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: What I read:
For context I also read main and this head in the scratch checkout (the kernel, Gates on this head: 49 check-runs, 0 failing, 0 pending.
① Derived judgmentsThe guard's verdict logic is right on all five cases.
The predicate is structural, and wider than the ruling's list: non-blocking. It is "object has tenant false, declares no
That is a defensible reading of D7, where membership is decided by the writer and the platform-global paragraph is part of the decision. The cost: a database that was never v17, whose object was flipped to tenant false after its table was created, is refused with "v17" wording. The way out is the ceremony (application-default fate) or dropping the column. That path is not measured here. The
Composition by
No path skips the refusal: right.
Other hosts:
Constant moved to
Error code: right.
Regenerated surface: right.
Dogfood dependency and source alias: right.
② Semver levelThe changeset matches what publishes.
The ADR-0087
③ Boundary flagsOpen questions in the three reports:
"Lands together with S2" is only stated, not enforced.
If S5 lands without S2:
If S2 lands without S5 (the ruling forbids this): the family stops declaring its column with no refusal behind it. Landing condition. This is the seat's act, not a fix to this head:
The ordering claim holds.
Escalations for the seat (non-blocking):
Implemented-by: VERDICT: PASS Generated by Claude Code |
… the organization-ownership ceremony (#23429 E1) Seat answer 6171152049 (E1 → S5). A v18 runtime declares no organization column on the ceremony's column-drop tables, so the drift report offered each physical one to `os migrate apply --allow-destructive`; dropped ahead of the ceremony it skips the promotion and report steps, turns legacy organization rows into environment rows, and leaves the v18 boot refusal no column to find — the escape hatch D10 item 5 forbids. `os migrate apply` now refuses `drop_column organization_id` on any table in the ceremony's inventory while marker adr-0131-organization-ownership is not verified, naming `os migrate organization-ownership`; exit 1, and under --json `refused_organization_ownership_unmigrated` with the tables (the #17440 / #23281 shape). No flag stands it down. The ceremony's own column-drops step applies its entries directly and is not gated. Pins (integration, real CLI over a v17-shape sys_platform_setting): refused with no marker and with an unverified one; an unrelated destructive drop on the same table is not gated; the drop proceeds once the marker is verified. Ablation: forcing the gate's table set empty turns both refusals red. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
Contract reviewServed-tier: Scope: a delta review. It covers
What I read.
Gate verdicts on
① Derived judgmentsE1:
The merge with
② Semver levelThe levels match.
The line appears in the changeset and the body. The body does not yet describe the second narrowing (see ③). The ADR-0087 marker is still right. It reads
The changeset's text does not match what publishes (blocking fix 3, prose only).
③ Boundary flagsThe
The ablation and the local counts are the dev's. CI's verdict on the pins is in the gate summary above. The #23461 body note, "Where the refusal sits", which names the wrong boot.
The other items in 6179430370.
Earlier escalations.
Escalation to the seat: the landing form (6171152049, "two PRs queued together") meets the same collision.
The landing conditions in 6170906686 still hold. The e2e pin is green only beside S2, and Implemented-by: VERDICT: FAIL
Scope of the re-review: the delta after this head (the Generated by Claude Code |
|
Superseded — closing unmerged. Seat 1 ( Per landing ruling 6180024026 on #23429, S2 and S5 land as one PR, #23469. Both register a dispatcher error code and regenerate the same
Nothing more is pushed to Generated by Claude Code |
Fixes #23429 · Part of #15211
Clause-②: yes (narrowing)
What changed
The v18 boot refusal (ADR-0131 D10 item 5; ruling 6163114098, Q3 → A).
OrganizationOwnershipBootGuardPlugin(packages/runtime/src/organization-ownership-boot-guard.ts). Atkernel:readyit asks one question: does a table whose object declares no organization column (systemFields.tenant: false, no declaredorganization_id) still carry a physicalorganization_id? Schema sync only adds columns, so on a database a v17 runtime created the answer stays yes until the ceremony's column-drops step runs. If the answer is yes, the guard reads the ceremony'ssys_migrationmarkeradr-0131-organization-ownership. Unless that marker is verified, it throwsOrganizationOwnershipBootRefusal(codeORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED). The refusal names the tables andos migrate organization-ownership, and it stopsbootstrap()beforekernel:listeningopens the socket.os serveonly (packages/cli/src/commands/serve.ts), besideMigrationRecoveryPlugin. It is never increateStandaloneStackordata-migration-plugins.ts: those are the CLI data and schema boots, the ceremony's own boot among them, and a guard there would refuse the one command that clears it.ORGANIZATION_OWNERSHIP_MIGRATION_IDmoves to@objectstack/spec/system(migration.zod.ts) so that the runtime and the ceremony read one constant. The ceremony re-exports it.ERROR_CODE_LEDGERunder@objectstack/runtime. No door answers it: the refusal is thrown before any request is served. The generated api surface, export origins and error-code docs are regenerated.already-registered, naming the step-18 column-retired entries of the tables it reads. The metadata family's four entries land with S2.Where the refusal sits, and why it is its own plugin
The card names the
kernel:readyreader shape ofmigration-recovery-plugin.ts, refusing where that plugin warns. The guard takes that shape: the same hook, the same engine reads, and the same "inert without an engine" rule. It is a sibling plugin rather than code insideMigrationRecoveryPluginfor one reason:MigrationRecoveryPluginis also composed by the CLI data-migration boot (packages/cli/src/utils/data-migration-plugins.ts), the boot the ceremony runs on. A refusal there would refuse the ceremony itself.Landing
sys_metadatafamily's no-column schema), and only after feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211's S1 (feat(cli): the organization-ownership ceremony carries the sys_metadata organization layer into the environment, and a choice deletes only the rows the reviewed plan listed (ADR-0131 D10, C7b S1) #23452) is onmain(6163669810).mainthe family still declares its column, so the guard would not judge the family at all. S2 makes the familytenant: false, and only then does the guard cover it.Pins
organization-ownership-boot-guard.test.ts: the unit pins over a real SQLite database.kernel:ready;organization-ownership-boot-guard.kernel.test.ts(new): the same verdicts on a realObjectKernel(SQL driver, ObjectQL, platform objects, the guard).bootstrap()with the refusal, and the kernel never reachesrunning;The kernel pin exists because the unit pins call the hook by hand, and no unit pin proved that the throw stops a real boot.
Added after the first report (os-dev-report 6168648434):
organization-ownership-boot-refusal-order.dogfood.test.ts, an end-to-end pin of the upgrade order. It checks five things in turn:sys_user_permission_set.permission_set, because the refusal fires atkernel:ready, after every plugin'sstart()and its additive schema sync;os migrate organization-ownership --apply --backup-takencompletes with no boot in between;So no deadlock is possible. The pin is green only beside S2: it was 4/4 on a scratch merge of S5 + S2, and again of S5 + S2 + S3, and red on S5 alone by design, because the family still declares its column there. Dogfood runs on
main's full battery, not on PR CI, and S5 lands only with S2.A real guard defect, fixed: the guard read only the driver's
config.client. A served file database runs onSqliteWasmDriver, whose client is a dialect class, so the guard was inert on the default SQLite driver. It now readsSqlDriver.dialectName, falling back to the client string, with a unit pin.The dogfood package gains an
@objectstack/runtimedependency, and an anchored source alias for it in itsvitest.config.ts(check:test-source-alias).Verification
Latest head
e39386f2cd: the guard unit and kernel pins pass, 14 tests. Runtime and dogfood typecheck exit 0. PR CI is green, 37 of 37 with 5 skipped. The first round follows, all on head540c5b3cof this branch (origin/mainmerged once at24f4483dbefore work).pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2: 378 files, 6,276 passed, 19 skipped.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 4,278 passed, 29 skipped. Two files needed a builtcli(published-subpath-console.pin,published-subpath-hook-body.pin). Afterpnpm --filter @objectstack/cli buildthey pass, 29 tests. Theintegrationlayer is left to CI.pnpm --filter @objectstack/runtime typecheckandpnpm --filter @objectstack/cli typecheck: exit 0.pnpm --filter @objectstack/spec check:generated --fix: three artifacts were stale (api-surface, export-origins, docs) and are regenerated.vitest run src/api/error-code-ledger.test.ts: 21 passed.node scripts/ablation-replace.mjs. The guard'sif (tables.length === 0) return;was replaced by an unconditional return. The kernel pin went red as predicted: the two refusal cases failed and the two boot cases stayed green. The file was restored, and its blob equalsHEAD(git diff HEADempty).node scripts/pm/dispatch-gates.mjs --commandsderived 116 families, and all 116 ran (--ranreconciles: 0 unrun). 109 exited 0. Seven are NOT MEASURED, because they refused on a local prerequisite before reading anything; CI runs them:check-plugin-teardown-shape --self-test: the shallow clone;check:skill-examples,check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parityandcheck:type-check-debt: unbuilt packages.node scripts/check-adr-0087-registration.mjs: the changeset carries its disposition.Body updated by
domain:engineseat 1 (session_01Rerax7QTjKMPCUZxQUtPFR), on the dev's request (6168648434, open question 1 → A).🤖 Generated with Claude Code
Generated by Claude Code