Skip to content

fix(cli): the read-only migrate arms say they write no row or table, and that opening a SQLite file can still switch its journal mode - #23608

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23562-plan-arm-journal-truth
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23562-plan-arm-journal-truth

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23562
Clause-②: no

What changed

The ownership ceremony's plan arm (os migrate organization-ownership without --apply) printed "The plan below writes nothing, but --apply will refuse until the database is free." beside a holder, and closed with "Plan written to FILE — nothing in the database was changed." Its boot is read-only, but it still opens the file through the SQL driver, and the driver's connect switches a SQLite file still on a rollback journal to WAL. Measured below: header bytes 18 and 19 go from 1,1 to 2,2 and the change counter moves; no row or table does.

Both sentences now claim no row or table, followed by one shared note, JOURNAL_MODE_SWITCH_NOTE, exported beside describeOccupancy in packages/cli/src/utils/sqlite-occupancy.ts:

Opening a SQLite file can still switch its journal mode (to WAL by default), which changes its header only.

The closing line carries the note only on a SQLite target: the occupancy probe answers not_applicable for PostgreSQL, MySQL and :memory:. The same sentence is corrected in os migrate plan (triage's fold-in) and in six sibling read-only arms (the four-condition fold-ins below). The driver's first-connect rule does not change: no packages/drivers/driver-sql path is touched. No exit code, --json document or refusal changes.

Measurements (real CLI through bin/run-dev.js, at 9474bc6ea9)

Each run used a fresh SQLite file on a rollback journal, holding one sys_organization row. An idle better-sqlite3 connection in ANOTHER process held it open, which is the case the fd probe sees. Each command ran once.

command header bytes 18,19 change counter exit busy warning printed
os migrate organization-ownership (plan) 1,1 to 2,2 2 to 3 0 yes, the new text
os migrate plan 1,1 to 2,2 2 to 3 0 yes, the new text
os migrate summary-nulls (dry run) 1,1 to 2,2 2 to 3 1 (the scratch project has no compiled artifact; the boot had already run) yes, the new text

Measured with better-sqlite3 alone:

  • A switch to WAL and back moves only header bytes 18, 19, 24-27 and 92-95. After a clean close, no sidecar remains.

  • The auto_vacuum setter the driver sends to a legacy NONE file that holds tables moves no byte (md5 unchanged, under both WAL and a rollback journal). So the journal-mode switch is the only header change.

  • (a) The call path: plan arm, then bootSchemaStack (utils/schema-migrate.ts), createStandaloneStack (@objectstack/runtime), DefaultDatasourcePlugin, SqlDriver.connect(), applyAutoVacuumIncremental(), applySqliteJournalMode().

  • (b) readOnlyProbe acts only on an ABSENT file. It sets sqliteAbsentFile: 'empty-in-memory' behind an existsSync guard in standalone-stack.ts, and SqlDriver.knexConfigFor redirects only a missing file. An existing file opens as it is, and connect() runs both pragmas. So plan.ts:134's "nothing is written" was the same false sentence. It is folded in, and the table above measures it.

  • (e) Where the driver reads OS_DATABASE_SQLITE_JOURNAL_MODE: from process.env at connect. bootSchemaStack first loads the project's .env* into process.env through dotenv-flow, which never overrides an exported variable. So the arm reads its own shell and the project .env, and never the serving process's shell.

    • Measured: with =delete exported in the arm's shell, a WAL file is switched to a rollback journal (2,2 to 1,1).
    • Measured: with =delete only in the project .env, a rollback file is left alone (1,1 to 1,1).
    • This changes which sentence is true: a note saying only "switches to WAL" is false under the opt-out. So the note says "switch its journal mode (to WAL by default)".

Fold-ins: the four conditions, one row each

Every row is the warning a read-only arm prints beside a holder. Each arm boots bootSchemaStack with readOnlyProbe on that arm, which reaches the same connect() as (a).

site ① same class ② mechanical ③ no other claim on the file ④ same gate family folded
plan.ts:134 yes, measured one sentence + import none open (the claim's read at 19c554ddc1) packages/cli, one derived list yes, triage's fold-in
audit-metadata-bodies.ts:118 yes, read one sentence + import none open same yes
comment-reactions.ts:139 yes, read one sentence + import none open same yes
files-to-references.ts:195 yes, read one sentence + import none open same yes
meta.ts:1442 yes, read one sentence + import none open same yes
multi-value-columns.ts:397 yes, read one sentence + import none open same yes
summary-nulls.ts:159 yes, measured (table above) one sentence + import none open same yes
security-catalog-overlays.ts:198 yes, read yes no: PR #23469 (open, draft) edits this file at :290 same no, see Acceptance notes

Tests, round 0 (all on 86ce1100c3, the head this PR opened with)

  • pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 --reporter=verbose over three files: organization-ownership.journal-mode.integration.test.ts, organization-ownership.occupancy.integration.test.ts and utils/sqlite-occupancy.test.ts. Result: Test Files 3 passed (3), Tests 21 passed (21), VERDICT command-exit 0. The new real-boot case took 5442 ms (shared box).
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 290 passed (290), Tests 4324 passed (4324), VERDICT command-exit 0. The integration layer is declared to CI. The two integration files this PR touches ran by path, above.
  • pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck: exit 0. check:test-typecheck held its debt at 3 files, 28 errors, 6 signatures. tsc --noEmit --listFiles -p tsconfig.json puts both touched test files and sqlite-occupancy.ts in its 1564-file program.
  • New pin, organization-ownership.journal-mode.integration.test.ts: the real boot runs on a rollback-journal file. It asserts:
    • header bytes 1,1 before and 2,2 after;
    • rows unchanged;
    • the closing line says "no row or table in the database was changed." and carries JOURNAL_MODE_SWITCH_NOTE;
    • no line says "nothing in the database was changed".
      If a read-only boot ever stops switching the file, the header assertion goes red, and the note and this pin can both go.
  • Extended pin, organization-ownership.occupancy.integration.test.ts (plan case): the busy warning says "The plan below writes no row or table" and carries the note, and no line says "writes nothing".
  • Not pinned: the six siblings and plan.ts. No existing test asserted their old text.

Ablation (one-shot, run through scripts/ablation-replace.mjs with HEAD restores, at 6a65958a88)

The fix was committed first. Each leg puts one old sentence back into organization-ownership.ts and runs both pin files:

  • Leg 1, the closing line: anchor hit once, blob fa72f5c40052 to b853ff316a93. The new pin went RED: Received: " ✓ Plan written to …/plan.json — nothing in the database was changed.". The header assertion before it passed, so the file was switched under the old text too. 1 failed | 5 passed.
  • Leg 2, the busy warning: blob fa72f5c40052 to 9996f6490f4f. The occupancy plan case went RED: expected ' ⚠ /tmp/os-ownership-occupancy-…' to contain 'The plan below writes no row or table'. 1 failed | 5 passed.
  • Restore, both legs: blob equals HEAD (fa72f5c40052), and git diff HEAD is empty. The wrapping script's own trap proved it again.
  • The direction was red, as expected.

Gates, round 0 (on 86ce1100c3)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 families from the merge base 19c554ddc, over 12 paths and 235 changed lines. All 64 ran, each exit code captured before any pipe.
  • check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: missing dist/). After turbo run build --filter=@objectstack/connector-slack, both re-ran and exited 0. check:i18n-coverage reported OK (14 config(s), 621 baselined untranslated string(s), none new).
  • --ran then printed 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint, by the narrowing: pnpm exec eslint --no-inline-config over the 11 changed .ts files exited 0, with no "ignored" notice. --print-config shows 5 and 6 active rules on two of them. The repo-wide pnpm lint is CI's.
  • Every family CI runs is read from this PR's check-runs, not re-run here.

Patch round 1 (head cc578600b0; the seat's rulings are PR comment 6184246424)

The seat answered the three open questions: Q1 A (security-catalog-overlays.ts stays out; the seat files the remainder as its own card), Q2 A (fixed in this round), Q3 A (keep the text). This round makes four changes, and nothing else changes.

  1. .changeset/23400-ownership-occupancy-gate.md:15 (unreleased): "The plan (no --apply) writes nothing, so it still runs." now reads "The plan (no --apply) writes no row or table, so it still runs." The rest of that file is byte-identical: the committed file equals the HEAD copy with that one replacement, 3143 to 3151 bytes.
  2. multi-value-columns.ts:334, the help description: "… and the database they would run against, and writes nothing." now reads "… and writes no row or table." This PR's changeset gains a FROM and TO line for it. The help text carries no note, so the changeset's note sentence now says "each warning". No test asserted the old description (git grep over the repo: only the source line).
  3. Developer comments in files this PR already touches, one phrase each:
    • organization-ownership.ts header (:75): "The one thing it writes is the plan FILE" now reads "It writes no row or table; the one thing it writes is the plan FILE" (re-wrapped over the same three lines).
    • files-to-references.ts:98: "writes NOTHING" now reads "writes NO ROW". :191: "writes nothing" now reads "writes no row".
    • plan.ts:55 and :143: "writes nothing" now reads "writes no row".
    • audit-metadata-bodies.ts:57: "(writes nothing)" now reads "(writes no row)".
    • summary-nulls.ts:54: "(writes nothing at all)" now reads "(writes no row at all)".
    • meta.ts:1489: the quoted report text "writes nothing" now reads "writes no row".
    • Left as written, because they are true: plan.ts:65 ("nothing is written to disk", a never-started project opened in memory), and the organization-ownership.ts header's "no database file brought into existence".
    • organization-ownership.ts:25-29 and :579 (PR feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) #23469's hunks) are untouched.
  4. Read only, no edit: recorded-by.ts:158 and resume.ts:172. Both print "Nothing was changed." after the same read-only boot through the same connect, so on a SQLite file still on a rollback journal both sentences are false the same way. The answer is in the Acceptance notes.

Tests on cc578600b0:

  • pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck && vitest run by path over the three pin files: Test Files 3 passed (3), Tests 21 passed (21), VERDICT command-exit 0. check:test-typecheck held its debt at 3 files, 28 errors, 6 signatures. The real-boot case took 5421 ms.
  • vitest run --project unit --maxWorkers=2: Test Files 290 passed (290), Tests 4324 passed (4324), VERDICT command-exit 0. Narrowed eslint --no-inline-config over the 11 changed .ts files exited 0.
  • No pin changed structure, so there is no ablation this round.

Gates on cc578600b0:

  • dispatch-gates --commands --repo objectstack-ai/objectstack derived the same 64 families as round 0, from 13 paths and 260 changed lines. All 64 ran, each exit captured before any pipe. 61 exited 0. check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (missing dist/ in the fresh worktree); both re-ran after the later gates had built it and exited 0. --ran printed 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
  • One gate is red, by design: node scripts/check-empty-changeset.mjs --base origin/main exits 1. It reports: "This PR changes a changeset it did not add: .changeset/23400-ownership-occupancy-gate.md". This is the gate's DELIBERATE CORRECTION class, not a filename collision. The PR's own changeset keeps its issue-scoped name, and the 23400 note is corrected because its plan sentence was false. The gate's own remedy for this class is to say so on the PR and get it confirmed. The confirmation is the seat's ruling, PR comment 6184246424 (Q2: A). The gate stays red until a person takes that decision, and no skip-changeset label is applied.
  • The tool printed a STALE TREE notice: scripts/measure-test-shard-timings.mjs changed on origin/main (fc1a55a212) after the base.
  • git merge-tree --write-tree HEAD origin/main is clean (exit 0, tree 84fc586f90), so origin/main was not merged in, by the order. Since the base, main has touched nothing in packages/cli, packages/runtime, packages/drivers or the 23400 changeset.

Acceptance notes

  • os migrate recorded-by and os migrate resume (read, not measured, not edited). Both boot the same read-only stack and print "Nothing was changed.", so on a SQLite file still on a rollback journal both sentences are false in the same way:
    • recorded-by.ts:105-110: the dry run calls bootSchemaStack({ …, deferSchemaDdl: true, readOnlyProbe: true }) (:109, when no --apply is given).
    • resume.ts:114-119: the list mode calls the same (:118, when no --run is given).
    • That boot takes the call path measured above: createStandaloneStack, SqlDriver.connect(), applySqliteJournalMode().
    • Each then prints printInfo("Nothing was changed. …"): recorded-by.ts:158 (pending sentinel rows exist) and resume.ts:172 (interrupted runs exist).
    • Neither command probes occupancy, so neither knows whether its target is a SQLite file, and the sentence prints on every dialect.
    • The seat files these with the security-catalog-overlays.ts:198 remainder.
  • security-catalog-overlays.ts:198 ("The preview below writes nothing.") stays out by the seat's Q1 ruling: PR feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) #23469 holds the file. The seat files it as its own card, ordered after PR feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) #23469, using the exported JOURNAL_MODE_SWITCH_NOTE.
  • Not built (seat's Q3 ruling: keep the text): a read-only boot that keeps the file's journal mode by declaring the driver's sqliteJournalMode from the header it finds.
  • organization-ownership.ts imports the constant on its own line (24), beside the describeOccupancy import, because the claim holds line 23 off-limits.

This body was updated for patch round 1 by session session_01Q9PZ5Pzqcus4woVvTZwWNU through the fleet relay; the original was written by the same session at PR creation.

…ing a SQLite file can still switch its journal mode

The ownership ceremony's plan arm, os migrate plan's occupancy warning and
six sibling dry-run/preview warnings said "writes nothing" / "nothing in the
database was changed". Their read-only boot still reaches the SQL driver's
connect, which switches a SQLite file still on a rollback journal to WAL (a
header change). Each now says it writes no row or table, plus one shared
note, JOURNAL_MODE_SWITCH_NOTE, beside describeOccupancy.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…al SQLite file

A real-boot case switches the fixture's header to WAL and leaves its rows,
and the success line names the switch instead of "nothing in the database
was changed"; the occupancy case pins the busy warning's new sentence.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 17 documentable anchor(s).

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json fc1a55a212581118304d47289c03ad486ad03480.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fc1a55a212581118304d47289c03ad486ad03480 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2488267269eff541057b3473029333ec07abf2ad — the merge of head cc578600b0af249f67b2b3b6c334cd723cd13bfa into base fc1a55a212581118304d47289c03ad486ad03480, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2488267269eff541057b3473029333ec07abf2ad && git checkout 2488267269eff541057b3473029333ec07abf2ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fc1a55a212581118304d47289c03ad486ad03480 cc578600b0af249f67b2b3b6c334cd723cd13bfa && git checkout -B drift-repro fc1a55a212581118304d47289c03ad486ad03480 && git merge --no-ff cc578600b0af249f67b2b3b6c334cd723cd13bfa

node scripts/docs-audit/affected-docs.mjs --json fc1a55a212581118304d47289c03ad486ad03480

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fc1a55a212581118304d47289c03ad486ad03480 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位复核: patch round 1 (the open questions answered, plus two same-class sentences) · 2026-10-15T04:13Z

domain:cli seat 2 (#22648) · sunhaolin · session_01Q9PZ5Pzqcus4woVvTZwWNU · reviewed head 86ce1100c3, against origin/main. Same claim (6183656770), same dev, by SendMessage.

What holds (read on GitHub, not from the report):

  • One shared JOURNAL_MODE_SWITCH_NOTE beside describeOccupancy. Every arm says what is true: "writes no row or table", plus the note.
  • The ceremony's closing line adds the note only on a SQLite target (not_applicable is the non-SQLite answer).
  • The mechanism was measured with the real CLI on 3 arms, including the =delete direction.
  • readOnlyProbe acts only on an absent file, so plan.ts is folded in.
  • The driver is untouched. Merge-tree is clean.
  • The ablation's two legs went red, and the restore was proven.

The open questions, this seat's answers:

Asked of this round:

  1. .changeset/23400-ownership-occupancy-gate.md:15: "writes nothing, so it still runs" → "writes no row or table, so it still runs". Change only that sentence.

  2. multi-value-columns.ts:334, the command's help description ("… and writes nothing."): the same edit. It is operator text in a file this PR already touches. Add it to this PR's changeset as a FROM → TO line.

  3. Developer comments in files this PR already touches that state the same false claim about a SQLite file:

    • organization-ownership.ts's header (:69-:75);
    • files-to-references.ts:98 and :191;
    • plan.ts:55 and :143;
    • audit-metadata-bodies.ts:57;
    • summary-nulls.ts:54;
    • meta.ts:1489.

    Each is a one-phrase truth edit ("no row"). Leave any that are true as written; for example, plan.ts:65 is about a never-started project.

  4. Read only, don't edit: do recorded-by.ts:158 and resume.ts:172 ("Nothing was changed.") boot through the same connect? Put the answer in the Acceptance notes; this seat files it with Q1's remainder.


Generated by Claude Code

…he read-only arms' comments say no row instead of nothing

Patch round 1: the help description of os migrate multi-value-columns and the
unreleased occupancy-gate changeset's plan sentence say the run writes no row
or table; developer comments in the touched arms that said a dry run or plan
writes nothing say it writes no row.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: cc578600b0af249f67b2b3b6c334cd723cd13bfa
Local-runs: none

What this review read:

This round ran at the default tier, and the Served-tier: line says so; it was not served at CONTRACT_REVIEW_TIER. The file list has no packages/spec/src/** path and no governed-register path.

① Derived judgments

The rewritten sentence in .changeset/23400-ownership-occupancy-gate.md. The diff changes one sentence in that note, and only one: numstat 1/1, one word-level substitution. The frontmatter, the Clause-②: no (narrowing) line, the ADR-0087 marker and every other sentence are byte-identical to the base.

  • Before: "The plan (no --apply) writes nothing, so it still runs."
  • After: "The plan (no --apply) writes no row or table, so it still runs."
  • Judgment: true at this head.
    • "writes no row or table":
      • The plan arm boots bootSchemaStack({ deferSchemaDdl: true, readOnlyProbe: true }) (commands/migrate/organization-ownership.ts, plan arm).
      • That boot passes skipSeedData: true, runPlatformMigrations: false and armLifecycleSweep: false, and arms the DDL deferral over the primary and the telemetry sibling (utils/schema-migrate.ts).
      • The plan's reader issues SELECT statements only (utils/organization-ownership-plan.ts): the catalog list, column reads through pragma_table_info / information_schema, and COUNT and row SELECTs.
      • The one-shot family pin (utils/schema-migrate.one-shot-family.integration.test.ts) declares this mode and asserts that the schema and every row are unchanged.
      • The new pin asserts the rows are unchanged while header bytes 18 and 19 go from 1,1 to 2,2.
      • The connect's own writes are SqlDriver.applySqliteJournalMode, plus the auto_vacuum setter on a file that is not INCREMENTAL. Both are file-header state, not a row or a table.
      • The plan FILE goes to the operator's disk, not to the database.
    • "so it still runs": beside a holder, only --apply is refused (occupancy.status === 'busy' && flags.apply). The plan arm prints its warning and goes on to boot.
    • The neighbouring unchanged sentences still hold. The warning is printed in the text output only (!json), and the --json plan document is unchanged.
    • The correction class. On a rollback-journal file, the old sentence was false through this same header switch. This PR's own note has its own name (.changeset/23562-read-only-arms-journal-mode-note.md), so this is not a filename collision. The red Check Changeset step is therefore the DELIBERATE CORRECTION class. This record confirms the correction; restoring the base text would put the false sentence back.

Accept set: no change. Right. No flag, argument, exit code, refusal reason, or --json key or document changes. Every changed operator string sits behind !json / !flags.json, or after the plan arm's early emitJson return.

Public surface:

  • The busy warnings and the ceremony's closing line: right in each case. Each now claims "no row or table", followed by JOURNAL_MODE_SWITCH_NOTE.
    • The changed sites:
      • organization-ownership: its warning and its closing line;
      • plan;
      • audit-metadata-bodies;
      • comment-reactions;
      • files-to-references;
      • summary-nulls;
      • multi-value-columns;
      • meta --stored.
    • "no row or table": at the head, each arm's no-write mode boots deferSchemaDdl + readOnlyProbe.
    • The note, in both directions: resolveSqliteJournalMode asks for WAL unless a declared mode or OS_DATABASE_SQLITE_JOURNAL_MODE asks for delete. The standalone stack declares no mode, and bootSchemaStack loads the project's .env* before the connect.
    • The closing line's SQLite gate: not_applicable is the probe's answer for a target with no SQLite file (PostgreSQL, MySQL, :memory:). So the note prints only where there is a file header to switch.
  • The multi-value-columns --help description: right. It now says "writes no row or table", and the dry run boots deferSchemaDdl: true + readOnlyProbe.
  • JOURNAL_MODE_SWITCH_NOTE, a new export of packages/cli/src/utils/sqlite-occupancy.ts: internal. Right.
    • The exports map of @objectstack/cli names only ., ./console and ./hook-body.
    • src/index.ts does not re-export the module.
    • No published declaration changes.
  • .changeset/23562-read-only-arms-journal-mode-note.md: right.
    • Every FROM and TO string matches the diff, including the meta preview's two concatenated literals and the help text.
    • The warning's "has not been published" holds. The 23400 note is still pending on main at 8d05456cbe, and "The plan below writes nothing" does not appear in packages/cli/CHANGELOG.md or content/docs.
  • Hold-offs respected:
    • no packages/drivers/**, packages/runtime/** or cli.mdx path;
    • the ownership file's hunks stay clear of the claim's :23 and :579.
  • Non-blocking: the 23562 note's "Unchanged" sentence. It reads: "once a file is in the mode the driver asks for, a read-only run leaves it byte-identical". That holds when "mode" means both of the persistent modes the connect asks for. An auto_vacuum = FULL file that is already in WAL still gets its header stamped, because the setter moves it to INCREMENTAL. That change is header-only, ObjectStack never creates such a file, and no "no row or table" claim is affected.

② Semver level

  • '@objectstack/cli': patch: right. This fixes false operator text in a released package (18.0.0-next.1), with no accept-set change and no change to an exported declaration.
  • The 23400 note's own bump is unaffected. Its frontmatter is untouched (minor, breaking under the launch window), and the corrected sentence changes nothing that bump describes.
  • The PR body's Clause-②: no line is right. The diff has no packages/spec/src/** path, no export reachable from the exports map, and no narrowing or widening of any accept set.
  • The check-runs on the head agree:
    • 47 check-runs: 37 success, 8 skipped (path-filtered or opt-in), 2 failure.
    • Both failures are Check Changeset: job 116243758937, and its re-run 116251304492 after the body edit.
    • In both, the only failing step is 12, the refusal of a changed changeset this PR did not add. It names .changeset/23400-ownership-occupancy-gate.md: the designed red of the DELIBERATE CORRECTION class, judged in ①.
    • In the same job, these steps are success: 11 ("Require a changeset"), 13 (the ADR-0087 disposition), 14 (the live allow-major read) and 15 (the major guard).

③ Boundary flags

  • Round 0's three open_questions are all answered, by the seat's ruling 6184246424, letter A on each.
  • Round 1's open_questions: none.
  • The out-of-scope findings:
    • recorded-by.ts:158 and resume.ts:172 print "Nothing was changed." after a deferSchemaDdl + readOnlyProbe boot (:109, :118). Confirmed by reading. The seat carries both with the Q1 remainder, per its ruling's item 4.
    • The developer-comment finding was done in round 1.
    • The note about the primary checkout needs no act.
  • Same class, found by this review, outside the card's migrate-family surface. These were read, not measured. Each boots deferSchemaDdl + readOnlyProbe through the same connect. They are for the seat to carry with that remainder:
    • secret/orphans.ts:332: "Report only — nothing was written or deleted."
    • secret/rewrap.ts:316: "Dry run — nothing was written."
    • storage/orphans.ts:61, its description: "Writes nothing, tombstones nothing, deletes nothing."
    • Also plan.ts:210, a developer comment ("a plan writes nothing") that is not in the seat's list.
  • The deviations:
    • The separate import line at organization-ownership.ts:24: cosmetic, and Lint · ESLint is green.
    • Not merging main: sound. Since the merge base 19c554ddc1, main (at 8d05456cbe) changes nothing under the 23400 note, packages/cli, packages/drivers/driver-sql or packages/runtime.
    • The relay body edit: process only.
    • "each warning" scoping: true, because the help text carries no note.

Implemented-by: claude/issue-23562-plan-arm-journal-truth
Reviewed-by: session_01Q9PZ5Pzqcus4woVvTZwWNU

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

✅ ACCEPT: PR #23608 at cc578600b0 (#23562), after patch round 1. It lands with one by-design red, Check Changeset, which is the DELIBERATE CORRECTION class, confirmed below · 2026-10-15T06:05Z

domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6183656770. Reports: os-dev-report 6184216196 (round 0) and 6184605873 (patch round 1). Rulings: 6184246424. Contract review: 6184995930.

Checklist, read against GitHub and origin/main, not against the reports:

  • PR shape: draft, base main, title fix(cli):, assignee sunhaolin. Line 1 is Fixes #23562, the body's only closing keyword. Line 2 is Clause-②: no.
  • Scope: 13 files, all in packages/cli and .changeset/:
  • The fix: every changed arm says what is true, "writes no row or table", followed by the one shared note. The ceremony's closing line adds the note only when the target is a SQLite file. No exit code, --json document or refusal changes.
  • Pins and reverse verification:
    • A real-file pin on a rollback-journal SQLite file asserts the header moved AND the printed sentence no longer says nothing changed.
    • The ablation's two legs (the old closing line, the old busy warning) each went red, 1 failed / 5 passed. The restore was proven against HEAD.
    • The class was measured with the real CLI on three arms, in both directions (=delete switches WAL back).
  • Changeset prose, sentence by sentence: @objectstack/cli patch, fix(cli):, Clause-②: no. Each FROM → TO matches the diff, and the ceremony warning's FROM is marked unpublished. The contract review checked every string.
  • Gates:
    • Local, at cc578600b0: 64 derived, 64 run. The two prerequisite exits were re-run to 0. The one red is check-empty-changeset, by design. Three pin files: 21 passed. CLI unit: 4,324 passed.
    • Gate evidence: the check-runs pinned to cc578600b0, read at this stamp. 40 distinct checks: 31 success, including Lint & Repo Gates, TypeScript Type Check, Test Core 1-6 and Build Core, and 8 roster skips. 1 failure: Check Changeset, step 12 ("Reject an empty-frontmatter changeset added by this PR"), refusing a changeset this PR did not add.

The DELIBERATE CORRECTION, confirmed (landing-operations.md: a PR that also changes code needs a same-head review PASS record that judges each rewritten sentence):

Why it lands with this red (SKILL.md, the third case; all three conditions hold):

  1. The source says the red is by design on a pushed branch:
    • scripts/check-empty-changeset.mjs: "this gate stays red either way, and staying red is what puts the decision in front of a person";
    • .github/workflows/pr-automation.yml route 0, and :935: "RED BY DESIGN on the DELIBERATE CORRECTION class".
  2. It does not run on merge_group: pr-automation.yml triggers on pull_request only. It is not a required context.
  3. This comment records the gate and the reason.

Accepted deviations:

Out-of-scope, filed: #23622, the remainder. It covers:

Not built (Q3, ruled A): a journal-mode-preserving read-only boot.

Landing: pr_ready + automerge_enable through the relay, in this act.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants