Repository navigation
service-analytics 的 where 门:字段约束里 $ 算子与非 $ 键混写时,非 $ 兄弟键被静默丢掉(方向是加宽;与值无关,不是 #6386 的 undefined) #6444
Description
Activity
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, theconst opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$'))/if (opKeys.length > 0) { … return out; }arm. Verified onorigin/main(be87153): the early return is still there at :502-504, so the non-$siblings are still never visited.packages/services/**⇒domain:servicesper 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 touchd8e8d9c; 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)
- service-analytics 的
where门把undefined值的键整个丢掉 —— 单键 where 退化成「无过滤器」,方向是加宽(#6125 五面表漏记的第六、七种读法) #6386 — same function, explicitly a different defect (comparand-positionundefined, value-dependent). Its implementation is in flight as draft PR fix(analytics):where门的undefined比较数改为拒收,不再把整个键丢掉(#6386) #6445 (opened 19:43Z), which pins this card's measurement as a test without fixing it. Not a duplicate; the two are correctly separate. - [finding] filter-normalizer 的
$notallowNull 守卫在 #5298 第二批之后变成冗余的第二层(谓词等价,SQL 多一层括号) #6005 (finding) — same file,$notallowNull guard redundancy. Disjoint. - read-scope-sql 的
$null/$exists按真值性读比较数 ——{$null: "false"}编成IS NULL,#5347 / #5369 的先例没推到 RLS 编译器 #6387 / PR fix(analytics): read scope 的$null/$exists非布尔比较数改为拒收,不再按真值性编成相反的谓词 (#6387) #6443 — the other door in this package (read-scope-sql.ts). Disjoint by file. - objectui / cloud: no shadow (analytics filter compilation is backend-only).
Why this is a decision card and not
pm:queueThe 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'sz.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 memberamount.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 samez.recordhalf, 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.tscompileField,keys.some((k) => !k.startsWith('$'))); and #6386's queued fix uses the sameINVALID_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
- service-analytics 的
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsMaintainer 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-decisioncomes 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 memberamount.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'scompileField) 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:
- The refusal message names the offending non-
$key(s) and gives both legal rewrites: the operator spelling (gte→$gte) and the nested-relation form, so the message distinguishes the two intents it cannot. - Refusal test asserts
code+status(ADR-0112 envelope) — a baretoThrow()does not constitute a refusal test. - The pin PR service-analytics 的
where门把undefined值的键整个丢掉 —— 单键 where 退化成「无过滤器」,方向是加宽(#6125 五面表漏记的第六、七种读法) #6386 left in place (a non-$ SIBLING of an operator is still dropped — a different defect, not this one) flips to a positive refusal assertion in this change. - Scope stays this one door; [finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930's five-compiler convergence remains on hold, and per the compiler-face checklist clause ([skill] pm-dispatch: semantic-ruling template must carry a compiler-face checklist — process half of the #5930 ruling #6410) the PR states, per face, whether it was changed / already conformant / out of scope.
Generated by Claude Code
- The refusal message names the offending non-
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsCLAIM — services-lane PM session
session_01USNUyHEr7uaU6MoEWXitei, branchclaude/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 assertscode+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
assertDefinedComparandsidiom to match. Work bases on post-#6445origin/main; the card's quoted line numbers have drifted — sites located by content.
Generated by Claude Code
- added 3 commits that reference this issue
on Sep 28, 2026
出自 #6386 的实施(PR 见下)。#6386 的范围是比较数位置的
undefined;本单是同一个函数里与值无关的另一条静默丢弃路径,按 Prime Directive #10 另立单、不指派、不扩大那张 PR 的 diff。缺陷
packages/services/service-analytics/src/strategies/filter-normalizer.ts的fieldLeaves:一个字段约束对象同时含$算子键与非$键时,非$的兄弟键被静默丢弃。实测(
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未命中分支的注释里明令禁止的事——最可能的触发不是嵌套关系,是漏写
$的算子拼法,这恰好是 AI 编写 metadata 的典型手误:作者拿到的是一张比他写的宽的图,没有任何错误可读。而同一个包的另一扇门拒了这个形状:
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),免得被后来人误读成已覆盖——但没有修它。可能的处置(供分诊,未自行拍板)
read-scope-sql对齐,走本模块现成信封INVALID_FILTER/ 400。与{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240(零算子字段约束)的裁决同向,"一个横竖读不出作者意图的形状要响亮地拒"。$兄弟键按嵌套关系编成d.nested,与只有非$键时的行为一致。{amount: {gte: 10}}这类手误编成对不存在成员amount.gte的谓词,把一个可诊断的错误变成一个更难懂的错误。倾向 A,但这是分诊的判断:B 触及"混合拼法算不算合法授权面",属契约问题。
触达性
本单未证实有生产调用方写出混合形状。过滤器主要来自库存 metadata(dashboard
filter/ reportruntimeFilter/ datasetfilter),是 JSON,所以这个形状过得了 JSON(与 #6386 的undefined不同),可以躺在库存 metadata 里,也可以由 AI 直接写出来。如实记的是"未证实触达",不是"不会发生"。关联
where门把undefined值的键整个丢掉 —— 单键 where 退化成「无过滤器」,方向是加宽(#6125 五面表漏记的第六、七种读法) #6386(本单的出处;同一函数,比较数位置的undefined){ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240(零算子字段约束{}判"拒收"的先例)会话:
session_015a5qkLzpGXhLL2F5gvJ7dD(#6386 实施过程中发现,未指派)