Repository navigation
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 |
…arand-shape face, whatever the column type (objectstack-ai#21448) (objectstack-ai#21484) Fixes objectstack-ai#21448 Clause-②: no (narrowing) ## What this changes The shared comparand-shape face (`assertListComparandShapes`, `@objectstack/spec/data`) now refuses a LIST at every scalar operator, whatever the column type. That covers `$gt`, `$gte`, `$lt`, `$lte`, the text operators (`$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$icontains`, `$like`, `$ilike`) and the flags (`$null`, `$exists`, `$empty`). `$eq` and `$ne` keep their own ruled arms. - **Envelope:** `INVALID_FILTER` / 400, before any read. - **Sentence:** one sentence naming the operator, the field, the list and the path. Its leading clause is `driver-memory`'s `arrayComparandError` for the same condition, word for word. - **Remedy:** one value; `$in` (authoring `in`) for "one of these values"; `$between` (authoring `between`) for a range. This implements triage's ruling (5958292323) as written. There is one verdict, at the shared face. It is not in the number or boolean declared-type verdicts. The lowering gains no second rule and no `values[0]` read of a list. ## Measured on `origin/main` `b94a2a727`, SQLite and PostgreSQL 16.14 alike Through `AnalyticsService.query` / `.queryDataset` (what `POST /api/v1/analytics/query` and `/api/v1/analytics/dataset/query` relay), on both faces, and through `engine.find` on the real `ObjectQL`: | filter | engine-aggregate face | native face | `engine.find` | |---|---|---|---| | `{ amount: { $gt: [10, 99] } }` (number) | **200, 2** (the driver got `$gt: 10`) | 400, the number verdict | 400, the number verdict | | `{ amount: { $gt: [10] } }` | **200, 2** | 400, the number verdict | 400, the number verdict | | `{ amount: { $lte: [12, 1] } }` | **200, 2** (`$lte: 12`) | 400, the number verdict | 400, the number verdict | | `{ note: { $gt: ['a', 'z'] } }` (text) | **200, 3** (`$gt: 'a'`) | **200, 3** (bound `'a'`) | 400, driver-sql's bind refusal | | `[['note', '>', ['a', 'z']]]` | **200, 3** | **200, 3** | 400, driver-sql's | | `{ note: { $eq: ['b'] } }`, `{ note: { $ne: ['b'] } }` | 400, the face | 400, the face | 400, the face | | `{ note: { $contains: ['b', 'm'] } }` | 400, this package's LIKE gate | 400, the same | 400, driver-sql's | | controls: `$in: ['b']`, `$nin: ['b']`, `$gt: 10` | 1 / 2 / 2 | the same | the same | **Triage's "measure first": the engine door's own answer for the text cell.** The engine door does NOT bind the first member. On `driver-sql` it refused 400 in the driver's own words ("…cannot be bound as a SQL parameter…"). So its answer was right and only its wording was per driver. The same verdict now answers it first, in the face's words (pinned: the `engine.find` text cell). One position over, `driver-memory` ANSWERS a list at a text operator (`memory-matcher-array-and-date-comparand.test.ts`). The face now refuses that before any driver runs. **Dispatch assumptions this measurement corrected:** - `$eq: [x]` / `$ne: [x]` already answered one 400 on both faces (objectstack-ai#19757 / objectstack-ai#19886's arms). The live defect was the ordering operators, plus a text column's native face. - The lowering reaches the face through `filter-normalizer.ts`'s `assertWhereComparandShapes` (objectstack-ai#20010), not through `comparand-shape.ts`. ## Design - **The operator set is the spec's own:** `SCALAR_COMPARAND_OPERATORS`, the comparand-TYPE face's split, which `filter-comparand-type.test.ts` reconciles against `FieldOperatorsSchema`'s keys. - It moved verbatim from `filter-comparand-type.ts` into a new module outside the `data` barrel (`filter-comparand-operators.ts`). Both faces and the save door read ONE split, and nothing is published: `check:api-surface` is unchanged. - The face test also derives the arm's operators from the schema: every declared operator whose enforced slot refuses an array, which is all but `$in` / `$nin` / `$between`. - **No rule in the lowering.** `lowerAnalyticsWhere` already hands every field entry to the face before any leaf exists. So the arm reaches both analytics faces at every position (`where`, `runtimeFilter`, a dataset's scope, a measure's `filter`) with no code change there. `filter-normalizer.ts` and `comparand-shape.ts` change docblocks only; they state the invariant that only a list operator's array is spread into a leaf's `values`. - **The save door asks the same face** (`filter-save-door-refusals.ts`). - A stored dataset, measure, widget or report filter carrying the shape is refused on save, in the face's sentence less its location. The parity test's §2 requires a save-door sentence for every face arm. - The HTTP routes that Zod-parse a filter in their body therefore answer `VALIDATION_FAILED` / 400, located on the member, as for every other face arm. In-process callers get `INVALID_FILTER` / 400. Both layers are pinned. - **The native number arm (PR objectstack-ai#21446).** `judgedComparands` lowers through `lowerAnalyticsWhere` first, so that arm's `array` refusal is no longer reached at a scalar operator from any native position. `native-sql-strategy.ts` is untouched; the now-unreachable branch is a note, not an edit. - **Flags.** A list at `$null` / `$exists` / `$empty` now reads in the shape sentence, because how many values comes before which value. A non-boolean scalar flag keeps the boolean rule's sentence. ## Pins - **New, `service-analytics`:** `list-at-scalar-operator-both-faces.test.ts`, run on SQLite and PostgreSQL. Each measured cell, plus `$gte` / `$lt: []` / `$contains` / `$startsWith`, `$or` / `$not`, and the FilterArray spelling, is checked at both doors. - Each cell answers one 400 on both faces, with the same message on each face and no raw statement, engine aggregate or driver read. - A registered dataset's scope and measure filter are refused the same way. `DatasetSchema` refuses the stored filter on save, in the sentence less its location. - `engine.find` refuses the text cell in the face's words, not the driver's. - Controls (`$in`, `$nin`, scalar `$gt`, `$between`) count alike on both faces. - **Both-faces pin:** PR objectstack-ai#21446's native-only `$gt: [10]` cell is now a both-faces cell in `native-sql-number-comparand-door.test.ts`. - **New, `@objectstack/spec`:** a `filter-comparand-shape.test.ts` block covering the derived operator set; every list shape (pair, one member, strings, empty); nested paths; every AST spelling that carries a value; the message and remedy (`$in`, `$between`, both declared); flags; controls; the 500-char bound. - **Moved because the face now answers first** (each row left its old table and is pinned as the shape face's): - the declared-type corpora (`filter-number-` / `filter-boolean-comparand-declared-type.ts`) and their tests: list rows only at list members now; - `filter-save-door-face-parity.test.ts` (§1: every declared operator is face-judged; §2: new rows); - objectql's number, boolean and aggregate-flag doors; - REST's number and boolean data doors; - `analytics-filter-refusal-envelope.test.ts` (a new HTTP cell); - analytics' flag, `$empty` and type-face tests. ## Ablations Each leg was committed first, mutated through `scripts/ablation-replace.mjs` (WRAP, with the restore trapped), and its restore proven by blob == HEAD and an empty `git diff HEAD`. - **A: the spec arm deleted.** Rebuilt; `ablation-dist-preflight.mjs @objectstack/spec 'throw arrayScalarComparandError(' --absent` exit 0. - **Predicted:** each list-at-scalar cell goes back to a 200 (or to the native number verdict on a number column); `$eq` / `$ne` and the controls stay green. - **Observed:** analytics went 60 failed / 142 passed (30 cells × SQLite and PG): - the text and number cells were answered 200 on the engine-aggregate face; - the LIKE cells fell to the analytics LIKE gate's words; - `engine.find` answered in driver-sql's words; - the save door accepted the stored filter; - every `$eq` / `$ne` and control cell stayed green. - The face test's new block went 12 red. Restore leg: rebuilt, marker present in `dist/`, tree clean, 202 / 202 green. - **B: the lowering's consumption deleted** (`assertWhereComparandShapes`' face hand-over). The subject resolves from `src`, so no rebuild is owed. - **Predicted:** the object-spelling analytics cells go red; the FilterArray spelling, `$eq`, the save door and `engine.find` stay green. - **Observed:** 48 failed (24 cells × 2 drivers: the object-spelling cells, `$ne` included, and the registered scope and measure). 0 failures among the FilterArray, `$eq`, save-door and `engine.find` cells. ## Verification, at the merged head `2b9fd4f5e` (`origin/main` merged in) - **Full suites:** - `@objectstack/spec` test: 602 files / 17754 tests green. - `@objectstack/service-analytics` test, with PostgreSQL 16.14: 172 files green. One file's 4 live-PG cells need a UTC server; see the acceptance notes. - `@objectstack/objectql` test: 366 files green. One barrel-import test timed out at 5 s at load ~7, then 34 / 34 when run alone. - REST door pins: 4 files, 67 tests (MySQL cells are named skips). - **Typecheck:** spec, service-analytics, objectql and rest all green. - **Face importers, at the pre-merge head:** driver-memory, driver-mongodb, driver-turso, driver-sql (with a non-UTC PostgreSQL server), lint, metadata-core, metadata-protocol, plugin-security and plugin-sharing are all green. - **Gates:** `dispatch-gates.mjs --commands` at `2b9fd4f5e` derives 90 families. All 90 ran with recorded exit codes, all 0, including `check:dual-build-cjs-loads` (105 require entries across 66 packages load). `--ran` reconciles 90 run / 0 NOT MEASURED. - **Lint (narrowed, measured):** `eslint --no-inline-config --format json` over the 24 changed `.ts` files gives 24 files, 0 errors, 0 warnings, none ignored. - The population is read from eslint's own output. - Invariance: `eslint.config.mjs` has no type-aware linting (no `parserOptions.project` / `projectService`), so this diff cannot move an untouched file's verdict. - **Docs:** grepping `content/docs/**` (outside `releases/`) and `skills/**` for the comparand-shape rules and the filter operators found no sentence made false. ## Blast radius - **Shipped producers** writing a list at a scalar operator (object form, `[field, op, value]` and `{ field, operator, value }`, across `examples/`, `skills/`, `content/docs/`, `apps/` and `packages/**` non-test sources): none. The CEL lowering already refuses one (`cel-to-filter.ts`). - **NOT MEASURED:** objectui's console filter builder. `../objectui` is not checked out here, and `packages/console/dist` is not built. ## File surface against the claim The claim named `filter-normalizer.ts`, `comparand-shape.ts`, spec `filter-comparand-shape.ts` and its test, the pins and the changeset. Added, each a consequence of the narrowing inside the rule's consumer radius: - `filter-comparand-operators.ts` (new, internal); - `filter-comparand-type.ts` (the split's import, and one now-false sentence); - `filter-comparand-refusal-text.ts` (the shared sentence); - `filter-save-door-refusals.ts` (the save door's sentence); - the two declared-type corpora and the parity test; - the objectql, REST and analytics pins listed above. None of `native-sql-strategy.ts`, `objectql-strategy.ts`, `analytics-service.ts` or `preview-evaluator.ts` is touched. ## Acceptance notes - **Out of scope; reported to the seat, not filed here.** On PostgreSQL with the server TimeZone set to `Asia/Shanghai`, `objectql-face-order-limit.test.ts`'s live cells answer the newest month bucket of a `date` column holding `2026-06-01` as `2026-05` (2 rows). At UTC the answer is `2026-06` (1 row). The cell is the engine-aggregate face, since the native face declines granularity. Not touched by this diff. - The compilers' `values[0]` reads stay as they are. With the face's arm, no list reaches a scalar leaf through any analytics door. - PR objectstack-ai#21452 (which held the analytics strategies) landed while this was open. It was merged in at `2b9fd4f5e` cleanly, with no overlap. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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