feat(spec,service-analytics)!: retire the cube metric types number / string / boolean, refused in both analytics strategies in the spec's words (#21000) - #21452
Conversation
…WIP) AggregationMetricType keeps the six aggregates; the three custom-SQL-expression members are refused at parse by name with a prescription (enumWithRetiredValues). D3 entry cube-metric-expression-types-retired and its step-18 rationale fragment. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
… type replaces the expression partition (WIP) Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…d-type fixture (WIP) Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…semantics wording (WIP) Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…tire-expression-metric-types
…ot a retired key another absence pin guards Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…tire-expression-metric-types # Conflicts: # packages/services/service-analytics/src/strategies/objectql-strategy.ts
…custom-SQL metric types were retired Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…tire-expression-metric-types # Conflicts: # packages/spec/src/data/analytics.zod.ts
📓 Docs Drift CheckThis PR changes 3 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 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 b26d58fe215a4b37e0270cee7104d329bf4e308e && git checkout b26d58fe215a4b37e0270cee7104d329bf4e308e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c 4278601b824d5c64fd7802cfc8cc4e7485b7d7ab && git checkout -B drift-repro 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c && git merge --no-ff 4278601b824d5c64fd7802cfc8cc4e7485b7d7ab
node scripts/docs-audit/affected-docs.mjs --json 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c
|
Contract reviewServed-tier: Isolated contract review of PR #21452 (card #21000) at the head above: the net diff against ① Derived judgmentsAccept-set delta — right. The only change to any accept set is the three members Census — right. Re-read on Published type surface — right, and declared. The exported Runtime half — right. The tier change — right, and stated truthfully. On ObjectQL the three move from Text this PR makes false — one owed correction left undone (FAIL point). ② Semver levelTwo changesets, read sentence by sentence against the diff.
③ Boundary flagsEvery deviation in
The dev's NOT MEASURED items, and the check-runs on this head that measure them: the six Check-runs on this head, read 2026-10-02T19:00Z (33 runs): 27 What a patch round owes: rewrite the one Implemented-by: VERDICT: FAIL |
…s are gone and what still passes a non-column sql through Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Narrow re-review of PR #21452 (card #21000) at the head above, following record ① Derived judgmentsThe delta is exactly the one sentence — confirmed. One commit ( The new sentence is true at the head. Read against
Text this PR makes false — none left. The
② Semver levelCarries from record ③ Boundary flagsCarries from record Check-runs on this head — every run concluded; read 2026-10-02T19:38Z (42 runs, the PR workflow having been re-triggered once by the body edit; last conclusion Implemented-by: VERDICT: PASS |
Fixes #21000
Clause-②: no (narrowing)
Dispatched by the claim
5955932074(PM loop round 1,domain:specseat 1, sessionsession_01UtnxvdiN376GF3sgXwAw4d), on triage's answer B5952826307: the enum retirement only. The row wildcard boundary is #21409's (landed asb79301000c, merged here).What this does
@objectstack/spec:AggregationMetricTypelosesnumber,stringandboolean(ADR-0049 enforce-or-remove, grade5923362062).sqlwas the whole computation. Since ruling D on [Decision] analytics field gate (#20917): an authored cube member whosesqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943 (5d5e679873), a cube member'ssqlis a column reference, so the three had nothing left to compute.enumWithRetiredValues(shared/retired-key.ts), the house value-level mechanism. The six aggregates (count,sum,avg,min,max,count_distinct) are the whole vocabulary.tsc, because it is gone from the type. It is refused at parse with a named prescription, at the enum, at a metric'stypeand at a cube'smeasures.METRIC.type.sum,avg,minormaxover the column;countover'*'or over a column; orcount_distinct. A per-row value becomes a stored or formula field of the object that the measure aggregates. A value derived from measures isderived: { op, of }on an ADR-0021 dataset.cube-metric-expression-types-retired(migrations/entries/semantic/), with a step-18 rationale fragment (order 62).registry.tswas regenerated bygen:migration-registryafter each merge, never edited by hand.RETIRED_KEYS_BY_MAJORrow, because no key left the shape.analytics_cuberowmeasures.typestayslive, re-verified 2026-10-02. The narrowing is recorded, and the evidence now namesaggregateOfMeasureinstead of the partition.content/docs/references/data/analytics.mdxmoved, losing the three values from the enum list and from bothtypecells.api-surface,authorable-surface,json-schema.manifestandapi-surface-signaturesare byte-identical, as the playbook predicts for an enum-value narrowing.spec-changes.jsonand the upgrade guide stay at protocol 17, and both checks are green.@module data/analyticsmarker. Without it, moving the imports below the header dropped the page's opening paragraph:lib/file-description.tsrule 3 does not select a block inside the import list without the marker. The page's description is byte-identical tomain.CUBE_MEMBER_SQLdocblock no longer says that the ObjectQL path refuses the partition, in the present tense.@objectstack/service-analytics: theEXPRESSION_METRIC_TYPESpartition is deleted. It is replaced by ONE verdict both strategies call,aggregateOfMeasure(strategies/native-sql-strategy.ts).aggregateOfMeasureadmits a type this runtime lowers (theAGGREGATE_SQLkeys, pinned EQUAL to the enum's options). It refuses everything else with the SPEC's own words,AggregationMetricType.safeParse(type), so the runtime keeps no list of metric types, retired or otherwise.NativeSQLStrategy#resolveMeasureSqlasks it before anything is lowered. The verbatim emit is gone, and so is the unrecognised-type throw it replaced.ObjectQLStrategy#resolveMeasureAggregationasks it at the one resolver both doors call. TheINVALID_FIELDarm for the partition is gone.plugin.ts(the bridge's comment, its docblock, and its runtime message, which said "a custom-SQL measure is refused earlier"),preview-evaluator.ts,analytics-service.ts,cube-measure-field-type-door.tsanddataset-refusal.ts.@objectstack/lint(test and comment only). #21435 landed between my merges with a pin asserting that the three types sit outside the aggregate table. That is false after this retirement, so the assertion now reads[]. The skip-5silentcase is kept. No changeset is needed: a comment and a test, nothing in the published output changes.Clause-②, measured
node scripts/pm/check-widening-tells.mjs --declaration no --diff(the merge-base diff againstb79301000c) exits 0, with no widening tell. Three key lines are reported as a stated silence. They are the retired-member prescription entriesnumber:/string:/boolean:in theenumWithRetiredValuesmap, which are refusals, not accept-set members.No export-listing row was added (
check:api-surfacegreen, byte-identical), so the line isClause-②: no (narrowing). Both changesets are BREAKING, with!, a BREAKING banner, the(narrowing)arm, exactly one ADR-0087 marker (registered cube-metric-expression-types-retired) andminor.Census (examples, packages, platform objects, objectui)
The instrument is an AST walk over every object-literal member of a
measures:record. It covered 7,654.ts/.jsfiles underexamples/**andpackages/**at4ec505761d, platform objects included.count164,sum71.service-analyticstests, built without the parse.examples/**, in non-testpackages/**, inskills/**and incontent/docs/**(one cube example there,count/sum).examples/app-showcase/src/data/analytics/showcase.cube.ts: 3 measures (count,sum,avg). Its threetype: 'string'lines (48, 53, 63) are dimensions.DimensionTypeis a separate enum, unchanged, and pinned in the new test file.measures: zero..objectui-shapin89cad75d55: 0 mentions ofAggregationMetricType, and 0 record-form measure entries over 528 files that mentionmeasures. Control:clientValidation.tsnamesCubeSchema. The Console Pin Gate is not at risk: no export left.Premise check (zone 2)
68c5ab7eba,AggregationMetricType(data/analytics.zod.ts:27) listed the three, andMetricSchema.typeused it. Measured throughAnalyticsServicewith a columnsql:SELECT status AS "status", amount AS "m" FROM "orders" GROUP BY status;INVALID_FIELD/ 400.api/analytics.zod.ts:231: the/analytics/metamember's describe ("Aggregation type for a measure (AggregationMetricType)") is not made false by the retirement, so it is not edited.typeis a separatez.string()field, deliberately not the enum, because the projection copies the value verbatim. Measured: it is not the enum.What a stored cube carrying a retired type meets (fail closed, never stood down)
Pinned in
cube-metric-expression-types-retirement.test.ts:ObjectStackDefinitionSchema, the parseMetadataPluginruns a built artifact through) refuses it atanalyticsCubes.0.measures.m.typewith the prescription;defineStackrefuses it withSTACK_SCHEMA_INVALID/ 422;defineCubeand theanalytics_cubewrite door (getMetadataTypeSchema('analytics_cube'), whatPUT /api/v1/meta/analytics_cube/NAMEvalidates) refuse it too;applyConversionsToStoredItem) replays NOTHING over it. Control: the same row's retired sub-day granularity IS rewritten, so the seam is live. The stored row reaches the parse as stored, and the parse refuses it.Measured through the real dispatcher routes (a temporary
packages/runtimeprobe, not committed), for a cube a host registers in-process WITHOUT the parse:POST /api/v1/analytics/queryandPOST /api/v1/analytics/sql, on both strategies:500, witherror.messagecarrying the spec's prescription verbatim. Nothing executed.sumcontrol:200.GET /api/v1/analytics/meta:200, listing the measure withtype: "number"as registered (see the Acceptance notes).Merges (serial constraints)
0d182f0549mergedmainat3a6d92f78b, bringing PR fix(plugin-security,service-analytics): a boolean comparand is judged by the spec verdict at the RLS compile seam and in the NativeSQL strategy #21424 (A boolean comparand is judged only at the engine door: the RLS compile seam and analytics NativeSQL pass a string against a declared boolean field as written (the family of #21333) #21376, the NativeSQL filter-compile region) and PR feat(spec)!: a pie / donut / funnel / treemap / sankey widget takes one measure with a dimension too — refuse two or more at values, and rename the check to checkDashboardWidgetChartMeasureArity (#21293) #21425 (spec(ui): refuse two or more measures on a dimensionedpie/donut/funnel/treemap/sankeywidget too — the single-series types bind one measure whatever the dimension (extends #20958) #21293, its registry entry). Clean.587d9d4b63mergedmainatd7d5b4f96a. One hand conflict, in theobjectql-strategy.tsimport fromnative-sql-strategy.js: fix(service-analytics): the ObjectQL face echoes an offset with no limit as a statement the dialect runs #21440 addedwindowClauseSql, and both are kept.4ec505761dmergedmainatb79301000c, bringing PR feat(spec)!: the analytics row wildcard '*' is admitted only where a count consumes it (#21409) #21431 (analytics:'*'runs only undercount, but a cube measure's or dimension'ssqland a dataset measure'sfieldadmit it under any aggregate — a summed'*'answers 500 at the dataset door (split from #21000) #21409, the count-only boundary). One hand conflict, in theanalytics.zod.tsimports:enumWithRetiredValuesand analytics:'*'runs only undercount, but a cube measure's or dimension'ssqland a dataset measure'sfieldadmit it under any aggregate — a summed'*'answers 500 at the dataset door (split from #21000) #21409'sANALYTICS_COLUMN_PATH/rowWildcardOutsideCount/rowWildcardOutsideCountRefusal, both kept.scripts/pm/os-regen-merge.sh. Its step 4 was run each time, andgen:migration-registryafterwards wrote no diff.4ec505761d: 349 semantic, 244 retired-key and 212 retired-def entries. The siblings' idsanalytics-row-wildcard-outside-count-refusedanddashboard-widget-single-series-multi-measure-refusedappear at the same counts as onmain(1 each).cube-metric-expression-types-retiredappears twice: the semantic entry and its step-18 rationale fragment.4ec505761d: themeasures.sqlanddimensions.sqlnotes carry analytics:'*'runs only undercount, but a cube measure's or dimension'ssqland a dataset measure'sfieldadmit it under any aggregate — a summed'*'answers 500 at the dataset door (split from #21000) #21409's count-only wording and no longer name this card.measures.typecarries this retirement's own wording.Tests (head
4ec505761d)@objectstack/spec:vitest --project local: 602 files, 17737 passed, 1 todo;--project repo: 43 of 49 files, 705 passed (the other six are NOT MEASURED, below);typecheck(tsc, scripts, test layer): OK. The new@ts-expect-error(a typedMetricwithtype: 'number') sits in the compiled test program.@objectstack/service-analytics: 170 files, 3846 passed, 126 skipped.typecheckOK.Cubewithtype: 'number'failedtscwith TS2322 against the rebuilt.d.tsuntil it was cast.@objectstack/lint: 119 files, 5592 passed.typecheckOK.Ablations
Both run from the committed tree through
scripts/ablation-replace.mjs. In each, the anchor hit once and the blob changed; the restore was proved by blob equal toHEADand an emptygit diff HEAD. Both subjects resolve fromsrc, so no rebuild was needed.aggregateOfMeasure's table check removed): 28 failed, 12 passed, overmetric-type-coverage,measure-expression-both-strategiesandmeasure-expression-sql.numberprescription is unmapped: 8 failed, 17 passed incube-metric-expression-types-retirement.test.ts. Everynumberdoor pin went red; thestring/booleanpins and the controls stayed green.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat4ec505761dderived 117 families. All 117 were run, each exit code recorded, and--rananswered "117 derived, 117 run, 0 NOT-MEASURED (a DERIVED zero)". Among them:check-adr-0087-registration("2 declared-breaking changeset(s), each carrying an ADR-0087 disposition");check-changeset-no-major;check:generated("All 15 generated artifacts are up to date");check:liveness,check:doc-authoring,check:nul-bytesandcheck:skill-examples(after building the client closure);check:type-check-debt(325 s);check:dual-build-cjs-loads(after a whole-repo build, 71 of 72 tasks cached).NOT MEASURED
@objectstack/specrepo-project filesbuild-schemas-check-mode,dist-freshness,dist-freshness-adoption,publish-smoke-boot-failure,publish-smoke-port-collisionandschema-tree-freshness. Reason: they drive whole builds and exercise build tooling this diff does not touch.@objectstack/cliintegration tier, declared to CI.Acceptance notes
GET /analytics/metastill lists a host-registered unparsed cube's measure with its retiredtype, while the query doors refuse it. This is pre-existing for any enum-invalid type (medianread the same at base). It is reachable only by a host that registers a cube literal withoutCubeSchema, because every parsing door refuses it first. Not filed; carrier: none.analytics_cuberow read atGET /api/v1/meta/analytics_cube/NAMEcomes back as stored. Stored rows keep being read (the runtime gate's D4 asymmetry), and no conversion rewrites it (pinned at the seam).sql(the branch becomes unreachable) #20965's measurement), so such a row reaches no query.INVALID_FIELD/ 400 to the undeclared-500 tier. The message is readable and carries the prescription. This is the tierdataset-refusal.tsassigns to a cube that never met the parse, and the changeset says so.median) on the ObjectQL path is now refused at the resolver in the same tier. Before, it was forwarded toexecuteAggregate: the auto-bridge refused it, a host's own executor received it, and/analytics/sqlechoedMEDIAN(amount).aggregate-bridge-function-vocabulary.test.tstherefore pins both seams. The bridge is driven directly through the service's strategy context, because no cube path reaches it with a non-aggregate method any more.measures.sqlledger note ([Decision] analytics field gate (#20917): an authored cube member whosesqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943's, re-pointed by analytics:'*'runs only undercount, but a cube measure's or dimension'ssqland a dataset measure'sfieldadmit it under any aggregate — a summed'*'answers 500 at the dataset door (split from #21000) #21409) was made false by this diff, as at-tier record5959409102ruled. It was corrected in patch round 2 (4278601b82): both runtime expression branches are gone (the gate's stand-down with service-analytics: delete the analytics field gate's stand-down on an authored cube expression member once #20943 retires raw expressions in a cube member'ssql(the branch becomes unreachable) #20965, the raw-SQL verbatim emit here). What remains for a cube that reaches the service without meeting the parse isqualifyAndRegisterJoinpassing a non-columnsqlthrough inside the aggregate, measured throughgenerateSql.service-analytics:analytics-service.ts,cube-measure-field-type-door.ts,dataset-refusal.ts, and the testsaggregate-bridge-function-vocabulary,caller-member-column-reference-gate,cube-authored-format-granularity,field-read-admission-gateandunlisted-refusal-envelope;lint:validate-dataset-measure-aggregates(source comment and test).Generated by Claude Code