Skip to content

fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work - #21550

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21529-absent-db-empty-work
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21529-absent-db-empty-work

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21529
Clause-②: no

On a project whose database does not exist yet, os migrate resume, os migrate recorded-by and os migrate value-shapes now 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 with isMissingTableError and only for the command's own deferred table; fold in the boot journal scan.

Measured at the public door

Fixture: one artifact with os21529_account and os21529_contact (a lookup, so value-shapes has a covered field), --database-url file: a path that does not exist, run as node packages/cli/bin/run-dev.js migrate ... --json. Base 49161683fb; head ce1afa66f6 with @objectstack/runtime rebuilt.

command (--json, absent database) base head
resume exit 1, {"error":"The database refused to run this query for object 'sys_migration_journal'..."} exit 0, {"interrupted":[],"count":0}
recorded-by exit 1, same error for sys_metadata_history exit 0, {"pending":0,"applied":false,...}
value-shapes exit 1, truncated: true, unreadableObjects: [sys_metadata, sys_view_definition, os21529_contact], gatePassed: false exit 0, truncated: false, unreadableObjects: [], gatePassed: true
"Migration journal scan failed" lines, per run 1 0
WARN lines on stderr, per run 6 5
database file left behind none none

Human 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 with hasTable (previewDeferredSchemaWork). bootSchemaStack now exposes that fact as SchemaStack.tableAbsent(objectName). It is true only for a create_table entry, false when the boot did not defer, and cleared by flushSchemaDdl. Each command consults it in its read-only mode only:

  • resume (list mode): sys_migration_journal absent, so there are no runs and findInterruptedRuns is not called.
  • recorded-by (dry run): sys_metadata_history absent, so there are no sentinel rows and findSentinelHistoryRows is not called.
  • value-shapes (scan): the scan reads through a view whose find answers 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 in scannedObjects, 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 in unreadableObjects. ⛔ 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). MigrationRecoveryPlugin cannot see the deferral: #20821 measured that no IDataDriver member or kernel key carries it. So its kernel:ready scan catches the refusal and asks isMissingTableError(err, MIGRATION_JOURNAL_OBJECT). A match means "no runs", logged at debug. Every other failure still warns "scan failed", including a missing relation the scan did not ask about.

No second message regex. packages/spec is untouched.

Documented exit (A4)

  • resume / recorded-by: an os migrate subcommand's --json success exits 0. The source is the platform checklist's migrate item (docs/qa/platform-checklist/areas/cli.json, the os 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 is content/docs/deployment/cli.mdx, "Exit status is 0 only when the self-check passes", the gate value-shapes mirrors.
  • Each is also the exit these commands give the booted control with nothing to do (measured above).

content/docs/deployment/cli.mdx documented 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 that value-shapes, recorded-by and resume answer with empty work. Another dry run can still fail that way (see Out of scope).

Tests

  • CLI pin, new: packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts. It is in the integration tier because it spawns bin/run-dev.js. Every spawn runs in beforeAll, and the cases only read output.
    • On the absent database, for each command, --json and 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.
    • The control is a booted database seeded with one row of work per command: an interrupted journalled run, a sentinel history row, and a stored contact. resume lists the run, recorded-by counts pending: 1, and value-shapes walks the record. This shows the commands still read a table that exists.
  • Runtime pin, new: packages/runtime/src/migration-recovery-plugin.missing-journal.test.ts. It runs a real ObjectQL on a real SQLite SqlDriver, with the platform's SysMigrationJournal registered.
    • With DDL deferred and the table absent, the scan's own read is refused (code: DATABASE_ERROR, status: 500) and the scan says nothing.
    • Control: with DDL performed and an interrupted run, the scan reads and reports it.
    • A dialect refusal naming another relation still warns "scan failed".
  • Runs below are at e860ae7a21 unless 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 at b3f7cf99e4; 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 at 6531dd1450; 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 because packages/cli/dist was absent. After pnpm --filter @objectstack/cli build they passed: 2 files, 29 tests.
    • pnpm --filter @objectstack/cli --filter @objectstack/runtime run typecheck: exit 0, test layers included.
    • The CLI integration tier, limited to the files this diff edits or that spawn or import them, at e860ae7a21: data-commands.absent-database, resume.recorded-by, plan.deferred-reads, preview-read-only, schema-migrate.one-shot-family, schema-migrate.deferred-ddl and schema-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 to HEAD with an empty git diff HEAD.

  • Leg A, "not asked" removed (tableAbsent answers false; the CLI spawns run packages/cli/src through 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.
  • Leg B, the scan's missing-table branch disabled (OS_ABLATION_21529 marker). @objectstack/runtime was rebuilt, and ablation-dist-preflight found the marker in dist/index.js and dist/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 --absent found 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/objectstack derived 94 commands. All 94 exited 0. --ran, with an exit code recorded per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)".
    • On the first pass, five commands exited 3 with PREREQUISITE NOT MET because their packages were not built: check:skill-examples, check:dual-build-cjs-loads and check:i18n / -coverage / -walk-parity. After turbo run build --filter=!@objectstack/docs, all five read 0. The 94 above come from the second pass.
    • check:query-options-erasure was red on the first pass: the test surface grew from 236 to 238, from two as any options in the new runtime pin. Those options are now typed (e860ae7a21), and the gate holds at 236.
  • Run as well: the five roster gates whose list sits under one of these paths (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census). All exited 0.
  • The derivation flags its tree as STALE. origin/main moved 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 ran eslint --no-inline-config --format json on the 8 changed JS/TS files: 8 files, 0 errors, 0 warnings, and no file reported as ignored.
    • The population comes from eslint.config.mjs (files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}), so the .md / .mdx files are outside it.
    • Invariance: the config enables no type-aware linting. It has no parserOptions.project, and every block's parserOptions is { 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.
  • Run under os-verify-lock: pnpm --filter @objectstack/cli --filter @objectstack/runtime run typecheck exited 0. check:nul-bytes is in the 94.

Acceptance notes

Out of scope (reported to the seat, not filed here)

The same "read-only boot reads the table it deferred" shape, measured at b3f7cf99e4 on 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, for sys_audit_log, sys_activity and sys_metadata_audit)
  • os migrate meta --stored (sys_metadata)
  • os secret orphans and os secret rewrap (sys_secret)
  • os storage orphans (sys_file)

files-to-references, summary-nulls, multi-value-columns and duplicates exit 0 there. The triage ruling names three doors; the rest of the family is the seat's to route.


Generated by Claude Code

claude added 4 commits October 3, 2026 04:57
…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
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/runtime, touching 11 documentable anchor(s).

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

  • content/docs/deployment/cli.mdx (via create_table (literal, a string literal in bootSchemaStack), os migrate recorded-by (command, read off packages/cli/src/commands/migrate/recorded-by.ts), os migrate resume (command, read off packages/cli/src/commands/migrate/resume.ts), os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))
  • content/docs/protocol/objectql/types.mdx (via os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))
  • content/docs/upgrading.mdx (via os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))

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

  • content/docs/releases/v17/17-0.mdx (via os migrate value-shapes (command, read off packages/cli/src/commands/migrate/value-shapes.ts))

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 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 43 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 fd5a1cd5973983bf8b1ad69a10148a85c74c9137 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 21805ecd4b2c1704b8619d9d71fa0705cbecc2d0 — the merge of head e860ae7a212c1882f0c0d11cda0d81819c481a4b into base fd5a1cd5973983bf8b1ad69a10148a85c74c9137, 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 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

⚠️ 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 fd5a1cd5973983bf8b1ad69a10148a85c74c9137 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 06:30
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 06:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit aa0d4b9 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21529-absent-db-empty-work branch October 3, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants