Repository navigation
Description
出自 #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 拒收,与
read-scope-sql对齐,走本模块现成信封INVALID_FILTER/ 400。与{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240(零算子字段约束)的裁决同向,"一个横竖读不出作者意图的形状要响亮地拒"。 - B 展平,把非
$兄弟键按嵌套关系编成d.nested,与只有非$键时的行为一致。⚠️ 但这会把{amount: {gte: 10}}这类手误编成对不存在成员amount.gte的谓词,把一个可诊断的错误变成一个更难懂的错误。
倾向 A,但这是分诊的判断:B 触及"混合拼法算不算合法授权面",属契约问题。
触达性
本单未证实有生产调用方写出混合形状。过滤器主要来自库存 metadata(dashboard filter / report runtimeFilter / dataset filter),是 JSON,所以这个形状过得了 JSON(与 #6386 的 undefined 不同),可以躺在库存 metadata 里,也可以由 AI 直接写出来。如实记的是"未证实触达",不是"不会发生"。
关联
- service-analytics 的
where门把undefined值的键整个丢掉 —— 单键 where 退化成「无过滤器」,方向是加宽(#6125 五面表漏记的第六、七种读法) #6386(本单的出处;同一函数,比较数位置的undefined) - analytics: ObjectQLStrategy 静默忽略 timeDimensions[].dateRange —— 恰好是 date-granularity 图表必走的那条路 #3650 / analytics filter-normalizer:未映射的算子被静默丢弃 → 查询放宽到全表($between 已修,还剩四个) #4128("丢谓词 = 加宽"的原始事故与复发)
{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240(零算子字段约束{}判"拒收"的先例)- [finding] 仓内存在 5 个独立的过滤器→谓词编译器,每次语义裁决成本 ×5 —— 值得立「谓词编译收敛」调查程序(#5298 成本清单副产品) #5930(仓内 5 个独立谓词编译器,每次裁决 xN)
会话:session_015a5qkLzpGXhLL2F5gvJ7dD(#6386 实施过程中发现,未指派)
Activity
claude commentedon Aug 7, 2026
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)
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.$notallowNull 守卫在 #5298 第二批之后变成冗余的第二层(谓词等价,SQL 多一层括号) #6005 (finding) — same file,$notallowNull guard redundancy. Disjoint.$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.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:FilterConditionSchema'sz.record(z.string(), z.unknown())half declares legal today;{ 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
os-project-manager commentedon Aug 8, 2026
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-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:
$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.code+status(ADR-0112 envelope) — a baretoThrow()does not constitute a refusal test.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.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
CLAIM — 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
fix(service-analytics)!: the analytics `where` door runs every arm of…
fix(service-analytics)!: the analytics where door runs the shared com…
fix(service-analytics)!: the analytics where door refuses a non-boole…