Repository navigation
Commit abb6df0
fix(cli): three more read-only arms claim no row or table, and name the journal-mode switch on a SQLite file (#23895)
Fixes #23726
Clause-②: no
The family close-out for the "read-only arm says it wrote nothing"
class, with the pattern PR #23608 set and PR #23725 applied (the earlier
card in this family, #23622, landed with PR #23725; it is named here
only as context). Three runs boot through the SQL driver's connect
before they print. On a SQLite file still on a rollback journal, that
connect switches the journal mode, which rewrites the file header and no
row. Each run then said "nothing was written". Each now claims no row or
table, and on a SQLite target its line is followed by the exported
`JOURNAL_MODE_SWITCH_NOTE`: "Opening a SQLite file can still switch its
journal mode (to WAL by default), which changes its header only."
The basis for `Clause-②: no` is the claim's: operator text only. No
flag, exit code, prompt, refusal, accepted input or export signature
moves, no `--json` document gains a field, and there is no
`packages/spec` edit.
## The premise, measured at the door, before and after
The real CLI through `packages/cli/bin/run-dev.js` (tsx), with
`@objectstack/service-storage` and `@objectstack/objectql` rebuilt at
each sha. Before: `6422da58e` (base). After: `7bdbb42af` (this head).
Every run got its own copy of the fixture and a fresh `OS_HOME`, with
the database env vars unset. Header bytes 18 and 19 are the journal mode
(1,1 rollback journal; 2,2 WAL); the counter is bytes 24 to 27.
"Content" is an md5 over the schema plus every row of every table, read
on a connection of my own.
- Rows 1 and 2: a fresh file with one table and one row, put on
`journal_mode = delete`, run from an empty directory.
- Row 3: `examples/app-crm` compiled with `os build`, its tables created
by one plain boot, one `crm_opportunity` row inserted with `line_total`
NULL, then the file put on `journal_mode = delete`; run from the
example's directory.
| Run | Header 18,19 · counter | Content | Line printed before | Line
printed after |
|---|---|---|---|---|
| `os storage orphans` | 1,1 · 2 to 2,2 · 3 (both shas) | unchanged |
"This pass is READ-ONLY: nothing was written, tombstoned or deleted." |
"This pass is READ-ONLY: no row or table was written, tombstoned or
deleted." plus the note |
| `os secret rewrap --no-declared-datasources` (dry run, text) | 1,1 · 2
to 2,2 · 3 (both) | unchanged | "Dry run: every attributed row was
opened, re-sealed and verified in memory, and nothing was written.
Re-run with --apply to write." | "… and no row or table was written.
Re-run with --apply to write." The closing line already carries the note
(PR #23725) |
| the same with `--json` | 1,1 · 2 to 2,2 · 3 (both) | unchanged |
`report.notes[2]` = the old sentence | `report.notes[2]` = the new
sentence |
| `os migrate summary-nulls` (dry run, 1 NULL roll-up) | 1,1 · 4 to 2,2
· 5 (both) | unchanged | "Dry run — nothing was written. Re-run with
--apply to backfill." | "Dry run — no row or table was written. Re-run
with --apply to backfill." plus the note |
Row 3's own line was measured at the door for the first time here (the
earlier reading was of its boot only).
## How the note reaches rows 1 and 3, and which doors it reaches
- The two formatters live in `@objectstack/service-storage`
(`formatStrandedOrphanInventory`) and `@objectstack/objectql`
(`formatSummaryBackfillReport`). Neither imports `packages/cli`, so
neither can reach `JOURNAL_MODE_SWITCH_NOTE` or the SQLite test. Each
formatter's own line changes its claim; the CLI command that prints it
appends the note to that line when
`describeDriverSqliteFile(stack.driver?.config) !== undefined`, the test
PR #23725 uses (`utils/connection-display.ts`, read and not edited).
- **No parameter was added to either formatter.** Their export
signatures stay byte-identical, so no public surface widens and
`Clause-②: no` stays true. The claim allowed a parameter; the CLI-side
append made it unnecessary.
- The appended line is the formatter's last line in both: the READ-ONLY
line is pushed last and unconditionally, and the dry-run line is pushed
last under `!report.applied && report.nullRows > 0`, the condition
`summary-nulls.ts` repeats. Each formatter's docblock or comment now
says so. The CLI pins run the real formatters, so a reorder there turns
a pin red.
- **Doors, by `git grep`:** each formatter has exactly one caller, its
command's text output. The `--json` documents of `os storage orphans`
and `os migrate summary-nulls` carry the report object and no sentence,
so they are unchanged.
- **Row 2 has two doors.** `REWRAP_NOTES.dryRun` is printed under "Read
before acting" in the text output and is an element of `--json`
`report.notes`; both now carry the corrected claim. The note is not
appended to this sentence: in the text output the dry run's closing line
already carries it on a SQLite file (PR #23725), and a second copy two
lines apart would add nothing. `--json` gets no note, following PR
#23725, which moved no `--json` document; no field or array element was
added.
## The texts
| # | Where | FROM | TO |
|---|---|---|---|
| 1 | `service-storage/src/stranded-orphan-inventory.ts:421` (`os
storage orphans`) | "This pass is READ-ONLY: nothing was written,
tombstoned or deleted." | "This pass is READ-ONLY: no row or table was
written, tombstoned or deleted." plus the note on SQLite, appended by
`cli/src/commands/storage/orphans.ts` |
| 2 | `cli/src/utils/sys-secret-rewrap.ts:507` (`REWRAP_NOTES.dryRun`) |
"… and nothing was written. Re-run with --apply to write." | "… and no
row or table was written. Re-run with --apply to write." |
| 3 | `objectql/src/summary-backfill.ts:539` (`os migrate
summary-nulls`) | "Dry run — nothing was written. Re-run with --apply to
backfill." | "Dry run — no row or table was written. Re-run with --apply
to backfill." plus the note on SQLite, appended by
`cli/src/commands/migrate/summary-nulls.ts` |
**Developer comments the card names, one phrase each:**
`migrate/recorded-by.ts:63` ("writes nothing" becomes "writes no row or
table"), `secret/rewrap.ts:46` (the same), and `secret/orphans.ts:221`
("this report promises to write nothing" becomes "this report promises
to write no row or table, and writes no key file either": the comment is
about the key file the settings service's default provider would mint).
## Pins (one per printed text)
- **Row 1:** `cli/src/commands/storage/orphans.test.ts` gains two report
cases with the boot replaced; the inventory and its formatter run for
real. A SQLite driver config gets exactly "READ-ONLY line + note"; a
PostgreSQL config gets exactly the line and no note anywhere.
- **Row 2:** `cli/src/commands/secret/rewrap.guards.test.ts` gains one
case: the dry run's note claims no row or table in the text output and
in `--json` `report.notes`, and no line says "nothing was written".
- **Row 3:** `cli/src/commands/migrate/summary-nulls.test.ts` gains two
cases (SQLite: line + note; PostgreSQL: line, no note). Its
`@objectstack/objectql` mock now keeps the real
`formatSummaryBackfillReport` (`importOriginal`); the walk stays
replaced. `objectql/src/summary-backfill.test.ts:355`, a byte-for-byte
literal of the formatter's lines, carries the new line.
- **Tier:** `storage/orphans.test.ts` now value-imports
`bootSchemaStack` to mock it, so `packages/cli/vitest-tiers.ts`'s KERNEL
signal moves it from `unit` to `integration`, beside
`summary-nulls.test.ts` and `rewrap.guards.test.ts`, which mock it the
same way. The partition is derived, so nothing else moves.
## Ablation (one: the pins check the note by a condition)
At `7bdbb42af`, through `scripts/ablation-replace.mjs` in WRAP mode with
an EXIT/INT/TERM trap that restores from `HEAD`. No rebuild was needed:
the subject is the CLI's own `src`, reached by relative import.
- Mutation: in `migrate/summary-nulls.ts`, `&&
describeDriverSqliteFile(stack.driver?.config) !== undefined` became `&&
false`. Anchor 1 to 0, blob `c51055eef276` to `dd0a41900e2c`.
- Result: `Tests 1 failed | 7 passed (8)`. The SQLite case went red:
Expected "Dry run — no row or table was written. Re-run with --apply to
backfill. Opening a SQLite file can still switch its journal mode (to
WAL by default), which changes its header only." Received "Dry run — no
row or table was written. Re-run with --apply to backfill." The
PostgreSQL case stayed green.
- Restore: blob equals HEAD (`c51055eef276`), `git diff HEAD` is empty,
the marker count is 0, and `git status --porcelain` is empty. The
direction was red, as expected.
## Verification (head `7bdbb42af`)
- **Build:** `turbo run build --filter='@objectstack/cli...'
--concurrency=2`: 61 tasks, VERDICT command-exit 0. The new texts are in
both dists (`service-storage/dist/index.{js,cjs}`,
`objectql/dist/index.{js,mjs}`: 1 hit each; the old ones: 0).
- **`@objectstack/service-storage`:** `test` Test Files 59 passed (59),
Tests 1021 passed (1021); `typecheck` exit 0 (`check:test-typecheck` OK,
0 files of debt).
- **`@objectstack/objectql`:** `vitest run --project local
--maxWorkers=2` Test Files 434 passed (434), Tests 8213 passed (8213),
VERDICT 0; `typecheck` exit 0 (`check:test-typecheck` held at 39 files,
233 errors, 64 signatures).
- **`@objectstack/cli`:** the four touched test files by path
(`storage/orphans`, `migrate/summary-nulls`, `secret/rewrap.guards`,
`utils/sys-secret-rewrap`): Test Files 4 passed (4), Tests 37 passed
(37), VERDICT 0. These are integration-tier files, run by path; the rest
of that tier is declared to CI. The unit tier (`vitest run --project
unit --maxWorkers=2`): Test Files 300 passed (300), Tests 4480 passed
(4480), VERDICT 0. `typecheck` exit 0 (`check:test-typecheck` held at 3
files, 28 errors, 6 signatures); `tsc --noEmit --listFiles -p
tsconfig.json` puts the three touched CLI test files in its 1587-file
program.
- **Lint, by the narrowing:** `pnpm exec eslint --no-inline-config` over
the 12 changed `.ts` files exited 0. The repo-wide `pnpm lint` is CI's
`Lint & Repo Gates`.
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derived 72 commands from merge base
`6422da58e` (13 paths, 248 changed lines). All 72 ran, each exit
captured before any pipe. 68 exited 0 on the first run; four refused on
a prerequisite and exited 0 when re-run after it was met:
`check-engine-split-ratio.mjs --days 90` (exit 2: shallow clone inside
its window; 0 after `git fetch --shallow-since=2026-07-11`),
`check:dual-build-cjs-loads` and `check:i18n-coverage` (exit 3: missing
`dist/`; 0 after `turbo run build`), `check:type-check-debt` (exit 3:
its own closure build did not get the lock; 0 run under the lock).
`--ran` printed "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN".
## The census: what is left of this family
Re-grepped every source file that boots a database (`bootSchemaStack`,
`createStandaloneStack`, the SQL driver, the occupancy probe) in
`packages/cli`, `packages/services`, `packages/objectql`,
`packages/runtime` and `packages/plugins` for "nothing was written /
changed", "writes nothing", "no changes made" and the like. **The census
is not complete:** four more sentences on the same mechanism remain,
outside this claim's file surface and its hold-off ("no other read-only
arm"), so they are left alone and named for the seat.
- **`os migrate plan`, `cli/src/utils/unmanaged-tables.ts:508`** — "They
are reported for information only — nothing here drops them, and a plan
writes nothing." Measured at the door at `7bdbb42af` (the line is
untouched here): the CRM fixture plus one `sys_legacy_widget` table, on
a rollback journal: header 1,1 · 5 to 2,2 · 6, content unchanged, and
the line printed.
- **`os migrate plan` / `apply` declaration boot,
`cli/src/utils/schema-migration-plugins.ts:1148`** — "Refused N write(s)
during the declaration boot — a plan writes nothing" (printed only when
a plugin's write was refused; read, not measured), and `:1163`, "If
those commands only read, nothing was written".
- **`os migrate summary-nulls --recompute-undefined-on-empty` with a
name it cannot find, `objectql/src/summary-backfill.ts:295`** — "Refused
before any row was read; nothing was written." It is thrown after the
boot in either mode (read, not measured). The claim limited this file to
the dry-run line.
- **`os secret rewrap`, `REWRAP_NOTES.resumable`,
`cli/src/utils/sys-secret-rewrap.ts:502`** — "… and a finished run
writes nothing.", printed by every run (both modes, text and `--json`),
and mirrored in `content/docs/deployment/cli.mdx:632`. Measured at
`7bdbb42af`: a finished re-run (`--apply --yes`, 0 rows re-wrapped) on a
provisioned file: header 1,1 · 5 to 2,2 · 6, content unchanged. The
first `--apply` on an unprovisioned file also created 10 tables and 2
rows.
- **A neighbouring class, write arms rather than read-only ones:**
`migrate/apply.ts:551` ("Aborted — no changes made.", declined after a
`deferSchemaDdl` boot) and `utils/organization-ownership-apply.ts:1542`,
`:2483`, `:2494`, `:2503` and `:2540` ("Nothing was written." refusals
after the apply boot). Not measured.
- Checked and true as written: the "Aborted — no changes made." lines in
`activity-changed-fields`, `audit-metadata-bodies`, `comment-reactions`,
`files-to-references`, `multi-value-columns`, `summary-nulls` and `meta`
print before their boot; `generate.ts`'s "Nothing was written." lines
open no database.
## Acceptance notes
- **No merge of `origin/main` before opening.** 13 commits landed since
the base. Four touch the boot path's neighbours
(`runtime/src/standalone-stack.ts`, `cli/src/utils/storage-driver.ts`,
`cli/src/commands/dev.ts`: URL-scheme and file-URL parsing, the dev
watcher), none of this diff's files, the driver's connect or the two
formatters. `git merge-tree --write-tree HEAD origin/main` is clean
(exit 0). This is a declared narrowing; CI's merge-ref run covers the
joint tree. `dispatch-gates` flagged one changed derivation input
upstream (`.github/workflows/spec-shape-gate.yml`), which no path here
reaches.
- **Two readings this run took that the card did not ask for:** the
`--json` door of row 2, and row 3's own line at the door (above).
Built by `session_018t6a5rvqPRvEDMCVVW9kYT`, the dev for `domain:cli`
seat 2's claim `6213581109`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0cb45e9 commit abb6df0
13 files changed
Lines changed: 228 additions & 20 deletions
File tree
- .changeset
- packages
- cli/src
- commands
- migrate
- secret
- storage
- utils
- objectql/src
- services/service-storage/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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
| 64 | + | |
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
15 | 21 | | |
16 | 22 | | |
17 | 23 | | |
| |||
21 | 27 | | |
22 | 28 | | |
23 | 29 | | |
| 30 | + | |
24 | 31 | | |
25 | 32 | | |
26 | 33 | | |
| |||
29 | 36 | | |
30 | 37 | | |
31 | 38 | | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
36 | 48 | | |
37 | 49 | | |
38 | 50 | | |
| |||
154 | 166 | | |
155 | 167 | | |
156 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 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 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
254 | 255 | | |
255 | 256 | | |
256 | 257 | | |
257 | | - | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
258 | 271 | | |
259 | 272 | | |
260 | 273 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
350 | 350 | | |
351 | 351 | | |
352 | 352 | | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
353 | 368 | | |
354 | 369 | | |
355 | 370 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
12 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
13 | 17 | | |
14 | 18 | | |
15 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
16 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
17 | 70 | | |
18 | 71 | | |
19 | 72 | | |
20 | 73 | | |
21 | 74 | | |
22 | 75 | | |
23 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| 21 | + | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| |||
181 | 183 | | |
182 | 184 | | |
183 | 185 | | |
184 | | - | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
185 | 197 | | |
186 | 198 | | |
187 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
503 | 506 | | |
504 | | - | |
505 | | - | |
| 507 | + | |
| 508 | + | |
506 | 509 | | |
507 | 510 | | |
508 | 511 | | |
| |||
0 commit comments