Skip to content

fix(service-analytics): dataset 降级路径先读 ADR-0112 信封,再读措辞 (#5717) - #6045

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-5717-no-degrade-enveloped-errors
Aug 6, 2026
Merged

fix(service-analytics): dataset 降级路径先读 ADR-0112 信封,再读措辞 (#5717)#6045
hotlong merged 1 commit into
mainfrom
claude/issue-5717-no-degrade-enveloped-errors

Conversation

@hotlong

@hotlong hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5717

前提复核(立单行号已过期,以 origin/main b5bdf48 现状为准)

立单说法 现状 结论
嗅探器 isMissingSourceError analytics-service.ts:92
降级 catch :795(isMissingSourceError 判据在 :796)
compiler 命中措辞 dataset-compiler.ts:281-284,且 #5963 后自带 DATASET_INVALID/400 在,且已带信封
抛点在 try 之外 编译在 :722(registerDataset),try 在 :793 才开始 成立 —— 雷仍未接电

前提全部成立,premise_still_valid: true

改了什么(范围 B + C,均落在 analytics-service.ts 一个文件)

B(主判据) —— catch 里新增优先分支:带 ADR-0112 信封的错误原样上抛,先于任何读措辞的逻辑。

信封判据抄仓内既有惯例、不发明第二种:rest-server.ts/analytics/dataset/query catch 用的就是 typeof status === 'number' + 非空字符串 code(envelopeStatus/envelopeCode)。

状态码区间刻意不入判据,这一点与派发令里的示例(status 小于 500)不同,理由写在代码注释里:400 那一支是本单的明面(一条 DATASET_INVALID/400 变成空网格),但已声明的 5xx(read-scope-sql.tsREAD_SCOPE_COMPILE_FAILED/500 —— fail-closed 的 RLS 下推)被吞掉只会更糟:一个服务端故障被画成一张自信的空图,没有任何人被告知。两者的共同事实是「生产者已经回答了分类问题」,措辞启发式没有资格重新打开它。issue 正文 B 的原话也正是「降级路径只对不带 ADR-0112 信封的错误生效」,4xx 是其动机实例而非判据本身。

C(可选支,做了) —— postgres 那一支从 includes('relation') && includes('does not exist') 收紧为锚定 postgres 真实措辞的 /relation\s+["']?[A-Za-z0-9_$.]+["']?\s+does not exist/i,与同文件里已经存在的兄弟函数 missingSourceRelation 用的是同一条模式(metadata/src/utils/schema-sync-errors.ts 也是这个形状)。收紧后「是不是缺了什么」与「缺的是什么」这两个函数不会再在这一支上互相矛盾。

C 的量化(实测,13 条仓内真实措辞)

措辞 收紧前 收紧后
sqlite/libsql no such table: t HIT HIT
sqlite 经 knex(SQL 前缀) HIT HIT
postgres relation "t" does not exist HIT HIT
postgres schema 限定 relation "public.acct" … HIT HIT
postgres 缺列(写路径)column "c" of relation "t" … HIT HIT
postgres 缺列(读路径)column "c" does not exist
mysql Table 'app.t' doesn't exist HIT HIT
objectql Datasource '…' … is not registered. HIT HIT
rest Object 'x' is not registered HIT HIT
analytics CUBE_NOT_FOUND(#3867) HIT HIT
dataset-compiler 关系拒收 HIT
read-scope nested/relation value …
analytics measure 闸门(#4437)

13 条里只有 1 条改判,就是那条雷;没有任何真实驱动措辞改判 —— 这正是「收紧而非改变 #5033 行为」的证据。

为什么不把编译点移进 try(实测后的判断)

派发令把这一步留给实测。结论:不移,并说明未覆盖面。

移进去会让 registerDataset 路径上的错误新落入降级面:dataset-compiler.ts:135 的内部不变量(non-derived measure … has no aggregate)、以及宿主提供的 relationshipResolver / getObjectDatasource / isExternalObject 三个回调抛出的任何东西 —— 其中宿主回调抛 Object 'x' is not registered 一类会被吞成空网格。那是把宽容度朝与本单相反的方向扩大(「Absence must be loud」/「prefer failing to falling back」),而本单要的是收窄。

雷本身不需要靠移编译点来拆:测试里直接把真实 producer 造出来的那个错误对象从 try 内部抛出(见下),这与「编译点被移进来」在 catch 看来是同一件事,却不引入上面那片新面。

测试

新增 packages/services/service-analytics/src/__tests__/dataset-degradation-envelope.test.ts(11 例),两块:

  1. 带信封不降级 —— 三例都用真实 producer 捕获的错误对象,不是手抄字符串:
    • CUBE_NOT_FOUND/404(assertInferableCube,消息里含「is not a registered object」,收紧后仍然命中嗅探器 —— 这一例专门隔离 B,任何措辞收紧都救不了它);
    • 已声明 5xx(READ_SCOPE_COMPILE_FAILED 形状,措辞为合成,注释里写明为什么必须合成);
    • compiler 那条拒收本体(compileDataset 真跑出来的错误,含 DATASET_INVALID/400),从 try 内部抛出 —— 雷被正式拆除的直接断言。外加同一措辞剥掉信封的裸 Error 一例:那一例只有 C 能救。
  2. service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033 语义原样保留 —— no such table、postgres 真实措辞、mysql 措辞照旧降级为 {rows: [], fields: [], totals: []} + warn;裸的「join 表缺失」照旧响亮报跨库拓扑错误;裸的语法错误照旧上抛。

反向验证(方向在跑之前就写死了,并且是分裂的)

预测:两条防线互相独立,单独回退任何一条,另一条仍然接住 compiler 那一例 —— 所以「回退就全红」在这里是错误期待。实测(逐个回退,11 例):

回退 结果 红的是
只回退 B 2 红 / 9 绿 CUBE_NOT_FOUND、已声明 5xx
只回退 C 1 红 / 10 绿 裸措辞的 compiler 拒收
两条都回退 4 红 / 7 绿 上面三例 + 带信封的 compiler 拒收(雷本身,唯一需要两条防线同时消失才会引爆的一例)

最后一行我第一版预测写的是 3 红(两个单退集合的并集),实测 4 —— 差的那一例正是雷本身。测试文件头把这个更正留在原处而不是悄悄改掉,因为那第四行才是这一单的发现。四种状态下 #5033 的五条用例全绿,这就是「刻意宽容未被触动」的证据形态。

验证记录(全部前台阻塞执行,持容器级 flock 锁,--max-old-space-size=4096)

  • pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=261 files / 1127 tests passed
  • packages/rest 消费半径(路由侧读信封的 7 个用例文件:analytics-dataset-refusal-envelope / analytics-dataset-unlisted-refusal-envelope / analytics-dataset-dimension-gate / analytics-dataset-where-gate / analytics-filter-refusal-envelope / analytics-read-scope-refusal-envelope / analytics-routes)→ 7 files / 84 tests passed
  • lint.yml 逐个枚举后跑过的门:pnpm lint(全仓 ESLint)、check:nul-bytescheck:doc-authoringcheck:adr-anchorscheck:route-envelopecheck:error-code-casingcheck:wildcard-fallthroughcheck:durability-log-levelcheck:startup-registry-verdictcheck:engine-double-contractcheck:slot-lookupcheck:query-options-erasurecheck:release-notescheck:type-check-coverage —— 全 PASS(该包在 DEBT 账本里冻结 3 个错误,新测试文件没有改变计数)
  • 控制字节自扫:grep -naP 扫三个改动文件的控制字节区间 → 干净

未触:packages/spec、drivers、buildQuery 粒度判据(#6003 面)、各拒收 throw 本体(#5963 面)。

必答项:对 #6007(compareTo 合并键错位)的影响

完全无影响,与预期一致。#6007 落在 dataset-executor.ts 的 compareTo 行合并逻辑(合并键构造),本 PR 只改 analytics-service.tsqueryDataset catch 的分类顺序与嗅探器的一条正则;两者不共享函数、不共享数据流。唯一的接触面是「若 compareTo 路径抛 datasetInvalidError,它现在保证不被降级」—— 那是本 PR 给 #6007 所在文件的保障,不改变 #6007 要修的合并行为,既不使其变简单也不使其变难、更不使其变得不必要。

出界发现


Generated by Claude Code

…12 envelope before the message (#5717)

`queryDataset`'s catch degrades to an empty result for the #5033 case (a widget
whose backing object is not mounted in this kernel). Its criterion was
`isMissingSourceError` — a substring match over the error MESSAGE — so the
leniency was available to any error phrased like a driver, and the outcome was
not a wrong status code but a silent empty result: no exception, no 4xx, no 5xx,
one warn and a confident empty chart.

`dataset-compiler.ts`'s "…includes relationship "R" which does not exist on
object "O"." already matched (both `relation`, inside "relationship", and
`does not exist`). It has never gone off only because the compile point sits
outside the try — a mine, wired and unarmed.

Two independent defences:

- an error carrying an ADR-0112 envelope (numeric `status` + non-empty `code`,
  the same structural fact `rest-server.ts`'s analytics catch reads) is
  re-thrown untouched, ahead of any message inspection. Status range is
  deliberately not part of the test: a declared 5xx is if anything worse to
  swallow than a 400;
- the postgres limb is anchored to postgres's actual wording
  (`relation "x" does not exist`) instead of "any sentence with both words" —
  the pattern the sibling `missingSourceRelation` already used.

Measured over the 13 real wordings this repo carries, exactly one verdict moves
and it is that compiler refusal; no driver wording changes, so #5033's leniency
for bare driver errors is untouched (asserted in all four reverse-verification
states, not merely claimed). The compile point deliberately stays outside the
try — moving it in would newly expose the compiler's bare invariants and the
host-supplied relationship resolver to this degradation path.

Residue filed as #6035 (postgres's write-path missing-COLUMN wording carries a
whole missing-relation phrase inside it; dormant on a read-only face).

Fixes #5717

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:35pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics.

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

  • content/docs/api/data-api.mdx (via @objectstack/service-analytics)
  • content/docs/api/index.mdx (via @objectstack/service-analytics)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/service-analytics)
  • content/docs/plugins/packages.mdx (via @objectstack/service-analytics)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v17.mdx (via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx (via @objectstack/service-analytics)

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.

@hotlong
hotlong marked this pull request as ready for review August 6, 2026 17:49
@hotlong
hotlong added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 705efeb Aug 6, 2026
56 of 70 checks passed
@hotlong
hotlong deleted the claude/issue-5717-no-degrade-enveloped-errors branch August 6, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants