Skip to content

service-analytics 的 where 门:字段约束里 $ 算子与非 $ 键混写时,非 $ 兄弟键被静默丢掉(方向是加宽;与值无关,不是 #6386 的 undefined) #6444

Closed

Description

@hotlong

出自 #6386 的实施(PR 见下)。#6386 的范围是比较数位置的 undefined;本单是同一个函数里与值无关的另一条静默丢弃路径,按 Prime Directive #10 另立单、不指派、不扩大那张 PR 的 diff。

缺陷

packages/services/service-analytics/src/strategies/filter-normalizer.ts 的 fieldLeaves:一个字段约束对象同时含 $ 算子键与非 $ 键时,非 $ 的兄弟键被静默丢弃。

const opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$'));
if (opKeys.length > 0) {
  for (const opKey of opKeys) { /* … */ }
  return out;               // ← 非 $ 兄弟键从没被看过
}
// 嵌套关系的展平在这行之后,opKeys 非空时走不到

实测(origin/main @ 5faa23ca3,直接调 normalizeAnalyticsFilterTree({ where }))

where 归一化结果 读法
{d: {$eq: 1, nested: 'x'}} d equals [1] nested 这一项静默消失
{d: {$eq: 1, nested: undefined}} d equals [1] 同上(与值无关)

对照:只有非 $ 键时({d: {nested: 'x'}})走的是嵌套关系展平,编成 d.nested——即这条路径本身是在的,只是被 opKeys.length > 0 的提前 return 挡住了。

为什么是缺陷而不是「可接受的读法」

方向是加宽,与 #6386 前两行同类:丢一个合取项不会收窄查询,只会让它返回作者排除掉的行。这正是同一个文件在 MONGO_TO_CUBE_OP 未命中分支的注释里明令禁止的事——

NEVER drop: a missing predicate does not narrow the query, it WIDENS it … That failure mode is #3650's

最可能的触发不是嵌套关系,是漏写 $ 的算子拼法,这恰好是 AI 编写 metadata 的典型手误:

{ amount: { gte: 10, $lte: 20 } }   // → 只剩 amount lte 20;下界静默没了

作者拿到的是一张比他写的宽的图,没有任何错误可读。而同一个包的另一扇门拒了这个形状:read-scope-sql.ts 的 compileField 判 keys.some((k) => !k.startsWith('$')) 就 fail-closed 拒收。一个形状,两扇门两种答案,且加宽的那扇是调用方直接写的那扇。

与 #6386 的关系

不同缺陷:#6386 是比较数位置的 undefined(值),本单与值无关(任何值都丢)。#6386 的 PR 已把这条测量钉在测试里(a non-$ SIBLING of an operator is still dropped — a different defect, not this one),免得被后来人误读成已覆盖——但没有修它。

可能的处置(供分诊,未自行拍板)

倾向 A,但这是分诊的判断:B 触及"混合拼法算不算合法授权面",属契约问题。

触达性

本单未证实有生产调用方写出混合形状。过滤器主要来自库存 metadata(dashboard filter / report runtimeFilter / dataset filter),是 JSON,所以这个形状过得了 JSON(与 #6386 的 undefined 不同),可以躺在库存 metadata 里,也可以由 AI 直接写出来。如实记的是"未证实触达",不是"不会发生"。

关联

会话:session_015a5qkLzpGXhLL2F5gvJ7dD(#6386 实施过程中发现,未指派)

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 7, 2026
  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Triage (triage seat, Routine trig_01XhwLupWiUBp7GUEigK1RFW, 19:47Z fire)

    Verdict: needs-user-decision · Domain: domain:services · ⛔ not queued, not claimed.

    Landing site (read, not guessed)

    packages/services/service-analytics/src/strategies/filter-normalizer.ts — fieldLeaves, the const opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$')) / if (opKeys.length > 0) { … return out; } arm. Verified on origin/main (be87153): the early return is still there at :502-504, so the non-$ siblings are still never visited. packages/services/** ⇒ domain:services per the SKILL domain table — same lane as the sibling #6386, and the same file the fix must land in.

    Stale-premise check

    git log origin/main -- …/filter-normalizer.ts ⇒ last touch d8e8d9c; nothing has landed on this file since the body was written, and the quoted code reads byte-identical on today's tip. Premise holds.

    Dedup (three repos, issues + PRs)

    Why this is a decision card and not pm:queue

    The defect half is not in doubt — dropping a conjunct widens, in the same function whose own comment forbids exactly that (NEVER drop: a missing predicate does not narrow the query, it WIDENS it … That failure mode is #3650's). What is in doubt is the accept face, and the two candidate remedies do not agree on it:

    • A (reject) narrows a shape that FilterConditionSchema's z.record(z.string(), z.unknown()) half declares legal today;
    • B (flatten) widens the authoring surface — it makes “operator keys and nested-relation keys mixed in one field wrapper” a supported way to author a filter, and compiles { amount: { gte: 10 } } (a missing-$ typo) into a predicate on a non-existent member amount.gte.

    So the question is not “which error envelope” but “is a mixed $/non-$ field wrapper a legal authoring shape?” — a public-contract call. This is the same class the maintainer took personally on #5240 (zero-operator field constraint: declared-legal by the same z.record half, ruled loud rejection on 2026-08-04), so escalating rather than letting a lane pick is the consistent handling. The author did not self-decide either.

    PM recommendation (non-binding, for the inbox)

    A, on three converging precedents: the #5240 ruling (“a shape that cannot be read must explode at authoring time”); the sibling door in this very package already fail-closes on this exact predicate (read-scope-sql.ts compileField, keys.some((k) => !k.startsWith('$'))); and #6386's queued fix uses the same INVALID_FILTER / 400 envelope, which is caller-input-facing and already established here (#5352 / #6050). B's cost is concrete: it converts a diagnosable typo into a silently compiled predicate on a member that does not exist.

    Ordering

    No Blocked-by: line, deliberately. #6444 and #6386/PR #6445 sit in the same seat (domain:services), touching the same function — the services lane sees its own in-flight batch, so serialisation is that lane's step-3 business, not a cross-seat dependency the unlock sweep needs to grep. (Contrast #6438 last round, whose sibling was in another seat.)

    Release board

    No target:v17. Reachability is honestly recorded as unproven, the direction is not yet decided, and the sibling defect #6386 — strictly more reachable — is not on the board either. If the decision lands as A and a producer is found, that judgment can be revisited at the next freshness pass.


    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling — 2026-08-08. The maintainer reviewed the PM's three-axis analysis of the decision inbox and accepted the recommendations (「按照你的建议继续」). Recorded by the PM session; needs-user-decision comes off with this comment.

    Decision: Option A — refuse. A field constraint mixing $ operators with non-$ sibling keys is rejected loudly through the module's existing envelope (INVALID_FILTER / 400). Option B (flattening the siblings as a nested path) is rejected: it would compile the likely-real cause — a dropped $ — into a predicate against a non-existent member amount.gte, turning a diagnosable mistake into a harder one.

    Rationale: dropping a conjunct widens the query, returning rows the author excluded — the #3650 family, and the same file's own comment forbids it in as many words ("NEVER drop: a missing predicate does not narrow the query, it WIDENS it"). The standing meta-criterion applies directly: one shape, two doors, and the sibling door (read-scope-sql's compileField) already fails closed on exactly this input — converge on the governed side. And per the AI-safety axis, { amount: { gte: 10, $lte: 20 } } is a canonical agent typo; silence there is a wrong answer no one can learn from.

    Requirements:


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 8, 2026
  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    CLAIM — services-lane PM session session_01USNUyHEr7uaU6MoEWXitei, branch claude/issue-6444-mixed-operator-refusal.

    Dispatching under the 2026-08-08 02:12Z maintainer ruling (Option A — loud refusal via INVALID_FILTER / 400), all four ruling requirements binding: refusal message names the offending non-$ key(s) and shows both legal rewrites; refusal test asserts code + status; #6386's sibling-drop pin flips to a positive refusal assertion; scope stays this one door with the per-face #6410 statement.

    Serialization check done before claiming: same-function sibling #6386's implementation (PR #6445) merged 2026-08-07 20:19Z, so the base already carries the pin to flip and the assertDefinedComparands idiom to match. Work bases on post-#6445 origin/main; the card's quoted line numbers have drifted — sites located by content.


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions