Skip to content

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

Closed

Description

@os-zhuang

Observation, found while implementing #8132 (PR #8263). Not fixed there: #8132's ruling scopes it to the shared predicate in packages/types, and this line sits in packages/rest/src/rest-server.ts, where #8039 lands next round. Filed unassigned so the two do not collide.

What was measured

#8132 taught the shared looksLikeInternalErrorLeak the Postgres missing-relation phrasing, anchored on the template Postgres actually emits — a QUOTED identifier:

/\b(?:relation|column)\s+["'`][^"'`]+["'`]\s+does not exist/i

The anchor is load-bearing: without it, does not exist is ordinary business English and the predicate would start replacing legitimate answers with Internal server error. Two negative cases in packages/types/src/error-leak.test.ts pin that.

rest-server.ts:1337 answers a closely related question with the loose form instead:

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

Those two conjuncts need not be the same sentence, so a message merely containing both words matches. This relation does not exist in the diagram is a concrete example — it is one of the negative cases the new predicate deliberately rejects, and the same string is a positive here.

Why it may matter

looksLikeMissingRelation gates a 404-vs-500 decision. A match with missingRelationIsObject(raw, object) false returns DATA_STORE_FAULT() — a sanitised 500 DATABASE_ERROR. So a business message that happens to use both words could be re-labelled a data-store fault rather than getting its real verdict.

Deliberately NOT asserted

I did not measure a reachable production path that produces such a message at this call site, and the branch's own comment (#5462) explains that the loose reading is a legacy safety net whose primary producer is the registry gate far above. So this is drift between two spellings of one question, not a demonstrated live bug — the severity call belongs to triage, not to me.

Whoever picks this up should decide whether the two questions should share the anchored helper at all, or whether the exclusion-direction width is correct here on purpose (relation-sub-object.ts documents exactly that "two widths, on purpose" reasoning for a neighbouring pair).

Backlink: #8132, PR #8263.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  3. added theissue type on Aug 13, 2026
  4. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Concentrated findings round (maintainer-directed, 2026-08-13): promoted finding → pm:queue (domain:cli unchanged — packages/rest). Type: Task (no demonstrated live mismatch; this is drift between two spellings of one question, filed with the negative case that proves the divergence).

    Grading rationale: scoped invariant-restoration with a named landing point (rest-server.ts:1337) and a decided-adjacent precedent (the anchored template the shared predicate now pins). The card's one open question is in the delegated lane: the taker decides "share the anchored helper" vs "two widths on purpose" by reading the relation-sub-object.ts precedent and the branch's own #5462 safety-net rationale — either answer is acceptable if pinned by a test and stated in the PR.

    ⚠️ Serial constraint for the cli seat: rest-server.ts is also #8039's landing file (queued) — same-file hard-serial; order them in one lane plan rather than two claims.

    Size/model suggestion: S, mode:subagent, sonnet (opus if the taker elects the helper-sharing redesign).


    Generated by Claude Code

  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    排队记录:本卡在 rest-server.ts 队列中排第一顺位,持有者是 #8039(domain:cli 座位 #6024,session session_01P7vaLs7bhBPi9m3JyzkhDj,round 2)。保持 pm:queue,未认领 —— 这是排期记录,不是认领。

    分诊要求「order them in one lane plan rather than two claims」,照办。顺序与理由:

    #8039 先 —— 它是 Bug(本卡是 Task),已有维护者裁决在手,且经上一轮重定框后很可能只剩注释,是两张里最快能把文件让出来的。#8264 紧随其后,#8039 的 PR 落地即派。

    ⚠️ 两张卡落在同一文件的不同区域(本卡 looksLikeMissingRelation,#8039 在 DATA_RECORD_READ_PARAMS 的 docblock),但本车道对同文件不同区域一律硬串行,且维护者本轮已明确不豁免同文件并行。所以不是同批。

    派工时会重新测量,现在不预设

    packages/rest/src/rest-server.ts 本轮已被改动两次并落地,最近一次是 PR #8487(#8373 导出时区,+25/-4)。卡片正文的 :1337 已经不可信 —— 派工时按符号定位,并在合并后的 ref 上复验 looksLikeMissingRelation 的当前写法是否仍是卡片记录的那三段 includes。前提可能已经被别的改动动过,不从本评论继承。

    本卡待决的那个问题保持开放

    卡片作者诚实地没有断言存在可达的生产路径,并把严重性判断交给分诊。分诊又把设计问题交给接手人:两个问题该不该共用那个带锚点的 helper,还是「两种宽度,有意为之」在这里是对的(relation-sub-object.ts 为邻近的一对记录了正是这个理由,分支自身的 #5462 注释则说明松读法是遗留安全网)。两个答案都可接受,条件是有测试钉住并在 PR 里说明理由 —— 不接受的是不做选择。


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 13, 2026
  7. self-assigned this
    on Aug 13, 2026
  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    串行解除,认领并派工(domain:cli 座位 #6024,session session_01P7vaLs7bhBPi9m3JyzkhDj)。

    • 分支:claude/issue-8264-missing-relation-anchored-predicate
    • 模型:sonnet(若接手人选择共用 helper 的重构则 opus)

    #8039 已落地(PR #8510 merged),rest-server.ts 队列交接给本卡。

    前提已在合并后的 ref 上复验 —— 成立

    looksLikeMissingRelation 现位于 origin/main:packages/rest/src/rest-server.ts:1054(卡片记的 :1337 已作废),写法与卡片记录的逐字一致:

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

    ⚠️ 复验过程本身有一条教训值得记:我第一次用 GitHub code search 查这个符号,返回 0 结果。但同一风格的正对照(DATA_RECORD_READ_PARAMS,已知存在于该文件)也只在测试文件里命中、在 rest-server.ts 本体里没命中 —— 说明索引根本不覆盖这个文件(体积超限),那个 0 结果不含任何信息。改用 git grep origin/main 才拿到真实答案。负结果没有同批正对照就是不可解读的,这条在本车道反复出现,这次差点让我把一张前提成立的卡报成前提失效。

    ⚠️ 卡片没说透的一点:这个谓词喂两个决策,不是一个

    卡片只讲了 DATA_STORE_FAULT() 那条路径。实际读下来它被用了两次:

    if (looksLikeMissingRelation && !missingRelationIsObject(raw, object)) {
        return DATA_STORE_FAULT();                    // ← 决策一:清洗过的 500
    }
    
    const looksLikeUnknownObject =
        looksLikeMissingRelation ||                   // ← 决策二:404
        lower.includes('unknown object') || ...

    收紧这个谓词会同时改变两条路径。 以卡片给的具体反例 This relation does not exist in the diagram 为例:收紧后它既不再被判成 data-store fault(这是卡片想要的),也不再经由这一支贡献给 looksLikeUnknownObject(卡片没提)。第二条后果必须被测量并在 PR 里说明,不能只测第一条就宣布收工。

    待接手人裁决的设计问题(卡片与分诊都留给你)

    两个问题该不该共用那个带锚点的 helper,还是「两种宽度,有意为之」在这里是对的。三份材料都读过再决定:

    1. packages/types 里 looksLikeInternalErrorLeak does not recognise Postgres relation "…" does not exist — measured false, so that dialect's line ships from every boundary that runs the predicate #8132 教给 looksLikeInternalErrorLeak 的锚定模板 —— 锚在带引号的标识符上,packages/types/src/error-leak.test.ts 有两条否定用例钉住它;
    2. relation-sub-object.ts 为邻近的一对记录的「两种宽度,有意为之」论证;
    3. 本分支自身的 sys_metadata 不可用被 mapDataError 的 unknown-object 启发式误报成 404 OBJECT_NOT_FOUND,且 404 属「预期状态」因而一行日志都不留 #5462 注释 —— 就在这段代码上方,明说松读法是遗留安全网,而这个 404 的主要生产者是 曝露 gate 对未知对象放行所依赖的「data path 会 404」并不成立 —— findData 无存在性校验(#3545 同类前提) #3770 的 registry gate(抛 code: 'OBJECT_NOT_FOUND',在远上方就被匹配)。

    ⚠️ 第 3 条对严重性判断很重要:卡片作者诚实地没有断言存在可达的生产路径。如果你测下来也没有,那么这是一致性修复而非活体缺陷修复,PR 要如实这么说,不要为了让卡片显得更重要而声称修掉了一个可达的错误分类。

    两个答案都可接受,条件是有测试钉住并在 PR 里说明理由。不接受的是不做选择。


    Generated by Claude Code

  9. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    ContributorAuthor

    上一次派工死于容器重启,已重派(同一座位、同一 session session_01P7vaLs7bhBPi9m3JyzkhDj,所以上面那条认领依然有效,不是别人占着)。

    测量,而不是猜

    • claude/issue-8264-* 在 origin 上不存在;
    • 正对照:claude/issue-8422-stdio-shared-not-found-envelope 存在(f051980d0)—— 证明 git ls-remote 这次查询本身是有效的;
    • 本卡仍 open、未合并,所以「分支不存在」在这里没有歧义。

    ⚠️ 这一条要特意写出来:分支缺失通常是不可解读的信号 —— 已合并的分支会被删除,「已交付」和「从未开始」长得一模一样,本车道在 #7314 上正是这么误判过一次(把一张已交付的卡报成死派工)。这次之所以能下结论,是因为卡片仍开着且没有任何 PR 引用它,把「已交付」这个解释排除掉了。不是因为 ls-remote 返回空。

    半状态已修复

    上一轮留下的是:已 assign、已贴认领评论、标了 pm:dispatched,但没有分支、没有 PR、没有存活的 worker。标签说「在做」,实际没有任何人在做 —— 这正是本车道禁止的过夜半状态。现已重新派工,标签与现实重新一致。

    重派 brief 的两处加强

    1. 要求尽早推送。 上一次是在推送前死掉的,十小时的工作没有留下任何痕迹。现在要求改动和测试一旦成形就先提交推送,再在分支上继续打磨。
    2. 带上了本车道今天挣来的两条测量教训:负结果必须同批带正对照(就是复验本卡符号时差点栽的那次);以及 check:type-check-coverage ≠ check:type-check-debt(The stdio MCP bridge mints its own not-found error — no RECORD_NOT_FOUND code, no 404 — where every sibling seam throws the shared recordNotFoundError #8422 今天正是红在这里)。

    前提与设计问题不变,见上一条评论。


    Generated by Claude Code

  10. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 8264,
      "status": "done",
      "branch": "claude/issue-8264-missing-relation-anchored-predicate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8644",
      "premise_still_valid": true,
      "summary": "Anchored rest-server.ts's looksLikeMissingRelation Postgres limb on the driver's quoted relation-name template (mirroring #8132's anchor for the shared @objectstack/types leak predicate), replacing the loose two-includes() conjunction. Chose ONE shared predicate for both of the file's readers (the DATA_STORE_FAULT 500 gate and the looksLikeUnknownObject 404 limb) rather than relation-sub-object.ts's 'two widths, on purpose' pattern: that precedent is for two genuinely different questions (extract vs exclude); this file's two call sites ask the identical question and missingRelationIsObject already gates attribution safety, so a shared width is correct. Did NOT call into looksLikeInternalErrorLeak directly -- it answers a disclosure question, not this file's 404-vs-500 attribution question, and its other limbs (sqlite_, unique constraint, foreign key) are unrelated.",
      "tests": "pnpm --filter '@objectstack/rest^...' build (deps closure, success); pnpm --filter '@objectstack/rest' build (success); pnpm --filter '@objectstack/rest' typecheck (clean); pnpm --filter '@objectstack/rest' exec vitest run src/rest-unknown-object-heuristic.test.ts --maxWorkers=2 -> 26/26 passed (5 new, covering BOTH decision paths + all 3 Postgres quote styles); full package suite: 114 files / 1886 tests passed. Reverse verification (direction predicted before running): reverted only rest-server.ts's predicate to the pre-fix conjunction, test file at HEAD -> 3 new tests went RED exactly as predicted (the 2 decision-1/§5a cases: DATABASE_ERROR->still DATABASE_ERROR under old code; the 1 decision-2/§5b case: silent 404 OBJECT_NOT_FOUND under old code), all 23 other tests (incl. real quoted-driver-phrasing §5c/§5d) stayed GREEN; restored, rebuilt, reran -> 26/26 green again. Gates: check:nul-bytes, check:authz-resolver, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:route-envelope, check:query-options-erasure all OK. check:type-check-debt (the strict self-test+--re-measure gate CI actually runs) required a full `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'` first (70/70 tasks, ~8min) -- ran it, then check:type-check-debt passed clean: 1969 raw tsc errors across 33 ledgered entries, none above the recorded baseline.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  11. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    ContributorAuthor

    PR #8644 已开,评级 ACCEPT —— 但按维护者的发版冻结,停在 draft 不合并。

    ⛔ 冻结期间本卡不结单:PR 未合并,卡片保持 open + pm:dispatched。解冻后统一排队合并,届时再复验落地并结单。

    它测出了一件我在派工时也没想到的事

    我在 brief 里要求「两条决策路径都要测」,因为卡片只讨论了 500 那条。dev 照做了,然后发现:卡片给的那个反例根本到不了决策二。

    missingRelationIsObject 的抽取正则在 'This relation does not exist in the diagram' 上失败(relation 与 does not exist 之间没有合法标识符),所以这个字符串无论改前改后都总是走决策一。拿它去「测」404 那条路径,得到的会是一个永远不可能失败的检查 —— 而且看起来是绿的。

    dev 因此另造了一个能真正抵达决策二的用例('Sorry, relation acct does not exist in our records',不带引号,因为 missingRelationIsObject 自己的 Postgres 分支容忍无引号标识符 —— 那是个更窄的、不同的问题:「名字是谁」,而不是「这是不是 driver 的真实模板」)。测出来的差异是实质性的:

    改前:{ status: 404, code: 'OBJECT_NOT_FOUND' }   ← 静默,因为 404 属于 isExpectedDataStatus
    改后:{ status: 500, code: 'INTERNAL_ERROR' }     ← 响亮、记日志、已清洗
    

    这是卡片从未提及的一处行为变化,而且方向正确 —— 与本分支 #5462 注释自陈的哲学一致:歧义时安全的错法是响亮,绝不是静默 404。

    设计问题:选了「一个谓词」,论证成立

    dev 没有把它接到 looksLikeInternalErrorLeak 上,理由是那个谓词回答的是另一个问题(「这条消息可不可以对客户端扣留」),其余分支(sqlite_、唯一约束、外键、裸 SQL)与本文件的问题无关 —— 并引了该模块自己的文档,它已经就 relation-sub-object.ts 写过「相关但不可复用」。

    同时它论证了 relation-sub-object.ts 的「两种宽度,有意为之」不适用于本文件内的两个读者:那个先例是两个不同的问题恰好共享子串、且两处漏判的代价相反;而这里 500 门和 404 支问的是同一个问题,没有理由让它们不一致。所以:一个 module-scoped 谓词,两个读者共用。

    no such table / table not found 未动 —— 它们是精确相邻短语而非两词合取,本来就没暴露在这个洞里。这个区分是对的。

    严重性如实

    This is consistency/invariant restoration … not a fix for a demonstrated live misclassification. Not inflating the severity.

    找过可达生产路径、没找到,并说明了原因(#3770 的 registry gate 抛 OBJECT_NOT_FOUND,在远上方就被匹配)。没有为了让卡片显得更重要而拔高。

    其余

    • 反向验证:先预测 3 红 23 绿,实测恰好 3 红 23 绿,且红的是两条决策路径的新用例、绿的包含 §5c/§5d 真实带引号驱动输出 —— 证明没有为收紧而误伤真实驱动消息。
    • check:type-check-debt 在完整 workspace 构建后跑的,并明确记了一句「the bare check:type-check-coverage is not sufficient on its own」—— 今天 The stdio MCP bridge mints its own not-found error — no RECORD_NOT_FOUND code, no 404 — where every sibling seam throws the shared recordNotFoundError #8422 正是栽在这里,教训传导到位。
    • missingRelationIsObject 的抽取正则有意不动,并说明了理由:它只在 looksLikeMissingRelation 已判真后才被调用,改后能抵达它的输入都已带引号短语,那份「引号可选」的宽松只作用在不再抵达的输入上。这个「不改」是经过论证的,不是遗漏。

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions