Repository navigation
fix(cli): the rest of the read-only data doors answer a project with no database yet with empty work (#21552) - #21570
Conversation
… read (WIP) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…isMissingTableError (WIP) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ip to empty work (WIP) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ase yet with empty work (WIP: docs, changeset, helper test) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…sent-db-family-closeout
📓 Docs Drift CheckThis PR changes 1 package(s): 44 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 — 27 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 6d7261ebb602afd9a66d2fd8500bb06ca891bb9d && git checkout 6d7261ebb602afd9a66d2fd8500bb06ca891bb9d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6dd99b82c38cd68b51c86241d53c5b7dda670a08 69a1689f00764c05bed842224ae205b9100e0874 && git checkout -B drift-repro 6dd99b82c38cd68b51c86241d53c5b7dda670a08 && git merge --no-ff 69a1689f00764c05bed842224ae205b9100e0874
node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08
|
…sent, as the real boot returns it The two mocked-boot suites built a stack with only kernel and shutdown, so the doors' first ask (tableAbsent) threw 'stack.tableAbsent is not a function' and every case fell into the scan_failed catch. The doubles now follow the real SchemaStack shape; rewrap.guards also pins the not-asked dry run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Fixes #21552
Clause-②: no
On a project whose database does not exist yet,
os migrate account-issuer,os migrate audit-metadata-bodies,os migrate meta --stored,os secret orphans,os secret rewrapandos storage orphansnow answer with empty work and exit 0. Before, each exited 1 from its own first read of a table its read-only boot had deferred. This completes the family that PR #21550 started forresume,recorded-byandvalue-shapes, under the same ruling (5965283666, applied to these six doors by triage's5966537984): prefer "not asked"; where a read cannot be avoided, recognise its refusal only withisMissingTableErrorand only for the command's own table.Measured at the public door
Fixture: one artifact with two app objects,
--database-url file:a path that does not exist, run asnode packages/cli/bin/run-dev.js ... --json. Basef83d0669d7(origin/main when the branch was cut), head491087ee0f(thesrcof the six doors is the same at the current head69a1689f00; the patch round touched two test files only).files-to-references,summary-nulls,multi-value-columnsandduplicatesare the controls;duplicateshas no--jsonflag and always prints its one document.migrate account-issuerRESOURCE_CONFLICT, "Cannot enumerate sys_account"scanned: 0, ok: truemigrate audit-metadata-bodiesfailures: 3(sys_audit_log,sys_activity,sys_metadata_audit)failures: 0migrate meta --storedDATABASE_ERRORforsys_metadatascanned: 0, clean: truesecret orphansscan_failedforsys_secretcounts.total: 0, all three familiesenumeratedsecret rewrapscan_failedforsys_secretcounts.total: 0, all three familiesenumeratedstorage orphansDATABASE_ERRORforsys_filefilesScanned: 0, stranded: 0Each of the six names, on stderr under
--jsonand on stdout in human mode, the tables it read as no rows.The boot path of each door (what the seam can and cannot say)
All six boot through
bootSchemaStackwithdeferSchemaDdlandreadOnlyProbe, soSchemaStack.tableAbsentis there to ask. Measured per door:audit-metadata-bodies,meta --stored,secret orphans,secret rewrap,storage orphans) before the first read. A shared helper,packages/cli/src/utils/absent-table-reads.ts, is the one place a door asks. It adds no second mechanism: it wrapstableAbsent, and a refused read of an ordinary table is still issued and still refused.account-issuercannot ask. Its boot composes no auth plugin, sosys_accountis not a registered object and the held-back sync never lists it:tableAbsent('sys_account')is false on every database. It is theisMissingTableErrorcase the ruling names: the probe's read is not avoidable, and the door reads that one refusal, forsys_accountonly, as no rows. Registering the object would not be a fix: the probe selects the retiredissuercolumn, which a registeredsys_accountno longer declares.meta --storedhands its read to the metadata protocol, which holds a private engine this command cannot wrap. The preview answers an absentsys_metadataitself with the report the protocol returns for zero rows, typed asStoredMigrationReportso a new field is a compile error here.secret orphansandsecret rewrapread at driver level and through the reference union. The union is given a read-only view of the engine (secretUnionReadView): onlyfindis carried onto the driver, so--deleteand--applytake their write verbs from the unwrapped driver, andlistDatasourceDefsstays absent when the engine has none (its absence is a declared gap).The table the boot cannot measure:
sys_activityThe control for
audit-metadata-bodiesshowedtableAbsent('sys_activity')true on a booted database.sys_activityis rotation-managed: its rows live insys_activity__rYYYYMMDDshard tables and its base name is a view. The deferred sync asks the driverhasTablefor the base name, a view is not a table, so on SQLite it lists the base ascreate_tableover a database that serves it. A door that believed it would have answered "Nothing to rewrite" over the cleartext credential copies the command exists to reach.So
absentTableReadstakes a schema lookup, and for a rotation-managed object it does not consult the measurement: the read is issued, and only its missing-table refusal (isMissingTableError, for that object) reads as no rows. The control pins it: a cleartext copy seeded into the rotation shard is found and counted (sys_activity.rewritten: 1). The seam itself is untouched here (see Out of scope).Documented exit (A3)
migrate account-issuer: its own description, "exits non-zero when the drop must not proceed". A database with no account table has no collision, and a booted database holding two clean accounts answersok: truewith exit 0.migrate audit-metadata-bodies,migrate meta --stored: the platform checklist's migrate item (docs/qa/platform-checklist/areas/cli.json: "EVERY migrate subcommand with --json exits 0 on success");meta --storedalso documents "A second pass reporting every row canonical exits 0".secret orphans,secret rewrap,storage orphans: report-only by their own documentation ("Report-only by default: without--deleteit writes nothing", "A dry run by default", "Writes nothing"); their report mode exits 0 whenever the read succeeded.content/docs/deployment/cli.mdxsaid the opposite in two places: the Data migrations edge paragraph ("Another dry run that reads a missing table can still fail and exit 1") andos secret orphans("it refuses and exits 1 ...scan_failed"). Both now describe the empty-work answer, and keep the refusal for any other read that fails.Tests
packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.tsgains a second block. For each of the six, on the absent database:--jsonexit 0 with the empty-work document, no refused read of its own tables, the table named on stderr; human mode exit 0 on the empty-work sentence; no database file created. The control is a booted database holding one row of work per door (two legacysys_accountrows, a cleartext audit copy insys_audit_logand in thesys_activityshard, a stored metadata row, an unreferencedsys_secretrow, a strandedsys_file): each door READS it, reports the row, and says nothing about absent tables. The four doors that already exited 0 are pinned to still do. Every spawn runs inbeforeAll.preview-read-only.integration.test.tspinned exit 1 formeta --storedandaudit-metadata-bodieson a missing database (the refusal [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 declared). That is the behaviour the ruling reverses for these doors; both cases now assert exit 0 and the empty-work document, with the file header updated.packages/cli/src/utils/absent-table-reads.test.ts, 17 cases: the helper's answers, the stderr/stdout split, and the rotation rule.491087ee0finrewrap.guards.test.ts. That suite, andorphans.guards.test.ts, replacebootSchemaStackand hand the command a stack of onlykernelandshutdown. The doors' first ask isstack.tableAbsent, a member ofSchemaStacksince PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550, so every case fell into thescan_failedcatch. Root cause read off the case's own output (a throwaway copy of the suite that printed the payload):{"error":"scan_failed","message":"stack.tableAbsent is not a function"}, in all five cases ofrewrap.guards.test.ts. Run locally against the unfixed head, the two suites had 7 red cases (5 inrewrap.guards, 2 inorphans.guards); CI listed four. The fix is on the double's side: both doubles now carrytableAbsentas the real boot returns it (falsefor every table on the plain boot of a writing run;rewrap.guardstakes the measured-absent set as an option). No consumer-side tolerance was added.rewrap.guards.test.tsalso gains one case: a dry run over tables the boot measured absent issues no read, reportscounts.total: 0with every familyenumerated, and a present-table control still reads. The stack double intest/exit-signal.pin.test.ts(stackWith) has notableAbsenteither, and no path that pin drives reaches it (recorded-by --applyandresume --runshort-circuit before the ask, andaccount-issuerdoes not use the seam), so it is left as it is.bootSchemaStackorschema-migrate.js(git grepforvi.mockof the module overpackages/cli):rewrap.guards,orphans.guards,test/exit-signal.pin,files-to-references.column-step-refusalandsummary-nulls. The last two reach other commands, not these doors. The test files that reach a door through any other seam (a real boot, the command module, or a source scan) were found bygit grepfor the door names and modules:data-commands.absent-database,preview-read-only,meta.stored-flow-resolution,meta.report-order,meta.stored-flags,orphans.driver-contract,rewrap.driver-contract,platform-migrations-arming,schema-migrate.one-shot-family,resume.recorded-by,sys-secret-rewrap,one-shot-settings.pin,migrate-meta-engine-guidance,migrate-meta-strict-factories.69a1689f00(this branch merged with origin/main at491087ee0f, which brought PR fix(cli): os init and os compile render each refusal once, not once on stdout and again as oclif's Error block (#21542) #21560's refusal pins; no later merge, the gate derivation does not call the tree stale):summary-nullsandfiles-to-references.column-step-refusalamong them),vitest run --maxWorkers=2, both projects: 13 files, 202 tests passed (it was 7 failed, 194 passed before the double fix). 7 of the 13 are integration-tier.data-commands.absent-database,preview-read-only,meta.stored-flow-resolution,platform-migrations-arming,schema-migrate.one-shot-family,resume.recorded-by),--project integration: 6 files, 137 passed, 1 skipped (the live PostgreSQL cell, not provisioned here).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 253 files, 3702 passed (afterpnpm --filter @objectstack/cli build, which the twopublished-subpath-*pins need).pnpm --filter @objectstack/cli buildandpnpm --filter @objectstack/cli typecheck(test layer included): exit 0.vitest list --project integration; 13 run here: those 7 and the six round-1 files). Reason: the tier runs past the 10-minute foreground cap here, so the rest is declared to CI. The narrowing is thegit grepabove: none of the 69 names a door, mocksschema-migrate.js, or imports the new helper. And the two*.e2e.test.tsfiles that spawn the doors (test/migrate-meta.e2e.test.ts,test/json-stdout-purity.e2e.test.ts) belong to neither of the package's two vitest projects, so no local run selects them (the runner printed "matches no test file in this package").Reverse verification
Each leg mutated the committed tree through
scripts/ablation-replace.mjs: anchor 1 to 0, blob changed, restored toHEADwith an emptygit diff HEADand a clean status. The CLI spawns runpackages/cli/srcthrough tsx, so there is no dist hop. Two first attempts were refused by the tool itself because the replacement text contained the anchor or already occurred in the file; nothing ran, and each leg was redone with a distinct replacement.Leg A,
tableAbsentforced false (schema-migrate.ts). Predicted: every fresh-project pin of the five seam doors red, the booted controls green,account-issuergreen (it does not use the seam). Observed: 19 failed, 25 passed, 1 skipped. The 19 are the 7 pins PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550 added, the 10 new fresh-project cases (five doors,--jsonand human) and the two flippedpreview-read-onlycases. Every booted control and the four exit-0 controls stayed green, andaccount-issuerstayed green.Leg B, the
isMissingTableErrorbranch ofaccount-issuerdisabled. Predicted: the twoaccount-issuerfresh-project cases red, nothing else. Observed: 2 failed, 34 passed.Leg C, the rotation rule disabled (
absent-table-reads.ts). Predicted: the audit control red becausesys_activityis skipped. Observed: integration 1 failed, 35 passed, withsys_activity.scanned0 where a cleartext copy is stored; the unit file 2 failed, 15 passed.Leg D, patch round: the not-asked answer disabled (
absent-table-reads.ts,absent()always false), overrewrap.guards.test.ts. Predicted: only the new absent-tables case red. Observed: 1 failed, 6 passed. Restored toHEAD, emptygit diff HEAD.Legs A to C ran at
491087ee0f;srcof the doors and the helper is unchanged since (the round touched two test files).Gates
At
69a1689f00, after the final commit:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 94 commands (the same list as at491087ee0f);--ran, with an exit code per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)". All 94 read exit 0 on this pass, with every package built first (turbo run build --filter='!@objectstack/docs'). The derivation says no commit it can see touched what it derives from, so no merge was made.pnpm lint, as a proven narrowing.eslint --no-inline-config --format jsonon the 12 changed TS files: 12 files, 0 errors, 0 warnings, none reported as ignored. The population comes fromeslint.config.mjs(files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}); the config enables no type-aware linting (noparserOptions.project, and every block'sparserOptionsis{ ecmaVersion, sourceType }), so this diff cannot move the verdict on a file it does not touch. The.mdand.mdxfiles are outside it.Acceptance notes
[ObjectQLPlugin] sys_metadata_activation is registered but could not be read. Triage ruled it out of this card.packages/cli/src/utils/absent-table-reads.ts(and its unit test). Five doors need the same view, the same notice line and the same rotation rule; five inline copies would drift.schema-migrate.tsis untouched.this.exitorthis.error: none. The exit-signal pin and PR fix(cli): os init and os compile render each refusal once, not once on stdout and again as oclif's Error block (#21542) #21560's refusal pin both ran green over the whole command table.rewrap.guards.test.tsandorphans.guards.test.tsare the two files added to the diff (see Tests).value-shapes(PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550) reads through the same seam without the rotation rule. It composes no audit plugin, so none of its scanned objects is rotation-managed today; noted, not changed.Out of scope (reported to the seat, not filed here)
SchemaStack.tableAbsentanswers true for a rotation-managed object's base name on a booted SQLite database, becausepreviewDeferredSchemaWorkaskshasTableand a view is not a table. Measured throughos migrate audit-metadata-bodiesagainst a booted database whosesys_activityis the view oversys_activity__rYYYYMMDD. The same list is whatos migrate planprints for a host that composes the audit plugin (not measured atplan). The fix belongs in the driver's preview, not in a consumer.Generated by Claude Code