Skip to content

feat(spec,service-analytics)!: retire the cube metric types number / string / boolean, refused in both analytics strategies in the spec's words (#21000) - #21452

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21000-retire-expression-metric-types
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-21000-retire-expression-metric-types

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21000
Clause-②: no (narrowing)

Dispatched by the claim 5955932074 (PM loop round 1, domain:spec seat 1, session session_01UtnxvdiN376GF3sgXwAw4d), on triage's answer B 5952826307: the enum retirement only. The row wildcard boundary is #21409's (landed as b79301000c, merged here).

What this does

@objectstack/spec: AggregationMetricType loses number, string and boolean (ADR-0049 enforce-or-remove, grade 5923362062).

  • They declared "a custom SQL expression returning a number / string / boolean": the measure's sql was the whole computation. Since ruling D on [Decision] analytics field gate (#20917): an authored cube member whose sql is an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943 (5d5e679873), a cube member's sql is a column reference, so the three had nothing left to compute.
  • The enum is declared through enumWithRetiredValues (shared/retired-key.ts), the house value-level mechanism. The six aggregates (count, sum, avg, min, max, count_distinct) are the whole vocabulary.
  • An authored retired type fails tsc, because it is gone from the type. It is refused at parse with a named prescription, at the enum, at a metric's type and at a cube's measures.METRIC.type.
  • The prescription names the aggregate that fits: sum, avg, min or max over the column; count over '*' or over a column; or count_distinct. A per-row value becomes a stored or formula field of the object that the measure aggregates. A value derived from measures is derived: { op, of } on an ADR-0021 dataset.
  • A value the enum never declared keeps zod's own message.
  • The prescriptions are module-private, so the export surface does not grow.
  • ADR-0087: D3 entry cube-metric-expression-types-retired (migrations/entries/semantic/), with a step-18 rationale fragment (order 62). registry.ts was regenerated by gen:migration-registry after each merge, never edited by hand.
    • There is no D2 conversion, by design: the column alone does not say which aggregate the author meant, and a stored cube is refused, never rewritten.
    • There is no RETIRED_KEYS_BY_MAJOR row, because no key left the shape.
  • Liveness: the analytics_cube row measures.type stays live, re-verified 2026-10-02. The narrowing is recorded, and the evidence now names aggregateOfMeasure instead of the partition.
  • Generated: only content/docs/references/data/analytics.mdx moved, losing the three values from the enum list and from both type cells.
    • api-surface, authorable-surface, json-schema.manifest and api-surface-signatures are byte-identical, as the playbook predicts for an enum-value narrowing.
    • spec-changes.json and the upgrade guide stay at protocol 17, and both checks are green.
  • The module header gained an @module data/analytics marker. Without it, moving the imports below the header dropped the page's opening paragraph: lib/file-description.ts rule 3 does not select a block inside the import list without the marker. The page's description is byte-identical to main.
  • The neighbouring CUBE_MEMBER_SQL docblock no longer says that the ObjectQL path refuses the partition, in the present tense.

@objectstack/service-analytics: the EXPRESSION_METRIC_TYPES partition is deleted. It is replaced by ONE verdict both strategies call, aggregateOfMeasure (strategies/native-sql-strategy.ts).

  • aggregateOfMeasure admits a type this runtime lowers (the AGGREGATE_SQL keys, 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#resolveMeasureSql asks it before anything is lowered. The verbatim emit is gone, and so is the unrecognised-type throw it replaced.
  • ObjectQLStrategy#resolveMeasureAggregation asks it at the one resolver both doors call. The INVALID_FIELD arm for the partition is gone.
  • Comments that named the partition or the three types are updated: 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.ts and dataset-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-5 silent case 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 against b79301000c) exits 0, with no widening tell. Three key lines are reported as a stated silence. They are the retired-member prescription entries number: / string: / boolean: in the enumWithRetiredValues map, which are refusals, not accept-set members.

No export-listing row was added (check:api-surface green, byte-identical), so the line is Clause-②: no (narrowing). Both changesets are BREAKING, with !, a BREAKING banner, the (narrowing) arm, exactly one ADR-0087 marker (registered cube-metric-expression-types-retired) and minor.

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/.js files under examples/** and packages/** at 4ec505761d, platform objects included.

  • 321 measure entries in total. Lit control: count 164, sum 71.
  • Retired-type entries: 7. All are deliberate refusal fixtures in the service-analytics tests, built without the parse.
  • Zero hits in examples/**, in non-test packages/**, in skills/** and in content/docs/** (one cube example there, count / sum).
  • examples/app-showcase/src/data/analytics/showcase.cube.ts: 3 measures (count, sum, avg). Its three type: 'string' lines (48, 53, 63) are dimensions. DimensionType is a separate enum, unchanged, and pinned in the new test file.
  • JSON fixtures carrying record-form measures: zero.
  • objectui at the .objectui-sha pin 89cad75d55: 0 mentions of AggregationMetricType, and 0 record-form measure entries over 528 files that mention measures. Control: clientValidation.ts names CubeSchema. The Console Pin Gate is not at risk: no export left.

Premise check (zone 2)

  1. Holds. On 68c5ab7eba, AggregationMetricType (data/analytics.zod.ts:27) listed the three, and MetricSchema.type used it. Measured through AnalyticsService with a column sql:
    • the raw-SQL path SERVED the column unaggregated: SELECT status AS "status", amount AS "m" FROM "orders" GROUP BY status;
    • the ObjectQL path refused the measure INVALID_FIELD / 400.
  2. api/analytics.zod.ts:231: the /analytics/meta member's describe ("Aggregation type for a measure (AggregationMetricType)") is not made false by the retirement, so it is not edited.
  3. That type is a separate z.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:

  • the artifact boot door (ObjectStackDefinitionSchema, the parse MetadataPlugin runs a built artifact through) refuses it at analyticsCubes.0.measures.m.type with the prescription;
  • defineStack refuses it with STACK_SCHEMA_INVALID / 422;
  • defineCube and the analytics_cube write door (getMetadataTypeSchema('analytics_cube'), what PUT /api/v1/meta/analytics_cube/NAME validates) refuse it too;
  • the rehydration seam (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/runtime probe, not committed), for a cube a host registers in-process WITHOUT the parse:

  • POST /api/v1/analytics/query and POST /api/v1/analytics/sql, on both strategies: 500, with error.message carrying the spec's prescription verbatim. Nothing executed.
  • sum control: 200.
  • GET /api/v1/analytics/meta: 200, listing the measure with type: "number" as registered (see the Acceptance notes).

Merges (serial constraints)

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 typed Metric with type: 'number') sits in the compiled test program.
  • @objectstack/service-analytics: 170 files, 3846 passed, 126 skipped. typecheck OK.
    • The reverse verification happened on the way: a fixture typed Cube with type: 'number' failed tsc with TS2322 against the rebuilt .d.ts until it was cast.
  • @objectstack/lint: 119 files, 5592 passed. typecheck OK.

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 to HEAD and an empty git diff HEAD. Both subjects resolve from src, so no rebuild was needed.

  • The runtime verdict admits every string (aggregateOfMeasure's table check removed): 28 failed, 12 passed, over metric-type-coverage, measure-expression-both-strategies and measure-expression-sql.
    • Every refusal case went red: both strategies, both doors, and the drift case.
    • The admitted-aggregate, cross-object-twin and coverage-equality cases stayed green, which is the predicted direction.
  • The spec's number prescription is unmapped: 8 failed, 17 passed in cube-metric-expression-types-retirement.test.ts. Every number door pin went red; the string / boolean pins and the controls stayed green.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 4ec505761d derived 117 families. All 117 were run, each exit code recorded, and --ran answered "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-bytes and check: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/spec repo-project files build-schemas-check-mode, dist-freshness, dist-freshness-adoption, publish-smoke-boot-failure, publish-smoke-port-collision and schema-tree-freshness. Reason: they drive whole builds and exercise build tooling this diff does not touch.
  • @objectstack/cli integration tier, declared to CI.

Acceptance notes


Generated by Claude Code

claude added 9 commits October 2, 2026 16:06
…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>
…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
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/service-analytics, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/lint/src/validate-dataset-measure-aggregates.ts, packages/services/service-analytics/src/analytics-service.ts, packages/services/service-analytics/src/cube-measure-field-type-door.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/natural-language-queries.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/data-modeling/queries.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/deployment/validating-metadata.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/kernel/contracts/data-engine.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/plugins/packages.mdx (via AnalyticsServicePlugin (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/ui/dashboards.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/releases/v17/17-0.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/releases/v17/17-5.mdx (via count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))
  • content/docs/releases/v17/17-6.mdx (via AnalyticsServicePlugin (symbol, a top-level class), count_distinct (literal, a string literal in AggregationMetricType; a string literal in resolveMeasureAggregation))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/lint/src/validate-dataset-measure-aggregates.ts, packages/services/service-analytics/src/analytics-service.ts, packages/services/service-analytics/src/cube-measure-field-type-door.ts, …) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 138 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 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b26d58fe215a4b37e0270cee7104d329bf4e308e — the merge of head 4278601b824d5c64fd7802cfc8cc4e7485b7d7ab into base 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9b7a0ef3faf18cb828ac18aaa2f701cf0227cd2c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4ec505761df4971c26625bcf46759d020c1070e3
Local-runs: none

Isolated contract review of PR #21452 (card #21000) at the head above: the net diff against main (merge-base b79301000c, 27 files, +1004 / -419), the card's body and every comment (triage 5923362062, retriage answer B 5952826307, claim 5955932074, dev report 5959076029), the PR body and both changesets, and the check-runs on this head. Nothing was built, tested or re-run; the one three-way merge reading below is git merge-tree without --write-tree, a read of the object store that touches no checkout.

① Derived judgments

Accept-set delta — right. The only change to any accept set is the three members number / string / boolean leaving AggregationMetricType (packages/spec/src/data/analytics.zod.ts), declared through enumWithRetiredValues (a plain ZodEnum of the six, with an error map). Each door carries the narrowing by reference, with no second copy to drift: MetricSchema.type is the enum (analytics.zod.ts:387); CubeSchema.measures is z.record(z.string(), MetricSchema) (:671); defineCube parses CubeSchema; ObjectStackDefinitionSchema.analyticsCubes is z.array(CubeSchema) (stack.zod.ts:709) and defineStack answers STACK_SCHEMA_INVALID / 422; getMetadataTypeSchema('analytics_cube') is CubeSchema (metadata-type-schemas.ts:283), so the analytics_cube write door refuses it too. Each retired member is refused by name with the prescription (the six aggregates, where a per-row value goes, where a derived value goes); a value the enum never declared keeps zod's own message; the six aggregates and DimensionType are unchanged — all pinned in the new cube-metric-expression-types-retirement.test.ts and the narrowed analytics.test.ts. The rehydration seam replays nothing over a stored row carrying one (pinned with a live-seam control), so a stored cube is refused, never stood down, as triage required. The /analytics/meta member shape (api/analytics.zod.ts:231) is z.string() by design and its describe is not made false; AggregationFunction (the dataset measure's vocabulary) is a separate enum and is untouched.

Census — right. Re-read on origin/main and at the head: no cube measure in examples/**, non-test packages/**, skills/** or content/docs/** carries one of the three; every type: 'string' / 'number' hit is a cube dimension, a dataset dimension or a result fields[] entry (the showcase cube's three type: 'string' lines are dimensions). objectui at the pin .objectui-sha = 89cad75d55 (unchanged by this PR): zero mentions of AggregationMetricType or EXPRESSION_METRIC_TYPES, and no cube measure of the three; its type: 'number' hits are dataset result fields. No export this PR removes is imported by the sibling, so Console Pin Gate (skipped on this head, path-filtered) is not at risk.

Published type surface — right, and declared. The exported AggregationMetricType type (the enum's z.input) narrows from a nine-member to a six-member union in the published .d.ts, so tsc now refuses the three at a typed Metric (pinned with @ts-expect-error). The four ratchets are untouched by the diff and encode no enum members: api-surface/data.json lists AggregationMetricType (const) / (type) by name only, api-surface-signatures.json hashes the define* signatures, json-schema.manifest/** lists emitted schema names. So a value-level narrowing is byte-invisible to them, exactly as enumWithRetiredValues's own docblock predicts, and the declaration channels it owes are the changeset and the ADR-0087 entry — both present. No export-listing row is added (aggregateOfMeasure is a module export of native-sql-strategy.ts, not re-exported from service-analytics' entry, whose exports map has . only; EXPRESSION_METRIC_TYPES was likewise never on the entry). Clause-②: no (narrowing) is the right arm: nothing widens, and the narrowing is declared BREAKING.

Runtime half — right. EXPRESSION_METRIC_TYPES is deleted and both strategies call one verdict, aggregateOfMeasure(cube, member, type): it admits exactly the own keys of AGGREGATE_SQL (pinned equal to AggregationMetricType.options in metric-type-coverage.test.ts, and to CONDITIONAL_AGGREGATE_SQL's keys in aggregation-lockstep.test.ts) and refuses everything else with AggregationMetricType.safeParse(type)'s own message — the retirement prescription for a retired member, zod's six-member list for a never-declared value — as a bare Error. NativeSQLStrategy#resolveMeasureSql asks it before any lowering, so the verbatim-emit branch and the unrecognised-type throw are both gone; ObjectQLStrategy#resolveMeasureAggregation asks it at the one resolver both doors share, so /analytics/query and /analytics/sql accept and refuse the same set. Pinned on both strategies and both doors in measure-expression-both-strategies.test.ts and measure-expression-sql.test.ts, with the aggregate controls still served.

The tier change — right, and stated truthfully. On ObjectQL the three move from INVALID_FIELD / 400 to the undeclared-500 tier, and median is now refused at the resolver in that tier instead of being forwarded to executeAggregate. The 400 was right while the three were spec-valid: an authored cube could carry one and the engine could not serve it, so the caller was the right addressee. After this retirement no parsing door lets one through, so a cube carrying one reaches the service only when a host registers a literal without CubeSchema — the population dataset-refusal.ts's header already assigns to the undeclared-500 tier, with the stated reason that a 400 would tell a dashboard user to fix metadata they cannot see and hide a platform defect from ops alerting. The message stays readable and carries the prescription verbatim, so the input is still named to whoever reads the response. The service-analytics changeset states the move in its before/now table and in a sentence of its own, and names the median forwarding it removes.

Text this PR makes false — one owed correction left undone (FAIL point). packages/spec/liveness/analytics_cube.json, row measures.sql, still reads: "The runtime's expression branches (verbatim emit on the raw-SQL path, the gate's stand-down) remain for a cube that reaches the service without meeting the parse; their deletion is the services-lane follow-up." The verbatim emit named there is resolveMeasureSql's if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;, which THIS diff deletes (native-sql-strategy.ts, the hunk at the old :1576-:1626); the stand-down went with #20965 before this PR. In this PR's own tree neither branch remains and this PR is the follow-up the sentence points forward to, so the sentence is made false by this diff and is owed here. It is one sentence in a file this diff already edits (the neighbouring measures.type row), and row ownership by the card that last re-pointed the note is not a rule that defers it. The dev saw it (second out-of-scope finding, "Not edited, because the row is the just-landed boundary's") and declined. Everything else checked reads true at the head: the measures.type note and evidence (narrowing recorded, aggregateOfMeasure named, keys pinned EQUAL, verifiedAt re-dated); the dimensions.sql note; the regenerated content/docs/references/data/analytics.mdx (the enum list and both type cells lose the three, the page description byte-identical); the CUBE_MEMBER_SQL docblock (past tense for the retired partition, present tense for the forwarding that still happens); every changed comment in plugin.ts, preview-evaluator.ts, analytics-service.ts, cube-measure-field-type-door.ts, dataset-refusal.ts and the lint source; and the D3 entry's acceptance criteria (parse every cube through CubeSchema, the write door or defineStack; each refusal is one measure to retype; a six-aggregate measure parses byte-identically). The earlier step-18 entries that describe "a custom-SQL measure" are historical records of what those steps did and are not rewritten.

② Semver level

Two changesets, read sentence by sentence against the diff.

  • .changeset/21000-cube-metric-expression-types-retired.md — '@objectstack/spec': minor, feat(spec)! with a BREAKING banner: the package is right (the enum, the D3 entry, the registry region, the ledger row and the reference page all live in packages/spec); the FROM → TO table and the one-line fix match the prescription text; "no D2 conversion", "no RETIRED_KEYS_BY_MAJOR row", "the four surface ratchets byte-identical", "liveness row stays live", "reference page regenerated" and the objectui census all read true at the head. Clause-②: no (narrowing) matches the diff. Exactly one ADR-0087 marker, registered cube-metric-expression-types-retired, and that semantic entry exists under step 18 in migrations/registry.ts with its rationale fragment (order 62).
  • .changeset/21000-analytics-metric-type-verdict.md — '@objectstack/service-analytics': minor, fix(service-analytics)! with a BREAKING banner: the before/now table (native served unaggregated → refused; ObjectQL INVALID_FIELD / 400 → refused in the undeclared-500 tier; median forwarded → refused) matches the two strategy diffs and the dev's base reading; "Removed export: EXPRESSION_METRIC_TYPES … internal to the package; not re-exported from its entry point" is confirmed against src/index.ts (the entry exports the two strategy classes only) and the exports map; "GET /analytics/meta … unchanged" is confirmed (getMeta is untouched). Clause-②: no (narrowing) matches. Exactly one ADR-0087 marker, the same entry.
  • Level: both narrowings are BREAKING and take minor under the launch-window convention; check-changeset-no-major and check-adr-0087-registration run in Check Changeset, which concluded success on this head. @objectstack/lint moves only a test and a source comment, so it publishes nothing and owes no changeset; skip-changeset is correctly absent.
  • The prescriptions name @objectstack/spec 17.7.0 (spec is at 17.6.0, published); that is the next minor and holds while check-changeset-no-major holds, as the dev's acceptance note says.

③ Boundary flags

Every deviation in 5959076029, answered:

  1. packages/lint test and comment edits, no lint changeset — accepted. fix(lint)!: os validate refuses an analyticsCubes sum / avg / min / max measure over a column the cube door refuses — every cube measure is judged by the aggregate × field-type table #21435 landed a pin asserting that ['boolean', 'number', 'string'] sit outside the aggregate table; this retirement makes that assertion false, so correcting it here is the "text a PR makes false" rule applied, and the silent skip-5 case is kept as the control. A comment-only source change publishes no behaviour, so no changeset is owed.
  2. Comment-only service-analytics files and the re-typed test fixtures — accepted. Each named the partition or built a fixture on a retired type; where a test's subject was the sql expression, the fixture was re-typed to sum and the subject kept (caller-member-column-reference-gate, field-read-admission-gate, the dotted-expression case in measure-expression-sql), and cube-authored-format-granularity now pins the refusal on all three routes with the former served control turned into a refusal control.
  3. The tier change — right and truthfully declared (see ①).
  4. The private baseCtx bridge in aggregate-bridge-function-vocabulary.test.ts — accepted, noted as a coupling. No cube path reaches the auto-bridge with a non-aggregate method any more, so the seam is driven directly through the service's strategy context; a rename of baseCtx turns this one test red, which is visible, not silent. Both seams are now pinned (resolver refusal with calls empty; bridge refusal driven directly).
  5. The @module data/analytics marker — accepted. packages/spec/scripts/lib/file-description.ts states that inside an import list only an explicit @module marker selects the header block ([finding] 10 spec modules put a doc block inside their import list where no mechanical signal says whether it is a module header — 8 of them publish a schema's doc #13334); the header now sits between two import groups, and the regenerated page's description is unchanged in the diff.
  6. Three main merges through os-regen-merge.sh, hand conflicts in two import blocks — both sides kept, confirmed at the head. objectql-strategy.ts:54 imports aggregateOfMeasure, windowClauseSql (this PR's name beside fix(service-analytics): the ObjectQL face echoes an offset with no limit as a statement the dialect runs #21440's); analytics.zod.ts imports enumWithRetiredValues, retiredKey and analytics: '*' runs only under count, but a cube measure's or dimension's sql and a dataset measure's field admit it under any aggregate — a summed '*' answers 500 at the dataset door (split from #21000) #21409's ANALYTICS_COLUMN_PATH, ANALYTICS_COLUMN_REFERENCE, rowWildcardOutsideCount, rowWildcardOutsideCountRefusal, and the rowWildcardOutsideCount refinement is present in MetricSchema. The registry diff is exactly the new semantic entry plus its rationale fragment; check:generated runs in Type Check · source gates, success on this head.
  7. main advancing after the last merge — not a blocker. origin/main is at 086ad0aa68 (fix(service-analytics): native SQL judges a comparand against a declared number column by the spec's verdict, as the comparand walk's second arm #21446, docs(spec): cli-extension TSDoc step 2 "Discover" says what loads an oclif plugin into os #21443), two commits past the merge-base b79301000c; the one overlapping file is native-sql-strategy.ts. A read-only git merge-tree of the base, this head and origin/main produces no conflict markers, and the hunks are disjoint (main's in the import block and the boolean-comparand region at the base's :15-:432, and in the class at :1094-:1153; this PR's at :2, :62, :122-:148, :439, :946, :985, :1544-:1626). The queue leg merges main on landing.
  8. The Clause-② wording — the claim's conditional resolved to no (narrowing), which is the arm the diff supports.

open_questions is empty. The two out-of-scope findings:

  • GET /api/v1/analytics/meta lists a host-registered unparsed cube's measure with its retired type verbatim while both query doors refuse it — a reading, escalated to the domain:spec seat; nothing filed here. The route's member type is z.string() by design ("copies the value through verbatim"), so for a cube that never met the parse, discovery and query disagree about one member. It is pre-existing for every enum-invalid type (median read the same at base), reachable only without CubeSchema, and has no named real producer; whether a card is owed is the seat's call, with the dev's dedupe words (analytics meta lists refused measure type, discovery unparsed cube type, getMeta enum-invalid measure).
  • The measures.sql ledger note — made false by this diff and owed here; see ① (the FAIL point).

The dev's NOT MEASURED items, and the check-runs on this head that measure them: the six @objectstack/spec repo-project files (build-schemas-check-mode, dist-freshness, dist-freshness-adoption, publish-smoke-boot-failure, publish-smoke-port-collision, schema-tree-freshness) are all listed in packages/spec/vitest.repo-tests.json, and spec#test:repo runs inside Test Core (1/6) through Test Core (6/6) (pnpm turbo run test test:repo over the changed-package filter); the @objectstack/cli suite runs in the same Test Core shards as OS_TEST_SHARD slices. check:api-surface runs in Type Check · consumer gates; check:liveness in Spec property liveness (success); check:generated in Type Check · source gates (success); check-adr-0087-registration and check-changeset-no-major in Check Changeset (success). check-widening-tells is the seat's enqueue gate, not a check-run; the diff's only added closed-set lines are the three prescription-map entries, which are refusals, not members. The check-run state at the time of this record is in the closing line below.

Check-runs on this head, read 2026-10-02T19:00Z (33 runs): 27 success — among them Build Core, Lint & Repo Gates, Type Check · source gates, Type Check · consumer gates (check:api-surface), Type Check · debt ledger, Check Changeset, Spec property liveness, Governed Surface Queue Guard, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, Dogfood Regression Gate (1/3, 2/3, 3/3 and the aggregate), Test Core (2/6), Test Core (4/6), Test Core (5/6); 2 skipped — Console Pin Gate, Packed-tarball smoke (opt-in); 0 red. Still in_progress at that read, not reported as passed: Test Core (1/6), Test Core (3/6), Test Core (6/6), Type Check · workspace. The spec#test:repo files and the @objectstack/cli slices named above run in the Test Core shards, so three of their six carriers were still running when this record was written; the seat reads their conclusions off the head before any landing act. This verdict does not turn on them.

What a patch round owes: rewrite the one measures.sql sentence so it says the two expression branches are gone (the stand-down with #20965, the verbatim emit with #21000) and that what remains for an unparsed cube is qualifyAndRegisterJoin passing a non-column sql through inside an aggregate; nothing else in this diff needs to move.

Implemented-by: claude/issue-21000-retire-expression-metric-types
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4278601b824d5c64fd7802cfc8cc4e7485b7d7ab
Local-runs: none

Narrow re-review of PR #21452 (card #21000) at the head above, following record 5959409102 (FAIL on 4ec505761d, one point: the measures.sql ledger sentence this diff made false). Read: the branch re-fetched and its head confirmed, the delta 4ec505761d..4278601b82, patch-round dev report 5959746846, the PR body as it now stands, and the check-runs on this head. Nothing was built, tested or re-run; the one three-way merge reading below is git merge-tree without --write-tree, a read of the object store.

① Derived judgments

The delta is exactly the one sentence — confirmed. One commit (4278601b82), one file, one line: packages/spec/liveness/analytics_cube.json, row measures.sql, field note. Compared mechanically between 4ec505761d and this head: the note's prefix (through "Still LIVE: the key itself is unchanged and read at the three sites above.") is byte-identical, the suffix from "The D3 entry is cube-member-sql-expression-retired" onward — #21409's count-only wording included — is byte-identical, and with that one note set aside the rest of the ledger file is byte-identical. verifiedAt stays 2026-10-02, which is the date of the re-measurement the sentence cites, so no bump is owed. No other path moved: the accept set, the published type surface, both strategies, the changesets, the registry and the reference page are the bytes record 5959409102 judged, so every ① judgment there stands.

The new sentence is true at the head. Read against native-sql-strategy.ts:

Text this PR makes false — none left. The measures.sql note no longer says the branches remain or points forward to a follow-up this PR is. The PR body's Acceptance bullet that called the note unedited has been replaced (seat-side) with one that names the correction and the head, so the PR body reads true as well. measures.type, dimensions.sql, the reference page, every changed comment and the D3 entry's acceptance criteria are unchanged since the previous record and still read true.

main since the merge-base (b79301000c): four commits, now at 9b7a0ef3fa (#21443, #21446, #21449, #21450). The one overlapping file is still native-sql-strategy.ts; a read-only git merge-tree of the base, this head and origin/main produces no conflict markers. The queue leg merges main on landing.

② Semver level

Carries from record 5959409102: the delta publishes nothing (a liveness-ledger note is not a released artifact, and no changeset is owed for it), so both changesets — @objectstack/spec: minor and @objectstack/service-analytics: minor, each BREAKING with !, Clause-②: no (narrowing) and exactly one ADR-0087 marker registered cube-metric-expression-types-retired — still match what the diff publishes. Check Changeset (which runs check-adr-0087-registration and check-changeset-no-major) concluded success on this head.

③ Boundary flags

Carries from record 5959409102: every deviation there was answered and accepted, the patch-round report declares deviations: [], open_questions: [] and out_of_scope_findings: [], and the earlier /analytics/meta reading remains escalated to the domain:spec seat with nothing filed. The dev's one owed seat-side edit (the PR body bullet) is done. The dev's NOT MEASURED carriers named in the previous record are unchanged: the spec#test:repo files and the @objectstack/cli slices run in the Test Core shards; check:api-surface in Type Check · consumer gates; check:liveness in Spec property liveness; check:generated in Type Check · source gates.

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 19:37:59Z): success on Build Core, Build Docs, Lint & Repo Gates, Type Check · source gates, Type Check · consumer gates (check:api-surface), Type Check · workspace, Type Check · debt ledger, TypeScript Type Check, Check Changeset, Check Documentation Links, Flag docs affected by code changes, Spec property liveness, Governed Surface Queue Guard, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, Dogfood Regression Gate (1/3, 2/3, 3/3 and the aggregate), Test Core (1/6 through 6/6 and the aggregate — the carriers of spec#test:repo and the @objectstack/cli slices), filter, and the four claim/single-writer guards; skipped on Console Pin Gate and Packed-tarball smoke (opt-in) (path-filtered, as on the previous head); Auto Label and Check PR Size each have one skipped and one success run across the two triggers. Nothing is still running, and nothing is red.

Implemented-by: claude/issue-21000-retire-expression-metric-types
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 19:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 19:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 99589f9 Oct 2, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21000-retire-expression-metric-types branch October 2, 2026 20:08
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 protocol:data size/xl tests tooling

Projects

None yet

2 participants