fix(metadata-protocol): resolve the raw-SQL driver seam through one execute-first helper - #14119
Conversation
…xecute-first helper (#14083) Three sites resolved a raw-SQL entry point off a driver in two different orders: `migrations/partial-index-probe.ts` and `protocol.ts`'s `ensureOverlayIndex` tried `raw` first, `migrations/seed-tenancy-backfill.ts` tried `execute` first. They now share `migrations/driver-exec.ts`, which tries `execute` first and keeps `raw` as the fallback. `execute` goes first because `IDataDriver` declares it non-optionally and has never declared `raw`, so it is the only raw-execution surface the contract guarantees. Same reasoning and same order as `@objectstack/metadata`'s `migrations/driver-exec.ts`; the two headers cross-reference each other. No behaviour change on any shipped driver: none defines `raw`, so that limb was unreachable and `execute` already ran at all three sites. The flip matters for a host or third-party driver defining BOTH, which previously took `raw` at two sites and `execute` at the third. `raw` is kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2cbcf13bdf30b3cf65aec3dcd55456429cc143b6 && git checkout 2cbcf13bdf30b3cf65aec3dcd55456429cc143b6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e1b2de73ef5d7ce5f3f665612a12731d3608725 6b45e0c4040ca2df690ecbd98861bb11e67de786 && git checkout -B drift-repro 9e1b2de73ef5d7ce5f3f665612a12731d3608725 && git merge --no-ff 6b45e0c4040ca2df690ecbd98861bb11e67de786
node scripts/docs-audit/affected-docs.mjs --json 9e1b2de73ef5d7ce5f3f665612a12731d3608725
|
…ted by one line The import added at `packages/metadata-protocol/src/protocol.ts:30` shifts every line below it by +1, including the `if (context?.isSystem) return data;` elevation read in `stripReadonlyForInsert` (1736 -> 1737). The census page still anchored 1736, so `check-system-context-census` reported both halves of one rot: `[site-without-a-row]` at 1737 and `[anchor-is-not-a-read-site]` at 1736. Repaired with the gate's own `--fix`, which re-pointed exactly one anchor and added or deleted no row. Pure line rot: the population is unchanged at 109 elevation read sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
|
CI repair pushed as
Repaired with the gate's own Exactly one anchor re-pointed; no row added or deleted, row 21's prose byte-identical. Gate bare, exit captured before any pipe:
Why the authoring seat's local family run did not catch it: Generated by Claude Code Generated by Claude Code |
Fixes #14083
Three sites in
packages/metadata-protocolresolved a raw-SQL entry point off a driver, in two different orders. They now share one helper and all tryexecutefirst, withrawkept as the fallback.src/migrations/partial-index-probe.ts(line 94)raw, thenexecutesrc/migrations/seed-tenancy-backfill.ts(line 347)execute, thenrawsrc/protocol.ts—ensureOverlayIndex(line 5230)raw, thenexecuteLine numbers re-derived on this branch's base. Note the third one: the card cited roughly line 5159 and the real location is 5230.
Why this order
IDataDriver(packages/spec/src/contracts/data-driver.ts, line 108) declaresnon-optionally, and has never declared
rawat all. Soexecuteis not merely the surface the shipped drivers happen to have — it is the only raw-execution surface the contract guarantees, and any driver satisfying the interface has it. This is the 2026-08-07 meta-criterion (one operation, several implementations, inconsistent behaviour, decide by the declaration-bound side) applied a second time; the first application ispackages/metadata/src/migrations/driver-exec.tsfrom #14084, which this follows. The two modules are twins and their headers cross-reference each other.rawis deliberately kept. Nothing that worked before stops working.New file:
packages/metadata-protocol/src/migrations/driver-exec.tsA twin of
metadata's, not an import of it.@objectstack/metadatais already a declared dependency ofmetadata-protocoland there is no cycle — so the dependency direction was not the obstacle the card assumed. The live reasons for a twin are:driver-exec.tsis internal tometadata's migrations directory; it is not re-exported from@objectstack/metadata/migrations. Importing it would mean widening that package's published surface to serve three call sites in a sibling package../migrationsbarrel, andensureOverlayIndexruns on every boot. Putting a migrations barrel on the boot path to save ten lines is the wrong trade.While there,
protocol.ts's stale justification is corrected: it claimed a circular dependency because "metadata already depends on objectql". That is false —@objectstack/metadatadoes not depend on@objectstack/objectql;objectqldepends on both.Behaviour
No change on any driver this repo ships. No data driver here defines
raw:InMemoryDriver,MongoDBDriverandSqlDrivereach declareexecuteand none declaresraw, andSqliteWasmDriverandTursoDriverextendSqlDriver. The onlyraw(members in the tree are two test doubles andpackages/verify/src/harness.ts, an HTTP harness whose signature is(path, init).The flip matters for a host or third-party driver defining both surfaces: it used to be driven through
rawat two sites andexecuteat the third — the same operation on two paths in one process.Two consequences of routing all three through one helper, named explicitly because they are more than a reordering:
rawlimb.seed-tenancy-backfill.ts'srawfallback was(sql) => driver.raw(sql)— it dropped itsparamsargument entirely. Invisible only because that limb is unreachable on every shipped driver. Same defect class, same file, mechanical, and the correct shape was already pinned bymetadata's helper.canRunSql/canRun/ the inline check cannot drift away from what actually gets selected.Both surfaces are now called as
(sql, bindings), matching the precedent.Known limitation, named rather than endorsed
typeof driver.execute === 'function'separates "declares the surface" from "does not", not either from "can actually run SQL". Two shipped drivers satisfy the non-optional declaration and execute nothing. That is a capability-declaration question and is out of scope here — it is tracked on its own card (#14082, which stays open and is not addressed by this PR). The new module's header says so, so the comments read as agreement about the ORDER only.Tests
New pin
src/migrations/driver-exec.test.tscovers all four driver shapes — execute-only, raw-only, both, neither — plus binding pass-through and predicate/resolution agreement. Thebothrow is the only one that can tell the two orders apart; a realistic double would pass under either and pin nothing.Fixture triage, judged per fixture rather than renamed in bulk:
view-definition-active-index.test.ts— asserted raw-first with a both-surface double. That is exactly the branch this PR rules on, so the assertion and its title were updated to the new order.sys-setting-identity-index.test.ts— its doubles offer onlyraw, so the owner-vs-default question it exists for is untouched; only the call-argument shape changed.Evidence, all on
7417c3c469:pnpm --filter @objectstack/metadata-protocol test— 2082 passed, 10 skipped, exit 0.pnpm --filter @objectstack/metadata-protocol typecheck— exit 0, andtsc --listFilesconfirms all three edited/new test files are inside the program (3/3), so that green covers them.eslint . --no-inline-config) — exit 0, zero findings. Not a narrowed run.scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack) — 36 families, 35 pass.check-test-completenesswith no log argument is NOT MEASURED by the gate's own design ("record this gate as NOT MEASURED; it is not a red").check:type-check-debt --re-measure: "28 ledger entries re-measured, 1468 raw tsc errors total, none above its recorded number" — no ratchet moved up.Ablation of the new pin
There is no behaviour difference to ablate — the
rawlimb is dead on every shipped driver — so the pin itself was ablated instead, predicting the direction first (only the two both-surface assertions should redden).2 failed | 32 passed— exactlyselects execute() — NOT raw() — on a driver that offers BOTHandresolveIndexExec prefers execute(), falls back to raw(). Every single-surface case stayed green.partial-index-probe.tsbyte-exact from the base commit (the real pre-alignment resolver, not a hand-written mutant):1 failed | 33 passed.Each leg proved its mutation on disk in both directions (the injected text present, the removed text absent) plus a changed blob hash; each restore was proved by blob-hash equality against the HEAD blob and an empty
git diff HEAD, under atrap ... EXIT INT TERMusing absolute paths pinned toHEAD. Both legs reddened without a rebuild, which mechanically confirms the pin resolves the source (a same-package relative import) rather than a staledist/.Generated by Claude Code
Generated by Claude Code