Repository navigation
fix(service-analytics)!: a filtered dataset measure asked as CUBE.MEASURE applies its filter, as the bare name does - #26007
Conversation
…lies its filter The cube door accepts a dataset measure by its bare name or as `<cube>.<name>`, the spelling getMeta lists. The member resolved under both, but every reader of the measure's scoped filter looked it up by the spelling as asked, so the qualified one aggregated without its filter (`SUM(amount)` under the won-deals measure's name) and the read gate never judged the filter's fields. One rule, `scopedMeasureKey` (the one `durationMeasureKey` already used), now feeds every `measureFilters` reader: native SQL compileClauses and its two routing declines, ObjectQL execute, its statement echo and its cross-object member view, and the service's namedQueryFields. Claude-Session: https://claude.ai/code/session_014YyczUJLen7od4GYKjPdY2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7c3cd60944a2bda37831fa13338ca360e9d017a5 && git checkout 7c3cd60944a2bda37831fa13338ca360e9d017a5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c8cb510a5e65d25671c64061906b0954abf1fb4c 6c314cfe1bb6c4f2f7165509f4f57890d106449f && git checkout -B drift-repro c8cb510a5e65d25671c64061906b0954abf1fb4c && git merge --no-ff 6c314cfe1bb6c4f2f7165509f4f57890d106449f
node scripts/docs-audit/affected-docs.mjs --json c8cb510a5e65d25671c64061906b0954abf1fb4c
|
Contract reviewServed-tier: Face: narrowing ( ① Derived judgmentsThe accept-set change. On
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | draft, base main, Fixes #25906, Clause-②: no (narrowing), Metadata-protocol: none; every path under packages/services/service-analytics/** plus one changeset, inside the claim's file surface; no packages/spec/** path. |
| Narrowing | a filtered dataset measure asked as CUBE.MEASURE now has its filter's fields judged by the field-read gate, so a caller who may not read such a field, answered 200 with the unfiltered number on main, is refused 403 PERMISSION_DENIED, as the bare spelling already was. No spelling is newly accepted, no code or status is new. |
| Fix | every reader of DatasetScope.measureFilters (native SQL, ObjectQL, dry-run echo, namedQueryFields) resolves the measure through scopedMeasureKey: the bare own key, else the name minus its own cube's prefix. A caller who reads every field gets the FILTERED number under both spellings. |
| Evidence | red first (5 failed / 2 passed at the base), green on the head; ablation of the read-gate line turns exactly the read-gate case red; check-runs on the head completed with none failing. |
| Review note | the changeset's "field-level read gate" wording under-names the three sibling gates sharing namedQueryFields; the behavioural claim covers them, no change required. |
| Carried | the dogfood authz-probe-blind-spot red was main's own, fixed on main by #26002. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay. |
Generated by Claude Code
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 40302541664 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
席位核对 · queue note ·
|
Requested by Leehom · project thread
Fixes #25906
Clause-②: no (narrowing)
Metadata-protocol: none
Contract review owed: the
(narrowing)arm. The field-level read gate now judges the fields of a filtered measure's filter when the measure is asked asCUBE.MEASURE, so a caller who may not read such a field, answered200(with the unfiltered number) onmain, is now refused403 PERMISSION_DENIED, exactly as the bare spelling already was. Nothing is widened, and nopackages/spec/**path is touched. The seat's claim (6411271126) carriesClause-②: yes (expected …); the lines above are the values measured on this diff, as the dispatch order asked.What was wrong
A registered dataset measure can carry its own
filter.GET /analytics/metalists every cube member asCUBE.MEASURE, and the cube door accepts that spelling:lookupMemberstrips the cube's own qualifier, so the measure resolves under both. But every reader of the measure's scoped filter (DatasetScope.measureFilters) looked it up by the spelling as asked, so the listed spelling missed it. Measured atAnalyticsService.queryonorigin/main05f7758f, native SQL, SQLite dialect:measures['won_amount']COALESCE(SUM(CASE WHEN stage = $1 THEN amount END), 0) AS "won_amount"['sales.won_amount']COALESCE(SUM(amount), 0) AS "sales.won_amount"The ObjectQL face dropped the aggregation's
filterthe same way, its dry-run echo renderedSUM(amount), andnamedQueryFields(the read gate) never judged the filter's fields.What changed
One rule,
scopedMeasureKey(newscoped-measure-key.ts): the bare name if it is an own key of the record, else the name without its own cube'sCUBE.qualifier. It is the ruledurationMeasureKeyalready used for duration measures (PR #25903), anddurationMeasureKeynow delegates to it, so the two records cannot drift.scopedMeasureFilterreadsmeasureFiltersthrough it. Every reader now calls it:compileClauses(the statement)native-sql-strategy.tsjsonConstructOnUnknownDialectIn(routing decline)native-sql-strategy.tsnestedRelationConditionIn(routing decline)native-sql-strategy.tsexecuteaggregationsobjectql-strategy.tsgenerateSqlechoobjectql-strategy.tsfilterMemberView(cross-object refusal)objectql-strategy.tsnamedQueryFields(the read gate)analytics-service.tsThe two native routing declines had to move with the statement: once the statement compiles the qualified measure's filter, a decline that still missed it would let a nested-relation filter reach the native compiler, which refuses it as a bare fault.
Not changed:
DatasetExecutor(thePOST /analytics/dataset/querypath) builds bare names; the draft-preview evaluator runs under it. Another dotted prefix keys nothing, as before. No rawmeasureFilters?.[…]lookup is left in the package (grepoversrc/outside tests).Tests
New
src/__tests__/cube-qualified-measure-filter.test.ts, red first on unfixed readers (5 failed / 2 passed, the 2 being the helper's own unit cases):['won'];filterunder both spellings; the dry-run echo renders the same conditional aggregate;stageunreadable, both spellings answerPERMISSION_DENIED/ 403 namingstageand nothing runs; with every field readable both run (control);NativeSQLStrategy.canHandledeclines a nested-relation measure filter asked either way;scopedMeasureKey: bare, own-cube qualified, another prefix (none), no cube name (none),Object.prototypenames (none).Ablation (one-time, committed fix first):
scripts/ablation-replace.mjsput the read gate's raw lookup back inanalytics-service.ts(anchor 1 to 0, blobf6a5b05btoc366dfb4); exactly the read-gate case went red (1 failed / 6 passed); restored blob equals HEAD,git diff HEADempty.Local, at head
6c314cfe:pnpm --filter @objectstack/service-analytics test: Test Files 189 passed (189), Tests 4570 passed, 282 skipped (os-verify-lock VERDICT command-exit 0)pnpm --filter @objectstack/service-analytics typecheck: exit 0eslint --no-inline-config packages/services/service-analytics: exit 0dispatch-gates.mjs --commands, 70 commands): 68 exit 0, 2 NOT MEASURED on a local prerequisite (exit 3):check-plugin-teardown-shape.mjs --self-test(shallow clone; a checker-health self-test) andcheck:dual-build-cjs-loads(needs a full workspace build); CI runs both.dispatch-gates --ran: 70 derived, 68 run, 2 NOT-MEASURED, 0 UNRUN, exit 0Acceptance notes
Generated by Claude Code