Repository navigation
fix(cli): os migrate apply on an empty database writes the organization-ownership birth marker, so the first os serve starts - #24875
Conversation
…rker after its flush A deferred boot stores and skips every object before its driver counts it, so the birth plugin of `os migrate apply`'s boot saw nothing created and wrote no marker; the tables were created later, at the confirmed flush, and nothing recorded the birth after it. The first `os serve` then refused the fresh install as a database an earlier release created. `SchemaStack.flushSchemaDdl` now runs the runtime's own birth half (`recordOrganizationOwnershipBirth`) once the deferred DDL is performed, judged on the ledger store's driver counts, which name every table the flush found as existing: a database that was not empty before the apply never receives a marker. A failed write fails the command. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
… a database it did not create Four cases over a real SQLite file through the real command: an empty file is born and admitted; an 18.0 pre-release-shaped and a 17.x-shaped database gain no marker though the flush creates a table in each, and the guard still refuses them; a second apply leaves the one row unchanged. The drop-gate suite builds its v17 stand-in with `os migrate apply` on an empty file, which now writes the birth marker; the stand-in drops it, as the platform-global column-drop suite already does for its served boot. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…grate-apply-birth-marker
…ommand The real-database pins reach the decision; this unit pin reaches the one path they cannot: the tables exist, the marker write fails, and the flush hands the failure to its caller naming the row, the cause and the ceremony that certifies such a database. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 45 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 8c542a89bea8eb6ddab11b600a6f3f2f86111bd4 && git checkout 8c542a89bea8eb6ddab11b600a6f3f2f86111bd4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1676441ba59cc32682ffd41e63e74474ad7fa42 0da73a49d58e7b7b2b7c3f5fb0c8a6f4db81b13d && git checkout -B drift-repro b1676441ba59cc32682ffd41e63e74474ad7fa42 && git merge --no-ff 0da73a49d58e7b7b2b7c3f5fb0c8a6f4db81b13d
node scripts/docs-audit/affected-docs.mjs --json b1676441ba59cc32682ffd41e63e74474ad7fa42
|
…r, with no error channel `BirthLogger` declared an optional `error` and no `warn`, a sink whose contract permits silence (`check:optional-error-sink`). Nothing on this path logs a failure: `recordBirthOfFlushedStore` throws it to its caller, and the runtime's birth half reads `info` alone. The type is now that parameter, `Parameters<typeof recordOrganizationOwnershipBirth>[1]`, so no error member exists to leave silent. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read: card #24827 (body; claim 6288623633; os-dev-report 6290825368). PR #24681's body (merged ① Derived judgments0. Gate verdicts on the head 1. The one question — can a database that was NOT empty before
The cases:
2. The failed-write path. 3. The guard's refusal rule and text, and the ceremony, are unchanged. The runtime diff is the 4. The fixture outside the claim ( 5. The pins. 6. Public surface. 7. A wording gap, no behaviour: the 8. The delta ② Semver level
③ Boundary flagsThe dev's deviations (report 6290825368), each answered:
Not in the report: the patch round itself (①8) carries no os-dev-report on the card and no PR comment at this read; its commit message states the reason, and the dispatch-gates note at the gate's line 232 (REFUSE-WIDE: no path derives Other flags, none blocking: Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Accepted deviations: all as the review's ③1–③7 answers them.
Out-of-scope dispositions (one line each):
Landing: through the merge queue by this seat once every check on this head reads Generated by Claude Code |
Fixes #24827
Clause-②: yes
Basis for the Clause-② line (the claim's):
os serveadmits a database thatos migrate applycreated from empty, which it refuses today. That widens the boot guard's admission set by one writer. The guard's rule and its refusal are unchanged, and no published schema or export moves. The review of record judges that no database the guard should refuse can gain a marker.What changed
PR #24681 states the contract: "Every CLI boot writes the birth half too. … Without it, an install whose first command is
os migrate apply… would be refused by its firstos serve." Foros migrate applyit did not hold. Its boot defers schema DDL, so each driver stores and skips every object before counting it. The birth plugin in its boot saw nothing created and wrote nothing. The tables were created later, at the confirmed flush, and nothing recorded the birth after that.packages/cli/src/utils/schema-migrate.ts:SchemaStack.flushSchemaDdlperforms the deferred DDL, then runs the runtime's own birth half,recordOrganizationOwnershipBirth(already exported by@objectstack/runtime). The newrecordBirthOfFlushedStoreresolves the engine and runs it through the kernel logger, whichDeferSchemaDdlPluginnow keeps from itsinit(). A failed write throws: the error names the row, the cause and the ceremony, soos migrate applyexits 1 and never reports success. It runs only when the boot armed a deferral, because only a deferred boot can have created the store at its flush. This is the one shared seam.os migrate applyis its only caller (see "Other deferred boots" below).recordOrganizationOwnershipBirthdecides exactly as it does at boot. The ledger's store must read as created from empty on its own driver's counts (existingis 0 andcreatedis above 0), with no marker and no table in the v17 shape. The flush re-enters the driver'sinitObjectswith every deferred object. ItshasTableprobe counts each declared table as created or found, so after the flush the counts describe this run's own observation of the database.create_table" also describes a populated database that merely gained a table, and that is the database that must never receive a marker (measured below).packages/runtime/src/organization-ownership-boot-guard.ts: docblock only. The birth plugin's note now says that the one deferred boot that creates tables records the birth after its flush, on the same counts.packages/cli/src/commands/migrate/apply.ts: one comment at the flush.@objectstack/climinor (Clause-②: yes). The runtime change is a comment, so it has no changeset.Contract
The boot guard's verdict on each database state, before (
mainat41646a578) and after (this head). "Apply" isos migrate apply --yes; "serve" isos serveover the same file.mainos serve,mainos serve, this PRdetails.birth.shapeGeneration1)mainos serve)organization_idthe object no longer declares)main; the second apply finds the database in sync and returns before any flushPaths through the same seam, by construction (not separately pinned):
--jsonwithout--yes: the command returns before the flush, so nothing is created and nothing is written. The firstos servecreates the store and writes the birth marker, as before.libsql://) primary: the apply skips schema sync there (deferredDdlUnsupported: 'skip-schema-sync'). The deferral exempts the primary and the flush creates nothing on it, so its counts show nothing created and no marker is written. The first serving boot creates the tables, and the remote face's own counts decide its birth. The rule PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681's review settled in its B1 finding (comment 6279650792) is kept as it is.flushSchemaDdlthrows, naming the row, the cause and the ceremony's two steps, and the apply exits 1. Pinned byschema-migrate.birth-after-flush.test.ts.Zone 2 measurements (the PM's mechanism assumptions)
The card's chain holds. Repro on
main41646a578, over a minimal project (one object,dist/objectstack.json; notexamples/app-showcase):os migrate apply --yes --database-url file:RUN/mig.dbexited 0 and printedCreated/extended 74 table(s).sys_migrationhad 0 rows.os serveexited 1 with the refusal quoted in the table.os serveover a never-touched file listened, and its birth row was written.{"created":0,"existing":0}.[organization-ownership] a datastore created from empty: birth marker written (shape generation 1, platform 18.0.0-next.5);os servelistening; the control unchanged.How "empty before" is known on a deferred boot. The ledger store's driver counts, read after the flush:
{created: 0, existing: 0}{created: 36, existing: 0}create_tablemain-created file, one table dropped){created: 0, existing: 0}{created: 1, existing: 35}create_tableThe returned work reads the same in both rows ("only
create_table"). The counts do not. So the reading is the counts, which name every table the flush found.os migrate applycreatedrepro_widgetand wrote no marker.os servethen exited 1, refusing it as a database "17.x or an 18.0 pre-release" created.Other deferred boots.
flushSchemaDdlhas one caller,os migrate apply(git grepoverpackages/**sources). The otherdeferSchemaDdlcallers are the no-write modes of 18 commands (readOnlyProbe, which never create a table) and the ceremony's two boots (skip-schema-sync, never flushed). None of them creates a table on a deferred boot, so none has this defect. The write modes (--apply,--run,--delete) boot without a deferral, and their own birth plugin decides at boot. Measured once:os migrate meta --stored --applyon an empty file wrote the birth row itself, andos servethen started. The fix is in the shared seam (flushSchemaDdl), so a future caller of the flush gets the birth half too.Drivers. The marker goes through the engine into the ledger the guard reads (
engine.insert('sys_migration', …), the guard's own writer). The Turso remote path is covered under Contract above. Not measured here against a livelibsql://store.Tests
Run on this branch, from the merged head (
0be22e895,mainabd5acdafmerged in), then again on the final head51849a38awhere noted. Exit codes were read from each run's own verdict line, never through a pipe.apply.organization-ownership-birth.integration.test.ts: the real command, a real SQLite file, and the guard's verdict fromassertOrganizationOwnershipMigratedon a boot that creates nothing. 4 passed.sys_migration, the guard is inert there and would admit every file, so the verdict boot composes the platform as the apply does.main: the pre-fixschema-migrate.tsrestored from41646a578(blobf97922f7), then the test file run. (a) and (c, d) went red:expected [] to have a length of 1. Both (b) cases stayed green. The file was restored toHEAD(bloba951e927), andgit diff HEADwas empty.scripts/ablation-replace.mjs --delete):if (!ledgerStoreCreatedFromEmpty(engine)) return null;was deleted fromrecordOrganizationOwnershipBirth, and the runtime was rebuilt.ablation-dist-preflight --absentpassed.expected [ { …(14) } ] to deeply equal []: a false birth row). The other 3 stayed green. The 17.x case held because the retired-column check also stops a birth there.HEAD(blobcf2e9113,git diff HEADempty), the runtime rebuilt, and the preflight found the anchor back indist.schema-migrate.birth-after-flush.test.ts: the failed write fails the flush, naming the row, the cause and the ceremony's two steps; with no engine, nothing is asked. 3 passed.return, and 1 went red. Restored to theHEADblob89c61790,git diff HEADempty.apply-organization-ownership-gate.integration.test.tsbuilds its v17 stand-in withos migrate applyon an empty file. That apply now writes the birth marker, which reads as verified to the drop gate. The stand-in deletes the marker, asorganization-ownership-platform-global-column-drop.integration.test.tsalready does for its served boot.@objectstack/cliunit project, in full: 313 files, 4,635 passed.@objectstack/cliintegration project, filtered tomigrateandorganization-ownership: 61 files (60 passed, 1 skipped), 579 passed, 9 skipped.sqlite-wasm.integration,schema-migrate.deferred-ddl.integrationand the platform-global column-drop suite: 5 files, 24 passed.organization-ownership-boot-guard.test.tsand.kernel.test.ts, 2 files, 32 passed (before and after the merge).51849a38a: the unit pin andtest/vitest-tiers-partition.test.ts, 2 files, 25 passed.typecheck:@objectstack/cli(withcheck:test-typecheck) and@objectstack/runtime, both exit 0.@objectstack/cliwas re-run on51849a38a, exit 0. The new test files are in thetsconfig.jsonprogram (--listFilesOnly: 2 of 2).Gates
node scripts/pm/dispatch-gates.mjs --commandson51849a38aderived 68 commands, and all 68 exited 0.--ranreconciliation: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.0be22e895,check:dual-build-cjs-loadsandcheck:i18n-coveragehad exited 3 (PREREQUISITE NOT MET: nodistfor packages outside the CLI closure). Both exited 0 on the final head.pnpm lint(eslint . --no-inline-config): exit 0 at51849a38a.Test Core/Build Core; the full@objectstack/runtimesuite (its change is a docblock); the CLI integration tier outside themigrate/organization-ownershipfilter.Acceptance notes
apply-organization-ownership-gate.integration.test.ts(its fixture, above).os dev's recover branch and its "v17 shape" text (os dev's in-place upgrade says every refused database is "still in the v17 organization-ownership shape", after an ERROR-level stack dump of the boot refusal — an 18.0 pre-release database (and a database HEAD itself created) is told it is v17 #24833's, which can now follow this card); the[schema-drift]lines;packages/drivers/**;packages/spec/**;content/docs/releases/.content/docs/deployment/cli.mdxis not edited. It says "the first boot that creates a datastore from empty writes a birth marker", which is now true ofos migrate applytoo. A line naming the apply would be a docs nicety, not a correction.examples/app-showcase. Same mechanism and the same refusal text; 74 tables against the card's 71.Generated by Claude Code