Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606
Conversation
…t envelope; isMissingTableError prefers it over readObject WIP — implementation, pins and changesets; verification pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ptions bag; keep the one-argument pin inside the defining package Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…tion Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift Check9 anchor(s) derived from 2 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
Coarse fallback — 10 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 f2165b9f8d9e9818a3462a4e35fd209429783217 && git checkout f2165b9f8d9e9818a3462a4e35fd209429783217
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fc15f0aceff5167f2a660433d193e78ac84dfef7 d0486a5cdafe2f31ed4bf1344de40b7e49e5340f && git checkout -B drift-repro fc15f0aceff5167f2a660433d193e78ac84dfef7 && git merge --no-ff d0486a5cdafe2f31ed4bf1344de40b7e49e5340f
node scripts/docs-audit/affected-docs.mjs --json fc15f0aceff5167f2a660433d193e78ac84dfef7 |
os-musk
commented
Sep 2, 2026
Landing provenance (engine execution seat, session
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…wn at minor Two defects in the previous commit's changeset, neither in the implementation. RESTORED. `.changeset/liveness-live-elsewhere-verdict.md` already existed on `origin/main` — it is #13483's, declaring `@objectstack/spec: patch` and describing the fifth verdict, its gate-executable criteria, the 180-day re-attestation discipline and the `manifest.runtime` migration. Writing this package's note to that path truncated it, which would have dropped the pending `@objectstack/spec` bump and taken all of that out of the next release notes. `packages/spec/CHANGELOG.md` has zero hits for `live-elsewhere`, so it is unconsumed and still owed, not a stale leftover. The file is restored byte for byte from `origin/main` (blob 5321f1b) and this package's note moves to `.changeset/lint-liveness-live-elsewhere-rule-id.md`. LEVEL. `minor`, not `patch`: `LIVENESS_LIVE_ELSEWHERE_PROPERTY` is a new export on `packages/lint/src/index.ts`, the public entry, and a new public-entry export is `minor` under the precedent this lane applied today (#14606 took `@objectstack/types: minor` for three new exports). Nothing narrows, so no BREAKING banner and no ADR-0087 marker are owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Fixes#13438
Maintainer ruling 2026-09-01 (comment 5486886922, director batch B, verbatim 「同意」: option 2): the driver declares the table it targeted on the error envelope, and
isMissingTableErrorprefers a declared name over the caller-supplied object name. This PR is exactly the ruled scope — envelope stamp + predicate preference + the federated-object test pair — and nothing else: no engine accessor (option 1, not adopted), no caller change inengine.ts/seed-loader.ts(the four call sites keep passing the API name), no other driver adopts the stamp,packages/restuntouched.Head at the time of this body:
d0486a5cd(gate union, lint and both suites were run at that head, after the last commit — a merge oforigin/mainat7085f9053, which touched neither of the two source files).What changed
@objectstack/types—driver-error-classification.tsDRIVER_TARGETED_TABLE=Symbol.for('objectstack.driver.targetedTable')— the well-known key, global registry so a duplicated package resolves it (the same choicedriver-sql's withheld-diagnostic symbols and spec'sFILTER_SUBTREE_PROVENANCEmade).declareTargetedTable(error, table)— the producer's half: defines the name non-enumerable and non-writable; first declaration wins; an empty or non-string name declares nothing (silently — this runs on an error path where a thrown TypeError would replace the envelope it annotates).targetedTableOf(error)— the reading half, string or null, tolerant of bare input.matchesDriverErrorcomputes, per node,relation = targetedTableOf(node) ?? inheritedand handsrelationboth toexcludedByReadObjectand down thecausechain. So: a declared table replacesreadObjectoutright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited.excludedByReadObjectkeeps its name and guard (its parameter is documented as "the relation to compare against");phraseNamesAnotherRelationandnormaliseRelationNameare untouched, so the same folding (schema/database qualifier, the legacyns__shortprefix, case) applies to a declared name.isMissingTableError(error, readObject?, depth = 0)— signature unchanged; docblock gains the isMissingTableError's read-table check compares an object API name against a federated object'sremoteName— a genuinely absent external remote now reads as loud, not benign #13438 paragraph.@objectstack/driver-sql—sql-driver.tsbackendStatementFaultError(object, cause, targetedTable)— module-private, gains the third parameter and callsdeclareTargetedTable(err, targetedTable)after definingcause. The disclosure clause in its docblock stands verbatim; a new docblock section states that the table is declared on the envelope and never written into the message.backendStatementFault(object, error)(the composition site, formerly:8030) resolvesconst targetedTable = this.physicalTableByObject[object] ?? objectand passes it. No hunk anywhere near perf: authed data-API throughput is pinned to the knex default pool (~10/replica) with no OS_* knob — a 3-replica cluster saturates at ~25 rps while Postgres sits at ~21/200 connections #14176's pool composition (:4793/:4916).Zone 2 — PM assumptions, measured
1. Placement.
backendStatementFaultErroris composed at ONE site,SqlDriver.backendStatementFault, reached from three read exits —find(the ladder's terminal),count, andaggregateviaaggregateFault— every one of which built its statement throughthis.getBuilder(object, options). The physical table is not on the path that reaches the composition site:getBuilderresolves it internally and hands back a knex builder, so the site has onlyobjectanderror. Measured ongetBuilderitself (:11929onorigin/main): the table it targets isthis.physicalTableByObject[object] ?? object— NOTStorageNameMapping.resolveTableName. TheresolveTableNamespelling the claim quotes (:6315/:6935) belongs to the autonumber config lookup, not to the statement target; andresolveTableNamestrips a legacyns__shortprefix thatgetBuilderdoes not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeatsgetBuilder's own expression (a private helper shared withgetBuilderwas considered and not done: four other sites in the file already inline the same expression, and refactoring them is outside this card). The pins hold the two in agreement empirically: on every dialect cell the declared table equals the relation the dialect's OWN phrase names —external.remoteNamefor the federated object, the object's own name for a native one. So the "storage-mapped name" half of the assumption is corrected: for a native object the statement targets, and the envelope declares, the object's own name (which under Prime Directive 6 IS the table name).2. Predicate. Signature unchanged (typecheck of
packages/typesand ofdriver-sqlgreen; the #13440 callers gate green — 535/535 in the types suite). Declared table is read off the error under the symbol; precedence is declared table overreadObject, from the declaring node down. The existing normalisation applies to the declared name — pinned with the dialect fixtures:no such table: legacy_orders,no such table: main.legacy_orders, PGrelation "legacy_orders" does not existandrelation "public.legacy_orders" does not exist(42P01), MySQLTable 'db.legacy_orders' doesn't exist(ER_NO_SUCH_TABLE / 1146) andUnknown table 'db.legacy_orders'— all six read benign with the declaration and loud without it (the "defect as a control" block). Case folding and a qualifier on the declaration itself (public.legacy_orders) are pinned too.3. The #13324 narrowing does not reopen. With the declared table present, a phrase naming a different relation — sqlite
no such table: main.absent_base(a view over a dropped base), PGrelation "sys_other_table" does not exist(a join target), MySQLTable 'db.sys_other_table' doesn't exist(a sys table hit inside the same statement) — stays not benign. Also pinned: the declared name is compared and the caller's is ignored (a phrase naming the OBJECT while the statement targeted the REMOTE reads loud with the declaration, benign without it); a declared node's mismatch is not rescued by a matchingcause. Live on SQLite: a view whose base table was dropped — the envelope declares the view, the phrase names the base, verdict false.4. Serialisation invisibility. Pinned at both levels:
Object.keys(err)is exactly['code', 'status'];JSON.stringify(err)does not contain the physical table; a spread copy declares nothing (targetedTableOfnull, symbol absent);Object.getOwnPropertySymbols(err)carries the key (code-readable). The composed message still names only the caller's object and never the remote (disclosure clause, live on each cell).5. Changeset levels.
@objectstack/driver-sql: patch — the envelope gains a code-readable member;code/status/messagebyte-identical;backendStatementFaultErroris module-private so no export moves (the #14367 precedent).@objectstack/types: minor, not the seat'spatchreading — the public entry gains three exports (DRIVER_TARGETED_TABLE,declareTargetedTable,targetedTableOf), which is the "a public-entry export moves ⇒ say so and use minor" branch; the predicate's own signature is unchanged. Measured:check:changeset-no-major,check:adr-0087-registrationandcheck:empty-changesetgreen (no BREAKING banner, no major).check:api-surfacelives inpackages/specand measures spec's surface only; this diff does not touch spec.6. Clause-② self-reading — see the section below.
7. Census. Not re-run; cited: the lane PM's measurement (comment 5484130045) — no shipped federated object carries autonumber or inbound references, so the two data-consequence call sites are unreachable by shipped code today; the fix is owed for customer-authored federated objects.
Clause-② self-reading
Yes. Read from the diff: (a) a published predicate's accept set moves —
isMissingTableErrorrestores the benign verdict for declared-table matches (widening back toward the declared contract), and, because a declaration is evidence, an envelope whose phrase names a relation other than its declared table now reads not-benign even through the one-argument published form (a narrowing in the cheap direction, only for errors that carry a declaration — undeclared errors are byte-for-byte unchanged, pinned); (b)@objectstack/types' public entry gains three exports; (c)driver-sql's terminal envelope gains a code-readable member.needs:contract-reviewis on the PR (union write, read back).Verification (all at
d0486a5cd, after the last commit)Exit codes captured after a redirect, never through a pipe; verdict lines quoted from each tool's own output.
pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0),@objectstack/specrebuilt after the merge (the only closure member the merge touched).pnpm --filter @objectstack/types exec vitest run --maxWorkers=2→Test Files 18 passed (18)·Tests 535 passed (535)(includes the Nothing stops an in-repo caller from callingisMissingTableErrorwithout its read-table argument — and the silent result is the wide verdict #13324 just removed #13440 callers gate and the newdriver-error-classification.targeted-table.test.ts).pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2over the new suite plussql-driver-backend-fault-envelope.test.tsandsql-driver-11455-aggregate-fault-envelope.test.ts→Test Files 3 passed (3)·Tests 27 passed | 7 skipped (34). The 7 skips are the live PG / MySQL cells, unprovisioned in this container (OS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URLunset — 0OS_TEST_*vars); CI's live-dialect job runs them.packages/typestsc --noEmitexit 0,--listFileslists 18 test files including the new one;packages/drivers/driver-sqltsc --noEmitexit 0,--listFileslists 170src/files of which 161 are.test.tsand the new suite is among them. The dispatch note that driver-sql's tsconfig excludes**/*.test.tsis falsified: itsincludeissrc/**/*and itsexcludeis onlynode_modules/dist, so the test layer IS type-checked there.@objectstack/types— narrowed, declared: the full...@objectstack/typesrun needs the whole-repo build, which is CI's. Evidence in its place: (i) the export-surface diff of the module is purely additive (three+exportlines, zero-export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control:isMissingTableErrorhas 217 hits outsidepackages/types); (iii) no consumer re-exports@objectstack/typeswholesale, so noexport *collision is possible; (iv)@objectstack/coreand@objectstack/metadatahave notypecheckscript (debt ledger), and@objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt@objectstack/lint/@objectstack/metadatadist, not on anything this diff touches).pnpm lint(whole repo,eslint . --no-inline-config) → exit 0.pnpm check:nul-bytes→check-nul-bytes: OK (scanned 7965 text file(s) … no raw ASCII control bytes); a self-scan of the five touched files for control characters found none.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no path list; 36 commands, identical before and after the merge) and run in full at this head: 32 exit 0, includingcheck:dispatcher-error-vocabulary,check:driver-conformance,check:cross-package-test-inputs,check:test-source-alias,check:type-check-coverage,check:query-options-erasure("ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new … baseline key set verified against 7085f90"), and the changeset gate family. 4 are NOT MEASURED locally, exit 3 in each gate's own words:check-test-completeness("grades a saved turbo run test log, and no log was named"),pm/check-half-states("repo-scoped reads are refused — this container cannot make one repo-scoped request", the 403 shape),check:dual-build-cjs-loads("reads built output, and some package has no dist/"),check:type-check-debt("Build the closure first, exactly as lint.yml does"). None is a red; all four are whole-repo-build or platform-bound and belong to CI.pnpm check:error-status-conformance(always-run, outside the derived union) → exit 0: "✓ every derivable runtime status is documented, and every documented status is reachable" (51 codes reconciled, 2174 source files scanned).Ablation — revert the predicate preference, keep the stamp
Mutation on a committed tree (HEAD
d0486a5cd):const relation = targetedTableOf(err) ?? readObject;→const relation = readObject === 'ABLATION_13438_IGNORE_DECLARATION' ? undefined : readObject;(a load-bearing string literal, so the marker survives into the built output). Predicted BEFORE the run: types-level "benign again" / precedence / declaration-location pins RED, the different-relation fence GREEN; driver-level "reads BENIGN again" RED, the stamp pins and the sqlite view fence GREEN.grep -cof the original expression 1→0, of the marker 0→1, blob206459b7…→2bcb9a7d….Tests 13 failed | 93 passed (106)— the 13 are exactly the benign-again, precedence, one-argument-narrowing, folding and declaration-location pins; every isMissingTableError never checks WHICH table the "no such table" names — a view over a missing base table is read as "this table is not provisioned yet" #13324 pin and the different-relation fence stayed green.pnpm --filter @objectstack/types build, thennode scripts/ablation-dist-preflight.mjs @objectstack/types ABLATION_13438_IGNORE_DECLARATION→ "✓ dist/: marker present in 2 built files (plus 2 sourcemap hits, not counted) — the ablation is live in the artifact the suite consumes" (driver-sql resolves@objectstack/typesthrough dist; it is on theKNOWN_UNALIASED_TEST_IMPORTSledger).Tests 1 failed | 6 passed | 2 skipped (9)— the one red is "reads BENIGN again through the real predicate"; the stamp pins and the sqlite fence green.git checkout HEAD -- ABSOLUTE-PATHinside atrap … EXIT INT TERM; proven by blob equals HEAD (206459b7…),git diff HEADempty, and whole-treegit status --porcelainempty; rebuilt, preflight--absent→ "marker absent from all 12 built files"; both suites rerun green (106 passed,7 passed | 2 skipped).//in the replacement; caught by the grep counts, aborted, restore proven), and the second used a comment marker that tsup stripped, so the preflight read a sourcemap-only hit and declared that run void even though the behavioural direction was already the predicted one. The run above is the third, with the marker in executable output.Notes for review
DROPof a base table under a dependent view withoutCASCADE(which drops the view), and MySQL answers an invalid-view error the predicate never recognised. The cross-dialect fence is pinned with fixtures at the predicate level.remoteSchema(PG / MySQL.withSchema) is not exercised live; the qualified phrase it would produce is covered by therelation "public.legacy_orders"fixture, and the declaration is bare on purpose — both sides fold the qualifier.this.physicalTableByObject[object] ?? objectinsql-driver.ts;@objectstack/metadata/errorskeeps re-exporting onlyisMissingTableError.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Generated by Claude Code