Skip to content

fix(rest): anchor looksLikeMissingRelation on the driver's quoted template - #8644

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8264-missing-relation-anchored-predicate
Aug 14, 2026
Merged

fix(rest): anchor looksLikeMissingRelation on the driver's quoted template#8644
os-zhuang merged 1 commit into
mainfrom
claude/issue-8264-missing-relation-anchored-predicate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8264

What changed

mapDataError's Postgres limb (packages/rest/src/rest-server.ts) read
relation and does not exist anywhere in the message, not necessarily the
same sentence:

constlooksLikeMissingRelation=lower.includes('no such table')||(lower.includes('relation')&&lower.includes('does not exist'))||lower.includes('table not found');

does not exist is ordinary business English ("This relation does not exist
in the diagram" — the exact string error-leak.test.ts already pins as a
negative case for #8132's shared leak predicate). Anchored the Postgres limb
on the driver's own quoted template instead, the same technique #8132 used
for looksLikeInternalErrorLeak in @objectstack/types:

constRELATION_DOES_NOT_EXIST=/\brelation\s+["'`][^"'`]+["'`]\s+doesnotexist/i;

no such table and table not found are untouched — both are exact
adjacent phrases, not a two-word conjunction, so they were never exposed to
this hole.

Design decision: one predicate, not "two widths, on purpose"

The card asked me to decide whether this should share the @objectstack/types
anchored helper, or whether relation-sub-object.ts's "two widths, on
purpose" precedent applies here. Read all three sources named in the card:

  • NOT a call into looksLikeInternalErrorLeak. That predicate answers a
    different question — "may this message be withheld from the client at
    all?" — and its other limbs (sqlite_, unique constraint, foreign key,
    a bare SQL statement) have nothing to do with this file's question ("is
    this specifically an unknown-relation condition, for the 404-vs-500
    split?"). Its own module doc already warns that relation-sub-object.ts's
    functions are "related but NOT reusable" for the same reason; the same
    argument applies to this predicate.
  • relation-sub-object.ts's "two widths, on purpose" does NOT extend to
    the two USES inside this file.
    That precedent is about two different
    questions
    (extract which column? vs. exclude "is this a sub-object?") that
    happen to share a substring, where a miss costs opposite things at each
    call site. Here, both of looksLikeMissingRelation's two readers —
    the DATA_STORE_FAULT() 500 gate and the looksLikeUnknownObject 404 limb
    — are asking the exact same question ("does this message look like a
    missing-relation condition?"). There's no reason for them to disagree, and
    missingRelationIsObject already provides the attribution safety net that
    would otherwise justify a width difference (see measurement below). So:
    one predicate, module-scoped, shared by both readers.
  • The branch's own #5462 comment, directly above this code, argues the safe
    direction on ambiguity is LOUD (a logged 500), never a silent 404. Tightening
    moves the predicate's remaining false-positive risk toward the terminal
    UNCLASSIFIED_FAULT() (500, logged) rather than a misleading 404 — consistent
    with that stated philosophy, not in tension with it.

Both decision paths measured — not just the reported one

The card's own text discusses only the DATA_STORE_FAULT() 500 path. The
predicate actually feeds two decisions, and I measured both:

Decision 1 — the 500 gate (if (looksLikeMissingRelation && !missingRelationIsObject(...)) return DATA_STORE_FAULT()):

mapDataError(driverError('This relation does not exist in the diagram'))
before: { status: 500, code: 'DATABASE_ERROR' } (mislabelled a data-store fault)
after: { status: 500, code: 'INTERNAL_ERROR' } (generic terminal fault — still a safe, sanitised, logged 500)

Decision 2 — the looksLikeUnknownObject 404 limb, which the card's own
counter-example cannot actually reach: missingRelationIsObject's own
extraction regex fails on 'This relation does not exist in the diagram'
(there is no valid identifier between relation and the literal does not exist tail), so that string was already routed to decision 1 on every
input, old or new. To exercise decision 2 I built a second case where
missingRelationIsObjectdoes extract a name — unquoted, since its own
Postgres branch tolerates unquoted identifiers (a narrower, different
question: "which relation is named", not "is this the driver's real
template"):

mapDataError(driverError('Sorry, relation acct does not exist in our records'), 'acct')
before: { status: 404, code: 'OBJECT_NOT_FOUND', error: "Object 'acct' is not registered" } (silent — 404 is isExpectedDataStatus)
after: { status: 500, code: 'INTERNAL_ERROR' } (loud, logged, sanitised)

Both are pinned in rest-unknown-object-heuristic.test.ts (§5a/§5b), and
the genuine quoted-form cases (§5c/§5d) confirm neither decision path
regressed for real driver output — all three quote styles Postgres could use
still trip the 500 gate, and a quoted, correctly-attributed relation name
still reaches the 404 limb exactly as before.

missingRelationIsObject's own extraction regex is left as-is: it's only
ever invoked once looksLikeMissingRelation has already gated true, so for
every input that reaches it post-fix, the message already carries the
quoted phrase — its optional-quote leniency is exercised on inputs that no
longer reach it. No changed behavior there, and no separate fix needed.

Reachable production path: none found

Per the card's own honesty and the branch's own #5462 comment — the
primary producer of the missing-relation 404 is #3770's registry gate,
which throws code: 'OBJECT_NOT_FOUND' and is matched far above this
heuristic — I looked for a live path that would actually reach this loose
conjunction with an ambiguous message and found none. This is
consistency/invariant restoration between two spellings of one question
(this file's copy vs. the @objectstack/types anchor #8132 taught), not a
fix for a demonstrated live misclassification.
Not inflating the severity.

Reverse verification

Prediction recorded before running: reverting only rest-server.ts's
predicate to the pre-fix conjunction (test file left at HEAD) turns the two
new decision-path tests (§5a x2, §5b) RED and leaves everything else
(including §5c/§5d, the real quoted-driver-phrasing cases, and the other
23 pre-existing tests in the file) GREEN — ordinary RED, not an inverted
direction.

Measured:

git checkout PARENT_COMMIT -- packages/rest/src/rest-server.ts # test file stays at HEAD
pnpm --filter @objectstack/rest build && vitest run src/rest-unknown-object-heuristic.test.ts
FAIL §5a decision 1 (the 500 gate): business prose using both words is no longer DATA_STORE_FAULT
FAIL §5a holds with an object present too, and does not spill into the 404 limb either
FAIL §5b decision 2 (the 404 limb): an unquoted "relation NAME does not exist" no longer silently 404s
Test Files 1 failed (1)
Tests 3 failed | 23 passed (26)

Restored (git checkout THIS_BRANCH -- packages/rest/src/rest-server.ts),
rebuilt, reran: 26/26 passing again. Matches the prediction exactly.

Tests

pnpm --filter '@objectstack/rest^...' build # dependency closure — success
pnpm --filter '@objectstack/rest' build # package build — success
pnpm --filter '@objectstack/rest' typecheck # tsc --noEmit — clean
pnpm --filter '@objectstack/rest' exec vitest run \
src/rest-unknown-object-heuristic.test.ts --maxWorkers=2 # 26/26 passed (5 new)
pnpm --filter '@objectstack/rest' exec vitest run --maxWorkers=2 # full package: 114 files / 1886 tests passed

Gates (derived from the changed paths via node scripts/pm/dispatch-gates.mjs):

pnpm check:nul-bytes OK
pnpm check:authz-resolver OK
pnpm check:changeset-gate-self-tests OK
pnpm check:cross-package-test-inputs OK
pnpm check:objectui-changeset OK
pnpm check:route-envelope OK (pre-existing rest-server.ts ratchet #7035 unaffected)
pnpm check:query-options-erasure OK (67 unswept non-test sites, none new; test surface unchanged)
pnpm check:type-check-debt OK — self-test held; --re-measure: 1969 raw tsc errors
total across 33 ledgered entries, none above the
recorded number (required a full workspace
`turbo run build` first, per the ledger's own
instruction — the bare `check:type-check-coverage`
is not sufficient on its own)

Scope

Diff is confined to packages/rest/src/rest-server.ts (the one hot region:
the module-scoped RELATION_DOES_NOT_EXIST constant + the
looksLikeMissingRelation definition, no wide reformatting) and its own test
file. .changeset/anchor-missing-relation-quoted-template.md added (patch,
@objectstack/rest). No content/docs/releases/ edits.


Generated by Claude Code

…plate (#8264)
`mapDataError`'s Postgres limb read `relation` and `does not exist` anywhere
in the message, not necessarily the same sentence, so ordinary business prose
using both words matched. Anchored on the quoted identifier Postgres always
emits, mirroring #8132's anchor for the shared `looksLikeInternalErrorLeak`
predicate in @objectstack/types (deliberately NOT reused here — it answers a
different question and its other limbs are unrelated to this file's
404-vs-500 attribution).
Both call sites of the predicate are covered: the DATA_STORE_FAULT (500) gate
the issue named, and the looksLikeUnknownObject (404) limb the issue's own
text did not measure. Tests pin both decision paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 6:19am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx(via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx(via @objectstack/rest)
  • content/docs/api/index.mdx(via @objectstack/rest)
  • content/docs/permissions/authentication.mdx(via @objectstack/rest)
  • content/docs/permissions/system-context.mdx(via packages/rest)
  • content/docs/plugins/index.mdx(via @objectstack/rest)
  • content/docs/plugins/packages.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/rest)
  • content/docs/releases/v12.mdx(via @objectstack/rest)
  • content/docs/releases/v17.mdx(via @objectstack/rest)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 08:31
@os-zhuang
os-zhuang merged commit e7bccaa into mainAug 14, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8264-missing-relation-anchored-predicate branch August 14, 2026 08:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest-server.ts's looksLikeMissingRelation matches unquoted prose, where the shared leak predicate now requires the driver's quoted template

2 participants

@os-zhuang@claude