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

Copy link
Copy Markdown
Contributor

Fixes#5717

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

立单说法现状结论
嗅探器 isMissingSourceErroranalytics-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: tHITHIT
sqlite 经 knex(SQL 前缀)HITHIT
postgres relation "t" does not existHITHIT
postgres schema 限定 relation "public.acct" …HITHIT
postgres 缺列(写路径)column "c" of relation "t" …HITHIT
postgres 缺列(读路径)column "c" does not exist
mysql Table 'app.t' doesn't existHITHIT
objectql Datasource '…' … is not registered.HITHIT
rest Object 'x' is not registeredHITHIT
analytics CUBE_NOT_FOUND(#3867)HITHIT
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 例):

回退结果红的是
只回退 B2 红 / 9 绿CUBE_NOT_FOUND、已声明 5xx
只回退 C1 红 / 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

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 3:35pm

Request Review

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

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 queueAug 6, 2026
Merged via the queue into main with commit 705efebAug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@hotlong@claude