Repository navigation
Commit b206403
fix(cli,runtime): one-shot CLI boots run no seed loader and arm no lifecycle sweep; every no-write mode boots read-only (#21432)
Fixes #21391
Clause-②: yes (narrowing)
## What changed
The family ruling on #21391 (triage `5950851232`), built in the
`bootSchemaStack` funnel and its callers:
- **No one-shot CLI boot runs the seed loader.** `bootSchemaStack`
passes `skipSeedData: true` on every boot, keyed like
`runPlatformMigrations: false` (unconditional, not on `deferSchemaDdl`).
That covers the `--apply`, `--delete`, `--run` and `--yes` paths.
Seeding stays with `os dev` and `os serve`.
- **Every no-write mode boots read-only** (`deferSchemaDdl: true,
readOnlyProbe: true`, the boot `os migrate plan` takes): `os migrate
value-shapes` (scan), `summary-nulls`, `files-to-references` and
`recorded-by` (dry run), `os migrate resume` (list), `os secret orphans`
(report), `os storage orphans` (its only mode), and `os meta resync`
when it can never reach its write (no `--yes`, and `--json` or no TTY).
The enumeration pin found `os meta resync` as an eighth member. Write
modes keep the plain boot, minus the seed.
- **No lifecycle sweep is armed on a one-shot boot.** New runtime key
`createStandaloneStack({ armLifecycleSweep })`, default `true`. With
`false`, `ObjectQLPlugin` gets `lifecycle: { enabled: false }` and the
ADR-0057 timers are never created. `bootSchemaStack` passes `false`.
- **DDL deferral covers every SQL datasource the boot connects.**
`DeferSchemaDdlPlugin` used to arm the first `driver.*` SQL service
only. It now arms every `driver.*` service, every driver the engine
already holds (the default by name, and the driver each registered
object resolves to), and every driver registered later. The last is done
by a shadow on the engine instance's `registerDriver` that arms the
driver before forwarding it, the seam the declaration-boot write guard
already uses. `pendingSchemaWork` and `flushSchemaDdl` cover every armed
driver. No driver change.
- The two comments that listed `os migrate meta` among the non-deferred
boots are corrected: the `runPlatformMigrations` block in
`schema-migrate.ts`, and
`platform-migrations-arming.integration.test.ts`.
- Two `--json` faces the read-only boot newly reaches on a database
without the app's tables. `os secret orphans` had no catch for a scan
error, so `--json` printed nothing; it now answers
`{"error":"scan_failed","message":…,"code":…}` with exit 1. `os migrate
value-shapes` re-reported its own `this.exit(1)` as a second document,
`{"error":"EEXIT: 1"}`; its catch now rethrows exit signals, as
`summary-nulls` and `files-to-references` already do.
The fenced files are untouched: `migrate/audit-metadata-bodies.ts` and
plugin-audit's `stored-metadata-body-migration.ts`. The family keys
reach that command through `bootSchemaStack`.
## Measured: the #21349 repro on `examples/app-crm`
Setup at base `1d0600bf66`: `os build`, then `os dev --seed-admin -d
file:base.db`. The seed loaded 28 rows across 5 app tables and the dev
admin was seeded (82 tables). The server was stopped. Each command ran
on its own copy with `--json`, and the state was read on a separate
read-only connection. "28/28" means 28 of the 28 seeded rows changed
(`updated_at` bumped, `organization_id` stamped). The PR readings use
the CLI built at `3f61ebcdb6`.
| no-write mode | base | this PR |
|:--|:--|:--|
| `migrate value-shapes` / `summary-nulls` / `files-to-references` /
`recorded-by` / `resume`, `secret orphans`, `storage orphans`, `meta
resync` (no `--yes`) | each 28/28, schema identical, seeder
`"updated":28` | each 0/28, schema and every row identical, `[Seeder]
skipSeedData` |
| controls: `account-issuer`, `multi-value-columns`, plus `plan`,
`duplicates`, `meta --stored`, `audit-metadata-bodies` | each 0/28,
identical | each 0/28, identical |
| write mode | base | this PR |
|:--|:--|:--|
| `--apply` of `value-shapes` / `summary-nulls` / `files-to-references`
/ `recorded-by` / `meta --stored` / `audit-metadata-bodies`, `resume
--run`, `secret orphans --delete`, `meta resync --yes` | each 28/28 (the
seed ran alongside) | each 0/28; the only other tables that changed are
the command's own (`sys_migration` for the value-shapes and
files-to-references applies, `sys_permission_set` for `meta resync
--yes`) |
| controls: `multi-value-columns --apply`, `apply --yes` (deferred
boots) | 0/28 | 0/28 |
A no-write mode pointed at a SQLite file that does not exist:
| command | base | this PR |
|:--|:--|:--|
| `value-shapes` | exit 0, file created | exit 1 (gate fails on
unreadable objects), no file |
| `summary-nulls`, `files-to-references` | exit 0, file created | exit
0, no file |
| `recorded-by`, `resume`, `storage orphans` | exit 0, file created |
exit 1, the driver's refusal names the table, no file |
| `secret orphans` | exit 0, file created | exit 1, `scan_failed`, no
file |
| `meta resync` (no `--yes`) | exit 0, file created | exit 0
(`confirmation_required`), no file |
| `meta --stored`, `audit-metadata-bodies`, `account-issuer` | exit 1,
no file | exit 1, no file |
The exit-0-to-exit-1 rows are the declared narrowing: `Clause-②: yes
(narrowing)`, a `minor` changeset with the BREAKING banner and the
ADR-0087 disposition `not-required (no-migration-prescription)`.
## Pins (each measured red before the fix, at `5e7fd69bc3`)
-
`packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts`
(new, 44 cases). The family is derived from source: every module under
`src/` that value-imports `bootSchemaStack`, held equal to a table of
each caller's no-write and write modes. A new caller fails by file name
until it is declared. Against a database a served boot seeded (and an
operator then edited), with the artifact one release ahead:
- every no-write mode leaves the schema and every row identical (14);
- every no-write mode at a missing file creates no file and still
answers with one JSON document (14);
- every write mode runs no seed write (11);
- neither the read-only nor the plain one-shot boot arms the sweep, and
the served boot does (3);
- the enumeration itself (2).
- Before the fix: 27 failed, 17 passed (the passes are the controls and
the enumeration).
- `schema-migrate.deferred-ddl.integration.test.ts`: two cases with a
second SQL datasource an artifact declares, connected through the real
`DatasourceAdminServicePlugin` and driver factory. The deferred boot
creates nothing there and reports its `create_table`; `flushSchemaDdl`
creates it. Before the fix: 2 failed (the boot created `defer_remote` on
the second database).
- `packages/runtime/src/standalone-stack-lifecycle-sweep.test.ts` (new):
the key's declaration on `ObjectQLPlugin`, and its effect on a started
kernel's timers. Before the fix: 2 failed.
- `preview-read-only.integration.test.ts`: the #21349 exit-1 edge is
pinned for both commands, with the exit code and the refusal
(`DATABASE_ERROR` naming `sys_metadata`; `failures: 2, scanned: 0` over
`sys_audit_log` and `sys_activity`). Its fixture now seeds through a
served boot, since the funnel no longer seeds.
- `schema-migrate.teardown.integration.test.ts`: a first case pins that
a one-shot stack arms no sweep and that its teardown still closes the
kernel and the pool. The #4747 pair (audits while live, reads nothing
once down) now runs on the standalone stack booted without the one-shot
policy (see acceptance note 1).
- `multi-value-columns.no-auto-run.test.ts` names the family pin as the
one other reader of that module.
## Ablations
The fix was committed first. Each leg ran through
`scripts/ablation-replace.mjs` in WRAP mode: the anchor hit once, the
mutation landed (anchor count 1 to 0, blob changed), and the restore was
proven (blob == HEAD, `git diff HEAD` empty), at `0758b2330d`. Every pin
imports its subject by relative path, so it resolves to `src`; no
rebuild was needed between legs.
| leg | mutation | red |
|:--|:--|:--|
| A1 | `schema-migrate.ts`: `skipSeedData: false` | 23 (every no-write
identity case and every write case except `plan` and `apply --yes`,
whose composed boots refuse row writes through the declaration-boot
guard) |
| A2 | `schema-migrate.ts`: `armLifecycleSweep` line removed | 3 (both
family lifecycle cases, the teardown first case) |
| B1-B8 | each command's read-only spread removed, one leg per command |
2 each (that command's identity and missing-file cases) |
| C1 | `registerDriver` shadow not installed | 2 (both second-datasource
cases) |
| C2 | flush over the first armed driver only | 1 (the flush case) |
| C3 | preview over the first armed driver only | 1 (the deferred-boot
case) |
| D1 | runtime: the `lifecycle: { enabled: false }` passthrough removed
| 2 (both runtime cases) |
| E1 | `schema-migrate.ts`: `readOnlyProbe` mapping removed | 2 (both
no-file cases; the exit-1 pins stay green because the refusal comes from
the deferral) |
| E2 | `schema-migrate.ts`: deferral never armed | 2 (both exit-1 edge
pins) |
| F1 | `secret orphans`: the `scan_failed` emit removed | 1 (its
missing-file JSON case) |
| F2 | `value-shapes`: the exit-signal rethrow removed | 1 (its
missing-file JSON case) |
## Verification
- `@objectstack/cli` unit tier at `11d48f07f7`: 248 files, 3552 passed.
- `@objectstack/cli` integration tier, in 4 shards: shards 1-3 at
`5cfaffbc87` (19 + 19 + 19 files; one failure, the #4747 teardown pin,
reworked in `0758b2330d`), shard 4 and the teardown file at `0758b2330d`
(19 files, 190 passed; 2 passed). After merging `origin/main`
(`39a912ea73`), at `77a89b7b53`: the six touched suites, 72 passed and 1
named skip (the live PostgreSQL cell).
- `@objectstack/runtime` at `0758b2330d`: 309 files, 5094 passed, 11
skipped.
- `test/json-stdout-purity.e2e.test.ts` (nightly tier,
`OS_TEST_TIERS=nightly`) at `0758b2330d`: 44 passed.
- `pnpm --filter @objectstack/runtime typecheck` and `pnpm --filter
@objectstack/cli typecheck` at `77a89b7b53`: exit 0,
`check:test-typecheck` OK for both. Commit `11d48f07f7` adds one
module-top side-effect import to a test file. (Seat correction at
landing: the final commit is now `753bec1955`. It gives the one-shot
family pin its own `OS_SECRET_KEY`, 32 random bytes saved and restored
around the file, so the pin runs on a clean CI runner. The
integration-tier readings above predate it; CI's Test Core shard 4/6 on
`753bec1955` is the reading for that file.)
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `11d48f07f7` derived 95
families; all 95 ran with exit 0 recorded before any pipe. `--ran`
reports 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).
Three of them first answered exit 3 (PREREQUISITE NOT MET: eight
packages outside the CLI closure had no `dist`); after a turbo build of
those eight they exited 0.
- Lint, as a proven narrowing, at `11d48f07f7`: eslint with the repo
config and `--no-inline-config` over the 18 changed TypeScript files
(`--format json`) reported 18 files, 0 errors, 0 warnings, and no file
reported as ignored. The config enables no type-aware linting
(`eslint.config.mjs` says so: no `parserOptions.project`, no typed
rules), so this diff cannot move the verdict on any untouched file. The
full `pnpm lint` is left to CI.
## Acceptance notes
1. **The lifecycle key also makes an explicit `sweep()` inert on a
one-shot stack.** `lifecycle.enabled` is `LifecycleService`'s master
switch: with it off, `start()` arms nothing and `sweep()` returns an
empty report. #4747's triage declined "one-shot commands skip the audit"
(its option C) as the fix for shutdown pollution, and its pin asserted
that an explicit `sweep()` on a `bootSchemaStack` stack audits. Under
this card's ruling the scheduled sweep, and the audit riding its clock,
is off every one-shot boot; no code in this repo calls `sweep()` on a
one-shot stack. The #4747 pair still runs, on the composition that still
sweeps. Keeping an explicit `sweep()` alive on a one-shot stack while
its schedule stays unarmed needs an `ObjectQLPlugin` option that
separates "arm the schedule" from the master switch, which is a
`packages/objectql` change outside this claim.
2. `ObjectQLPlugin`'s `lifecycle` option doc says that with `enabled:
false` "the `lifecycle` service stays registered so tooling can still
run `sweep()` explicitly". `LifecycleService.sweep()` returns an empty
report when the service is not enabled. No caller in this repo depends
on the sentence.
3. `os migrate recorded-by --apply --yes --json` on a database with one
sentinel row converts the row, prints its result document, then prints
`{"error":"EEXIT: 0"}` and exits 1. Its catch re-reports the
`this.exit(0)` that follows a completed run (measured with the CLI built
at `77a89b7b53`, on a copy of the app-crm database). `os migrate resume
--run` has the same shape by reading (not measured). This is a write
path this change does not reach, so it is reported, not fixed here.
4. The read-only boot keeps a missing SQLite file of the **default**
datasource from being created. A second SQLite datasource's file is
still opened by its connect; the ruling asks for DDL deferral on every
datasource, and that holds.
5. The family pin is SQLite-only. The read-only boot is one code path
for every dialect, and the live PostgreSQL CI leg is not this card
(triage).
6. `content/docs/deployment/cli.mdx` gains a paragraph under Data
migrations and one in the `os secret orphans` entry. `os storage
orphans`, `os meta resync`, `os migrate recorded-by` and `os migrate
resume` have no entry in that page.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d70353f commit b206403
20 files changed
Lines changed: 1202 additions & 77 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- cli/src
- commands
- meta
- migrate
- secret
- storage
- utils
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
530 | 530 | | |
531 | 531 | | |
532 | 532 | | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
533 | 538 | | |
534 | 539 | | |
535 | 540 | | |
| |||
994 | 999 | | |
995 | 1000 | | |
996 | 1001 | | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
997 | 1013 | | |
998 | 1014 | | |
999 | 1015 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
131 | 139 | | |
132 | 140 | | |
133 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
134 | 146 | | |
135 | 147 | | |
136 | 148 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
214 | 218 | | |
215 | 219 | | |
216 | 220 | | |
217 | 221 | | |
| 222 | + | |
218 | 223 | | |
219 | 224 | | |
220 | 225 | | |
| |||
Lines changed: 7 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
93 | 99 | | |
94 | | - | |
| 100 | + | |
95 | 101 | | |
96 | 102 | | |
97 | 103 | | |
| |||
Lines changed: 46 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
57 | | - | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
61 | 66 | | |
62 | 67 | | |
63 | 68 | | |
| |||
236 | 241 | | |
237 | 242 | | |
238 | 243 | | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
245 | 254 | | |
246 | | - | |
| 255 | + | |
247 | 256 | | |
248 | | - | |
| 257 | + | |
249 | 258 | | |
250 | 259 | | |
251 | 260 | | |
| |||
254 | 263 | | |
255 | 264 | | |
256 | 265 | | |
257 | | - | |
| 266 | + | |
258 | 267 | | |
259 | 268 | | |
260 | 269 | | |
| |||
469 | 478 | | |
470 | 479 | | |
471 | 480 | | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
472 | 507 | | |
473 | 508 | | |
474 | 509 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
99 | 103 | | |
100 | 104 | | |
101 | 105 | | |
102 | 106 | | |
| 107 | + | |
103 | 108 | | |
104 | 109 | | |
105 | 110 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
108 | 112 | | |
109 | 113 | | |
110 | 114 | | |
111 | 115 | | |
| 116 | + | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
193 | 197 | | |
194 | 198 | | |
195 | 199 | | |
196 | 200 | | |
| 201 | + | |
197 | 202 | | |
198 | 203 | | |
199 | 204 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
134 | 135 | | |
135 | 136 | | |
136 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
137 | 142 | | |
138 | 143 | | |
139 | 144 | | |
140 | 145 | | |
| 146 | + | |
141 | 147 | | |
142 | 148 | | |
143 | 149 | | |
| |||
240 | 246 | | |
241 | 247 | | |
242 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
243 | 257 | | |
244 | 258 | | |
245 | 259 | | |
| |||
0 commit comments