Gate every in-repo isMissingTableError call on naming the object it read - #14346
Merged
Conversation
…e object it read #13324 gave `isMissingTableError` a `readObject` argument so a driver fault naming a different relation can no longer be answered "this table is not provisioned yet". The parameter had to ship optional -- `@objectstack/types` is published (17.2.0) and re-exported from `@objectstack/metadata/errors`, so a required parameter is a major bump -- which left the in-repo obligation stated only in JSDoc. Prose is exactly what #13324 proved insufficient: a caller that omits the argument silently receives the pre-#13324 wide verdict, and on the authz path that resolves a permission-store outage to `[]` permissions. `driver-error-classification.callers.test.ts` walks every TypeScript source under `packages/` with the TypeScript compiler API and fails any call of the predicate that omits `readObject` or passes it as `undefined`/`null`, naming each site file:line with the remedy. The only exemption is this module's own contract tests, which exercise the published one-argument form on purpose. Two positive controls keep an empty violation set from being indistinguishable from a broken scanner: with the exemption disabled the defining test file must yield a substantial one-argument population (30 today), and the two-argument population outside `packages/types` must be substantial (18 today, across 5 packages). A third check fails if a renamed import binding appears, since the callee matcher is by name. Registration, required by check:cross-package-test-inputs for any test that reads outside its own package: one entry in the declaration table and one turbo task, both scoped to `packages/**`. The repo root is reached by arithmetic off this package's manifest rather than by a marker-file walk precisely to keep that radius inside `packages/**`, which ci.yml's `core:` filter already covers -- so no scheduler change is needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
…esidue specimen The first shape declared `globs: ['packages/**']`. That entry's globs are inherited as watch hints by `check:cross-package-test-inputs`, and it was the only entry in the table covering a `.tsx` file, which turned one dispatch-gates self-test case red: it pins that no hint of that family reaches the `realtime-hooks.test.tsx` specimen in `packages/client-react` -- the live member of "a test class the hint route cannot reach". Narrowed on both sides at once, which is the only honest way to move either: the scanner now takes `.ts` alone via a named `SOURCE_FILE` predicate, and the declaration and turbo inputs are `packages/**/*.ts` to match. Measured under `packages/` on this commit: 5181 `.ts` tracked and 48 mention the predicate; `.tsx` 8 tracked / 0 mention; `.mts` 17 / 0; `.cts` 0 / 0 -- so the narrowing loses no call site today. `coversDirectory` never applied here: the roster for this package holds exactly one literal and no directory entry, so `packages/**/*.ts` both covers the roster and is held by it. Two pins keep the boundary from decaying into a comment: the extension set is asserted directly (pure predicate, no I/O, so it adds nothing to the declared radius), and a filename-only walk asserts `.tsx` files really exist under `packages/`, so the exclusion stays a decision about a real population. Both the header and the table entry name that specimen file in two halves, because the registrar collects quoted whole paths out of comments -- spelling it in full put it on this package's roster and demanded the very `.tsx` glob the change exists to avoid. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ssing-table-arg-gate
The `.ts` total in that header was measured before this branch merged `origin/main` and read 5181; the tree it now describes has 5193. Only the three ZEROS in that table are load-bearing -- they are what makes "narrowing to `.ts` loses no call site" true -- and the total moves with every merge, so the reading now names the commit it belongs to rather than implying "now". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
os-trump
marked this pull request as ready for review
September 2, 2026 10:01
os-trump
enabled auto-merge
September 2, 2026 10:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13440
What this closes
#13324 repaired
isMissingTableErrorby giving it the object the caller was reading, so a driver fault naming a different relation can no longer be answered "this table is not provisioned yet". PR #13437 shipped that as an optional parameter, and optional was right:@objectstack/typesis published (17.2.0,exports.and./node) and re-exported again from@objectstack/metadata/errors, so a required parameter is a breaking change needing a major bump.That left the in-repo half of the obligation stated only in JSDoc.
isMissingTableError(err)still compiles, still type-checks, and still returns the pre-#13324 wide verdict — silently. On the authz path (packages/core/src/security/resolve-authz-context.ts:237) that verdict resolves a permission-store outage to[]permissions instead of failing loud, so the omission fails in the open direction. Prose is exactly what #13324 existed to prove insufficient.This adds the enforcement half as an in-repo gate, which buys it without the major bump: it binds only callers inside this repository, and external consumers keep the optional form the published API promises them. The published signature is unchanged.
The mechanism
packages/types/src/driver-error-classification.callers.test.tswalks every.tssource underpackages/with the TypeScript compiler API, collects everyCallExpressionwhose callee resolves toisMissingTableError, and fails naming each sitefile:line:colwith the remedy. It flags two shapes, not one:undefined/null/void 0— the read object explicitly discarded, which passes an arity check and means the same wide verdict.A line-oriented regex was not used and would not have been a census: calls span lines, and the callee appears both bare and as a property access.
The exemption axis, and why it is exactly this narrow
driver-error-classification.test.tscalls the one-argument form 30 times on purpose — those are the tests of the optional form, pinning thatisMissingTableError(err)still behaves for the external consumers the optional parameter protects. A gate written to the naive rule would fail all 30, and the obvious "fix" of passing them a read object would delete the coverage of the published one-argument contract.So the exemption is the defining package's own contract tests and nothing else:
packages/types/src/driver-error-classification*.test.ts. Everything else underpackages/— production and test code alike — must pass the read object. The defining module itself is deliberately not exempt: the predicate delegates tomatchesDriverErrorand never calls itself, so a self-referential one-argument call there would be a new fact worth failing on.Measured population, on
f60061a460.tscall sites scanned underpackages/apps/,examples/,e2e/,scripts/The production sites confirmed passing the argument:
resolve-authz-context.ts:237,metadata-protocol/src/protocol.tsx5,seed-loader.tsx2,sys-metadata-repository.ts,metadata/src/loaders/database-loader.tsx2,objectql/src/engine.tsx5,lifecycle-service.tsx2 — 18 across 4 packages (core,metadata,metadata-protocol,objectql). Two corrections to the card's own numbers, both re-measured here:protocol.tshas 5 such calls, not the 4 the R64 note lists; and the count is 18, not 17.The extension boundary —
.tsonly, and why that is not a detailThe first shape of this PR scanned
.ts/.tsx/.mts/.ctsand declaredglobs: ['packages/**']. That reddened CI, and the failure is worth recording rather than just fixing.A package's entry in
CROSS_PACKAGE_TEST_INPUTSis inherited as watch hints bycheck:cross-package-test-inputs. The dispatch-gates self-test pins that no hint of that family reaches therealtime-hooks.test.tsxfile inpackages/client-react— its live specimen for "a test class the hint route cannot reach in principle".packages/**was the only entry in the whole table that covered a.tsxfile, so it turned that case red:That is a real red, not a nuisance: the specimen is how that tool proves its residue classes are non-empty. So the fix narrows both sides together, which is the only honest way to move either — the scanner now takes
.tsalone through a namedSOURCE_FILEpredicate, and the declared glob and turbo inputs arepackages/**/*.tsto match. Measured underpackages/onf60061a460:.ts.tsx.mts.ctsThe narrowing therefore loses no call site. Two pins keep that from decaying into a comment: the extension set is asserted directly (pure predicate, no I/O, so it adds nothing to the declared radius), and a filename-only walk asserts
.tsxfiles really do exist underpackages/, so the exclusion stays a decision about a real population rather than a statement about nothing. The filename-only spelling is deliberate — opening those files would create a content dependence on files outside the declared glob, which is the very #7802 shape the declaration table exists to prevent..mtsand.ctsare excluded for a second, independent reason:globHolderVerdictrequires every declared glob to be rostered or witnessed, and this package's roster is exactly one literal.packages/**/*.mtsmatches nothing in it, so declaring it would red the gate as a glob nothing holds.Why the green is not vacuous
A scanner that silently stops matching yields the same empty violation set as a clean repo, and the assertion cannot tell them apart. Each control was proven red by ablation — mutate, prove the mutation landed on disk by counting both the removed and the injected text, run, restore, prove the restore by comparing bytes against the HEAD blob:
isMissingTableError(err, object)toisMissingTableError(err)inresolve-authz-context.tspackages/core/src/security/resolve-authz-context.ts:237:9 isMissingTableError(err)isMissingTableError(error, this.tableName)toisMissingTableError(error, undefined)indatabase-loader.tspackages/metadata/src/loaders/database-loader.ts:803:9 isMissingTableError(error, undefined)EXEMPTregex replaced with one that never matches30 call site(s) of isMissingTableErrorSCANNED_TREEpointed atpackages/types/srcEvery leg restored to its exact HEAD blob hash with
git diff HEADempty. The ablation script carried atrap ... EXIT INT TERMrestore on absolute paths and refused to report an exit code unless vitest had actually run — the first attempt returned 127 from a bad invocation and was rejected as measuring nothing rather than read as a red gate, and a later leg's substitution silently failed to land and was caught by the on-disk proof rather than by an exit code.The gate needs no build: it reads source text and touches no
dist/.Deviation from the dispatched file surface
The card's surface was
packages/types/only. The commit also touchesscripts/cross-package-test-inputs.mjs(one table entry) andturbo.json(one task). That is this repo's own mandatory registration for any test that reads outside its package, not scope drift.check:cross-package-test-inputsrefused the new test until it was declared, in its own words:No gate logic was authored or edited; nothing landed in
packages/lint, no new rootscripts/gate, andscripts/pm/dispatch-gates.mjswas not touched. The gate now reports:coversDirectoryturned out not to apply here, which is why apackages/**-class glob is not needed.packages/has zero direct files, socoversDirectory('packages', ...)would be false for any glob set — but it is never asked: the roster'sdirEntriesis empty, because the recursive walk descends on a loop variable and yields no name. The roster holds exactly one literal,packages/core/src/security/resolve-authz-context.ts, andpackages/**/*.tsboth covers it and is held by it.The deviation was actively kept to those two files. An earlier draft anchored the repo root on a workspace marker file, which named a root-level path; that forced a third and fourth file — an entry in
.github/workflows/ci.yml'scrosspkg:filter, and two hardcoded self-test counts inscripts/check-ci-filter-parity.mjs. Re-anchoring the repo root by arithmetic off this package's own manifest keeps the whole declared radius insidepackages/, which ci.yml'score:filter already covers, so the cascade disappears andcheck-ci-filter-parity.mjsstays green with no ci.yml change. The arithmetic is not trusted on faith: the anchor test requires the walk to find this package's own defining module, which no wrong root can satisfy.The comment registrar, twice
That registrar collects quoted whole paths out of comments, without parsing — so a path merely named in prose becomes a declared input. It bit this PR twice, and both are now written in halves:
check-cross-package-test-inputs.mjsby full path putscripts/on this package's roster;.tsxspecimen by full path put it on the roster and demanded the very.tsxglob this change exists to remove.Both files carry a short note saying why the spelling is split, so the next author does not "tidy" them back.
Not chosen
Making the parameter required (the card's option B).
@objectstack/typesis published, so that is a major bump and a maintainer's decision; it stays available as a follow-up and is not foreclosed by this gate.Verification
Head
69c4226d64, tree clean.f60061a460(the previous head) is green on CI;69c4226d64adds only a comment correction on top of it.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon the actual change set: 35 families, identical set at both heads. All 35 run: 34 exit 0.node scripts/check-test-completeness.mjs, exit 3, that gate's own NOT MEASURED code when invoked with no log argument ("It is not a red, and there is nothing here to fix"). CI tees it aturbo run testlog.pnpm check:pm-dispatch-gates— the gate this patch round fixes — 1232 of 1232 cases pass, exit 0, with✓ nor a .tsx test file inside it. Run on the merged tree; it exceeds the 10-minute foreground limit, so it was run to completion and polled rather than backgrounded and abandoned.pnpm lint(the full repo-wideeslint . --no-inline-config, not a narrowed run): exit 0, re-run at the final head.pnpm --filter @objectstack/types test: 17 files, 504 tests passed (502 plus the two boundary pins).packages/typestsc --noEmit: exit 0, and--listFilesconfirms the new test file is inside that program. An earlier reading showed 19 errors and was correctly discarded as NOT MEASURED — all 19 wereTS2307in other files, the unbuilt-closure cascade, and it went to 0 once@objectstack/types^...was built.check:type-check-debtandcheck:dual-build-cjs-loadswere refused as PREREQUISITE NOT MET until the closure was built; afterturbo run build(70/70) both are exit 0.Changeset
skip-changeset: nothing published changes. The new file is a test —packages/typespublishesfiles: ["dist", "README.md", "CHANGELOG.md"]and tsup's entries aresrc/index.tsandsrc/node.ts, neither of which reaches it — and the only edit to shipped source is a JSDoc comment. The other two files are CI-internal build orchestration.check:published-filesis green.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza