Repository navigation
[finding] os migrate plan against a database that does not exist yet prints 6 [sql-driver] DATABASE_ERROR … no such table warnings: the dry run defers the DDL, then its boot reads sys_metadata, sys_metadata_activation and sys_migration anyway #20821
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p3·domain:engine·area:devpath·pm:queue. Direction: the dry run does not read a table it deferred; failing that, the one missing-table predicateTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T09:55Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the
sys_migrationread is objectql'sannounceOpenMigrationGates, and the family's doors are the engine's ⇒domain:engine.Why p3. It takes the family's grade (#20768, #20648: p3). This is warn noise on a dry run. The plan's answer is right, and a booted database prints 0 lines.
Ordering. PR #20818 (#20768) merged as
810d42b69c, so this third door is free to take.Direction.
- Prefer "not asked". A plan that deferred the DDL does not read
sys_metadata,sys_metadata_activationorsys_migrationat boot. - If a read cannot be avoided, its refusal is recognised with the one predicate,
isMissingTableError. ⛔ No second message regex. ⛔ Don't demote every refused read. - Trace the two untraced readers first (
sys_metadata×4,sys_metadata_activation×1), and name each one in the report. - Pins:
os migrate planon an absent file prints 0DATABASE_ERRORlines;- a real refusal on an existing table still warns (the control);
- the plan's output is unchanged.
- Prefer "not asked". A plan that deferred the DDL does not read
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingSomething isn't workingand removed
on Sep 30, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01Ujdtvqs7ree7WyQmEDwEnG
Account:os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20821-migrate-plan-deferred-reads
Worktree:objectstack-issue-20821
Domain:domain:engine
Seat:domain:engine#2
File surface: triage's direction 5908818119 — the dry run does not read a table whose DDL it deferred; failing that, the refusal is recognised with the one predicateisMissingTableError.- The boot readers that
os migrate planreaches on an absent database. Each is traced and named in the report before any edit:ObjectQL.announceOpenMigrationGatesinpackages/objectql/src/engine.ts, its region only, reached frompackages/objectql/src/plugin.tsatkernel:bootstrapped;- the readers of
sys_metadata(×4) andsys_metadata_activation(×1), wherever they live inpackages/metadata*orpackages/runtime.
- The plan command's deferral signal:
packages/cli/src/commands/migrate/is a declared cross-lane touch (domain:cli), andpackages/runtimeonly if the signal must pass through it. - Pins beside the CLI's migrate dry-run suites:
- an absent file prints 0
DATABASE_ERRORlines; - a real refusal on an existing table still warns (the control);
- the plan's output is unchanged.
- an absent file prints 0
.changeset/20821-*.md.
Stop on breach and explain in the report.
packages/drivers/driver-sql/src/sql-driver.ts: only if measurement puts the fix there, and ⛔ never in PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988's regions (#5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 2, seat 1). Stop and report first.- ⛔ No second message regex. ⛔ Every refused read is not demoted.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5908818119
Serial constraints cleared: read at 2026-10-01T03:21Z againstorigin/mainb253fadfb. - The family's earlier doors have landed: PR fix(driver-sql): the first boot of a new database no longer prints a sys_migration DATABASE_ERROR (#20768) #20818 ([finding] the first boot of a new database prints
[sql-driver] DATABASE_ERROR … no such table: sys_migrationon the warn channel: the engine's migration-gate read runs before the table is created #20768) as810d42b69indriver-sql, and 17.5.0: every boot of a database created on 17.4 prints "Paged read of 'sys_migration' is NOT deterministic" for the platform's own primary-key lookup #20648. - In flight nearby, in other regions:
- engine: validateData forwards engine.validate's onFieldsDropped and answers the drops, and insertMany reports drops per outcome through insertManyData: #20701 item 1, engine half #20922:
engine.tsvalidate;protocol.tsvalidateData/insertManyData. - [finding] a relative-date filter token that resolves outside years 0001..9999 (
{8000_years_from_now},{2027_years_ago}) reaches the driver as extended-year text and matches every row: the comparand door runs before token resolution #20844:engine.ts, the resolve-then-lower stage. - picklist metadata kind — runtime: resolve
picklist→ options when serving field metadata, validate writes against the resolved set, apply package-level extensions (phase 1 of objectstack#18164) #19519 (cloud):engine.tsMETADATA_ARRAY_KEYSandmetadataplugin.ts's artifact map. - PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988 (seat 1):
driver-sql/driver-turso.
- engine: validateData forwards engine.validate's onFieldsDropped and answers the drops, and insertMany reports drops per outcome through insertManyData: #20701 item 1, engine half #20922:
- No open PR touches
announceOpenMigrationGatesorpackages/cli/src/commands/migrate/. Clause-②: no: a read is not asked, or a warn is not printed. Nothing is accepted or refused differently, and nothing is exported. If the deferral signal needs a new public option, the dev reports it and the line is re-judged.
- The boot readers that
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20821, "status": "needs_decision", "branch": "claude/issue-20821-migrate-plan-deferred-reads", "pr": null, "session": "session_01Ujdtvqs7ree7WyQmEDwEnG — mode:subagent, the PM's session id; this run is told apart by the branch", "premise_still_valid": true, "summary": "Stopped before the first edit, as the dispatch requires: the fix measures into packages/drivers/driver-sql/src/sql-driver.ts, and the 'not asked' route would need a new contract member (Clause-2 would change). The premise holds: on an absent file the plan's boot reads tables whose DDL it deferred. The 6 lines the card names come from 6 readers, all traced at b253fadfb. (1) sys_metadata: ObjectQLPlugin.restoreMetadataFromDb, then ObjectStackProtocolImplementation.loadMetaFromDb (metadata-protocol), reading state active and organization_id null, in ObjectQLPlugin.start() phase 2. (2) sys_metadata: readAuthoredTranslationLayer (core fallbacks/authored-translation-sync.ts), from wireAuthoredTranslationSync's kernel:ready hook, type translation. (3) sys_metadata: ObjectQLPlugin.readAuthoredHookRows via resyncAuthoredHooksNow at kernel:ready, type hook. (4) sys_metadata: ObjectQLPlugin.readAuthoredActionRows via resyncAuthoredActionsNow at kernel:ready, type action. (5) sys_metadata_activation: ObjectStoreActionActivationStore.probe (core utils/metadata-activation-store.ts), via ObjectQLPlugin.hydrateActionActivations at kernel:ready. (6) sys_migration: ObjectQL.announceOpenMigrationGates, then readMigrationFlagVerified, then findOne for id adr-0104-value-shapes, at kernel:bootstrapped. Each reader already handles the refusal: loadMetaFromDb uses isMissingTableError, (2) to (4) log at debug and return null, (6) answers not verified, and (5) logs its own functional warn. The DATABASE_ERROR line itself is written inside the driver. H1: at b253fadfb the count is 12, not 6. The other 6 come from app-crm's own onEnable hook on kernel:bootstrapped (registerCrmPositionBindings), which reads sys_position x3 and sys_permission_set x3, tables the plan's composition never declares. `os migrate apply --yes` on an absent file prints the same 12. A plan on the file that apply just migrated prints exactly those 6 app-hook lines. I did not bisect why the card counted 6. H2: the deferral signal is SqlDriver.setDeferredDdl(true), armed by the CLI's DeferSchemaDdlPlugin.init() (packages/cli/src/utils/schema-migrate.ts). It is held in the protected fields deferredDdl and deferredSchemaObjects. No IDataDriver member carries it, and neither does any engine or kernel key. skipSchemaSync means the opposite: DDL runs out of band and the tables are assumed present. So none of the 6 readers can see the signal when it reads. Deferred is also not absent. Under deferral, initObjects records every object before any hasTable call. A reader keyed on deferral would therefore skip existing sys_metadata rows on an existing database, and overlay objects would drop out of the plan (a code reading, not measured). The only absence fact that costs nothing is the sqlite absent-file getter sqliteOpenedEmptyInMemory, which is on SqlDriver, not on IDataDriver. Postgres and MySQL would need a catalog probe. H3 held: SqlDriver.backendStatementFault (sql-driver.ts, near line 10074) writes the warn before any reader sees the error, so a reader-side isMissingTableError cannot remove the line. H4 held: an absent ledger already answers verified false, conclusive false (the catch in readMigrationFlagVerified). The base plan prints the open value-shape gate announcement, and both a skipped read with that answer and a demoted refusal leave it unchanged. H5: I simulated the driver-side route at runtime through a scratchpad instrument, with no repo edit. All 6 of the card's lines were demoted. The 6 app-hook lines still warn. Human stdout and the --json payload are identical (see tests). The card's assignee os-litant was not touched.", "tests": "No code was changed, so no package suite ran. Measurements: (a) build: os-verify-lock.sh -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/example-crm^...' build\", VERDICT command-exit 0, held the lock 284s. (b) H1 at b253fadfb in examples/app-crm: node ../../packages/cli/bin/run.js migrate plan --database-url file:ABSENT1.sqlite exits 0. It prints 12 DATABASE_ERROR lines on stderr: sys_metadata 4, sys_metadata_activation 1, sys_migration 1, sys_position 3, sys_permission_set 3. No file is left behind. A stack was captured for every line with a scratchpad preload that wraps console.warn. (c) os migrate apply --yes on an absent file prints the same 12. os migrate plan on that applied file prints 6, all sys_position or sys_permission_set. That is the control: the card's three tables print 0 lines once they exist. (d) Driver-side simulation (scratchpad ESM --import that wraps SqlDriver.prototype.backendStatementFault; nothing in the repo edited): DATABASE_ERROR goes from 12 to 6, and the instrument printed 'total demoted=6' (sys_metadata x4, sys_metadata_activation, sys_migration). Human stdout, normalised for timestamps, durations and file name, gives diff exit 0 against the base run. The plan-report block hashes to md5 3a74e7bb6238580ba46ae2c5e40f4133 on both runs. The --json payload without duration hashes to md5 64661190d15927a8464e0028fab5d64b on both (total 0, pending 15, managedTables 15). (e) node scripts/check-driver-conformance.mjs before: 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt' (exit 0). After: not applicable, because nothing was edited. (f) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 'this branch changes nothing against origin/main (merge base b253fadfb) — nothing to derive' (exit 2), so no gate is owed and none ran. NOT MEASURED: postgres and mysql, reason: not provisioned in this container. Neither route was run against a live server dialect.", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/20821/comments (this os-dev-report, written with scripts/pm/post-stamped.mjs through the fleet-write relay). The git push of the empty branch is not a REST write. No pr_create and no label-write ran: no PR was opened, because the run stopped before its first edit.", "open_questions": [ { "question": "Where does the fix land? The warn is written inside SqlDriver.backendStatementFault before any reader sees the error. No contract member tells a boot reader that a table's DDL was deferred. The two honest routes are therefore a driver-side demotion in driver-sql, or a new contract fact that every boot reader asks.", "options": [ "A — driver-side, keyed on the driver's own deferral. In SqlDriver.backendStatementFault, a refusal goes to debug instead of warn only when all three hold: this driver has DDL deferred, the targeted table is one whose DDL it deferred (deferredSchemaObjects), and isMissingTableError(envelope, object) is true. The throw and the envelope are unchanged. Cost: one condition beside PR #20818's scope check in sql-driver.ts, near lines 10030 to 10085. That region is disjoint from every PR #20988 hunk (lines 91, 4402 to 5051, and 15435 to 17312, read from that PR's patch). It needs a driver unit pin with three cases: deferred and missing goes to debug; a malformed read on an existing deferred table still warns; a missing table outside the deferred set still warns. It also needs a CLI pin beside the migrate dry-run suites: 0 lines on the card's three tables, with stdout and --json unchanged. A patch changeset for driver-sql, and Clause-2 stays no. Measured by simulation, it removes exactly the card's 6 lines and leaves both outputs identical. It also covers the boot of os migrate apply and every dialect, not only sqlite. Business need: real. It is the first plan or apply of every new deployment. Long-term soundness: it keys on a fact the driver recorded itself, uses the one shared predicate, adds no vocabulary, and is the same family as PR #20818. Its weakness is that it demotes a question still asked. It also keeps one stdout WARN on a dry run ('sys_metadata_activation is registered but could not be read — packaged-ACTION enable/disable is UNAVAILABLE'), which the stdout-identity pin requires anyway. AI-proofing: it adds no public or authorable surface. Startup scope: it is the smallest option, with no new gate and no contract growth.", "B — 'not asked'. Declare a fact the readers can ask (an optional IDataDriver member, or an engine or kernel key), and make the 6 readers consult it: the ObjectQL plugin x3, the ObjectQL engine, core's translation sync and metadata-protocol. The fact has to mean absent, not deferred (see H2). That is free only on the sqlite absent-file path; Postgres and MySQL need a catalog probe, which is the new public driver method PR #20818 declined. Cost: a contract member and an api-surface regeneration, Clause-2 becomes yes (a minor changeset), and edits to engine.ts, which takes merges daily. Six call sites change, and every future boot reader must remember to ask, an invariant that no gate holds. It would also remove the stdout activation WARN. Business need: the same, but fully honest only for sqlite. Long-term soundness: it is the cleaner end state, but it spreads a plan-mode check across every boot reader. AI-proofing: it adds an optional driver member that a driver can declare and not honour. Startup scope: it grows the contract for p3 warn noise.", "D — demote every missing-table refusal while DDL is deferred, with no deferred-set check. It would also hide the 6 app-hook lines. But those lines name tables that nothing in this boot declared, so they carry information. It goes against triage's 'Don't demote every refused read'." ], "recommendation": "A. It meets triage's fallback exactly: one predicate, no second regex, and only the refusals for tables whose DDL this driver itself deferred. It meets all three pins (measured by simulation) with no contract growth, which keeps Clause-2 at no. B would buy 'not asked' with a new public member, and it is fully honest only for sqlite. Routing for the PM: the edit lands in driver-sql (seat 1's lane, where PR #20988 is in flight but textually disjoint). Either re-route the card, or widen this claim's surface to SqlDriver.backendStatementFault plus one driver test. The CLI pin stays in packages/cli/src/commands/migrate/." }, { "question": "Does the card's pin 'os migrate plan on an absent file prints 0 DATABASE_ERROR lines' cover app-crm's own hook lines? At b253fadfb, 6 of the 12 lines come from examples/app-crm's onEnable kernel:bootstrapped hook. It reads sys_position and sys_permission_set, which no route on this card can reach.", "options": [ "A — scope the pin to the tables whose DDL the plan deferred: 0 lines on sys_metadata, sys_metadata_activation and sys_migration. Track the app-hook door separately (out_of_scope_findings[0]).", "B — widen this card to the app-hook door as well. That is a different mechanism: a declaration boot running a host hook that reads tables the plan's composition does not declare." ], "recommendation": "A, because it is a different door with a different owner. The plan on a file that apply just migrated still prints those 6 lines, so they are not a deferred-DDL artefact." } ], "out_of_scope_findings": [ "class: a · reach: `os migrate plan --database-url file:X` on examples/app-crm, where X is the file `os migrate apply --yes` just migrated, prints on stderr 6 `[sql-driver] DATABASE_ERROR` lines (sys_position x3, sys_permission_set x3) and 2 'Paged read ... is NOT deterministic' warns, and on stdout 6 WARN '[crm] position binding lookup failed' lines plus 3 'skipped (row missing)' lines. Measured at b253fadfb. On an absent file the same lines come on top of the card's 6. · evidence: examples/app-crm/objectstack.config.ts onEnable, then registerCrmPositionBindings (examples/app-crm/src/security/bind-position-sets.ts:111), which hooks kernel:bootstrapped. The hook runs on the plan's declaration boot (stdout: 'Executing runtime.onEnable') and reads plugin-security tables the plan's composition does not declare ('Examined 15 managed table(s)', neither listed), so apply never creates them. · dedupe words: `migrate plan app hook kernel:bootstrapped sys_position DATABASE_ERROR` · `declaration boot onEnable hook reads undeclared table` · `position binding lookup failed migrate plan` · `migrate apply plan composition plugin-security tables`", "carrier: this card's routing decision (option B would remove it, option A keeps it under the stdout-identity pin) · noted, not filed — on a plan against an absent file, ObjectQLPlugin.hydrateActionActivations also prints a stdout WARN, 'sys_metadata_activation is registered but could not be read — packaged-ACTION enable/disable is UNAVAILABLE on this deployment ... Check that schema sync ran for its datasource'. It is the same deferred read, and the warning is a functional false alarm on a dry run. It is not a DATABASE_ERROR line, so it is outside the card's pin." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsSeat answer — #20821's two open questions (report 5924224857): Q1 A, Q2 A; in-seat, open to veto · claim surface amended
domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T03:42Z. Open to the maintainer's veto; a veto lands before the PR is queued.Governing text: triage's direction 5908818119, verbatim: "Prefer "not asked" … If a read cannot be avoided, its refusal is recognised with the one predicate,
isMissingTableError. ⛔ No second message regex. ⛔ Don't demote every refused read."Q1 — where the fix lands: A, driver-side, keyed on the driver's own deferral
- "Not asked" fails its own condition. The dev measured that no reader can see the deferral, and that deferred is not absent: an existing database's
sys_metadatarows would drop out of the plan. So "not asked" needs a new contract fact (Clause-②: yes), and it is honest for SQLite only. - So the direction's fallback applies. The warn is written inside
SqlDriver.backendStatementFaultbefore any reader sees the error. That makes the driver the one place the refusal can be recognised withisMissingTableError. - A's three conditions are the narrowing triage asked for, not "every refused read":
- this driver has DDL deferred;
- the table is in its own
deferredSchemaObjects; isMissingTableErrorholds.
- Unchanged: the throw and the envelope. The other readers' handling is already correct (debug,
null, not-verified). - Lane precedent: PR fix(driver-sql): the first boot of a new database no longer prints a sys_migration DATABASE_ERROR (#20768) #20818 (
810d42b69), the same family and the same driver. - D is rejected: it would hide [finding]
os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054's lines, which carry information. B is rejected: it grows the contract for p3 warn noise.
Q2 — the pin's scope: A, the deferred tables only
- The pin is 0
DATABASE_ERRORlines forsys_metadata,sys_metadata_activationandsys_migrationon an absent file. - The 6 app-hook lines are a different door, measured on a migrated file. They are filed bare as [finding]
os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054.
四轴(中文)
- 实际业务需求:每个新部署第一次 plan / apply 都会看到 6 条假告警,实测可达。
- 长远合理性:只认驱动自己记下的「这张表的 DDL 被我推迟了」这一事实,用唯一的缺表谓词;不新增契约词汇。代价是问题仍会被问,只是不再告警。这一点写明。
- 防 AI 写错:不加任何可编写或公开的面;三个条件缺一即照常告警,真拒绝不被吞。
- 不扩散:一个条件加 pin;B 为 p3 噪声扩契约,不取。
Claim surface amended (claim 5924062757)
- Added:
packages/drivers/driver-sql/src/sql-driver.ts,SqlDriver.backendStatementFaultand its warn decision only (near:10030–:10085atb253fadfb);- one driver unit test with the three cases the report names.
- The CLI pin stays beside the migrate dry-run suites in
packages/cli/src/commands/migrate/. - Not touched by this card:
- PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988's hunks (seat 1:
:91,:4402–:5051,:15435–:17312, read from its patch), which are textually disjoint; - the JSON-column gate region ([finding]
$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009, serial behind PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988); - the JSON-membership helpers ([finding]
$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987, services lane).
- PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988's hunks (seat 1:
- Unchanged:
Clause-②: no;- a
patchchangeset fordriver-sql, pluscliif the pin's file ships in it; - the stdout WARN from
hydrateActionActivations(the stdout-identity pin keeps it), noted in the PR body.
- "Not asked" fails its own condition. The dev measured that no reader can see the deferral, and that deferred is not absent: an existing database's
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20821, "status": "done", "branch": "claude/issue-20821-migrate-plan-deferred-reads", "pr": "https://github.com/objectstack-ai/objectstack/pull/21093", "session": "session_01Ujdtvqs7ree7WyQmEDwEnG — mode:subagent, the PM's session id; this run is told apart by the branch", "premise_still_valid": true, "summary": "Round 1, implementing seat answer 5924276655 (Q1 A, Q2 A). The change is in SqlDriver.backendStatementFault (packages/drivers/driver-sql/src/sql-driver.ts), in the warn decision only. A refused statement now goes to logger.debug instead of warn only when all three hold: this driver has DDL deferred, the targeted table is in its own deferredSchemaObjects, and isMissingTableError(envelope, object) is true. The throw and the envelope are unchanged. There is no second regex, and no demotion outside those three conditions. PR #21093 is a draft at ea9309b80, merged with origin/main twice (at c6aff7c0c, and at ea9309b80 just before pr_create). It is assigned to os-litant. The labeler added documentation, size/m, tests and tooling, and I set none of them. The PR body names two things as kept: the stdout WARN from hydrateActionActivations, which the stdout-identity pin requires, and the app-hook lines, which belong to #21054 and are not addressed here. The changeset is a patch for @objectstack/driver-sql only. @objectstack/cli ships dist only, and tsconfig.build.json excludes src/**/*.test.ts, so the pin is not published. The six readers are unchanged from the round-0 report: restoreMetadataFromDb/loadMetaFromDb, readAuthoredTranslationLayer, readAuthoredHookRows and readAuthoredActionRows on sys_metadata; ObjectStoreActionActivationStore.probe on sys_metadata_activation; announceOpenMigrationGates/readMigrationFlagVerified on sys_migration. One deviation in the CLI pin: src/ is cli's tsconfig rootDir, so the pin cannot import test/helpers/serve-process.ts. It names bin/run-dev.js itself and strips the same two env families that childEnv() strips (fd9f151a4). CI was still running when this report was written: 11 check runs completed with no failure, 20 in_progress.", "tests": "examples/app-crm, node ../../packages/cli/bin/run.js migrate plan [--json] --database-url file:ABSENT, base 576afc17b against head ea9309b80 (rebuilt). DATABASE_ERROR lines go from 12 to 6. Lines naming sys_metadata, sys_metadata_activation or sys_migration go from 6 to 0. Stderr differs by exactly those 6 lines removed and 0 added. Human stdout normalised for timestamps and Nms gives diff exit 0. The plan block hashes to md5 e102064c7b13bce96303c1e9b698693d on both. --json stdout without duration is byte-identical (cmp exit 0). No file is left behind. A gate run had written the gitignored examples/app-crm/dist/objectstack.json; the after-run was taken with it removed (see the PR's Acceptance notes). New driver pin, sql-driver-20821-deferred-ddl-missing-table.test.ts: (1) deferred and missing goes to debug; after the flush the read answers [] with nothing logged. (2) A malformed read (40,000 variables) on an existing deferred table still warns. (3) A missing table outside the deferred set still warns. Each case asserts code DATABASE_ERROR and status 500. 3 of 3 pass. New CLI pin, src/commands/migrate/plan.deferred-reads.integration.test.ts (integration tier, spawns bin/run-dev.js on a host-config fixture that reproduced 4/1/1 on the base). Human mode and --json each pass with 0 lines on the three tables, the [value-shape] announcement and the activation WARN present, the three tables still pending create_table, and no file written. Ablation 1, through scripts/ablation-replace.mjs at baf0d12ff: removing the deferred-set condition (anchor 1 to 0, blob c626b59d to fecb8704) turned the driver pin red on (3) only ('expected [] to have a length of 1 but got +0'). The restore put the blob back equal to HEAD with git diff HEAD empty. Ablation 2, at fd9f151a4, on the committed pin: removing the whole debug branch (blob to e7acc7e7), rebuilding driver-sql, and running ablation-dist-preflight --absent showed the marker absent from all 6 built files. The CLI pin was red on both cases ('expected [ …(6) ] to deeply equal []'). The restore put the blob back equal to HEAD; after a rebuild the preflight found the marker in 2 built files and the tree clean, and both pins were green. Suites at ea9309b80: driver-sql vitest run, 204 files passed and 11 skipped, 3285 tests passed and 188 skipped (the live PG and MySQL cells). cli --project unit: 240 files and 3419 tests passed. cli --project integration on this PR's file plus schema-migrate.readonly-probe and schema-migrate.deferred-ddl: 3 files and 9 tests passed. NOT MEASURED: the full cli integration tier, reason: the 10-minute foreground cap ended it (exit 124 at 595s); declared to CI. Typecheck is green for driver-sql and cli, including check:test-typecheck with debt unchanged. Earlier run at baf0d12ff: pnpm --filter @objectstack/driver-sql test -- --maxWorkers=2 passed the whole package (203/11 files); the flag after the bare -- was dropped, and that run is superseded by the run above. Gates: dispatch-gates --commands at ea9309b80 derived 66 families. All 66 ran, with exit codes captured before any pipe, and all exited 0. --ran reports '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN'. On the earlier head, check:dual-build-cjs-loads and check:i18n-coverage first returned PREREQUISITE NOT MET (exit 3) because nine packages had no dist. I built them and both gates re-ran green. The derivation flagged lint.yml and engine-double-contract.pinned.json as stale inputs. I read both diffs: timeout keys, comments, and pins for other files, so no family changes. Driver conformance before and after: 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.' Lint narrowing at ea9309b80: eslint --no-inline-config --format json over the 3 touched TypeScript files reports files 3, errors 0, warnings 0. eslint --print-config resolves all 3, with no parserOptions.project. eslint.config.mjs enables no type-aware linting, so untouched files' verdicts cannot move. The control-byte scan is clean (grep exit 1). spec check:generated after the first merge: all 15 artifacts up to date.", "mcp_calls": "0", "api_writes": "This run: 4 writes, all through the fleet-write relay as repository_dispatch, then executed by .github/workflows/fleet-write.yml. Round 0: POST /repos/objectstack-ai/objectstack/issues/20821/comments (report 5924224857). Round 1: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #21093, body read back identical at 9967 bytes); POST /repos/objectstack-ai/objectstack/issues/21093/assignees (scripts/pm/label-write.mjs --assign os-litant, read back MATCHES); POST /repos/objectstack-ai/objectstack/issues/20821/comments (this report, through scripts/pm/post-stamped.mjs). The git pushes are not REST writes.", "open_questions": [], "out_of_scope_findings": [ "carrier: #21054 (filed bare by the seat) · noted, not filed. The app-crm onEnable kernel:bootstrapped hook still prints 6 DATABASE_ERROR lines on sys_position and sys_permission_set on a plan against an absent file, and this PR's pin does not count them. Measured: once a compiled artifact (examples/app-crm/dist/objectstack.json) is present, the plan takes the artifact boot path, onEnable does not run, and those 6 lines disappear too, so the door depends on whether that file exists.", "carrier: PR #21093 body (Kept on purpose) · noted, not filed. The stdout WARN from ObjectQLPlugin.hydrateActionActivations ('sys_metadata_activation is registered but could not be read …') is still printed on a dry run against an absent file. It is a functional false alarm, kept because the stdout-identity pin requires it." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21093 @
ea9309b8(os migrate planon a database whose DDL it deferred no longer warnsDATABASE_ERRORfor the tables it deferred)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T06:22Z. Judged against GitHub, not the reports (5924224857, round-1 report).-
Form: draft PR on
main. The body opensFixes #20821/Clause-②: no, its only closing keyword. -
Scope: 4 files, inside claim 5924062757 as amended by the seat answer 5924276655:
driver-sql'sSqlDriver.backendStatementFault: a 29-line early return in the warn decision only;- one driver unit test;
- one CLI integration pin under
packages/cli/src/commands/migrate/; .changeset/20821-plan-deferred-ddl-reads.md:@objectstack/driver-sqlpatch(cli shipsdistonly).
Not governed. +418 / −0.
-
Contract review: at tier, PASS on this head (5925795690). The demotion keys on exactly the three conditions of the seat answer:
- the driver's own
deferredDdl; - the table is in its own
deferredSchemaObjects; isMissingTableError.
A miss falls through to the unchanged
warn, so no condition demotes on its own. The envelope (DATABASE_ERROR/ 500) and the throw are unchanged. There is no regex, and triage's two ⛔ hold. The hunk is disjoint from the JSON-column gate, from the JSON-membership helpers and from merged PR fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) #20988. No public surface moves. - the driver's own
-
CI on
ea9309b8: 34 check-runs. 31 success and 3 skipped, all skips on the roster (check-expected-skips --pr 21093: OK, exit 0). Merges cleanly ontomain(git merge-tree). -
Tests (dev's evidence):
- On examples/app-crm against an absent file,
DATABASE_ERRORdrops from 12 to 6. The 6 lines namingsys_metadata,sys_metadata_activationandsys_migrationdrop to 0. - Human stdout (normalised) and
--jsonoutput are byte-identical to the base. - Driver pin: 3 of 3 cases. CLI pin: human and
--json, both 0 lines on the three tables. - Ablations: removing the deferred-set condition turned case 3 red; removing the debug branch turned the CLI pin red. Both were restored and the restores proven.
driver-sql: 3285 passed. cli unit: 3419 passed.- Gates: 66 of 66, exit 0.
- Driver conformance: 50 / 0 / 0.
- On examples/app-crm against an absent file,
-
Findings:
- The 6 remaining lines come from app-crm's
onEnablehook. They are [finding]os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054 (filed,domain:cli). The dev measured one fact for that card: they disappear once a compileddist/objectstack.jsonis present, because the plan then takes the artifact boot path. - The stdout WARN from
hydrateActionActivationsstays on purpose, because the stdout-identity pin requires it → Acceptance notes.
- The 6 remaining lines come from app-crm's
-
Landing: this seat readies and arms the PR through the queue.
-
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded — PR #21093 as
cf0346ec7(the card closes)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T06:48Z.-
Merged through the merge queue at 2026-10-01T06:47Z as squash
cf0346ec7onmain. Two readings agree: the PR object andorigin/main's log. The squash carries the PR's 4 files (418 insertions, 0 deletions). -
Records it landed on:
- seat answer 5924276655 (Q1 A, Q2 A);
- contract review PASS 5925795690 @
ea9309b8; - the ACCEPT on this card.
-
What changed for operators:
os migrate plan, andapply's boot, against a database whose DDL the driver deferred no longer warnsDATABASE_ERRORforsys_metadata,sys_metadata_activationorsys_migration. Those refusals log at debug. Three conditions must all hold:- the driver's own deferral;
- its own deferred-table set;
isMissingTableError.
Every other refused read still warns. The plan's output is unchanged.
-
The card closes
completedby the PR'sFixes #20821.pm:dispatchedis removed in the same act as this record. -
Still open, elsewhere: the app-crm
onEnablehook's 6 lines are [finding]os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054 (domain:cli).
-
- added 3 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a defect with a named producer. Finding class (a).
reach:the public CLI dooros migrate plan --database-url file:ABSENT.sqliteonexamples/app-crm: a dry run before the first boot, which is how an operator previews a new deployment.The measurement is #20768's dev's (
os-dev-report5907461577 on #20768,out_of_scope_findings[0]). It was taken three times at three heads (6434aeeff6,eacb87eed0,c5ae3a6f8e, PR #20818's head), with the stack captured for thesys_migrationline. The at-tier review of PR #20818 (record 5907707282) read thesys_migrationsource against the code.Filed by the
domain:engineexecution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY,os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
os migrate planon a database file that does not exist yet prints these lines on stderr. It printed 7 at base96e724475cand prints 6 at PR #20818's head:[sql-driver] DATABASE_ERROR … no such tablesys_metadatasys_migrationstack was capturedsys_metadata_activationsys_migrationadr-0104-value-shapesread byObjectQL.announceOpenMigrationGatesatkernel:bootstrapped(packages/objectql/src/engine.ts, which callsreadMigrationFlagVerifieddirectly)The dry run defers the DDL, so these tables are never created, and the boot then reads them. Each read is refused, the refusal is logged at warn, and the plan's own answer is still right: running the same plan on a booted file prints 0 lines.
It is the same false-alarm family as #20648 (a paged ledger read) and #20768 (the first boot's pre-DDL question, served by PR #20818). This is a third door. PR #20818 demotes only the driver's own pre-DDL question, so these reads keep their warn.
Scope for whoever takes it (⛔ not a ruling)
DATABASE_ERRORlines where nothing is wrong. Either a table that the dry run has deferred is not read, or a refusal for such a table reaches no warn channel.[sql-driver] DATABASE_ERROR … no such table: sys_migrationon the warn channel: the engine's migration-gate read runs before the table is created #20768's triage direction applies by analogy: prefer "not asked"; otherwise recognise the refusal with the one missing-table predicateisMissingTableError. ⛔ No second message regex. ⛔ Don't demote every refused read.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:[sql-driver] DATABASE_ERROR … no such table: sys_migrationon the warn channel: the engine's migration-gate read runs before the table is created #20768 (open) is the first boot, not the plan door. 17.5.0: every boot of a database created on 17.4 prints "Paged read of 'sys_migration' is NOT deterministic" for the platform's own primary-key lookup #20648 is closed, a different read. cli/driver-sql:os migrate plan自称 dry-run,却仍会在全新项目上创建空数据库文件(#6469 的残余写副作用) #6743 and fix(cli,runtime): os dev / os start / os migrate 解析出三个不同的默认数据库 —— migrate plan 因此在自己刚建的空库上报告全量 drift #6469 are closed and about the plan creating the file and resolving the path. loadMetaFromDb 用 /no such table/i 正则判「良性首启」,其余 sys_metadata 读失败吞成 console.warn + loaded:0 —— isMissingTableError 的手抄第二份 #5841 and sys_metadata 不可用被 mapDataError 的 unknown-object 启发式误报成 404 OBJECT_NOT_FOUND,且 404 属「预期状态」因而一行日志都不留 #5462 are closed sys_metadata classifications. None is this.os migrate planagainst a remote Turso datasource performs the DDL and the canonical backfill it was meant to preview, and prints no pending work #19823 is closed (remote Turso deferred DDL). [17.0.0-rc.0] os migrate apply: no occupancy/lock detection for SQLite, and boot-time DDL runs before the confirmation prompt #3917 and unique 索引迁移在启动时静默执行 DDL,os migrate plan 看不到 —— 运维无预检手段 #3728 are closed. None is this.Dedupe words:
migrate plan absent database DATABASE_ERROR·os migrate plan new database no such table sys_metadata·announceOpenMigrationGates deferred DDL missing ledger·dry run plan before first boot warn noiseGenerated by Claude Code