fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438) - #14606

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation
Sep 2, 2026
Merged

fix(driver-sql,types): declare the targeted table on the backend-fault envelope; isMissingTableError prefers it over readObject (#13438)#14606
os-musk merged 3 commits into
mainfrom
claude/issue-13438-missing-table-declared-relation

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

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 isMissingTableError prefers 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 in engine.ts / seed-loader.ts (the four call sites keep passing the API name), no other driver adopts the stamp, packages/rest untouched.

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 of origin/main at 7085f9053, which touched neither of the two source files).

What changed

@objectstack/typesdriver-error-classification.ts

  • DRIVER_TARGETED_TABLE = Symbol.for('objectstack.driver.targetedTable') — the well-known key, global registry so a duplicated package resolves it (the same choice driver-sql's withheld-diagnostic symbols and spec's FILTER_SUBTREE_PROVENANCE made).
  • 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.
  • matchesDriverError computes, per node, relation = targetedTableOf(node) ?? inherited and hands relation both to excludedByReadObject and down the cause chain. So: a declared table replaces readObject outright from that node down; the declaration NEAREST the dialect phrase wins; a string node cannot declare and compares against what it inherited. excludedByReadObject keeps its name and guard (its parameter is documented as "the relation to compare against"); phraseNamesAnotherRelation and normaliseRelationName are untouched, so the same folding (schema/database qualifier, the legacy ns__short prefix, 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's remoteName — a genuinely absent external remote now reads as loud, not benign #13438 paragraph.

@objectstack/driver-sqlsql-driver.ts

Zone 2 — PM assumptions, measured

1. Placement.backendStatementFaultError is composed at ONE site, SqlDriver.backendStatementFault, reached from three read exits — find (the ladder's terminal), count, and aggregate via aggregateFault — every one of which built its statement through this.getBuilder(object, options). The physical table is not on the path that reaches the composition site: getBuilder resolves it internally and hands back a knex builder, so the site has only object and error. Measured on getBuilder itself (:11929 on origin/main): the table it targets is this.physicalTableByObject[object] ?? object — NOT StorageNameMapping.resolveTableName. The resolveTableName spelling the claim quotes (:6315 / :6935) belongs to the autonumber config lookup, not to the statement target; and resolveTableName strips a legacy ns__short prefix that getBuilder does not strip, so stamping it would have declared a table the statement never named. The stamp therefore repeats getBuilder's own expression (a private helper shared with getBuilder was 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.remoteName for 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/types and of driver-sql green; 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 over readObject, 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, PG relation "legacy_orders" does not exist and relation "public.legacy_orders" does not exist (42P01), MySQL Table 'db.legacy_orders' doesn't exist (ER_NO_SUCH_TABLE / 1146) and Unknown 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), PG relation "sys_other_table" does not exist (a join target), MySQL Table '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 matching cause. 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 (targetedTableOf null, 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 / message byte-identical; backendStatementFaultError is module-private so no export moves (the #14367 precedent). @objectstack/types: minor, not the seat's patch reading — 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-registration and check:empty-changeset green (no BREAKING banner, no major). check:api-surface lives in packages/spec and 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 — isMissingTableError restores 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-review is 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.

  • Dependency closure built under the shared verify lock (pnpm --filter '@objectstack/driver-sql^...' build, VERDICT command-exit 0), @objectstack/spec rebuilt after the merge (the only closure member the merge touched).
  • pnpm --filter @objectstack/types exec vitest run --maxWorkers=2Test Files 18 passed (18) · Tests 535 passed (535) (includes 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 gate and the new driver-error-classification.targeted-table.test.ts).
  • pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 over the new suite plus sql-driver-backend-fault-envelope.test.ts and sql-driver-11455-aggregate-fault-envelope.test.tsTest 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_URL unset — 0 OS_TEST_* vars); CI's live-dialect job runs them.
  • Typecheck: packages/typestsc --noEmit exit 0, --listFiles lists 18 test files including the new one; packages/drivers/driver-sqltsc --noEmit exit 0, --listFiles lists 170 src/ files of which 161 are .test.ts and the new suite is among them. The dispatch note that driver-sql's tsconfig excludes **/*.test.ts is falsified: its include is src/**/* and its exclude is only node_modules / dist, so the test layer IS type-checked there.
  • Downstream (consumer-direction) typecheck of @objectstack/typesnarrowed, declared: the full ...@objectstack/types run 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 +export lines, zero -export); (ii) zero pre-existing bindings of the three new names anywhere in the repo outside the touched files (control: isMissingTableError has 217 hits outside packages/types); (iii) no consumer re-exports @objectstack/types wholesale, so no export * collision is possible; (iv) @objectstack/core and @objectstack/metadata have no typecheck script (debt ledger), and @objectstack/metadata-protocol's run is NOT MEASURED here (TS2307 on unbuilt @objectstack/lint / @objectstack/metadata dist, not on anything this diff touches).
  • pnpm lint (whole repo, eslint . --no-inline-config) → exit 0.
  • pnpm check:nul-bytescheck-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.
  • Gate union re-derived on the final tree with 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, including check: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.

  • Mutation proved on disk: grep -c of the original expression 1→0, of the marker 0→1, blob 206459b7…2bcb9a7d….
  • Types-level (src-resolved): 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, then node 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/types through dist; it is on the KNOWN_UNALIASED_TEST_IMPORTS ledger).
  • Driver-level (dist-resolved): 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.
  • Restore: git checkout HEAD -- ABSOLUTE-PATH inside a trap … EXIT INT TERM; proven by blob equals HEAD (206459b7…), git diff HEAD empty, and whole-tree git status --porcelain empty; rebuilt, preflight --absent → "marker absent from all 12 built files"; both suites rerun green (106 passed, 7 passed | 2 skipped).
  • Disclosed: two earlier attempts were void and are not counted — the first mutation never landed (perl syntax error on a // 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

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 10 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 fc15f0aceff5167f2a660433d193e78ac84dfef7packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2165b9f8d9e9818a3462a4e35fd209429783217 — the merge of head d0486a5cdafe2f31ed4bf1344de40b7e49e5340f into base fc15f0aceff5167f2a660433d193e78ac84dfef7, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 14:59Z and armed auto-merge (squash) at 14:59:49Z on head d0486a5cd.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 87ad30cSep 2, 2026
51 checks passed
@os-musk
os-musk deleted the claude/issue-13438-missing-table-declared-relation branch September 2, 2026 15:33
os-musk pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-musk@claude