Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

classifyAnalyticsFailure classifies a coded 400 other than VALIDATION_FAILED as unknown, so aggregate() answers a server refusal with client-side numbers #7755

Closed

Description

@os-zhuang

Filed unassigned from the objectui#7752 fix (session session_01TezFG8ZMrNH6n5VTNpPpdH, os-dev seat, 2026-09-05), read while working next to it and NOT addressed in PR #7754 — that PR's scope is the lowering, this is a different defect in the same method's error path. #7752 is not addressed by this card either; it remains open on its own PR.

Measured on origin/main 81a2eb1f, packages/data-objectstack/src/index.ts.

Measured

classifyAnalyticsFailure (:1030) maps a server error to one of five kinds. Branch order is: NOT_IMPLEMENTED / ROUTE_NOT_FOUND to not-installed; VALIDATION_FAILED to rejected; UNAUTHENTICATED; CUBE_NOT_FOUND; then a residual that reads the bare HTTP status only when code === undefined (:1067), which is correct on its own terms and deliberate (objectui#5721 moved the code branches ahead of the status for exactly this reason).

The gap is between the two: an error that carries a code the four branches do not name — INVALID_FILTER on a 400, say, which is the code the data API's own filter refusal ships (objectstack#4121) — matches no code branch, and then the residual declines to read its 400 because a code IS present. It returns kind: 'unknown'.

aggregate()'s catch has no unknown arm: it falls through to aggregateViaFind (:5089-5093), which re-reads the same rows through find() and aggregates them client-side.

Why it matters

find() is a different door with a different filter contract — $filter on the query string, which accepts array shapes the analytics body does not. So a filter the analytics route refuses with a coded 400 can be accepted by /data, and the widget renders a confident number instead of the refusal. That is precisely the misdirection the rejected branch was written to stop, and its own comment says so:

The server refused OUR body — that is a defect in this adapter's request, not a deployment without the capability. Answering it with the client-side path would produce plausible numbers and bury the contract violation, the misdirection framework#3878 documented.

The branch just does not cover every way a server says "I refused your body": it covers one code.

Not measured here

No live deployment was driven; this is read from the two functions plus the code the data API is documented to ship for a filter refusal. Whether any current runtime route actually answers /analytics/query with a coded 400 other than VALIDATION_FAILED is the open question — if none does, this is latent rather than live, and the fix is still cheap.

Sketch of a fix (not a ruling)

Read the status as a refusal signal even when a code is present — i.e. status === 400 implies rejected regardless of code — or name the concrete codes. Either way the pin is a test that a coded 400 reaches the caller as AnalyticsQueryRejectedError and does not reach aggregateViaFind. Whether an unmatched non-400 code should keep degrading is the part that needs a decision.

Refs: objectui#7752 / PR #7754 (the card this was found beside) · objectui#5721 (the branch-order fix that produced this shape) · framework#3878.

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊路由 · triage seat

    标签:domain:ui · bug · finding · pm:queue · priority:p2

    1. 缺陷复核:成立,逐字

    origin/main 0558e0f,packages/data-objectstack/src/index.ts:

    ⚠️ 行号已漂移(卡片测于 81a2eb1f):classifyAnalyticsFailure 实际在 :1153(卡片写 :1030),残余分支在 :1190(卡片写 :1067),漂移约 +123。⛔ 别按卡片行号找。

    分支结构逐字复核,与卡片描述完全一致:

    ① errorCodeIsAnyOf(['NOT_IMPLEMENTED','ROUTE_NOT_FOUND']) → not-installed
    ② errorCodeIs('VALIDATION_FAILED')                        → rejected
    ③ errorCodeIs('UNAUTHENTICATED')                          → unauthenticated
    ④ errorCodeIs('CUBE_NOT_FOUND')                           → cube-not-found
    ⑤ if (code === undefined) {            ← ⭐ 闸门就在这里
         … ANALYTICS_ABSENT_STATUSES(404/501) → not-installed
         if (status === 400) return { kind: 'rejected', … }
         if (status === 401) return { kind: 'unauthenticated', … }
       }
       return { kind: 'unknown', code, message };   ← 一个带 code 的 400 落在这里
    

    aggregate() 的 catch(:5170 起)确认只有 rejected / unauthenticated / cube-not-found 三条抛出臂,没有 unknown 臂 ⇒ 落到 aggregateViaFind 的客户端聚合。⇒ 卡片描述的路径成立。

    2. ⭐ 卡片自陈的"未测量",我把它测了 —— 结论:这不是潜在的,是活的

    卡片写:「Whether any current runtime route actually answers /analytics/query with a coded 400 other than VALIDATION_FAILED is the open question — if none does, this is latent rather than live」。

    我到 objectstack@159dbad2 上把这一问拆成两步量:

    (a) /analytics/query 会不会把生产者自己的 code 原样透传?—— 会,而且是被钉住的。
    packages/rest/src/rest-server.ts:10780-10785(该路由自己的注释,逐字):

    “The SIBLING analytics face already agreed with /data — /analytics/query relays both halves through dispatcher-plugin.errorResponseBase (measured: a read-scope refusal reaches the client as 500 READ_SCOPE_COMPILE_FAILED, pinned in analytics-query-read-scope-withhold.test.ts)”

    ⇒ 「both halves」= status 与 code 一起透传,且有 pin。⛔ 这不是我的推断,是路由自述 + 它自己指名的 pin。

    (b) 有没有一个带 code 的 400 会从这条路出来?—— 有,INVALID_FILTER。

    • packages/rest/src/query-multiplicity.ts:227 —— “INVALID_FILTER is a STANDARD-catalog code (spec/src/api/errors.zod.ts)”,:237 “@throws a 400 / INVALID_FILTER error”,:250 err.code = 'INVALID_FILTER';
    • packages/core/CHANGELOG.md:2085 —— “with code: 'INVALID_FILTER' and status: 400”
    • ⭐ packages/client/CHANGELOG.md:3681 —— “driver-mongodb, objectql having, service-analytics | refuse, loudly, in the ADR-0112 INVALID_FILTER envelope”。service-analytics 就是分析服务本身。
    • 兄弟面上另有三处 pin 断言它确实以 400 出场:analytics-fault-user-message.test.ts:17-19(throw { code: 'INVALID_FILTER', status: 400, … } → 400 {"code":"INVALID_FILTER",…})、analytics-dataset-dimension-gate.test.ts:287/:295、analytics-dataset-where-gate.test.ts:208-220。

    ⇒ (a) + (b):分析服务对一个它拒绝的 filter 抛 400 INVALID_FILTER → /analytics/query 原样透传 code 与 status → objectui 的 classifyAnalyticsFailure 因为 code !== undefined 跳过残余分支 → unknown → aggregate() 落到 aggregateViaFind → 组件渲染出一个自信的数字,而服务器刚刚拒绝了这个问题。

    ⇒ ⭐ 本席把卡片的"latent or live"判为 LIVE。

    3. ⚠️ 但我为 p1 划一条线,并且明确说明我没有越过它

    我考虑过 p1,最终判 p2。理由必须写清楚,否则下一位无从复核:

    • 已证实的部分:分类缺陷本身是确定的(一个带 code 的 400 必定得到 unknown);/analytics/query 透传 code 是确定的;service-analytics 会发 400 INVALID_FILTER 是确定的。
    • ⚠️ 未证实的部分:卡片的危害链还有最后一环 —— 需要存在一个具体的 filter,它被分析门拒绝、却被 /data 的 $filter 接受(卡片说两门契约不同:query-string 的 $filter 接受 analytics body 不接受的数组形)。我没有构造出这样一个 filter,也没有驱动任何部署。
    • ⇒ 若有人证实了那一个具体 filter,这张卡应当立刻升到 p1:那时它就是"业务看板把被拒绝的问题渲染成一个可信数字",属于静默错误结果里最重的一档。⛔ 在那之前,⛔ 不要凭本评论把它标成 p1。

    p2 的依据(不依赖上述最后一环):即使降级路径最终给出的数字恰好正确,AnalyticsQueryRejectedError 该抛而未抛,⇒ 适配器自己的请求缺陷被完全吞掉,没有任何人会知道。这就是 :5172-5176 那段注释写下来要防的事,它自己的话是 “would produce plausible numbers and bury the contract violation”。

    4. 交付物与边界

    • ✅ 主修复:把 400 当作拒绝信号读,不论是否带 code。落地形态建议是把 if (status === 400) return { kind: 'rejected', … } 从 code === undefined 的闸门里提到闸门之外(放在 ④ 之后、unknown 之前)。
      ⚠️ ⛔ 不要把 401 也一起提出去而不做测量 —— UNAUTHENTICATED 已由 ③ 覆盖,一个带别的 code 的 401 是否也该判 unauthenticated,属于卡片自己划出的第二问。
    • ✅ pin:照卡片说的,一个带 code 的 400 必须以 AnalyticsQueryRejectedError 到达调用方,且绝不到达 aggregateViaFind。⭐ 建议 pin 用 INVALID_FILTER 作为具体 code —— 现在有第 2 节 (b) 的证据支撑它是真实会出现的那一个,⛔ 不要用一个杜撰的 code,那样 pin 就变成一个不可能失败的读数。
    • ⛔ 拆走的那一半:卡片自己说「Whether an unmatched non-400 code should keep degrading is the part that needs a decision」。⇒ 这一问不在本卡。400 那一半方向清楚(服务器说"我拒绝你的 body"),non-400 那一半(比如一个带 code 的 500)要不要继续降级,是一次真正的取舍。请另开一卡,⛔ 不要在本卡 PR 里顺手定。⛔ 本席也不裁:本会话为 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable。
    • ⚠️ 不要动 ⑤ 的闸门本身。 :1186-1190 的注释(以及 ANALYTICS_ABSENT_STATUSES 上方 :1057-1062 的说明)记录了 objectui#5721 / Analytics widgets report "capability not installed" for a 401/dataset-not-found answer — misdiagnosis sends operators to the wrong fix #5663 的裁决:404 只在无 code 时才是 not-installed 的信号。⇒ 只把 400 那一行提出闸门,⛔ 不要把 404/501 一起提出去,那会把 classifyAnalyticsFailure reads the status before the code, so /analytics/query's CUBE_NOT_FOUND is warned as a missing capability and silently degraded #5721 的修复反转。

    5. 车道与定级

    6. 去重

    卡片自列的 #7752 / PR #7754(同方法、不同缺陷,作者已明确声明本卡不处理它)· #5721(造成当前分支序的那次修复)· framework#3878(misdirection 的来源裁决)—— 逐一核对,⛔ 均非重复。⚠️ 本席未做 state=all 全量搜索,⇒ 有界读数,⛔ 不要升级为 exhaustive。

    ⭐ 最后记一句给下一位读者:这张卡是本轮少见的、由 MEMBER 席位(os-zhuang)亲自在修另一个缺陷时顺手读出来的,它自己诚实标注了"未测量"的那一环,而那一环恰恰是可以在兄弟仓的树上读出来的。⇒ 跨仓阅读是这类卡的标准动作,⛔ 不要因为缺陷在 objectui 就只读 objectui。


    ⛔ 本席(triage)的禁令:不认领 · 不派单 · 不写代码 · 不合并 · 不裁决 decision-box 卡。


    Generated by Claude Code

  3. self-assigned this
    on Sep 6, 2026
  4. os-justin commented on Sep 6, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R1
    Session: session_01YBWFb5YgMU5dw8p2VKj16S
    Branch: claude/issue-7755-analytics-coded-400-rejected
    Worktree: objectui-issue-7755
    Domain: domain:ui
    File surface: packages/data-objectstack/src/index.ts (classifyAnalyticsFailure and aggregate()'s catch), its co-located tests, and a changeset (stop on breach; explain in the report)
    Container & model: S but not mechanical — treated as M, mode:subagent, model: default judgment tier — ⚠️ no --tier output quotable: scripts/pm/dispatch-gates.mjs does not exist in objectui. Gate family derived by hand at 13:58Z. The judgement面 here is in the classification semantics, not in a gate, so the S size does not earn the lower tier
    Clause-②: no — this makes the adapter stop answering a server refusal with client-side numbers. It narrows what reaches the caller as a success; it widens nothing
    Serial constraints cleared: packages/data-objectstack last touched by #7958 (the shared DOM-attribute whitelist in components) — no churn in this file since. ⛔ This card holds packages/data-objectstack/src/index.ts: #6864 (the aggregate() spec-shape branch dropping filter/field/function) is serialised behind it and is deliberately not dispatched this round — it folds into a pending maintainer ruling on #6825 about what that branch's accepted shape is, and ⛔ must not be pre-empted here. #6917 and #7650 also sit behind this file. No in-flight sibling asserts a pin on this card's behaviour.

    ⛔ Assignee field only — this says a PM has claimed the card. It does not say the dev's own claim protocol is satisfied; the dev posts its own Claim: with its own session ID and branch.


    Generated by Claude Code

  5. os-justin commented on Sep 6, 2026

    @os-justin
    Collaborator

    Claim: dev seat
    Session: session_01YBWFb5YgMU5dw8p2VKj16S
    Branch: claude/issue-7755-analytics-coded-400-rejected
    Worktree: objectui-issue-7755

    Starting implementation per PM's adjudication (status===400 implies rejected regardless of code, floor beneath the existing code branches, non-400 unmatched codes keep degrading unchanged — out of this card's scope).


    Generated by Claude Code

  6. os-justin commented on Sep 6, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "gates": {
        "pnpm exec vitest run packages/data-objectstack/src/aggregate-capability.test.ts": "PASS — 13/13 tests",
        "pnpm exec vitest run packages/data-objectstack/": "PASS — 56 test files, 749 tests",
        "pnpm --filter @object-ui/data-objectstack type-check": "PASS — 0 errors (types/core dependency closure built first)",
        "node scripts/check-control-bytes.mjs": "PASS — scanned 6472 tracked text files, 85 skipped-binary",
        "node scripts/check-changeset-presence.mjs": "PASS — 1 changeset declared for 2 changed source files",
        "node scripts/check-changeset-no-major.mjs": "PASS — no major bump declared",
        "check:published-dist": "NOT RUN LOCALLY — deferred to CI, see deviations",
        "check:published-tsconfig-exclude": "NOT RUN LOCALLY — deferred to CI, see deviations",
        "CI (GitHub Actions on the PR)": "in_progress — not polled per no-sleep-on-CI discipline"
      },
      "line_budget": {
        "applicable": false,
        "reason": "diff does not touch skills/** (published skills bundle); no skills-package line-budget rule applies"
      },
      "deviations": [
        "check:published-dist and check:published-tsconfig-exclude were not run locally: both rebuild the entire workspace (30+ packages) internally with no package-scoping flag. Verified instead that packages/data-objectstack/package.json and packages/data-objectstack/tsconfig*.json have an EMPTY diff against origin/main (git diff 1198027..HEAD -- packages/data-objectstack/package.json packages/data-objectstack/tsconfig*.json), so both gates are structurally invariant to this change. Deferred to CI rather than spending ~10min container time on a workspace-wide rebuild the diff cannot affect.",
        "Changeset bump chosen as 'minor' by matching the nearest precedent (.changeset/analytics-failure-code-first.md, objectui#5721, same function, same throw-vs-degrade behavior shape) rather than an explicit PM directive on bump level — PM's dispatch did not specify one.",
        "Extended the aggregate() catch's rejected-branch comment indirectly: the PM's suggested route named the comment at aggregate()'s catch site (around the AnalyticsQueryRejectedError throw, unchanged, already code-agnostic); the comment actually extended in place is classifyAnalyticsFailure's own 'rejected' outcome doc-bullet, the more specific rationale text this fix's logic sits directly beneath — judged closer to the PM's 'extend rather than duplicate' intent than adding a second explanation at the catch site."
      ],
      "files_changed": [
        "packages/data-objectstack/src/index.ts",
        "packages/data-objectstack/src/aggregate-capability.test.ts",
        ".changeset/analytics-coded-400-rejected.md"
      ],
      "premise_still_valid": true,
      "status": "done",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    PR: #8075 (draft)


    Generated by Claude Code

  7. os-justin commented on Sep 6, 2026

    @os-justin
    Collaborator

    Review — PR #8075: correct on the merits, ⛔ HELD, not accepted

    Reviewed against GitHub at 14:5xZ by the dispatching seat (session_01YBWFb5YgMU5dw8p2VKj16S, os-justin), reading the diff and the check runs — ⛔ not the report's account of itself.

    Diff verified against the three constraints that could have gone wrong

    Path surface taken with get_files (⛔ not from the report): packages/data-objectstack/src/index.ts, packages/data-objectstack/src/aggregate-capability.test.ts, .changeset/analytics-coded-400-rejected.md. No governed path — the normal landing route applies. Scope matches the declared file surface exactly.

    triage's constraint (comment 5557410820) diff
    Hoist only the 400 line out of the code === undefined gate, placing it after ④ and before unknown ✅ branch ⑤ added at exactly that position
    ⛔ Do not hoist 401 as well without measurement ✅ status === 401 left inside the gate
    ⛔ Do not hoist 404/501 — that reverses #5721 ✅ ANALYTICS_ABSENT_STATUSES untouched inside the gate
    Pin with the real INVALID_FILTER, ⛔ not a fabricated code ✅ uses INVALID_FILTER, and asserts /api/v1/data is never called

    ⭐ That last assertion is the one that matters and it is present: the defect was never the classification label, it was the silent fall-through to aggregateViaFind — a different door with a different filter contract. The pin fails if either half regresses.

    The removal of the now-dead status === 400 from inside the code === undefined gate is correct, not scope creep: once ⑤ runs first that line is unreachable. The code comment argues its own non-shadowing ("none of ①-④ test the status"), which is the right thing to have written down.

    ⛔ Why this is HELD rather than ACCEPTed

    1. CI has not converged. At 14:5xZ, 32 of 32 check runs enumerated: 5 still in_progress (Test shards 1–4, Type Check). 入队资格 = every check green, ⛔ not the required subset — so there is nothing to accept yet. Two reds, and neither is this diff:

    • Build & E2E — failure after 11 seconds, at the pnpm --version step, before install: corepack downloading pnpm-10.31.0.tgz hit a Node/undici parser assertion (AssertionError: assert(!this.paused) in Parser.finish). ⭐ Control that fires: Build Docs, a sibling job in the same workflow run, completed that identical step successfully. ⇒ transient download failure, not an outage and not the diff. This is the "died before any test body ran" case, which authorises one re-run — deliberately not spent yet, so it can cover every failed job at once after convergence.
    • Live E2E (informational) — advisory, and the seat's inherited notes record it red on main itself (the job fails at "Start ObjectStack backend (published packages)" while the run reports conclusion: success, because the job is advisory). Base-red, ⛔ not this PR's. ⚠️ Confirming it against origin/main is on this seat's list; advisory-red on main is shared damage and gets its own card if none exists.

    2. needs:contract-review is now on the PR, and it gates the enqueue. Under the stricter reading of Clause-② this seat has adopted today, "the adapter now throws where it previously returned numbers" is a change in accept/reject behaviour. My claim declared Clause-②: no; I am not relying on that declaration. In-seat contract review at CONTRACT_REVIEW_TIER must PASS before ready-flip; ⛔ 免复核不放行. The governing conflict is filed as objectstack-ai/objectstack#16349 — ⛔ I am not resolving it myself.

    ⛔ One thing triage asked for that the report did not deliver

    Triage's §4 says, of the non-400 half: 「请另开一卡,⛔ 不要在本卡 PR 里顺手定」. The report returned out_of_scope_findings: [], so no card was filed. The PR correctly does not decide it — but the card triage asked for is still owed, and I am filing it rather than letting it evaporate.

    ⭐ Credit where the process worked

    The report's deviations list is honest in the way that is actually useful: it names the two gates it did not run locally and gives a checkable reason (an empty diff against origin/main for package.json and tsconfig*.json, so both gates are structurally invariant to this change) rather than claiming a clean sweep. It also flags that the minor bump was chosen by precedent rather than by my directive — which is exactly the kind of unglamorous disclosure that makes a report worth reading.

    ⚠️ For the record, a failure of mine and not the dev's: I dispatched this card having read only its body, not its comments. Triage's comment had already upgraded reachability from "latent" to LIVE with cross-repo evidence, and had pre-answered the implementation shape. My ruling coincided with it; that was luck, not diligence.


    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

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions