Skip to content

fix(service-analytics): the ObjectQL strategy serves two dataset dimensions over one column - #28762

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-0o422x
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-0o422x

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5kbUJTTWAxjV9nqr1P

Requested by Leehom · project thread

Fixes #28744

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — no file under skills/** or content/docs says the ObjectQL strategy refuses two dimensions over one column.

Card thread: session_01FEkQ5kbUJTTWAxjV9nqr1P.

维护者速读

What changes

Why

Measured on real SQLite (the #28201 / #28555 / #28690 harness), dataset over a deal object with dimensions region and zone (both over region), rows amer 100, amer 50, emea 30:

selection native ObjectQL before preview ObjectQL after
dims region, zone (either order) amer 150 / emea 30, keyed by both INVALID_QUERY (groupBy.1 names 'region') same as native same as native
…with td closed_at month three groups INVALID_QUERY same as native same as native
backed, with account.name three groups INVALID_QUERY refused (#28555) same as native
closed_at raw + OBJECT.closed_at month INVALID_QUERY INVALID_QUERY serves INVALID_QUERY (unchanged)

Tests

Red first, green, ablations

Acceptance notes

  • The order's hypothesis held for the groupBy and not for the re-key: the direct path's row mapping already reads every projected dimension from its resolved column, so once the column is grouped once, both names are filled. No re-key map was added.
  • S1 and S2 are folded in as the order allowed. S1 is in analytics-service.ts, not the strategy file, but it is three call sites in one file and behaviour-neutral (full suite green).
  • Out of scope, measured, not fixed: for closed_at raw beside OBJECT.closed_at bucketed by month, both live faces refuse INVALID_QUERY / 400 (two values under one name), while the draft preview serves both columns. Reported to the seat.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 62 families derived on cb122c70 (4 paths vs merge base 7a48bf73); each run with stdin from /dev/null, exit captured before any pipe, after a full turbo run build --filter=!@objectstack/docs --concurrency=2 (lock VERDICT command-exit 0). --ran: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0.
  • Hand gates: node scripts/check-platform-checklist.mjs --changed-from origin/main exit 0; node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD exit 0.
  • Dogfood battery: not run. Clause-② is widening only, with no ! and no narrowing arm.
  • CI families: read from this PR's check-runs.
The 62 derived families and their exits
  • node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-adoption.mjs :: exit 0
  • node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-dts-emitted.mjs --self-test :: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
  • node scripts/check-empty-changeset.mjs --self-test :: exit 0
  • node scripts/check-issue-citations.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
  • node scripts/check-tenant-audit-census.mjs :: exit 0
  • node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
  • node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
  • node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
  • node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
  • node scripts/docs-audit/check-affected-docs.mjs :: exit 0
  • node scripts/docs-audit/check-drift-comment.mjs :: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
  • node scripts/release-pending-publish.mjs --self-test :: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
  • pnpm check:changeset-gate-self-tests :: exit 0
  • pnpm check:ci-filter-parity :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:dispatcher-error-vocabulary :: exit 0
  • pnpm check:doc-authoring :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:dts-closure :: exit 0
  • pnpm check:dual-build-cjs-loads :: exit 0
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:error-status-conformance :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:issue-citations :: exit 0
  • pnpm check:keyed-text-bounds :: exit 0
  • pnpm check:lean-entry-closure :: exit 0
  • pnpm check:logger-receiver-detach :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:objectql-double-limit :: exit 0
  • pnpm check:org-identifier :: exit 0
  • pnpm check:page-declaration-shape :: exit 0
  • pnpm check:platform-object-tenancy-census :: exit 0
  • pnpm check:pm-changeset-deadline-census :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:slot-lookup :: exit 0
  • pnpm check:sourcemap-no-sources-content :: exit 0
  • pnpm check:swallow-census-controls :: exit 0
  • pnpm check:system-context-census :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:type-check-debt :: exit 0
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped run above; the repo-wide run is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the dispatch contract's).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5kbUJTTWAxjV9nqr1P

…en two dimensions read it

- objectql-strategy.ts: a module-level pushGroupKey() appends a groupBy key
  unless an identical one (same column, or same column at the same
  granularity) is already there. The direct path and the cross-object path
  both use it, replacing #28692's two FK-only checks. Two dataset dimensions
  over one column now share its group, and the existing row mapping keys the
  answer by every dimension name. A column raw beside its bucket stays two
  keys, so the engine's duplicate-column refusal still answers it.
- analytics-service.ts (review S1): one whereLeafMembers(where) reader for
  namedQueryFields, relationshipMembers and previewWhereColumns, in place of
  three copies of the guarded collectFilterLeaves read.
- Tests (review S2 and #28744): a filter on a measure key over a plain
  column, two dimensions over one column on the direct path, beside a month
  bucket and beside a cross-object dimension, and the raw-beside-bucket
  refusal, all on real SQLite.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 706ac52d570096a47b64283e970bc1a8e569bc91 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50ba4986c729f66193c72c4d80556ce8dde40ebb — the merge of head cb122c705f9028be052cd78d5027176a122b886d into base 706ac52d570096a47b64283e970bc1a8e569bc91, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50ba4986c729f66193c72c4d80556ce8dde40ebb && git checkout 50ba4986c729f66193c72c4d80556ce8dde40ebb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 706ac52d570096a47b64283e970bc1a8e569bc91 cb122c705f9028be052cd78d5027176a122b886d && git checkout -B drift-repro 706ac52d570096a47b64283e970bc1a8e569bc91 && git merge --no-ff cb122c705f9028be052cd78d5027176a122b886d

node scripts/docs-audit/affected-docs.mjs --json 706ac52d570096a47b64283e970bc1a8e569bc91

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: TIER_DEFAULT
Head-sha: cb122c705f9028be052cd78d5027176a122b886d
Local-runs: none

Face: declaration (Clause-②: yes (widening) alone; no packages/spec/**, no governed path, no narrowing arm in the tree). Net diff vs merge base 7a48bf73: 4 files, the same 4 as the PR file list and the dev report. origin/main is 4 commits past the merge base, and none of them touches packages/services/service-analytics/** or the changeset, so there is no overlap to re-read.

① Derived judgments

  • Accept set, ObjectQL direct path (execute): widened. Right. pushGroupKey drops a group key that is already in groupBy: the same column string, or the same {field, dateGranularity} pair. Two dimensions over one column (region, and zone declared over region) used to send groupBy: ['region','region'], which the engine refused (INVALID_QUERY / 400). They now send one key. The row mapping at head reads each projected dimension by resolveFieldName(cube, dim) → row[shortName], so both names are filled from the one group. That matches the card's Wanted ("groups by the column once and keys the answer by every dimension name over it"). The dev's statement that no re-key map is needed holds against the code.
  • Accept set, ObjectQL cross-object path (executeCrossObject): widened. Right. One helper now replaces analytics: the ObjectQL strategy refuses a backed account.id dimension with INVALID_QUERY because the cross-object expand groups by the related id twice #28692's two FK-only checks: includes(cd.fkField), and fkGrouped (only when there is no granularity and the field is an FK). For string keys the old checks are the same rule, and the bucketed case was never deduped before, so nothing that was served before changes shape. baseDimFields still pushes the column once per dimension. rebucketCrossObject builds its composite key from baseDimFields and writes bucket[f], so a repeated field adds the same value twice. That leaves the grouping unchanged and harmless.
  • What stays refused: kept. Right, and pinned. A column raw beside its own bucket (closed_at with OBJECT.closed_at by month) is a string key plus an object key, so same() is false and both are handed on. The engine's duplicate-column refusal still answers it on both live faces (test pinned; ablation B4 reds it). Two buckets of one column at different granularities also stay two keys. packages/objectql is untouched. This satisfies the card's ⛔ "the objectql duplicate-groupBy refusal stays for a caller that really names a column twice".
  • Newly served as a side effect: right and in scope. Two names over one column bucketed at the same granularity (for example a dimension plus a time-dimension-only entry resolving to {closed_at, month}) now share one bucket instead of being refused. It is the same rule, it is consistent with native, and it is covered by the changeset's sentence "the same column bucketed at the same granularity".
  • Narrowing: none. Every input that reaches the new dedupe was either a duplicate that the engine refused or an FK duplicate that analytics: the ObjectQL strategy refuses a backed account.id dimension with INVALID_QUERY because the cross-object expand groups by the related id twice #28692 already deduped by the same rule. Nothing that was served before is now refused or answered differently.
  • Review S1, analytics-service.ts whereLeafMembers: behaviour-neutral. Right. It replaces three copies (relationshipMembers, namedQueryFields' local filterMembers, previewWhereColumns). In each, a non-object or refused where returned nothing, and it still yields []. previewWhereColumns' early return {} becomes an empty loop that returns {}. The card's claim names this file surface ("optionally the review S1 helper in analytics-service.ts"), so the dev's deviation note is answered by the claim.
  • Review S2, test only: right. A runtimeFilter on measure key total (over amount, $gt: 60) gives [{region: amer, total: 100}] on every face, which is consistent with rows amer 100 / amer 50 / emea 30. Ablation A-S2 gives it a red leg.
  • Public surface: unchanged. Right. pushGroupKey and whereLeafMembers are module-private. There is no new export, spec key, error code or status. The response shape is unchanged.
  • Tests: 6 cases are added to preview-time-filter-fk-parity.test.ts on real SQLite: both dimension orders, beside a month bucket, beside account.name (live faces only, since the preview refuses per analytics: the dataset draft preview joins nothing, so a backed dotted dimension serves one null group with the whole total #28555), the raw-beside-bucket refusal pin, and S2. Each case is a parity assertion across faces, which is the right shape for this card.

② Semver level

  • Changeset .changeset/28744-objectql-two-dimensions-one-column.md: '@objectstack/service-analytics': patch. The diff moves only that package's src, and only that package publishes from it. Matches.
  • Metadata-protocol: none is right: nothing a metadata author can write changes. Under AGENTS.md § changesets, none takes patch, and a non-protocol widening ships patch. There is no skip-changeset label, which is right because this fix publishes.
  • Clause-②: yes (widening) is right. A query refused 400 on the ObjectQL face is now served. Both declaration lines are line-initial and identical in the PR body and the changeset body. The changeset body carries the declarations and says what still refuses. No BREAKING banner or ADR-0087 marker is owed.

③ Boundary flags

  • open_questions: []. Nothing to answer.
  • Dev deviations: (1) S1 outside the strategy file: answered by the claim's File surface (see ①). (2) The remote branch was recreated after fix(service-analytics): the draft preview groups a bucketed time dimension and reads a qualified filter member as the live faces do; ObjectQL serves a backed RELATION.id dimension #28742 merged: consistent with the claim's Resume-from: none, and the net diff carries only this card's 4 files. (3) One rebase onto 7a48bf73, with the full suite read pre-rebase: the rebase touches no service-analytics file, and CI Test Core on this head is the reading of record. (4) The PR was opened with no labels: a changeset is present, so none were owed. Auto Label has since applied documentation,size/m,tests,tooling, which is not contract-relevant.
  • out_of_scope_findings (one, class a): the draft preview serves closed_at raw beside OBJECT.closed_at by month, while native and ObjectQL refuse INVALID_QUERY / 400. Escalated to the seat (domain:server#3) to file or triage. Not a blocker here: this PR does not change that input on any face. The card thread has no triage of it yet.
  • Gate state on this head (45 check-runs, read once): 12 success (including Check Changeset, PR-body guards, Lint · PM dispatch-gates, Governed Surface Queue Guard, Dogfood Regression Gate, Lint · scoped · tooling self-tests, Test UI, Check PR Size, Flag docs affected, Auto Label, filter, Plan Test Core shards), 13 skipped (UI, Docs and opt-in lanes, not derived for this path set), 20 in_progress (Build Core, Test Core 1–8 of 8, Type Check × 4, Lint × 6, Live E2E UI), 0 failure. This contract verdict does not stand in for those 20 runs. Landing waits on them and on the PR leaving draft.

Implemented-by: claude/project-thread-0o422x
Reviewed-by: session_01FEkQ5kbUJTTWAxjV9nqr1P-review-28762

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28762 · 2026-11-01T18:33Z

Head-sha: cb122c705f9028be052cd78d5027176a122b886d

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28744,合并关卡正确,与派单 pm-orders/server/order-28744.md 一致。文件 4 个,在认领 6561266067 申报面内(仅 service-analytics)。
  2. 前提实测:dev 在真实 SQLite 上实测 region+zone:原生与预览 amer 150 / emea 30,ObjectQL INVALID_QUERY;base 上新增用例 4 红,head 23 绿;5 组消融(B1–B4、A-S2)各自变红后还原。订单里"需要重键映射"的假设不成立,行映射已按每个维度名读取解析后的列,复核对照代码确认。
  3. 门禁:本 head check-runs 35 success、13 skipped(路径未命中)、0 失败或待跑。dev 本地完整构建后 62 条派生门禁全部 exit 0,0 NOT-MEASURED;typecheck 与 eslint exit 0。
  4. 接受的偏差:均为过程性(远端分支在 fix(service-analytics): the draft preview groups a bucketed time dimension and reads a qualified filter member as the live faces do; ObjectQL serves a backed RELATION.id dimension #28742 合并后被删,推送新建;中途 rebase 到 7a48bf73 一次,未碰 service-analytics;PR 未带标签)。fix(service-analytics): the draft preview groups a bucketed time dimension and reads a qualified filter member as the live faces do; ObjectQL serves a backed RELATION.id dimension #28742 复核留下的 S1(whereLeafMembers 合并三份读取,行为不变)与 S2(度量键筛选用例)已一并完成。复核无 should-fix。
  5. 范围外去向:dev 实测"同列原值 + 自身月分桶"预览能答、两个线上出口拒绝,已立卡 analytics: the draft preview serves a column raw beside its own time bucket, while native and ObjectQL refuse it with INVALID_QUERY #28765(p3,待本 PR 合入后派)。Metadata-protocol: none,Skills-docs: none。
  6. 落地条款:Clause-②: yes (widening),非 spec 路径 ⇒ TIER_DEFAULT 复核 PASS(6561984153,Head-sha 与现 head 一致)。semver:service-analytics patch,与 changeset 一致(非协议扩大取 patch,Check Changeset 本 head success)。check-governed-merges --pr 28762:NOT governed,180 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 18:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 18:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 52de5a5 Nov 1, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0o422x branch November 1, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant