Commit 0252320
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
File tree
- .changeset
- packages
- services/service-analytics/src
- __tests__
- spec/src
- data
- migrations
- entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
7 | 20 | | |
8 | 21 | | |
9 | 22 | | |
| |||
47 | 60 | | |
48 | 61 | | |
49 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
Lines changed: 103 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
Lines changed: 29 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
63 | 74 | | |
64 | 75 | | |
65 | 76 | | |
66 | 77 | | |
67 | 78 | | |
68 | 79 | | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
74 | 90 | | |
75 | 91 | | |
76 | 92 | | |
77 | 93 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
82 | 102 | | |
83 | 103 | | |
84 | 104 | | |
| |||
Lines changed: 10 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
60 | 66 | | |
61 | 67 | | |
62 | 68 | | |
63 | 69 | | |
64 | 70 | | |
65 | 71 | | |
66 | | - | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
67 | 75 | | |
68 | 76 | | |
69 | 77 | | |
| |||
0 commit comments