Skip to content

Commit 0252320

Browse files
os-muskclaude
andauthored
feat(service-analytics)!: judge min / max against the aggregate x field-type table — all 74 refused pairs answer DATASET_INVALID at one compile door (#17560) (#18011)
Fixes #17560 Executes the director-seat ruling on this card (decision batch #127 item 3, comment 5651572190) in **one pass, not per field class**: `min` and `max` are judged by `AGGREGATE_FIELD_TYPE_COMPATIBILITY` like every other aggregate, and all 74 pairs that were refused-by-the-table-and-enforced-by-nothing now answer `DATASET_INVALID` / **400** at the compile door. ## What the tree said before this Four declarations, three answers, one pair: | declaration | `min` x `text` | |---|---| | `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (spec, #16353) | refused | | `dataset-compiler`'s compile leg | never judged — `if (!DERIVING_AGGREGATES.has(aggregate)) return;` | | `measureResultType` (service-analytics, #15768) | a supported `'string'` result | | two shipped test files, in prose | "ruled C — the table is to be AMENDED to accept it, tracked as #17513" | The fourth row had nothing behind it: #17513 is closed as a duplicate of this card carrying zero rulings, and the one recorded ruling on this table — decision batch #59 on #16099 — refuses those rows. The ruling settled all three sub-questions together because one shared fixture drove members of both halves. ## The ruling's Execution list, line by line - **`dataset-compiler.ts`** — the `DERIVING_AGGREGATES` scope condition is gone; `assertAggregateFieldTypeCompatible` judges all six aggregates through the same `DATASET_INVALID` / 400 door. The refusal message now names the divergence each aggregate class really has (`min`/`max` SELECT a stored value and diverge on ORDER — collation-dependent for text, absent altogether for `jsonb`; `sum`/`avg` DERIVE a number and diverge on arithmetic) and prescribes accordingly. The `sum`/`avg` sentence is byte-identical to what shipped, so #16099's and #16778's message pins are untouched. - **`measureResultType`** — asks `isAggregateCompatibleWithFieldType` before it answers, so the rule and the table agree **by construction** rather than by review. `STRING_SOURCE_FIELD_TYPES` and the `formula` branch are retired; `min`/`max` over the temporal class still answers `'time'`. - **Tests** — the shared fixture in `measure-result-type.test.ts` is re-aimed off refused pairs following the #16737 precedent in the same file; the **two conditional pins are FLIPPED, never deleted** (`aggregate-nontemporal-measure-refusal.test.ts` "min / max are NOT judged by this gate", relocated by text rather than by the ruling's line number — see Deviations; `aggregate-datetime-measure-refusal.test.ts` "a min over a TEXT field still compiles here"), each naming batch #59 and this ruling; the negative control that accepted pairs still compile (`min` x `number`) is **kept** and joined by four more; the eight `#17513` citations are rewritten to this card. - **The table's TSDoc "override" paragraph** is rewritten as settled ground — the overridden opinion is retired, not standing beside it. - **Breaking** — `minor` under the launch-window convention with the BREAKING banner, plus a new ADR-0087 **semantic migration entry** (`dataset-measure-selecting-aggregate-field-type-refused`, protocol major 18) writing the structured TODO that names the measure and the field type. No lossless conversion exists, which is why it is a semantic TODO and not a D2 conversion. ## One consequence the ruling implies and did not name Retiring the `formula` branch left `measureResultType`'s third input (`formulaReturnType`, #16236) with no reader, and `AnalyticsServiceConfig.sourceFieldMeta`'s `returnType` key with no consumer. Both are **removed**: a declared input nobody reads is the declared-not-enforced shape Prime Directive #10 refuses. `FieldSchema.returnType` itself is untouched — display formatting and validation are its other declared consumers. ## Verification Every exit code captured **on the command** (redirect first, `EXIT=$?`, then read), never after a pipe. All figures below are from the FINAL tree, `035a41c7e` — the second merge of `origin/main` (`8261ff717`) that this branch carries, built whole. They were re-taken in full on this tree after the patch round; the pre-patch tree `399d12244` read the same shape. Build and tests, through `scripts/pm/os-verify-lock.sh` (`OS_VERIFY_LOCK_SLOT=issue-17560`; four holds across both rounds, VERDICT command-exit 0 on every one — 590s / 21s / 795s / 890s held): ``` pnpm install --frozen-lockfile :: exit 0 pnpm build (whole repo) :: exit 0 pnpm --filter @objectstack/spec check:generated :: exit 0 all 15 generated artifacts up to date pnpm --filter @objectstack/service-analytics exec vitest run :: exit 0 111 files / 2392 tests passed pnpm --filter @objectstack/spec test :: exit 0 476 files / 13556 tests passed pnpm --filter @objectstack/service-analytics typecheck :: exit 0 pnpm --filter @objectstack/spec typecheck :: exit 0 pnpm lint (whole repo) :: exit 0 ``` Gate families derived from the **actual** changed paths, not from a list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 15 paths, **87 commands**, no STALE TREE warning on this tree. All 87 re-run on `035a41c7e` after the patch round, each recorded as `COMMAND :: exit CODE`, and reconciled: ``` dispatch-gates --ran :: exit 0 87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3) ``` ⚠️ On the pre-merge tree four of them answered **exit 3 — PREREQUISITE NOT MET** (`check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`): each reads built output and the closure was not built yet. That is each gate's own "nothing was measured" code, neither a pass nor a finding. All four were re-run after the whole-repo build and all four exit 0. ⛔ Not a complete account of what CI runs: the 48 artifact-roster families, the 11 declared wide-population families, the 5 path-scheduled CI jobs and the always-runs tail are each outside the derived 87, as `dispatch-gates` prints. ### Reverse verification — direction predicted BEFORE running, and it held Ablation: the deleted scope condition put back as `if (aggregate !== 'sum' && aggregate !== 'avg') return;` in `dataset-compiler.ts`, on the committed fix, under the same verify lock. ``` HEAD blob 9062405 mutated blob b2935944651c57ba3cd8ec759a3303fb33ff105c (differs; an equal or empty hash was coded to abort) on-disk proof anchor occurrences 1 before / 1 after · injected marker 0 before / 1 after (occurrence counts on the mutated text — never an editor's exit code, never a --stat) MUTATED :: exit 1 4 files failed · 13 of 209 tests red RESTORED :: exit 0 4 files passed · 209 of 209 green restore proof blob back to 9062405… · marker count 0 · git diff HEAD clean · WHOLE-TREE git status --porcelain empty (trap '<restore>' EXIT INT TERM, paths absolute from git rev-parse --show-toplevel) ``` Predicted: red in the ordinary direction — with the scope back, the compile SUCCEEDS, so every refused-pair case fails on "expected a refusal, none was thrown" rather than passing vacuously on an empty result. Observed: exactly that, spread across all four files — the two flipped pins, the re-aimed section E, and the formula end-to-end section. `node scripts/ablation-dist-preflight.mjs @objectstack/service-analytics ABLATION-17560-SCOPE-RESTORED` **:: exit 1**, reported rather than worked around: the package has no `dist/` in this tree at all. Its prerequisite is inapplicable here rather than unmet — these suites import their subject relatively (`../analytics-service.js`), so they resolve to `src/`, and the 13 reds on the mutated tree beside 209 greens on the restored one are the direct evidence that the edit reached the subject. ## Acceptance notes — noted, not filed - The table's module TSDoc still calls itself "the contract both consumer legs execute — the compile-time refusal in the dataset compiler and the authoring-time lint rule". The authoring-time leg still does not exist; `packages/lint` never calls the predicate. Carrier: **#16354**, open and labelled `pm:blocked`, and the ruling leaves it there ("The lint leg is unchanged"). Not filed. - A **ninth** `#17513` citation exists outside `packages/`, in the still-pending changeset `.changeset/deriving-aggregate-nonnumeric-field-refused.md` (#16099's). It ships in the same release as this one and its "min / max are still not judged here" section would contradict this entry in one compiled CHANGELOG, so it carries a superseded-within-the-same-release-window note and its tracker pointer is repointed. Reported rather than assumed in scope. - `packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts` (#16778's entry) carried two scope sentences this change makes false — "the compile leg is scoped to that class and to nothing else" and "a measure over a field of any OTHER class is neither refused nor certified here" — plus a pointer to #16785, an issue number that does not resolve. #16099's own changeset recorded that widening it "is a `packages/spec` edit this card is fenced out of and is reported to the `domain:spec` seat rather than done here"; this is that seat and this card is the carrier, so the two sentences are corrected in place and now name all three entries. ## Patch round — the at-tier contract review's three FAIL grounds, closed The review (comment `5653288459`) passed the behaviour in full — one door for all six aggregates, 74 re-derived off the raw enum, table unamended, `formula` refused on the storage ground, both pins flipped not deleted, controls kept, `Clause-②: no` correct — and upheld the `returnType` removal as compelled by the ruling's own words. What failed was the truth of the contract TEXT shipping beside it. All three fixes are text-only; ⛔ no behaviour, pin, control or the `returnType` decision was touched. **T1 — a same-release changeset said the opposite, twice.** `.changeset/16236-formula-return-type-measure-column.md` (still pending, so it compiles into the same CHANGELOG block as this entry) promised a typed formula measure column and a `returnType?: string` fourth member on `sourceFieldMeta`. It now carries the same superseded-within-the-release-window treatment already given to #16099's, at the head and again on the `sourceFieldMeta` paragraph. This PR's own changeset now states that the removed key **was never released** and gives the host its one line. > Reading: released `packages/services/service-analytics/CHANGELOG.md` (17.4.0) — `16236` 0 hits, "fourth member" 0 hits. Lit controls on the same file: `15768` 1, `measureResultType` 2, `sourceFieldMeta` 4. Dark control `qzwxrt4419` 0. Stronger still, the one `returnType` hit in that released text says in as many words that the key "is not on `AnalyticsServiceConfig.sourceFieldMeta`'s return shape". And `git grep "fourth member" origin/main -- .changeset/16236-…` is 1 — the adding changeset is still pending on `main`, so no tarball ever carried the key. **T2 — a dead tracker pointer the ruling itself named.** `.changeset/dataset-measure-aggregate-field-type-refused.md` (#16778's, pending) still said the string rows were "under #16785, ruled C — the table itself is to be amended". Corrected where it stands: `16785` resolves to nothing, batch #127 found no ruling C behind the citation, and the table is not amended. The file also gains the superseded banner, and its two other now-false sentences — `sum` over a `percent` "compiles exactly as it did before", and `avg`/`sum` over temporal being "the only pairs whose behaviour changes in this release" — are marked where they stand. > Reading: `git grep 16785` over the whole repo was **1** hit, all of it in that file — the ruling's own claim reproduced. It is now 2 in the same file, both naming it as the retired pointer; repo-wide it appears nowhere else. Dark control `qzwxrt4419` 0. **T3 — the corrected #16778 registry entry miscounted itself.** It claimed to be "the FIRST of three" and pointed at "the two entries that widened it — #16099 …". #16099 registered **no entry**: its changeset declares `not-required (already-registered dataset-measure-aggregate-field-type-refused)`, so its widening rides this id. The `surface` now reads "ONE OF TWO", names that `not-required` relationship explicitly, and says there is no third; `acceptanceCriteria` keeps its one true sentence (every refused pair is refused at the compile door at major 18) and drops the phantom entry. ⛔ Its scope criterion itself is **not** widened — that is #16099's open ask and the seat is tracking it separately. `registry.ts` regenerated. > Reading: semantic entries whose id contains `aggregate-field-type` = **2** (lit control: 211 entries in the directory; dark control `qzwxrt4419` 0). No entry file is named for #16099 (`git grep -l deriving-aggregate|nonnumeric` over `entries/` exits 1). In the regenerated `registry.ts`: "ONE OF TWO" 1, "no third entry to look for" 1, "FIRST of three" 0, "the two entries that widened it" 0 — and repo-wide both stale phrases are 0. ⚠️ Also taken, declared rather than smuggled: the review's **F5 nit** — the changeset's opening list read as exhaustive while naming 10 of 37 types. It now says "any of the **37** field types outside the numeric, temporal and boolean classes — for example …", and points at the ADR-0087 entry for the full list. One phrase, in a file T1 already reopens. ⛔ Not done, deliberately: widening #16778's `acceptanceCriteria` to `sum`/`avg` over every class (#16099's open ask, fenced out by the dispatch), and the four items the review listed as "not this PR's to fix". ## Landing ⛔ **Draft, and it stays that way until the seat's contract review clears it.** The claim grades this `Clause-②: no` — nothing starts being accepted, this pulls code back to the declared contract — but the **path** limb of the clause-② enqueue gate fires on `packages/spec/src/**` regardless of the declaration, so `needs:contract-review` is carried on both the PR and the card. ⛔ Not flipped ready, ⛔ auto-merge not armed, ⛔ not queued. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47e6601 commit 0252320

17 files changed

Lines changed: 1184 additions & 877 deletions

.changeset/16236-formula-return-type-measure-column.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
fix(service-analytics): a `min`/`max` over a `formula` field is typed from the formula's declared `returnType`, not described as `number` (#16236)
66

7+
> ⚠️ **Superseded within the same release window — ⛔ do not act on this entry.**
8+
> Everything below was accurate when it was written and is kept as the record of what
9+
> #16236 measured and built. It never reached a published version: **#17560** (director
10+
> ruling, decision batch #127, 2026-09-13) refuses `min` / `max` over a `formula` field
11+
> outright, on the compatibility table's own storage ground — a formula is VIRTUAL in SQL
12+
> storage, no column is emitted, so no aggregate can be lowered to it whatever
13+
> `returnType` says. At the version that compiles this entry such a measure answers
14+
> `DATASET_INVALID` / **400** at compile time instead of carrying any `fields[].type`, and
15+
> the `returnType?: string` member described at the foot of this entry is **not** on
16+
> `AnalyticsServiceConfig.sourceFieldMeta` — it was added and removed inside one release
17+
> window, so no published version ever carried it. ⇒ Read #17560's entry instead; the
18+
> FROM → TO below never became a shipped behaviour.
19+
720
**Behaviour change — read this if any dataset measure aggregates a `formula`
821
field.** `AnalyticsResult.fields[].type` for such a measure column was always
922
`number`, whatever the formula computes. It is now translated from the field's
@@ -47,3 +60,8 @@ a word outside the declared four: left alone, never guessed at.
4760
host that returns the three-member shape still satisfies the contract and gets
4861
exactly today's behaviour for every column. `AnalyticsServicePlugin` relays the
4962
key automatically, so a host on the plugin needs no change at all.
63+
64+
⚠️ **Superseded — see the banner at the top.** #17560 removed that member again in
65+
the same release window, so the shape a host writes against is the three-member one
66+
this paragraph calls today's. Nothing to do either way: a host that returns the
67+
fourth key is ignored, not refused.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
feat(service-analytics)!: `min` and `max` are judged by the aggregate × field-type table too — all 74 refused pairs answer `400 DATASET_INVALID` through one compile door (#17560)
7+
8+
<!-- adr-0087: registered dataset-measure-selecting-aggregate-field-type-refused -->
9+
10+
**BREAKING** — an accept-set narrowing on a published authoring surface, and the last
11+
one this table owed. A dataset measure pairing `aggregate: 'min'` (or `'max'`) with any
12+
of the **37** field types outside the numeric, temporal and boolean classes — for example
13+
`text`, `select`, `lookup`, `autonumber`, `json`, `multiselect`, `file`, `location`,
14+
`vector` or `formula`; the ADR-0087 entry registered below carries the full list — used to
15+
compile and reach the backend; it is now refused by
16+
`compileDataset` with `DATASET_INVALID` / **400** before any query is built. Shipped as
17+
`minor` under the repo's launch-window convention for accept-set narrowings.
18+
19+
⛔ This changeset adds no rows to any table and restates none. The verdict is
20+
`AGGREGATE_FIELD_TYPE_COMPATIBILITY`'s — the one table `@objectstack/spec` declared in
21+
#16353 under the director ruling of decision batch #59 ("both legs, table in spec") —
22+
read through `isAggregateCompatibleWithFieldType`.
23+
24+
## What was wrong
25+
26+
The table refused these 74 pairs from the day it was declared, and **four declarations
27+
gave three different answers about them**:
28+
29+
| declaration | what it said about `min` × `text` |
30+
|---|---|
31+
| `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (spec) | refused |
32+
| `dataset-compiler`'s compile leg | never judged — `if (!DERIVING_AGGREGATES.has(aggregate)) return;` |
33+
| `measureResultType` (service-analytics, #15768) | a supported `'string'` result |
34+
| two shipped test files, in prose | "ruled C — the table is to be AMENDED to accept it" |
35+
36+
Driven through the real service door before anything was written, `min` / `max` over 13
37+
sampled refused pairs all compiled and emitted SQL, with `avg` × `datetime` as the
38+
firing control (refused, `DATASET_INVALID` / 400, no SQL) — so the zero was a reading of
39+
the tree rather than of a blind harness.
40+
41+
The fourth row had nothing behind it. The card it cited (#17513) is closed as a
42+
duplicate carrying zero rulings, and the one recorded ruling on this table says the
43+
opposite. ⇒ The director ruling of decision batch #127 (2026-09-13) settled all three
44+
sub-questions in one pass, because one shared fixture drove members of both halves:
45+
46+
1. **the string classes** (42 pairs) stay refused, as batch #59 ruled — ⛔ the table is
47+
not amended;
48+
2. **the non-string classes** (32 pairs) are refused **and enforced**;
49+
3. **`formula`** is refused on the table's own storage ground — it is VIRTUAL in SQL
50+
storage, no column is emitted, so no aggregate can be lowered to it whatever
51+
`returnType` says.
52+
53+
The divergence is real, and for these two aggregates it is the **ORDER** rather than the
54+
arithmetic: string order is collation-dependent, so two backends answer two different
55+
"smallest" values for one metadata document, and `min(jsonb)` does not exist on
56+
PostgreSQL at all.
57+
58+
## What changed
59+
60+
- **`dataset-compiler`**: the scope condition is gone. `assertAggregateFieldTypeCompatible`
61+
judges all six `AggregationFunction` members against the table, through the same
62+
`DATASET_INVALID` / 400 door. The refusal message names the divergence its own
63+
aggregate class really has (`min` / `max` SELECT a stored value and diverge on order;
64+
`sum` / `avg` DERIVE a number and diverge on arithmetic) and prescribes accordingly.
65+
- **`measureResultType`** asks `isAggregateCompatibleWithFieldType` before it answers, so
66+
the rule and the table agree **by construction**. Its `STRING_SOURCE_FIELD_TYPES`
67+
branch and its `formula` branch are retired with them; `min` / `max` over the temporal
68+
class still answers `'time'`, unchanged.
69+
- **`AnalyticsServiceConfig.sourceFieldMeta`** no longer declares `returnType`. It was
70+
carried (#16236) for one reader — the retired `formula` branch — and a declared input
71+
nobody consumes is the declared-not-enforced shape Prime Directive #10 refuses.
72+
73+
⚠️ **That key was never released, so against every published version this removal is a
74+
no-op.** #16236 is still a pending changeset in the same release window as this one;
75+
the last published entry (17.4.0) says in as many words that `FieldSchema.returnType`
76+
"is not on `AnalyticsServiceConfig.sourceFieldMeta`'s return shape". The key was
77+
therefore added and removed inside one window and no published tarball ever carried it.
78+
79+
**Host fix, one line:** drop `returnType` from whatever your `sourceFieldMeta` returns.
80+
You do not have to — the hook is a function RETURN position, so an extra key is not an
81+
excess-property error and is simply ignored at runtime — but keeping it declares an
82+
input nothing reads. Hosts on `AnalyticsServicePlugin` need no change at all: the plugin
83+
stopped relaying the key in this same change.
84+
85+
## FROM → TO, and the one-line fix
86+
87+
| you wrote | write instead |
88+
|---|---|
89+
| `{ aggregate: 'min' \| 'max', field: <a text/select/lookup/autonumber field> }` | `count` / `count_distinct` if you were counting; a **sort** on the list/report if you wanted the first or last RECORD |
90+
| `{ aggregate: 'min' \| 'max', field: <a json/multiselect/file/location/vector field> }` | store the quantity you meant as a numeric or temporal field and aggregate that |
91+
| `{ aggregate: 'min' \| 'max', field: <a formula field> }` | a formula emits no column; aggregate the stored field the formula reads, or persist the computed value |
92+
93+
⚠️ **Untouched:** those field types used as a **DIMENSION** (grouping, labelling,
94+
bucketing, filtering), `count` / `count_distinct` over any type, `min` / `max` over the
95+
numeric, temporal and boolean classes, and every `sum` / `avg` row #16778 and #16099
96+
already settled. The refusal also still stands down rather than guessing wherever the
97+
declared type cannot be resolved: no `sourceFieldMeta` wired, an unknown field, or a
98+
`relationship.field` path whose column lives on a joined object.
99+
100+
⚠️ The hand-migration prescription ships as the ADR-0087 semantic TODO registered above,
101+
which names the measure and the field type per affected pair — no lossless conversion
102+
exists, because nothing can compute "the smallest text value" in a way every backend
103+
agrees on.

.changeset/dataset-measure-aggregate-field-type-refused.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,45 @@ schema, not `os validate` / `os lint`, not the analytics service, not the render
6060

6161
## ⚠️ Scope: the compile leg executes the TEMPORAL rows only
6262

63+
> ⚠️ **Superseded within the same release window.** This section was accurate when it was
64+
> written and is kept as the record of where the compile leg stopped. Two later cards
65+
> widened it before any of the three entries shipped, so at the version that compiles this
66+
> entry the scope below is no longer the platform's: **#16099** judged `sum` / `avg` over
67+
> every remaining field class (including `sum` over a `percent`), and **#17560** (director
68+
> ruling, decision batch #127, 2026-09-13) judged `min` / `max` over every class the table
69+
> refuses. ⇒ Three sentences in this section are false at that version and are corrected
70+
> where they stand: the string rows are **not** awaiting a table amendment, `sum` over a
71+
> `percent` does **not** compile as it did before, and `avg` / `sum` over a temporal field
72+
> are **not** the only pairs whose behaviour changes. Read all three entries together.
73+
6374
The gate judges only a measure whose field is declared `date` / `datetime` /
6475
`time`; a field of any other class is never handed to the predicate. The
6576
verdict for the pairs it does judge is the table's — no row is restated — but
6677
which FIELDS are judged is narrower than the table, on purpose:
6778

6879
- **String rows** (`min` / `max` over `text`, `select`, `lookup`,
69-
`autonumber`, …) are **not enforced here**. They are under #16785, **ruled
70-
C**: the table itself is to be amended to accept them, because
71-
`measureResultType` (#15768) already types those results as `'string'` and
72-
pins them end to end. Enforcing them from this card would pre-empt that
73-
ruling.
80+
`autonumber`, …) are **not enforced here**. ⚠️ This card recorded them as
81+
「under #16785, **ruled C** — the table itself is to be amended to accept
82+
them」, because `measureResultType` (#15768) already typed those results as
83+
`'string'` and pinned them end to end, so enforcing them from here would
84+
pre-empt that ruling. **Both halves of that sentence turned out to be
85+
wrong.** `16785` resolves to no issue, and decision batch #127 (#17560,
86+
2026-09-13) found no ruling C anywhere behind the citation — the one recorded
87+
ruling on this table, decision batch #59, refuses the string rows. ⛔ The
88+
table is **not** amended; #17560 enforces those rows and retires the
89+
`measureResultType` opinion that disagreed with them.
7490
- **Boolean rows** are not a refusal at all any more: #16685 was ruled A and
7591
#16750 added `boolean` / `toggle` to `sum` / `avg` / `min` / `max`, so the
7692
table ACCEPTS them and this gate never judged them.
7793
- The table's `sum` × `percent` row is likewise **not** executed by this leg;
78-
`sum` over a `percent` compiles exactly as it did before.
79-
80-
⇒ The only pairs whose behaviour changes in this release are `avg` / `sum`
81-
over a `date` / `datetime` / `time` field. The full-table leg remains #16099's.
94+
`sum` over a `percent` compiles exactly as it did before. ⚠️ True of this
95+
card only — #16099 executes that row in the same release.
96+
97+
⇒ The only pairs whose behaviour changes **because of this card** are `avg` /
98+
`sum` over a `date` / `datetime` / `time` field. ⚠️ ⛔ Not a statement about the
99+
release: the full-table leg is #16099's and landed, and the `min` / `max` leg is
100+
#17560's and landed, so at the shipping version every pair the table refuses is
101+
refused at the compile door.
82102

83103
## FROM → TO
84104

.changeset/deriving-aggregate-nonnumeric-field-refused.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,22 @@ answer was a property of the dialect rather than of the data — the shape Prime
5656
measure never finishes compiling), and the three "cannot answer, do not block" tiers —
5757
no `sourceFieldMeta`, an unresolvable field, a `relationship.field` path.
5858

59-
## ⚠️ Scope: the DERIVING aggregates. `min` / `max` are still not judged here
59+
## ⚠️ Scope: the DERIVING aggregates — and see #17560, which closed the other half
60+
61+
> ⚠️ **Superseded within the same release window.** This section was accurate when it was
62+
> written and is kept as the record of why this change stopped where it did. #17560
63+
> (director ruling, decision batch #127, 2026-09-13) then judged `min` / `max` too, so at
64+
> the version that ships this entry **every** pair the table refuses is refused at the
65+
> compile door. Read that entry beside this one.
6066
6167
`min` / `max` SELECT one of the stored values; `sum` / `avg` DERIVE a number. This is the
6268
line this package already draws — `measureResultType` branches on exactly that pair of
6369
aggregates — and the defect is about a derived number, so the deriving aggregates are its
6470
population.
6571

66-
The `min` / `max` rows stay with **#17513**, and that is measured rather than assumed.
72+
The `min` / `max` rows stayed with the table-amendment card (then **#17513**, since closed
73+
as a duplicate of **#17560**, which ruled and landed them), and that is measured rather
74+
than assumed.
6775
Enforcing the residual whole was tried on this card: with `min` / `max` × the string
6876
classes subtracted, **15** cases in `measure-result-type.test.ts` still went red, every
6977
one of them on `min` × `json` — a pair the table refuses, in no ruling's scope, driven

0 commit comments

Comments
 (0)