feat(analytics): an authored cube's measures.format and dimensions.granularities take effect on the query doors (#20282, stage 2) - #20635
Conversation
…anularities reach the query doors (wip) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ty at the query doors, with dataset controls Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… over POST /analytics/query and /analytics/sql Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…es are live; pin the declared narrowing; changeset Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…be-format-granularities
…cribe what the analytics service does with them Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…be-format-granularities # Conflicts: # packages/spec/liveness/README.md
…e-granularity-default-enforced Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 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 88f8213363e3942323d42b9e1f8a18c0b4e4cb57 && git checkout 88f8213363e3942323d42b9e1f8a18c0b4e4cb57
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6bff748bbd484f00906c8d9a58f5a3621a20c2e6 162e6c0f01ff5aa17223905ca292e7c32d0f1431 && git checkout -B drift-repro 6bff748bbd484f00906c8d9a58f5a3621a20c2e6 && git merge --no-ff 162e6c0f01ff5aa17223905ca292e7c32d0f1431
node scripts/docs-audit/affected-docs.mjs --json 6bff748bbd484f00906c8d9a58f5a3621a20c2e6
|
Contract reviewServed-tier: Read: card #20282 (body and all 24 comments; in particular ① Derived judgmentsEnforcement, judged from the readers at the head.
The narrowing: arm RIGHT, class WRONG (under-enumerated). This is the FAIL.
Public-surface changes.
The D3 entry
Liveness shard and README. Both rows Showcase and preview. Gate coverage, from the 42 check-runs on the head (read once): 38 success, 4 skipped, 0 failed, 0 not concluded. Skipped: ② Semver level
③ Boundary flagsRound-1 report
Round-2 report
Nothing is escalated: the one FAIL reason is settled from the code and has a bounded remedy. FAIL reason (one): the declared narrowing names one refused class where the re-route exposes the engine path's whole refusal set, namely cross-object members on an authored cube with Implemented-by: VERDICT: FAIL Generated by Claude Code |
…be-format-granularities
…default bucket reaches; pin the joined-cube case Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…f the declared narrowing Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round 3, re-judging the one FAIL reason of the round-2 record ① Derived judgmentsThe refusal set, re-derived from the code at the head, not from the dev's prose. A newly bucketed query is one where
The three texts against that set. The changeset's Three boundaries I weighed and did not count against the class, recorded so the next reader can disagree with the reasoning rather than re-find it:
The new pin Nothing else moved in round 3. Blob comparison between The merge of Enforcement, unchanged from round 2 and re-read at the head. Public-surface changes. The D3 entry. File Ledger. Both rows ② Semver level
③ Boundary flagsRound-3 report
Round-2 review Round-1 report Docs-drift bot Gate coverage, from the 42 check-runs on the head, read once: 38 success, 4 skipped, 0 failure, 0 not concluded. The roster equals round 2's 42; the eight second runs at 16:50Z are the pr-automation jobs re-fired by the body edit, and two of them ( Nothing is escalated. Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #20282
Clause-②: yes (narrowing)
Stage 2 of #20282, under claim
5886559774(session_014EJ1ED8X4MMrT18BhVx4tx,domain:specseat 2). An AUTHORED analytics cube'smeasures.formatanddimensions.granularitiesnow reach the readers a compiled dataset already reaches.refreshKeyis measured only, and nothing is built for it. The card stays open for stage 3 (descriptions) and for therefreshKeydecision.What changes
One Cube shape has three producers: authored cubes (
AnalyticsServiceConfig.cubes, which the CLI threads fromanalyticsCubes), compiled datasets, and ad-hoc inference. Until now, both keys were read only on the compiled-dataset path.measures.formatanalytics-service.ts#withDeclaredMeasureFormatsruns inqueryIn, beside the SQL-echo gate.formatasfields[].format. This holds for every strategy (NativeSQL, ObjectQL, the delegated fallback) and for both member spellings.format, the same valueenrichResultColumnswrites anyway.GET /analytics/metais unchanged. See the premise checks below.dimensions.granularitiesanalytics-service.ts#withDeclaredGranularityDefaultsruns onquery()and on thegenerateSql()dry run, before the source-field gates and strategy selection.dataset-executor.ts#declaredDefaultGranularity, a new function extracted fromgranularityOf, which now calls it too. Both producers are therefore read by one rule.timeDimensionsentry that carries only adateRange, for a dimension the query does not group by, stays a filter.packages/spec/src/data/analytics.zod.ts, after PR docs(spec): re-anchor the dead tracker citations in stack.zod.ts and data/analytics.zod.ts to the commits that decided them (stage 7) #20616 merged;origin/mainmerged first throughscripts/pm/os-regen-merge.shasd963f30e33)MetricSchema.formatandDimensionSchema.granularitiesgain describes that state the enforcement.fields[].formatslot documents.content/docs/references/data/analytics.mdxis regenerated withgen:docs.packages/spec/liveness/analytics_cube.jsongodead→live. Each cites its readers asfile#symboland the CLI threading producer.state-counts/analytics_cube.mdis regenerated withgen:liveness-counts: live/dead goes from 18/9 to 20/7.analytics_cubeNotes cell inliveness/README.md, which listed both keys among the dead, is rewritten.@objectstack/specminor and@objectstack/service-analyticsminor, with a**BREAKING**sentence.Clause-② (measured arm): yes (narrowing)
fields[].formatis populated for authored cubes. The contract already declares that member.scripts/ablation-replace.mjs(round 1 at958251b6ac; round 3 ate1383be04d, blob9d77adcd798f→94b1bc63165a, restored to HEAD). The probe is not committed.placed_atdeclaresgranularities: ['month'], whoseshipped_atdeclares two intervals, and (round 3) which declaresjoins: { account: { name: 'crm_account' } }.placed_atdeclares['month'],shipped_attwo intervals;joins: { account }where named)placed_atINVALID_FIELD, custom-SQL measure (resolveMeasureAggregation)sumofaccount.balance) grouped byplaced_atINVALID_FIELD, cross-object measure (planCrossObject)countbyplaced_atwith awherefield overaccountINVALID_FIELD, cross-object filter,param: wherecountgrouped by a one-interval time dimension overaccount(account.created_at)INVALID_FIELD, cannot bucket a cross-object time dimensioncountbyplaced_atbeside a multi-hop dimension (account.owner.region)INVALID_FIELD, single-hop onlyavgbyplaced_atbeside a cross-object dimension (account.industry)INVALID_FIELD, non-recombinable measurecount_distinctbyplaced_atbeside a cross-object dimensionINVALID_FIELD, non-recombinable measurequeryCapabilitiesoffers raw SQL only (a hand override;AnalyticsServicePluginwires both): plaincountgrouped byplaced_atcountbyplaced_atbeside a cross-object dimension (recombinable)avgbyplaced_at, no cross-object membershipped_at(two intervals)granularity: 'month'stated by hand already got: the fill hands the engine path the very query a hand-stated granularity does. The class is the engine aggregate path's whole refusal set as the new bucketing reaches it, read fromObjectQLStrategy.planCrossObjectandresolveMeasureAggregation: a custom-SQL measure; and, on a cube whose members resolve throughjoins, a measure orwherefield over a joined object, atimeDimensionsentry over a joined object, a multi-hop dimension, and anavg/count_distinctmeasure beside a dimension over a joined object.planCrossObject's two dataset-definition arms read only compiled-dataset scope, so they cannot fire on an authored cube. Two pins:DECLARED NARROWING(custom-SQL) andDECLARED NARROWING, joined cube(cross-object measure).**BREAKING**sentence (the class and its remedy) and the dispositionregistered analytics-cube-single-granularity-default-enforced, a new ADR-0087 D3 semantic entry (round 2, seat note5889752648Q4 = B).check-adr-0087-registration:✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.18.analytics-cube-single-granularity-default-enforced.tstells authors that a one-interval list is now a default bucket, including one the protocol-18 conversioncube-sub-day-granularities-removedminted, and (round 3) which queries grouped by it the engine path refuses: the whole class above, plus every newly bucketed query on a raw-SQL-only host.registry.tsis regenerated;spec-changes.jsonand the upgrade guide were regenerated with no change, because neither projects major-18 entries yet.Premise checks, against
origin/main7510663c87formatonCubeMeta: not done, because the premise does not hold.formatonly throughfields[]. A compiled dataset'sgetMetaprojection is{ name, type, title }too.AnalyticsMetadataResponseSchemarecords the narrowing for this (#6442).content/docs/api/data-api.mdxalready sends clients tofields[]forformat.contracts/analytics-service.ts,api/analytics.zod.ts) are outside this claim's surface.granularitiesrefusal: none invented.refreshKeycensus (tree958251b6ac;packages/services,packages/drivers,packages/rest, non-test):refreshKey: 0 hits. The repo-wide control finds 9 files.ICacheServiceconsumers: plugin-auth rate-limit and secondary storage, runtime inbound rate limit, dispatcher counter store, sms. None is in analytics.dimension-labels.ts#withLabelFetchCache(the lit control, 1 hit).service-job'sIJobService: cron, interval and db adapters), and nothing in analytics uses it.refreshKey, so building a cache is a separate card.Verification
Final head
162e6c0f01(round 3; merge basef4ce10c89d), unless a line says otherwise. Builds and test runs went throughos-verify-lock.sh. Thecheck:*gates and eslint ran outside it, as the lock's scope prescribes. So didgen:docs, after two lock acquisitions forcheck:generated --fixtimed out in the queue (exit 99).162e6c0f01): service-analytics 136/3185 (includes the new joined-cube pin), typecheck clean; runtime REST pin 1/3; specsrc/migrations3/161;migrate-meta-engine-guidance.test.ts3/3; spec--project repo43/761. Lit/dark: with the fill ablated, the pin file goes 7 red (the 6 prior bucketing and narrowing cases plus the joined pin), and the probe's E1 to E7 and R1 each answer 200; on head each is refused.dispatch-gates --ran: 115 derived / 115 run / 0 NOT MEASURED, 114 exit 0, 1 exit 1 (check:platform-checklist, not a PR gate). Regenerated artefacts vsorigin/main:registry.ts+62/-0 (the entry block only),spec-changes.jsonand the upgrade guide identical. Driver-free merge-tree probe against2473e26875: clean, and the migration and liveness checks are green on the merge tree.257ab1bc92):migrate-meta-engine-guidance.test.ts3/3; spec--project repo43/761; specsrc/migrations3/161;check:migration-registry,check:spec-changes,check:upgrade-guide,check-adr-0087-registrationgreen;dispatch-gates --ran115 derived / 115 run / 0 NOT MEASURED, 114 exit 0, 1 exit 1 (check:platform-checklist, inputs equal to merge base3f45b6cc13). The lines below are round 1's, atd665865d5b.pnpm --filter '@objectstack/service-analytics...' build: exit 0.turbo run build --filter='@objectstack/runtime^...': 29/29.pnpm --filter @objectstack/spec build, after the describe edit: exit 0.--project local--project repo958251b6ac; service-analytics src is unchanged since)check:test-typecheck, and the new file adds no debt.tsc --listFilesputs the new service test in service-analytics' program.formatand no time dimension:packages/clientanalytics-automation-json-erasure.test.ts, and the dogfood analytics files, which declare neither key.git diff HEADempty):9d77adcd798f→0d48cfde9469)9bf3b3b0b4dist/9bf3b3b0b49d77adcd798f→94b1bc63165a)958251b6acdist/9bf3b3b0b4ablation-dist-preflight(marker present in 2 built files). Each restore leg was rebuilt again and checked--absent, with the tree clean.dispatch-gates --commands: 111 derived.--ranwith exit codes: 111 run, 0 NOT MEASURED. 109 exit 0.1322cc72c:check:platform-checklist:areas/identity-auth.jsoncitesauth-plugin.ts#twoFactor, which is absent;check:docs-transcript-drift: 4 CLI transcripts print "author-time rules (47)", and the count derives to 46.check:liveness:analytics_cube 27 classified (live 20, dead 7), with the state-counts current.check:generated: all 15 artifacts up to date.eslint --no-inline-config --format jsonon the 4 changed.tsfiles: 4 files, 0 errors, 0 warnings..md/.json) are reported by eslint itself as "File ignored because no matching configuration".eslint.config.mjsnever enables type-aware linting (noparserOptions.project), so this diff cannot move a verdict on an untouched file.Acceptance notes
5886559774, each forced by the claim's own items:analytics_cubeNotes cell inpackages/spec/liveness/README.md. It is the prose half of the state table whose shard the claim names, and it named both keys as dead.packages/runtime/src/analytics-authored-cube-format-granularity.test.ts, the REST pin the claim asks for "where the analytics harness reaches". It is a new file, and the runtime harness drives the real dispatcher route.content/docs/references/data/analytics.mdx, which the describes regenerate.migrations/registry.tsand the changeset marker, per seat note5889752648.packages/services/service-analytics/src/preview-evaluator.tsstill open-codes the single-entry rule (dim.granularities?.length === 1) on the draft-preview path. That makes it a third spelling besidedeclaredDefaultGranularity. It is outside this surface;carrier:承接者:无.examples/app-showcase/src/data/analytics/showcase.cube.tsauthorsdone_rate: { format: 'percent' }. That named style now reachesfields[].formatverbatim, and a numeral-pattern renderer does not read it as a percentage. The spec describe now teaches the pattern vocabulary. The example's value is outside this surface and is reported to the seat.os serveartifact-fallback boot threads noanalyticsCubes.Generated by Claude Code