fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work - #21550
Conversation
…wer a fresh project with empty work The read-only boot of these three commands defers schema DDL, and the driver already measures which tables the deferred sync would create (`create_table`, decided by `hasTable`). The commands now consult that measurement (`SchemaStack.tableAbsent`) and do not read a table their boot found absent: no runs to list, no sentinel rows, no stored values. The boot's migration journal scan reads a missing journal table as "no runs" through `isMissingTableError`, for that table only. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…t journal scan Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…hangeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ournal pin Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 43 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 21805ecd4b2c1704b8619d9d71fa0705cbecc2d0 && git checkout 21805ecd4b2c1704b8619d9d71fa0705cbecc2d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd5a1cd5973983bf8b1ad69a10148a85c74c9137 e860ae7a212c1882f0c0d11cda0d81819c481a4b && git checkout -B drift-repro fd5a1cd5973983bf8b1ad69a10148a85c74c9137 && git merge --no-ff e860ae7a212c1882f0c0d11cda0d81819c481a4b
node scripts/docs-audit/affected-docs.mjs --json fd5a1cd5973983bf8b1ad69a10148a85c74c9137
|
Fixes #21529
Clause-②: no
On a project whose database does not exist yet,
os migrate resume,os migrate recorded-byandos migrate value-shapesnow answer with empty work and exit 0. Before, each exited 1 with "The database refused to run this query", from its own first read of a table its read-only boot had just deferred. The boot's migration journal scan no longer warns on such a database either.This follows triage's ruling
5965283666(the #20821 direction, applied to these three doors): prefer "not asked"; where a read cannot be avoided, recognise its refusal only withisMissingTableErrorand only for the command's own deferred table; fold in the boot journal scan.Measured at the public door
Fixture: one artifact with
os21529_accountandos21529_contact(alookup, sovalue-shapeshas a covered field),--database-url file:a path that does not exist, run asnode packages/cli/bin/run-dev.js migrate ... --json. Base49161683fb; headce1afa66f6with@objectstack/runtimerebuilt.--json, absent database)resume{"error":"The database refused to run this query for object 'sys_migration_journal'..."}{"interrupted":[],"count":0}recorded-bysys_metadata_history{"pending":0,"applied":false,...}value-shapestruncated: true,unreadableObjects: [sys_metadata, sys_view_definition, os21529_contact],gatePassed: falsetruncated: false,unreadableObjects: [],gatePassed: trueHuman mode was the same before and after: exit 1 before, exit 0 after.
The control, a booted database (
os migrate apply --yes, nothing to do), gave exit 0 and the same three documents on base and on head, with 4 WARN lines per run. The one WARN line that remains on the absent database is[ObjectQLPlugin] sys_metadata_activation is registered but could not be read. It is a boot reader's own line, which the #20821 change kept on purpose; see Acceptance notes.The change
Not asked (the three commands). The read-only boot already measures which tables are absent. The held-back sync lists each one as
create_table, and the SQL driver decides that withhasTable(previewDeferredSchemaWork).bootSchemaStacknow exposes that fact asSchemaStack.tableAbsent(objectName). It is true only for acreate_tableentry, false when the boot did not defer, and cleared byflushSchemaDdl. Each command consults it in its read-only mode only:resume(list mode):sys_migration_journalabsent, so there are no runs andfindInterruptedRunsis not called.recorded-by(dry run):sys_metadata_historyabsent, so there are no sentinel rows andfindSentinelHistoryRowsis not called.value-shapes(scan): the scan reads through a view whosefindanswers a measured-absent table with no rows, without issuing the read. The report has the same shape as a booted empty database (the objects are still listed inscannedObjects, with 0 records). One line names the objects that were not read: on stdout in human mode, on stderr under--json.A table that exists but lacks a column (
add_columns) is still read, and any refusal there still lands inunreadableObjects. ⛔ No refused read is demoted. The write modes (resume --run,--apply) boot plain, so their tables exist and every read is real. Human mode says that the table is not there yet, so it does not imply the command looked through one.The predicate (the boot scan).
MigrationRecoveryPlugincannot see the deferral: #20821 measured that noIDataDrivermember or kernel key carries it. So itskernel:readyscan catches the refusal and asksisMissingTableError(err, MIGRATION_JOURNAL_OBJECT). A match means "no runs", logged atdebug. Every other failure still warns "scan failed", including a missing relation the scan did not ask about.No second message regex.
packages/specis untouched.Documented exit (A4)
resume/recorded-by: anos migratesubcommand's--jsonsuccess exits 0. The source is the platform checklist's migrate item (docs/qa/platform-checklist/areas/cli.json, theos migrate成功时退出码是随机非零值(208/171/176/163/62…),--version/--help却干净退出 0 #4873 clause: "EVERY migrate subcommand with --json exits 0 on success").value-shapes: a clean scan exits 0. The source iscontent/docs/deployment/cli.mdx, "Exit status is0only when the self-check passes", the gatevalue-shapesmirrors.content/docs/deployment/cli.mdxdocumented the old behaviour for the data commands: "A dry run pointed at a database that lacks a table it reads ... can fail and exit 1". That paragraph now says thatvalue-shapes,recorded-byandresumeanswer with empty work. Another dry run can still fail that way (see Out of scope).Tests
packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts. It is in the integration tier because it spawnsbin/run-dev.js. Every spawn runs inbeforeAll, and the cases only read output.--jsonand human mode: exit 0, the empty-work document, zero refused reads of the command's own tables, zero "journal scan failed" lines, and no file created.resumelists the run,recorded-bycountspending: 1, andvalue-shapeswalks the record. This shows the commands still read a table that exists.packages/runtime/src/migration-recovery-plugin.missing-journal.test.ts. It runs a realObjectQLon a real SQLiteSqlDriver, with the platform'sSysMigrationJournalregistered.code: DATABASE_ERROR,status: 500) and the scan says nothing.e860ae7a21unless marked.pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 src/commands/migrate/data-commands.absent-database.integration.test.ts: 13 passed (run atb3f7cf99e4; the later commits touch the runtime test, the docs and the changeset only).pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2: 316 files, 5150 passed, 19 skipped (run at6531dd1450; the commit after it types two test options).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 251 files. 249 passed on the first run. The other 2,test/published-subpath-*.pin.test.ts, were a PREREQUISITE NOT MET becausepackages/cli/distwas absent. Afterpnpm --filter @objectstack/cli buildthey passed: 2 files, 29 tests.pnpm --filter @objectstack/cli --filter @objectstack/runtime run typecheck: exit 0, test layers included.e860ae7a21:data-commands.absent-database,resume.recorded-by,plan.deferred-reads,preview-read-only,schema-migrate.one-shot-family,schema-migrate.deferred-ddlandschema-migrate.readonly-probe. 7 files, 111 passed, 1 skipped (the live PostgreSQL leg, which is not provisioned here). NOT MEASURED: the rest of the cli integration tier. Reason: it runs past the 10-minute foreground cap here, so it is declared to CI.Reverse verification
Each leg mutated a committed tree through
scripts/ablation-replace.mjs. The anchor hit 1 to 0, the blob changed, and the leg was restored toHEADwith an emptygit diff HEAD.tableAbsentanswers false; the CLI spawns runpackages/cli/srcthrough tsx, so there is no dist hop). Prediction: every absent-database case red, the control green. Result: 7 failed, 6 passed. All 7 are absent-database cases (exit 1, refused reads), and all 6 control cases are green.OS_ABLATION_21529marker).@objectstack/runtimewas rebuilt, andablation-dist-preflightfound the marker indist/index.jsanddist/index.cjs. Prediction: the runtime "says nothing" case red, and the CLI "the boot scan does not warn" case red for each command. Result: runtime 1 failed, 3 passed. CLI 3 failed, 10 passed: exit codes and documents stayed green, and only the journal-scan cases went red. Restore: rebuilt, preflight--absentfound the marker in none of the 6 built files, and the tree is clean.Gates
All gates below were run at
e860ae7a21, after the final commit.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 94 commands. All 94 exited 0.--ran, with an exit code recorded per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)".check:skill-examples,check:dual-build-cjs-loadsandcheck:i18n/-coverage/-walk-parity. Afterturbo run build --filter=!@objectstack/docs, all five read 0. The 94 above come from the second pass.check:query-options-erasurewas red on the first pass: the test surface grew from 236 to 238, from twoas anyoptions in the new runtime pin. Those options are now typed (e860ae7a21), and the gate holds at 236.check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check:route-ledger-census). All exited 0.origin/mainmoved 6 commits after this branch was cut, and two of the files it derives from changed in that range (scripts/codemod/view-to-viewitem.mjs,scripts/engine-double-contract.pinned.json). None of those commits touches a file in this diff. CI runs on the merge ref.pnpm lint, as a proven narrowing. I raneslint --no-inline-config --format jsonon the 8 changed JS/TS files: 8 files, 0 errors, 0 warnings, and no file reported as ignored.eslint.config.mjs(files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}), so the.md/.mdxfiles are outside it.parserOptions.project, and every block'sparserOptionsis{ ecmaVersion, sourceType }(the config states the same thing, around line 328). So this diff cannot move the verdict on any file it does not touch.os-verify-lock:pnpm --filter @objectstack/cli --filter @objectstack/runtime run typecheckexited 0.check:nul-bytesis in the 94.Acceptance notes
[ObjectQLPlugin] sys_metadata_activation is registered but could not be read — packaged-ACTION enable/disable is UNAVAILABLE. It is a boot reader's own functional line, which [finding]os migrate planagainst a database that does not exist yet prints 6[sql-driver] DATABASE_ERROR … no such tablewarnings: the dry run defers the DDL, then its boot readssys_metadata,sys_metadata_activationandsys_migrationanyway #20821 kept on purpose ("on a dry run against an absent file it is a false alarm"). It is not one of the three doors.value-shapesin human mode printsScan clean (861):timer.elapsed()is a bare millisecond count. This was already there before this change and is untouched.SchemaStack.tableAbsentis a CLI-internal member, not a published export surface. Nopackages/speckey, accept-set or published type changes.Out of scope (reported to the seat, not filed here)
The same "read-only boot reads the table it deferred" shape, measured at
b3f7cf99e4on the same absent-database fixture (--json). The CLI source has not changed since. Each exits 1, from a refused read of its own table:os migrate account-issuer(sys_account)os migrate audit-metadata-bodies(failures: 3, forsys_audit_log,sys_activityandsys_metadata_audit)os migrate meta --stored(sys_metadata)os secret orphansandos secret rewrap(sys_secret)os storage orphans(sys_file)files-to-references,summary-nulls,multi-value-columnsandduplicatesexit 0 there. The triage ruling names three doors; the rest of the family is the seat's to route.Generated by Claude Code