Skip to content

fix(driver-sql): the first boot of a new database no longer prints a sys_migration DATABASE_ERROR (#20768) - #20818

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20768-first-boot-migration-gate-read
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20768-first-boot-migration-gate-read

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20768

Clause-②: no

On the first boot of a new database, the SQL driver printed one [sql-driver] DATABASE_ERROR ... no such table: sys_migration line on its warn channel. The read behind it is the engine's migration-gate read. The driver's own pre-DDL question reaches that read before the schema pass has created any table. The driver now asks that question inside an async scope. Inside the scope, one class of refusal goes to debug instead of warn: a missing table, recognised by the shared isMissingTableError over the envelope's declared target. Every other refusal still warns, and so does a missing table read anywhere else. No gate's answer changes.

Reproduction on the base

objectstack dev --database file:NEW.sqlite -p PORT on examples/app-crm, base 96e724475c (every package in the app's closure built), stdout and stderr captured separately:

run DATABASE_ERROR lines channel
first boot of a new file 1 stderr (the driver's default console.warn sink)
second boot of the same file 0 none

The line, verbatim:

[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR). The dialect message below is kept server-side: it carries the compiled statement, and on the dialects that inline them the bound literals too (#7929, #8931): select * from `sys_migration` where `id` = 'adr-0104-file-references' limit 1 - no such table: sys_migration

The mechanism: which dispatch hypotheses held

H1 held. For one boot, I added a temporary stack-trace line to the built packages/objectql/dist (restored afterwards, cmp byte-identical on all four bundles, marker count 0). The first gate read at boot is ObjectQL.readMigrationFlagVerified, reached through readFileReferencesFlagRow and haveFileColumnsMoved. It is called by the closure that registerDriver hands the driver, which SqlDriver.resolveFileColumnsMoved asks at the start of SqlDriver.initObjects, from the schema pass's first syncSchema, before any DDL. The other gate readers run after the schema pass has created sys_migration, so on the first boot they read without error: isValueShapesMigrationVerified from write hooks, and announceOpenMigrationGates at kernel:bootstrapped.

H2 falsified. The engine cannot tell whether the table exists without asking the backend:

  • The driver's registered-table set answers "registered in this process", not "exists". On a second boot the table exists but is not registered yet when the question is asked. Answering "not moved" there without a read would change the answer on a deployment whose columns have moved, which is the failure the resolver exists to prevent.
  • The schema pass has no plan at that moment: the question is asked before the first hasTable.
  • For an absent table, conclusive must be false. The same pass creates the table moments later, and a memoized "not verified" would freeze a whole boot's posture from a moment when nothing could answer. That is what the refused read already returns, so it is unchanged.
  • A catalog probe (hasTable) would give "not asked", but it needs a new public driver method or a new IDataDriver member. That enlarges a public surface, against this card's Clause-②: no, so this PR does not add one.

H3 held. SqlDriver.backendStatementFault writes the line before the engine sees the error. The engine's findOne path does not log, and its catch in readMigrationFlagVerified answers { verified: false, conclusive: false, columnsMoved: false } silently. So the demotion is in the driver, keyed on isMissingTableError, and limited to this read: the async chain of the driver's own question.

H4: measured at c5ae3a6f8e. The second boot prints nothing, as on the base. os migrate plan --database-url file:X on the database the boots created prints no DATABASE_ERROR. On a path that does not exist yet, the plan printed 7 lines on the base, 2 of them on sys_migration, and prints 6 now. The one sys_migration line left is the adr-0104-value-shapes read from announceOpenMigrationGates, outside the driver's question. The other 5 are sys_metadata (4) and sys_metadata_activation (1). They are not this card. See the acceptance notes.

Landing site: packages/drivers/driver-sql, not packages/objectql/src/engine.ts

The dispatch expected the engine, with the driver only if a demotion was needed. As H2 and H3 show, the demotion is needed, and the line's producer is the driver. The premature read is the driver's own pre-DDL question. engine.ts is unchanged.

What changed

packages/drivers/driver-sql/src/sql-driver.ts:

  • A module-level AsyncLocalStorage (PRE_DDL_QUESTION_SCOPE). resolveFileColumnsMoved runs the resolver inside it, and nothing else does. It is module-level rather than per instance because the question's read goes to whichever driver serves the ledger, which on a multi-datasource composition can be another instance in the same async chain.
  • backendStatementFault composes the envelope first. If the scope is active and isMissingTableError(envelope, object) holds, it writes the same dialect text to this.logger.debug?.(...) and returns the envelope. Otherwise it warns as before. The throw, the envelope, and what the resolver hears are unchanged.
  • The logger shape gains an optional debug channel. The default sink has none.
  • isMissingTableError is imported from its home, @objectstack/types. @objectstack/metadata/errors re-exports the same symbol, and driver-sql cannot depend on @objectstack/metadata. No second message regex.

.changeset/20768-first-boot-migration-gate-read.md: @objectstack/driver-sql patch. @objectstack/runtime gains only a test file, which its files[] (dist, README.md, CHANGELOG.md) does not ship, so it gets no changeset entry.

After: this branch at c5ae3a6f8e, built

run DATABASE_ERROR lines sys_migration mentions server ready
objectstack dev --database file:NEW.sqlite (examples/app-crm), first boot 0 0 yes
second boot of that file 0 0 yes
os migrate plan on that file 0 0 exit 0

Normalised for timestamps, port and file path, the first-boot stderr differs from the base run in exactly one line: the removed one. Both ledgers end in the same state after the first boot: both flags verified by the fresh-datastore attestation, and columns_moved_at null.

Tests

  • packages/drivers/driver-sql/src/sql-driver-20768-pre-ddl-question-missing-table.test.ts (new, SQLite on a new temp file). The resolver is a stand-in that reads the ledger the way the engine does.
    • ① First boot: no warn line, one debug line (no such table). The resolver gets the envelope (code: 'DATABASE_ERROR', status: 500), and the arm stays "not moved".
    • ② Same result when the ledger is served by a second driver instance.
    • ③ Second boot: a columns_moved_at row written between the boots makes the arm "moved", so the read reached the row. Nothing is logged.
    • Controls, each still warn: ④ a malformed read on an existing table inside the question (40,000 bound variables, SQLite refuses the statement); ⑤ a view over a dropped table inside the question (a missing table named by another relation); ⑥ a missing table read outside the question.
  • packages/runtime/src/first-boot-migration-gate-read.integration.test.ts (new). A real ObjectQL engine over a real SqlDriver on a new file, SysMigration registered, and engine.syncSchemas(), which is the same syncSchema, initObjects, resolver, gate-read chain.
    • First and second boot: no sys_migration DATABASE_ERROR on warn, and one demoted line on the first boot only.
    • Gate answers asserted on both boots: not moved and not verified on the first; verified after a row is written between boots.
    • Control: a malformed read on an existing table still warns, and so does a missing table read after the boot. It counts only its own reads' lines.
  • Suites at c5ae3a6f8e:
    • driver-sql: 201 files passed, 11 skipped (the live PG and MySQL cells, not provisioned here); 3254 tests passed, 188 skipped.
    • runtime --project local: 293 files, 4207 tests passed, 1 skipped.
    • Typecheck green for driver-sql and runtime. Runtime's check:test-typecheck debt is unchanged, and tsc --listFiles shows both new tests inside their packages' programs.

Reverse verification (from the committed fix)

packages/drivers/driver-sql/src/sql-driver.ts was restored to its base blob with git restore --source=96e724475c: on-disk blob 2462eccd = base, marker count 0. driver-sql was rebuilt, and ablation-dist-preflight --absent PRE_DDL_QUESTION_SCOPE found the marker absent from all 6 built files.

  • Driver pin: red on ① and ② (expected [ Array(1) ] to deeply equal []). ③ to ⑥ stayed green.
  • Runtime pin: the boot test red (same message). The control stayed green.

Restore: git checkout HEAD -- packages/drivers/driver-sql/src/sql-driver.ts. Blob a3647fd1 = HEAD, and git diff HEAD was empty. After a rebuild, the preflight found the marker in 2 built files and a clean tree, and both pins were green again.

The first run of this verification also turned the runtime control red: it had counted the boot's own sys_migration line together with its own. It now counts only its own reads, and the second run above is from that commit.

Gates

node scripts/pm/dispatch-gates.mjs --commands at c5ae3a6f8e derives 63 commands, and all 63 ran on that head with exit 0. --ran reconciliation: "63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)". On the first pass, check:dual-build-cjs-loads answered PREREQUISITE NOT MET (9 packages had no dist/). I built those 9 and re-ran it, and it exited 0.

Lint, narrowed: eslint --no-inline-config --format json over the 3 touched .ts files counted 3 files, 0 errors, 0 warnings. The population is the config's packages/**/*.{ts,tsx,mts,cts} and **/*.{ts,...} objects. The config never enables type-aware linting (no parserOptions.project), so this diff cannot change the verdict on an untouched file.

The branch merged main twice. The second merge brought in #20794, the serial constraint. Neither merge touched this diff's files, apart from the tracker-number wording in the same DATABASE_ERROR line, which this branch now carries as main spells it.

Acceptance notes

  • os migrate plan against a database that does not exist yet still prints 6 DATABASE_ERROR lines at c5ae3a6f8e: sys_metadata 4, sys_metadata_activation 1, and sys_migration 1. The last is the adr-0104-value-shapes read by announceOpenMigrationGates at kernel:bootstrapped, which on a deferred-DDL plan runs over tables the plan never creates. It is the same false-alarm family, on a different door, and is reported to the seat. It is not fixed here.
  • "Demoted, not deleted" holds only where a host injects a logger that has debug. No production composition hands SqlDriver a logger today, so with the default sink the line is dropped. The engine's catch records nothing either.
  • PRE_DDL_QUESTION_SCOPE covers any read in the resolver's async chain, including one a hook starts inside it. That is intended: every such read is issued for the question, before the schema pass has run.

Generated by Claude Code

…stion leaves the warn channel

The first boot of a new database printed one `[sql-driver] DATABASE_ERROR ...
no such table: sys_migration` line. The read is the engine's migration-gate
read, reached through the ADR-0104 media-arm resolver the driver asks at the
start of its first initObjects, before its schema pass has created any table.

The resolver now runs inside an async scope, and the read-exit terminal
routes one class off the warn channel inside that scope: a missing table,
recognised by the shared isMissingTableError over the envelope's declared
target. Every other refusal still warns, and so does a missing table read
outside the scope. The resolver's answer is unchanged.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…te database, and the refusals that still warn

The driver pin runs a resolver shaped like the engine's through initObjects
on a new file; the runtime pin boots a real ObjectQL engine over a real
SqlDriver twice. Controls: a malformed read on an existing table, a missing
table named by another relation, and a missing table read outside the
question all still warn. Adds the patch changeset.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…wn reads log

Measured under the reverse verification: with the fix reverted, the boot's
own sys_migration line landed in the same capture and turned the control
red. A control must not move with the defect it sits beside.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…rst-boot-migration-gate-read

# Conflicts:
#	packages/drivers/driver-sql/src/sql-driver.ts
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

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 — 11 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 157baa75f276e5f9e157cf83c10b4774449cf481 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 80a01a70cd2eb54b2992c760d01c3fa074f3c656 — the merge of head c5ae3a6f8e2559353acb5a5ebe5f02de7b559dc7 into base 157baa75f276e5f9e157cf83c10b4774449cf481, 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 80a01a70cd2eb54b2992c760d01c3fa074f3c656 && git checkout 80a01a70cd2eb54b2992c760d01c3fa074f3c656
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 157baa75f276e5f9e157cf83c10b4774449cf481 c5ae3a6f8e2559353acb5a5ebe5f02de7b559dc7 && git checkout -B drift-repro 157baa75f276e5f9e157cf83c10b4774449cf481 && git merge --no-ff c5ae3a6f8e2559353acb5a5ebe5f02de7b559dc7

node scripts/docs-audit/affected-docs.mjs --json 157baa75f276e5f9e157cf83c10b4774449cf481

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c5ae3a6f8e2559353acb5a5ebe5f02de7b559dc7
Local-runs: none

PR #20818 (card #20768), branch claude/issue-20768-first-boot-migration-gate-read. The head was confirmed unchanged when this review read it; base main was at 157baa75f2, merge-base 5bed1f6caf. Inputs: the card body and its three comments (triage 5903742208, claim 5906207193, os-dev-report 5907461577), the PR body, its 4-file list, the net diff against main, PR #20766 (#20648, merged cd6d8a5ff, an ancestor of this head) as the prior fix on the same read, and the head's check-runs. Read-only: nothing built, run or re-run.

① Derived judgments

Diff: 4 files, +541 / -9. packages/drivers/driver-sql/src/sql-driver.ts (+69 / -9), one changeset, two new test files. packages/objectql/src/engine.ts unchanged, IDataDriver unchanged, SqlDriverConfig unchanged.

  1. Accept-set unchanged — right. backendStatementFault composes the same envelope (backendStatementFaultError(object, error, targetedTable)) on both arms and returns it on both; the early return for an already-declared status is untouched; every call site (findRows, count and the two other read exits) still throws what it threw. resolveFileColumnsMoved still answers (await …) === true with catch → false. The engine's readMigrationFlagVerified catch still answers { verified: false, conclusive: false, columnsMoved: false } silently, and readMigrationFlagRowMemoized still clears an inconclusive read (engine.ts 9640–9642), so a migration not verified stays not verified and nothing is frozen. Pinned: the runtime test asserts the gate answers on both boots; driver tests ① and ③ assert the arm.

  2. Demotion scoped to exactly the pre-DDL question — right. The only entry into PRE_DDL_QUESTION_SCOPE is PRE_DDL_QUESTION_SCOPE.run(true, resolver) inside resolveFileColumnsMoved, which initObjects awaits as its first statement, before registerObjectMetadata and before the DDL gate. The only reader is backendStatementFault: getStore() === true and isMissingTableError(envelope, object) → logger.debug?.(…); anything else → the unchanged logger.warn. So a real refusal on an existing table inside the scope warns (driver ④, runtime control), a missing table named by another relation warns (driver ⑤, through the envelope's declared target), and a missing table outside the scope warns (driver ⑥, runtime control). The raw-statement path and unresolvableFilterColumnRefusal are not touched and still warn.

  3. One predicate, no second regex — right. The call is isMissingTableError(envelope, object) on the envelope declareTargetedTable has stamped, so the declared physical table (a federated remoteName included) is what the phrase is compared with. object is passed as the second argument, which the Nothing stops an in-repo caller from calling isMissingTableError without its read-table argument — and the silent result is the wide verdict #13324 just removed #13440 callers census requires. The predicate is defined at packages/types/src/driver-error-classification.ts:652 and exported from the @objectstack/types root; packages/metadata/src/errors.ts:113 re-exports it. No regex anywhere in the diff.

  4. A concurrent request's refusal cannot fall inside another request's scope — right. AsyncLocalStorage carries the store along the async continuation chain that run() started, not across wall-clock time: run(true, resolver) enters the store for resolver()'s chain only, and the await after it resumes outside. A concurrent find rejects inside its own findRows catch (sql-driver.ts 7651), a continuation registered in that request's context, where getStore() is undefined → warn. The driver already relies on the same property for per-request perf attribution ("concurrent requests never cross-attribute", 7093). What does inherit the store: every read issued from inside the resolver's chain, on any driver instance (the store is module-level), including a read a hook starts there — the dev's acceptance note states this, and test ② pins the second-instance case. One limit, not a defect: if some other caller had already memoized a pending ledger read before the driver's question, the resolver would await that outside-scope chain and the line would warn; the dev's H1 stack (the first gate read at boot is this one) and the runtime pin (one demoted line on boot 1) say that is not the boot order today.

  5. Public surface: nothing exported changes — right. PRE_DDL_QUESTION_SCOPE is a module-level const, not exported. The one type-level change is an optional debug? member on the protected logger sink; it is additive, optional, set by no public seam (SqlDriverConfig has no logger), and the default sink still has only warn and error. node:async_hooks joins node:crypto and node:fs, which the module already imports, so no new platform constraint.

  6. Every other read exit still warns — right, by the structure in 2: the warn arm is the fall-through, unchanged in text (the head carries main's tracker-number-free wording from fix(driver-sql): refusals, drift reports and log lines state each decision in words instead of a tracker number (stage 1) #20795).

  7. Review face, the changeset (ships as CHANGELOG): every sentence true. Printed once on warn, stderr by default (the default sink is console.warn); asked at the start of the first schema sync before any table (the first statement of initObjects); the engine's resolver reads sys_migration (registerDriver hands the driver a closure over this.haveFileColumnsMoved()); inside the scope a refusal for a missing own target goes to debug, and the default logger has none so the line is not printed; the logger shape gains an optional debug; the refusal is still thrown and the answer is unchanged; what still warns is as listed; the predicate is isMissingTableError from @objectstack/types, re-exported by @objectstack/metadata/errors; nothing to migrate.

  8. Review face, the PR body. The reproduction, H1, H3, the mechanism, "What changed", "Landing site", the test descriptions, the wording-drift and the behind-main statements are each true against the diff and the engine at main. The measurements at the head (first and second boot counts, os migrate plan counts, suite counts, the reverse verification) are the dev's and are not re-measured here; they are consistent with the code. H2: three of its four points hold as written; the fourth ("a catalog probe (hasTable) … needs a new public driver method or a new IDataDriver member") is true for hasTable, of which no public form exists, but incomplete: IDataDriver already carries the optional introspectSchema, which SqlDriver implements (sql-driver.ts 14971) as a whole-catalog read. "Not asked" through it would run a full catalog introspection at every boot before DDL, on every datasource, to save one log line, and would still leave the read (and so the demotion) necessary on any driver without the optional member; the conclusion — the read stays, and the producer demotes — is the triage's stated second arm and stands. Recorded as an incomplete sentence in a review face, not a defect in what shipped. "No production composition hands SqlDriver a logger today" is true: the only non-test injection in the repo is the test-fixture helper runtime/src/expected-read-refusal-noise.ts (captureDriver, sink warn and error only, no non-test caller), and the two production SqliteWasmDriver constructions pass no logger.

  9. Check-runs on the head, as the API answered them once during this review and not polled again (this comment posted 2026-09-30T08:53Z): 31 runs — 15 success (Auto Label · Dogfood Verify CLI · Governed Surface Queue Guard · filter · Type Check workspace, source gates and debt ledger · Flag docs affected by code changes · Part-of PR must not also close its card · The card this PR closes must claim this branch · Check Changeset · No other open PR may claim the same issue · Check PR Size · No other open PR may claim the same single-writer path · Check Documentation Links), 3 skipped (Build Docs · Console Pin Gate · Packed-tarball smoke), 13 in_progress (Test Core 1–6 · Temporal Conformance · Dogfood Regression Gate 1–3 · Build Core · Type Check consumer gates · Lint & Repo Gates), 0 failure. The in-progress families are the gate verdicts for the suites, lint and build; this record does not stand in for them, and the landing waits on them as AGENTS.md says.

② Semver level

@objectstack/driver-sql: patch — right: a bug fix in a released package, no removal, no rename, no new export, no accept-set change. @objectstack/runtime gains only src/first-boot-migration-gate-read.integration.test.ts; its files[] is dist, README.md, CHANGELOG.md, its tsup entry is src/index.ts and its tsconfig excludes **/*.test.ts, so nothing it publishes changes and no entry is due; skip-changeset does not apply because driver-sql publishes. The dispatch's "patch for each package touched" is read as each package whose published surface is touched — accepted.

Clause-②: no on the PR body, no arm — well-formed and right: the diff neither widens what the driver accepts nor enlarges its public face; the optional debug? slot on a protected sink is the only type-level addition and is not a public seam. Check Changeset on the head: success.

③ Boundary flags

  • Landing site driver-sql, not engine.ts — answered. The claim's own file surface names packages/drivers/driver-sql "if that is where the warn is logged"; it is (backendStatementFault). engine.ts is unchanged, so the serial-constraint concern with feat(spec,objectql,plugin-security): one shared filter lowering, run once at the engine and RLS seams (#5930 step 2) #20794 is moot.
  • Card correction (find, not findOne) — answered, true. The engine's debug-level table-not-provisioned line is reportFindFailure, whose one caller is find (engine.ts 11482); findOne (11612) logs a pre-read debug('FindOne operation') and has no failure log, and the gate read's failure surfaces only in readMigrationFlagVerified's silent catch. The card body's "its own missing-table line is at debug" is true of find and does not reach this read.
  • isMissingTableError import home — answered. Defined in @objectstack/types, which driver-sql already depends on; @objectstack/metadata/errors re-exports the same binding. A driver-sql → @objectstack/metadata edge would close a workspace cycle (metadata devDepends on driver-sqlite-wasm, which depends on driver-sql), which check:workspace-manifest-cycles walks, so "cannot depend" holds. Same predicate as the triage named.
  • os migrate plan on an absent database still prints lines (H4 partial) — answered as out of scope, escalated. The card's reach: is the objectstack dev first boot and the pins are the first and second boot; the remaining sys_migration line is the value-shapes read in announceOpenMigrationGates (engine.ts 10076), which calls readMigrationFlagVerified directly at kernel:bootstrapped, outside the driver's question — consistent with the code. Reported in out_of_scope_findings[0] with dedupe words; the seat files it or folds it into the family's closure card. Not a condition on this PR.
  • main merged twice; three commits behind at PR open — answered. Merge-base 5bed1f6caf; the three (d2b188fb57, 4cc5bcd858, 157baa75f2) touch none of the four files. Between the branch's base 96e724475c and the merge-base the only main change to sql-driver.ts is df67985b0a (fix(driver-sql): refusals, drift reports and log lines state each decision in words instead of a tracker number (stage 1) #20795), and the head carries its wording. The queue rebuilds on current main.
  • open_questions: none filed. The second out_of_scope_findings entry (SQLite's bound-variable ceiling surfacing as a generic 500) is noted, not filed, and is not this card.
  • Harness trailer flag: reporting only; the commits carry the model-free pair per AGENTS.md. No action.

Implemented-by: claude/issue-20768-first-boot-migration-gate-read
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 08:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 810d42b Sep 30, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20768-first-boot-migration-gate-read branch September 30, 2026 09:21
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants