Repository navigation
fix(cli): four more read-only sentences name what they vouch for, and the journal-mode switch on a SQLite file - #24053
Conversation
… the journal-mode switch on a SQLite file os migrate plan (the unmanaged-table section and the declaration boot's write-guard note), a refused summary-nulls scope, and the rewrap "Re-running is safe" note stop saying nothing was written. On a SQLite target each line that makes the claim is followed by JOURNAL_MODE_SWITCH_NOTE in the text output. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 41 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 f973742056bd67354dbefd6bd54039435fd87257 && git checkout f973742056bd67354dbefd6bd54039435fd87257
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c2ac83a230d2eec038502a23a8142153cad814d c4adb868cff7600cb95f62bf263d3b4b92c831f4 && git checkout -B drift-repro 5c2ac83a230d2eec038502a23a8142153cad814d && git merge --no-ff c4adb868cff7600cb95f62bf263d3b4b92c831f4
node scripts/docs-audit/affected-docs.mjs --json 5c2ac83a230d2eec038502a23a8142153cad814d
|
席位核对ACCEPT · #23899 ·
Filed from this PR: #24059. Noted, not filed (the dev's, as
Landing: Generated by Claude Code |
Fixes #23899
Clause-②: no
The remainder of #23726, by the pattern PR #23608, PR #23725 and PR #23895 set. Four sentences, printed by three commands after their boot, said that nothing was written. On a SQLite file still on a rollback journal, that boot's connect switches the journal mode, which rewrites the file header and no row. Each sentence now names what it vouches for, and on a SQLite target the line that carries it 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-②: nois the claim's: operator text only. No flag, exit code, prompt, refusal, accepted input or export signature moves, no--jsondocument gains or loses a key, and there is nopackages/specedit.The premise, measured at the door, before and after
The real CLI through
packages/cli/bin/run-dev.js(tsx), with@objectstack/objectqlrebuilt at each sha. Before:bb14934cd(base). After:c4adb868c(this head). Every run got its own copy of the fixture and a freshOS_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.examples/app-crmcompiled withos build, provisioned byos migrate apply --yes, plus onesys_legacy_widgettable with one row, then put onjournal_mode = delete(72 tables, 1 row).objectstack.config.tscarries one plugin: itskernel:readyhook callscreate()on the driver (2a) orexecute('SELECT count(*) FROM sys_metadata')(2b).os secret rewrap --apply --yes, then put onjournal_mode = delete(10 tables, 2 rows).os migrate plan(CRM)os migrate plan, a plugin write refusedos migrate plan, a raw read forwardedos migrate summary-nulls --recompute-undefined-on-empty crm_account.nope, dry run and--apply --yes(CRM)os secret rewrap --apply --yes --no-declared-datasources, a finished re-run (0 re-wrapped)All four rows print after the connect, so none of them was left as it was. Rows 2 and 3 were read, not measured, on the card; they are measured here for the first time.
The
--jsondoors.os migrate plan --jsoncarries the guard note incomposition.notes(row 2a measured: the new sentence, no note; key set identical to base, 22 keys). The unmanaged-table section is theunmanagedTablesreport object and carries no sentence.os migrate summary-nulls --jsoncarries the engine's refusal inerror(the new sentence, no note;code: INVALID_FIELD,httpStatus: 400).os secret rewrap --jsoncarriesreport.notes[1](the new sentence, no note), in both modes; the key set and the notes' count and order are identical to base.A non-SQLite target, at the door:
os migrate plan --database-url :memory:from the 2a directory prints the 2a sentence with no note (0 occurrences). The pins below add the PostgreSQL case for every text.Why two sentences name a narrower noun than "no row or table"
Two runs boot plain, not read-only, and that boot writes on a database that has never been started. Measured at base on a fresh file (one placeholder table, rollback journal):
os migrate summary-nulls --apply --yes --recompute-undefined-on-empty crm_account.nope: 1 table to 14 tables, rows unchanged, then the refusal.os secret rewrap --apply --yes --no-declared-datasourceswith nothing to re-wrap (a finished run, exit 0): 1 table to 11 tables and 1 row to 3 (twosys_migrationrows), and the "Re-running is safe" note printed.So "no row or table" would be false there. Row 3 now vouches for the walk ("Refused before the walk read or wrote any row."), which is what
backfillSummaryNullscontrols, and row 4 for the rows the re-wrap writes ("a finished run writes no sys_secret row"). Both are true in either mode. Rows 1 and 2 are printed only byos migrate plan, whose boot defers DDL and opens no missing file, so they claim no row or table.How the note reaches each line
The test is the one PR #23725 uses:
describeDriverSqliteFile(stack.driver?.config) !== undefined(utils/connection-display.ts, read and not edited). The note is appended by the command that prints the line, in the text output only. No parameter was added anywhere, so no export signature moves.migrate/plan.ts. The composition notes and the unmanaged-table section are printed through one helper: a line that claims "no row or table" is followed by the note on a SQLite file. That is how the command finds the guard's note among the composition notes without a new export from the guard; the pin feeds it the REAL guard's text, so a guard rewording that drops the claim turns the pin red. The--jsondocument is unchanged in shape.migrate/summary-nulls.ts. The command's catch appends the note to anINVALID_FIELDerror on a SQLite file. Inside thattry,INVALID_FIELDhas one producer:backfillSummaryNulls's scope refusal (its onlythrow; the walk records failures rather than throwing, and the other refusals there areOBJECT_NOT_FOUNDor plain errors). Both modes. The--jsonenvelope carries the engine's message as it is.secret/rewrap.ts.renderReporttakes a map from a note's text to what follows it; the "Re-running is safe" note gets the note under--applyon a SQLite file. A dry run's closing line already carries it, so there it carries none (one copy per run, pinned).REWRAP_NOTESstays loaded at the point of use, as before.report.notesreaders, bygit grepover the repo: the resumable note's text had no reader outside its own definition and the docs mirror.sys-secret-rewrap.test.ts's RESUMABLE case title names the executor's own writes (it asserts the store's write count), not this text, and is unchanged.The texts
cli/src/utils/unmanaged-tables.ts:508cli/src/utils/schema-migration-plugins.ts:1148cli/src/utils/schema-migration-plugins.ts:1163objectql/src/summary-backfill.ts:300cli/src/utils/sys-secret-rewrap.ts:507(REWRAP_NOTES.resumable) andcontent/docs/deployment/cli.mdx:652Pins (one per text, a SQLite and a PostgreSQL case where the note is conditional)
cli/src/utils/unmanaged-tables.test.tsgains one case pinning the closing line byte for byte.cli/src/commands/migrate/plan.journal-mode-note.test.ts(new) runs the real command with the boot, the occupancy probe and the sweep's READ replaced (the renderer is real): SQLite gets exactly "line + note", PostgreSQL exactly the line and no note anywhere.createDeclarationBootWriteGuard()armed over a fake driver, made to refuse onecreate()or forward oneexecute(), and disarmed. One more case pins--json:composition.notesequals the guard's note, and the document carries no note. Inschema-migration-plugins.declaration-boot-write-guard.test.ts, every assertion on the old phrase moved to the new one, the negative ones included, so none of them went vacuous (11 occurrences); the raw-execute case now pins the 2b sentence itself.schema-migrate.host-composition.integration.test.tsandschema-migrate.deferred-ddl.integration.test.tsmoved the same way.objectql/src/summary-backfill.test.ts's REFUSES case pins the closing sentence for all six refusal shapes.cli/src/commands/migrate/summary-nulls.test.tsgains five cases with the engine's REAL refusal (vi.importActual): dry run and--apply, each SQLite and PostgreSQL, and--json(the envelope'serroris the engine's message, no note).cli/src/commands/secret/rewrap.guards.test.tsgains four cases:--applySQLite (the note follows the line),--applyPostgreSQL (no note), a dry run (the line carries none; the run carries the note once), and--jsonin both modes (report.notesholds the new text, no note).bootSchemaStackto mock it, sovitest-tiers.ts's KERNEL signal puts it inintegration, besidesummary-nulls.test.tsandrewrap.guards.test.ts.Ablation (one per wording)
At
c4adb868c, one run under the verify lock: a baseline, then each new wording put back to its old text throughscripts/ablation-replace.mjs(WRAP mode) with an EXIT/INT/TERM trap restoring fromHEAD, every pin file re-run in each leg (objectql/src/summary-backfill.test.tsplus the seven CLI pin files). Each leg: the anchor hit once and fell 1 to 0, the blob changed, and the restore was proven by blob equal toHEAD,git diff HEADempty andgit status --porcelainempty.6add5c0c8d26toc647aec174bc498e46872d08to0fa153eef1a4498e46872d08toe2786928219d2fb88f18451dtof83de26a8bc23db476f2ebcetoec49817fb26fLeg 3's CLI cases stayed green, as expected: they read the engine's refusal through
@objectstack/objectql'sexports, sodist/, which this source mutation did not rebuild. The text's pin is the objectql one, which resolvessrc/. The CLI cases pin the note's follow-through, not the text. No other leg needed a rebuild: every other subject is the CLI's ownsrc, reached by relative import.Verification (head
c4adb868c)turbo run build --filter='@objectstack/cli^...' --concurrency=2: 60 tasks, VERDICT command-exit 0; then@objectstack/objectqlrebuilt after the edit (14 tasks, VERDICT 0). The new sentence is inobjectql/dist/index.jsandindex.mjs(1 hit each; the old one: 0).summary-backfill.test.ts20 of 20; CLI 13 files including the fourplan.*.integration.test.ts,rewrap.driver-contract.test.tsandsys-secret-rewrap.test.ts, 150 of 150). These are integration-tier files, run by path; the rest of that tier is declared to CI. The CLI unit tier (vitest run --project unit --maxWorkers=2): Test Files 307 passed (307), Tests 4574 passed (4574), exit 0.@objectstack/objectql(vitest run --project local --maxWorkers=2): Test Files 436 passed (436), Tests 8229 passed (8229), exit 0.@objectstack/cliexit 0 (check:test-typecheckheld at 3 files, 28 errors, 6 signatures);@objectstack/objectqlexit 0 (held at 39 files, 233 errors, 64 signatures).tsc --listFilesOnly -p tsconfig.jsonputs all seven touched CLI test files in its 1607-file program.pnpm exec eslint --no-inline-configover the 15 changed.tsfiles exited 0. The repo-widepnpm lintis CI'sLint & Repo Gates.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 99 commands from merge basebb14934cd(17 paths, 506 changed lines). All 99 ran atc4adb868c, each exit captured before any pipe. 94 exited 0 on the first run; five refused on a prerequisite (exit 3: no builtdist/for the CLI, some plugins orclient-react) and exited 0 when re-run afterturbo run build --filter='!@objectstack/docs' --concurrency=2(75 tasks, 74 cached, VERDICT 0):check:skill-examples,check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parity.check-engine-split-ratio.mjs --days 90ran aftergit fetch --shallow-since=2026-07-10(exit 0).--ranprinted "99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN". The derivation flagged one input changed upstream since the base (scripts/engine-double-contract.pinned.json); this diff adds no engine double.Acceptance notes
origin/mainbefore opening. 6 commits landed since the base (spec,rest,runtime, the hono adapters,cloud-connection, a dogfood test,engine-double-contract.pinned.json); none touchespackages/cli,packages/objectqlor a file this diff reads.git merge-tree --write-tree HEAD origin/mainexits 0. A declared narrowing: CI's merge-ref run covers the joint tree.schema-migration-plugins.tsthat quote the old phrase "a plan writes nothing" (lines 130, 187, 201, 776, 783, 937, 1680), thedescribetitle of the host-composition case (a plan writes nothing even when the host writes from init()), andsys-secret-rewrap.ts's module docblock ("Re-running a finished run writes nothing", about the executor's own writes).os migrate applyprints the same guard note (its composition notes,apply.ts:261), now with the new wording and without the journal-mode note: an apply's boot is followed by its own writes, and the claim's file surface namesos migrate plan. Not measured.migrate/apply.ts:551,organization-ownership-apply.ts), per the hold-off.Built by
session_018t6a5rvqPRvEDMCVVW9kYT, the dev fordomain:cliseat 2's claim6223113446.Generated by Claude Code