Repository navigation
Commit 1777a9b
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 rewrap` and `os storage
orphans` now 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 for `resume`, `recorded-by`
and `value-shapes`, under the same ruling (`5965283666`, applied to
these six doors by triage's `5966537984`): prefer "not asked"; where a
read cannot be avoided, recognise its refusal only with
`isMissingTableError` and 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 as `node packages/cli/bin/run-dev.js ...
--json`. Base `f83d0669d7` (origin/main when the branch was cut), head
`491087ee0f` (the `src` of the six doors is the same at the current head
`69a1689f00`; the patch round touched two test files only).
`files-to-references`, `summary-nulls`, `multi-value-columns` and
`duplicates` are the controls; `duplicates` has no `--json` flag and
always prints its one document.
| command (absent database) | base | head |
|:--|:--|:--|
| `migrate account-issuer` | exit 1, `RESOURCE_CONFLICT`, "Cannot
enumerate sys_account" | exit 0, `scanned: 0, ok: true` |
| `migrate audit-metadata-bodies` | exit 1, `failures: 3`
(`sys_audit_log`, `sys_activity`, `sys_metadata_audit`) | exit 0,
`failures: 0` |
| `migrate meta --stored` | exit 1, `DATABASE_ERROR` for `sys_metadata`
| exit 0, `scanned: 0, clean: true` |
| `secret orphans` | exit 1, `scan_failed` for `sys_secret` | exit 0,
`counts.total: 0`, all three families `enumerated` |
| `secret rewrap` | exit 1, `scan_failed` for `sys_secret` | exit 0,
`counts.total: 0`, all three families `enumerated` |
| `storage orphans` | exit 1, `DATABASE_ERROR` for `sys_file` | exit 0,
`filesScanned: 0, stranded: 0` |
| the four controls | exit 0 | exit 0 |
| database file left behind | none | none |
Each of the six names, on stderr under `--json` and 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 `bootSchemaStack` with `deferSchemaDdl` and
`readOnlyProbe`, so `SchemaStack.tableAbsent` is there to ask. Measured
per door:
- **Five ask it** (`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 wraps
`tableAbsent`, and a refused read of an ordinary table is still issued
and still refused.
- **`account-issuer` cannot ask.** Its boot composes no auth plugin, so
`sys_account` is not a registered object and the held-back sync never
lists it: `tableAbsent('sys_account')` is false on every database. It is
the `isMissingTableError` case the ruling names: the probe's read is not
avoidable, and the door reads that one refusal, for `sys_account` only,
as no rows. Registering the object would not be a fix: the probe selects
the retired `issuer` column, which a registered `sys_account` no longer
declares.
- **`meta --stored`** hands its read to the metadata protocol, which
holds a private engine this command cannot wrap. The preview answers an
absent `sys_metadata` itself with the report the protocol returns for
zero rows, typed as `StoredMigrationReport` so a new field is a compile
error here.
- **`secret orphans` and `secret rewrap`** read at driver level and
through the reference union. The union is given a read-only view of the
engine (`secretUnionReadView`): only `find` is carried onto the driver,
so `--delete` and `--apply` take their write verbs from the unwrapped
driver, and `listDatasourceDefs` stays absent when the engine has none
(its absence is a declared gap).
## The table the boot cannot measure: `sys_activity`
The control for `audit-metadata-bodies` showed
`tableAbsent('sys_activity')` true on a booted database. `sys_activity`
is rotation-managed: its rows live in `sys_activity__rYYYYMMDD` shard
tables and its base name is a view. The deferred sync asks the driver
`hasTable` for the base name, a view is not a table, so on SQLite it
lists the base as `create_table` over 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 `absentTableReads` takes 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 answers `ok:
true` with 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
--stored` also 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 `--delete` it
writes nothing", "A dry run by default", "Writes nothing"); their report
mode exits 0 whenever the read succeeded.
- Each is also the exit the same door gives the booted control (below).
`content/docs/deployment/cli.mdx` said the opposite in two places: the
Data migrations edge paragraph ("Another dry run that reads a missing
table can still fail and exit 1") and `os 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
- **Integration pin, extended:**
`packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts`
gains a second block. For each of the six, on the absent database:
`--json` exit 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 legacy `sys_account` rows, a
cleartext audit copy in `sys_audit_log` and in the `sys_activity` shard,
a stored metadata row, an unreferenced `sys_secret` row, a stranded
`sys_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 in `beforeAll`.
- **Two refusal pins flipped:** `preview-read-only.integration.test.ts`
pinned exit 1 for `meta --stored` and `audit-metadata-bodies` on a
missing database (the refusal #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.
- **Unit:** `packages/cli/src/utils/absent-table-reads.test.ts`, 17
cases: the helper's answers, the stderr/stdout split, and the rotation
rule.
- **Patch round 1: two stale test doubles.** CI Test Core (1/6) was red
at `491087ee0f` in `rewrap.guards.test.ts`. That suite, and
`orphans.guards.test.ts`, replace `bootSchemaStack` and hand the command
a stack of only `kernel` and `shutdown`. The doors' first ask is
`stack.tableAbsent`, a member of `SchemaStack` since PR #21550, so every
case fell into the `scan_failed` catch. 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 of `rewrap.guards.test.ts`. Run locally
against the unfixed head, the two suites had 7 red cases (5 in
`rewrap.guards`, 2 in `orphans.guards`); CI listed four. The fix is on
the double's side: both doubles now carry `tableAbsent` as the real boot
returns it (`false` for every table on the plain boot of a writing run;
`rewrap.guards` takes the measured-absent set as an option). No
consumer-side tolerance was added. `rewrap.guards.test.ts` also gains
one case: a dry run over tables the boot measured absent issues no read,
reports `counts.total: 0` with every family `enumerated`, and a
present-table control still reads. The stack double in
`test/exit-signal.pin.test.ts` (`stackWith`) has no `tableAbsent`
either, and no path that pin drives reaches it (`recorded-by --apply`
and `resume --run` short-circuit before the ask, and `account-issuer`
does not use the seam), so it is left as it is.
- **Test files that mock `bootSchemaStack` or `schema-migrate.js`**
(`git grep` for `vi.mock` of the module over `packages/cli`):
`rewrap.guards`, `orphans.guards`, `test/exit-signal.pin`,
`files-to-references.column-step-refusal` and `summary-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 by `git grep` for 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`.
- At `69a1689f00` (this branch merged with origin/main at `491087ee0f`,
which brought PR #21560's refusal pins; no later merge, the gate
derivation does not call the tree stale):
- The 13 files above that run in the package's two projects (the five
mockers and the door-reaching files, `summary-nulls` and
`files-to-references.column-step-refusal` among 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.
- The round-1 integration files (`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 (after `pnpm --filter
@objectstack/cli build`, which the two `published-subpath-*` pins need).
- `pnpm --filter @objectstack/cli build` and `pnpm --filter
@objectstack/cli typecheck` (test layer included): exit 0.
- **NOT MEASURED: the other 69 files of the cli integration tier** (82
files listed by `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 the
`git grep` above: none of the 69 names a door, mocks
`schema-migrate.js`, or imports the new helper. And the two
`*.e2e.test.ts` files 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 to
`HEAD` with an empty `git diff HEAD` and a clean status. The CLI spawns
run `packages/cli/src` through 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, `tableAbsent` forced false** (`schema-migrate.ts`).
Predicted: every fresh-project pin of the five seam doors red, the
booted controls green, `account-issuer` green (it does not use the
seam). Observed: 19 failed, 25 passed, 1 skipped. The 19 are the 7 pins
PR #21550 added, the 10 new fresh-project cases (five doors, `--json`
and human) and the two flipped `preview-read-only` cases. Every booted
control and the four exit-0 controls stayed green, and `account-issuer`
stayed green.
- **Leg B, the `isMissingTableError` branch of `account-issuer`
disabled.** Predicted: the two `account-issuer` fresh-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 because `sys_activity` is skipped.
Observed: integration 1 failed, 35 passed, with `sys_activity.scanned` 0
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), over
`rewrap.guards.test.ts`. Predicted: only the new absent-tables case red.
Observed: 1 failed, 6 passed. Restored to `HEAD`, empty `git diff HEAD`.
Legs A to C ran at `491087ee0f`; `src` of 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/objectstack` derived 94 commands (the same list as at
`491087ee0f`); `--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 json` on the 12 changed TS files: 12 files, 0 errors, 0
warnings, none reported as ignored. The population comes from
`eslint.config.mjs` (`files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`); the
config enables no type-aware linting (no `parserOptions.project`, and
every block's `parserOptions` is `{ ecmaVersion, sourceType }`), so this
diff cannot move the verdict on a file it does not touch. The `.md` and
`.mdx` files are outside it.
## Acceptance notes
- **Out of the card, kept:** every read-only data-command boot of an
absent database still prints `[ObjectQLPlugin] sys_metadata_activation
is registered but could not be read`. Triage ruled it out of this card.
- **A new file outside the claim's list:**
`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.ts` is untouched.
- **A new `this.exit` or `this.error`:** none. The exit-signal pin and
PR #21560's refusal pin both ran green over the whole command table.
- **Round 1 of the patch:** `rewrap.guards.test.ts` and
`orphans.guards.test.ts` are the two files added to the diff (see
Tests).
- **`value-shapes` (PR #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.tableAbsent` answers true for a rotation-managed object's
base name on a booted SQLite database, because
`previewDeferredSchemaWork` asks `hasTable` and a view is not a table.
Measured through `os migrate audit-metadata-bodies` against a booted
database whose `sys_activity` is the view over
`sys_activity__rYYYYMMDD`. The same list is what `os migrate plan`
prints for a host that composes the audit plugin (not measured at
`plan`). The fix belongs in the driver's preview, not in a consumer.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 74281a8 commit 1777a9b
14 files changed
Lines changed: 897 additions & 52 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cli/src
- commands
- migrate
- secret
- storage
- utils
| 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 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
532 | 532 | | |
533 | 533 | | |
534 | 534 | | |
535 | | - | |
536 | | - | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
537 | 539 | | |
538 | 540 | | |
539 | 541 | | |
| |||
1055 | 1057 | | |
1056 | 1058 | | |
1057 | 1059 | | |
1058 | | - | |
1059 | | - | |
1060 | | - | |
1061 | | - | |
1062 | | - | |
1063 | | - | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
1064 | 1070 | | |
1065 | 1071 | | |
1066 | 1072 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
131 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
132 | 155 | | |
133 | 156 | | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
134 | 160 | | |
135 | 161 | | |
| 162 | + | |
136 | 163 | | |
137 | 164 | | |
138 | 165 | | |
139 | 166 | | |
140 | 167 | | |
141 | 168 | | |
| 169 | + | |
142 | 170 | | |
143 | 171 | | |
144 | 172 | | |
| |||
Lines changed: 28 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
179 | 180 | | |
180 | 181 | | |
181 | 182 | | |
182 | | - | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
183 | 208 | | |
184 | 209 | | |
| 210 | + | |
185 | 211 | | |
186 | 212 | | |
187 | 213 | | |
188 | 214 | | |
189 | 215 | | |
190 | 216 | | |
| 217 | + | |
191 | 218 | | |
192 | 219 | | |
193 | 220 | | |
| |||
0 commit comments