Repository navigation
feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (#16685) - #16750
Conversation
…aggregate × field-type table (#16685) Decision batch #80 (2026-09-08) holds ruling #11152 - booleans aggregate as numbers on every backend, no per-aggregate exception - over batch #59's blanket "every other pair refused", which never named booleans. The four arithmetic / order rows gain the BOOLEAN_VALUE_TYPES members; the module TSDoc and the table's pending changeset no longer publish the refusal; the pins hold the boolean rows both literally and against AGGREGATION_CASES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 130 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 754fb97f65a777b26361966e8d94dc5e4aad6a15 && git checkout 754fb97f65a777b26361966e8d94dc5e4aad6a15
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cf33adbd8ea74306f60e1f0b9c827787aeaa383d bee6b0b091a3240e880416cb212ee91d41247168 && git checkout -B drift-repro cf33adbd8ea74306f60e1f0b9c827787aeaa383d && git merge --no-ff bee6b0b091a3240e880416cb212ee91d41247168
node scripts/docs-audit/affected-docs.mjs --json cf33adbd8ea74306f60e1f0b9c827787aeaa383d |
Contract review (
|
| aggregate | boolean |
toggle |
|---|---|---|
sum |
refused → accepted | refused → accepted |
avg |
refused → accepted | refused → accepted |
min |
refused → accepted | refused → accepted |
max |
refused → accepted | refused → accepted |
count / count_distinct stay at 49/49 (ANY_FIELD_TYPE, untouched). Row sizes: sum 6→8, avg 7→9, min 10→12, max 10→12. The mechanism is one new constant BOOLEAN_AGGREGATE_FIELD_TYPES = ['boolean','toggle'] (packages/spec/src/data/aggregate-field-type-compatibility.ts:142-144) spread into the four rows (:161-168); NUMERIC_ / ADDITIVE_ / TEMPORAL_ are byte-identical to base. AGGREGATION_CASES, driver-sql, analytics.zod.ts, seed.zod.ts are not in the diff. None of the five relevant files moved on main between the merge base and current main (8ccf7a1), so the base reading is current. "Nothing else moves" holds.
2. Does the table agree with what ruling #11152's suite actually asserts? — yes, with one attribution caveat (F1)
Read packages/spec/src/data/aggregation-conformance.ts on main, not the PR body. The flag column (AGGREGATION_ROWS, 3 true / 3 false, :230-235) is exercised by seven cases at :358-430: sum(flag)=3, avg(flag)=0.5, min(flag)=0, max(flag)=1, count(flag)=6, count_distinct(flag)=2, and min(flag) grouped by region (east=1, west=0). Every one of those pairs is accepted by the head table. The six enrolled faces listed at :87-118 each iterate for (const c of AGGREGATION_CASES) with no flag filter (verified in all six harness files; driver-sql's runs on SQLite + PG + MySQL cells when provisioned, no case dialect-gated). The ruled driver-sql Postgres cast is at sql-driver.ts:8670-8691 on current main (the card's :8497 is line drift only).
3. Consumer legs — not landed, not touched, not owed
git grep at the PR head for AGGREGATE_FIELD_TYPE_COMPATIBILITY / isAggregateCompatibleWithFieldType outside the module and its test finds only the data/index.ts barrel and the generated api-surface / export-origins entries. No hit under packages/lint/**, services/**, packages/objectql/**, runtime/**. #16099 is open and pm:blocked; the devx lint leg has no file yet. So the PR lands the widened table before either consumer executes it — which is the whole point of the ruling's time box. Nothing downstream to keep green.
4. Changeset and governed paths
.changeset/aggregate-boolean-members.md:"@objectstack/spec": minor— package name matchespackages/spec/package.json; the body states the before (refused via batch Validation Protocol: Cross-Field, Async, and Conditional validation #59's default) and after (accepted, with the row table) explicitly. Not breaking (accept set only widens;dist/*.d.tstype unchanged), so no ADR-0087 marker owed;Check Changesetgreen.minoris correct..changeset/aggregate-field-type-compatibility.md: edited outside the claim's two-file surface. Verified it is still pending (0 hits for the export name inpackages/spec/CHANGELOG.mdonmain), so this corrects unreleased text rather than rewriting history; the diff is two prose lines and the string-class override paragraph is verbatim. Declared in the PR body. Acceptable.- Governed paths: none touched. No
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**in the diff.Governed Surface Queue Guardgreen. This is not a maintainer-only merge on governed grounds.
5. Tests — what the file actually pins (re-read, not trusted)
packages/spec/src/data/aggregate-field-type-compatibility.test.ts at head. If the boolean members are removed again (whole class, or either spelling, or from any one of the four rows), these six it blocks redden:
:76-79sumliteral equalsADDITIVE ∪ BOOLEAN:81-83avgliteral equalsNUMERIC ∪ BOOLEAN:85-88minandmaxliterals equalNUMERIC ∪ TEMPORAL ∪ BOOLEAN:108-114BOOLEAN_VALUE_TYPESliterally equals['boolean','toggle']and each of the four rows' boolean members equals that set:131-136predicate accepts all four aggregates over both spellings:138-151cross-pin: the set of functions overflaginAGGREGATION_CASESequals all six, and each such function is accepted forbooleanandtoggle
The cross-pin also reddens in the other direction — if someone later removes the flag cases from AGGREGATION_CASES (option B), :144-146 fails. The old refusal pins are gone (:153-159 now lists only computed/text/structured types; :206-213 keeps only the string-class override). The six-test count the PR body reports for its ablation is consistent with the file; I could not execute it locally (no node_modules in this container), so execution rests on CI, where the spec suite runs inside Test Core, all six shards green.
Findings (none blocking)
- F1 —
toggleis not directly pinned by the conformance suite. Every harness declaresflagastype: 'boolean'; no harness has atogglecolumn. The cross-pin's comment (test.ts:139-142, "each case over it is a pair [finding]AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 pins on six backends") is accurate forbooleanand over-attributes fortoggle. Thetogglecells are still sound:toggle ∈ BOOLEAN_VALUE_TYPES(field-value.zod.ts:68-70), anddriver-sqlcollectstoggleintobooleanFieldsalongsideboolean(sql-driver.ts:9636, column mapping:2364), so the Postgrescast(?? as int)at:8685-8691fires for it. The ruling namestoggleexplicitly, so the table is right to carry it. Expectation for this PR: none required ("nothing else moves" forbids touchingAGGREGATION_CASES); an optional one-line comment fix attest.ts:139-142saying thetogglehalf rests on the boolean-class membership rather than a suite row. Atoggleconformance row is a driver-family card, not this one. - F2 — "six backends" carries the suite's own caveat.
aggregation-conformance.ts:97-105records that thedriver-mongodbcell is lowering-only (in-process evaluator, "does not answer 'does MongoDB agree?'"). The head TSDoc (:59-65) and the changeset repeat "enrolled on six backends" without that qualifier. Informational; it is not a table-cell claim and it restates the suite's own enrolment list. - F3 — bucket pins were weakened and re-strengthened correctly. The numeric pin (
test.ts:90-97) and temporal pin (:99-106) now subtract the boolean class before comparing; the new boolean-bucket pin (:108-114) holds the class literally, so a type joining any of the threefield-value.zodclasses elsewhere still reddens this file. Verified, no action. - F4 — surface deviation, accepted. Parent changeset edit (see §4): pending, prose-only, declared.
- F5 — CI. 38 check runs at
499d718: 30 success, 7 skipped (opt-in / path-filtered), 0 failure;Lint & Repo Gatesstillin_progressat time of review (running ~25 min). This verdict is conditional on that gate finishing green; the rest of the matrix — Build Core, all sixTest Coreshards, all Type Check jobs, Temporal Conformance (live PG + MySQL), Check Changeset ×2, Governed Surface Queue Guard, Spec property liveness — is green.
No approval, merge, label or file action taken by this seat.
Generated by Claude Code
…rs changeset; derive the flag-case vocabulary pin from AggregationFunction (#16685) Contract-review patch round. The changeset claimed dist/*.d.ts was byte-identical; it is not - the rewritten module TSDoc ships in dist/data/index.d.ts. It now states what holds: the exported declarations are unchanged, the private BOOLEAN_AGGREGATE_FIELD_TYPES constant is absent from the bundle, and api-surface / export-origins are untouched. The test header claims only what the cross-pin reaches (the boolean axis) and the flag-case vocabulary is derived from AggregationFunction.options instead of a literal six-member list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…— dist/*.d.ts and the bundles' export lists, not the bundle (#16685) Contract-review patch round 2. The constant does ship inside the bundles as a non-exported binding, so "absent from the bundle" over-claimed; the changeset now says only what was measured: absent from dist/*.d.ts and from the bundles' export lists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…eld type cannot carry, and reconcile the storage-form annotations to one measured statement (objectstack-ai#16778) * feat(service-analytics)!: refuse an aggregate a measure's field type cannot carry WIP — compile-leg refusal + the four reconciled datetime-storage annotations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 * wip: changeset + adr-0087 ledger entry * fix(service-analytics): scope the compile-leg refusal to temporal source fields The full table refuses `min`/`max` over the string classes and the boolean rows, both of which this platform answers on purpose and pins with tests (objectstack-ai#15768, maintainer ruling objectstack-ai#11152). Executing those is a product judgement that belongs to objectstack-ai#16099; the temporal rows carry no such collision and are the ones this card is about. Re-points the two `measure-result-type.test.ts` fixture measures that aggregated a datetime column, and corrects the module header that recorded the missing refusal as an open finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 * docs(changeset,spec): scope the breaking declaration to the temporal class it actually withdraws The compile leg was scoped to TEMPORAL source fields in 80ec9f2, but the changeset and the ADR-0087 ledger entry still described the pre-scoping full-table gate. A breaking-change record that overstates what changed tells every consumer reading the release notes that behaviour was withdrawn which was not. F1 — the breaking declaration: - changeset FROM/TO: the two `percent` rows dropped (`sum` x `percent` is a table row this leg does not execute; `avg` x `percent` was never a migration at all), and the surviving `avg` row widened to name all three temporal members it does refuse. - changeset: a new scope section states the temporal scope outright, and that the string rows sit under objectstack-ai#16785 (ruled C - the table is to be AMENDED to accept them), the boolean rows were settled as ACCEPT by objectstack-ai#16685 / objectstack-ai#16750, and `sum` x `percent` is not executed here. - ledger `surface`: scoped to the temporal class; "sum over a percent" removed. - ledger `replacement`: the "`avg` for a `percent`" prescription dropped with the surface rows it belonged to. - ledger `acceptanceCriteria`: qualified to a measure over a `date` / `datetime` / `time` field, and says outright that a field of any other class is neither refused nor certified by this leg. - `registry.ts` REGENERATED with `pnpm --filter @objectstack/spec gen:migration-registry`, never hand-edited; two consecutive runs are byte-identical and the diff is confined to this entry's block. - also corrected: the changeset said "four contradictory annotations"; the sweep reconciled seven source sites plus two test narratives. F2 — the boolean collision is settled, so stop narrating it as live. objectstack-ai#16685 was ruled A and objectstack-ai#16750 added `boolean` / `toggle` to the four arithmetic / order rows, so the table ACCEPTS them. Reworded in `dataset-compiler.ts`'s scope docblock, the refusal suite's module header, and `measure-result-type.ts` (whose boolean paragraph still referred a missing refusal onward). All three now also record objectstack-ai#16785 C for the string rows. F3 — the ledger `reason` presented both dialect halves as measured alike. The SQLite half is pinned by a live `sql.js` suite; the Postgres 42883 half was measured in-session and is pinned by nothing. Said so where it is stated. F4 — the scope-boundary test asserted `isAggregateCompatibleWithFieldType( 'min', 'text') === false`, a verdict objectstack-ai#16785 C is about to amend. Dropped: the case now pins only what this PR owns - a non-temporal field is not judged, so the measure compiles and SQL is emitted. Refutability is carried by a second case on `sum` x `text`, a row no ruling is moving, plus a non-vacuity assertion that SQL reached the driver in both. F5 — the changeset now names the two uncovered faces: `/analytics/query` and any `compileDataset` caller wiring no `declaredFieldType` probe. Refs objectstack-ai#16737. Review: PR objectstack-ai#16778 contract review, comment 5580295870. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 --------- Co-authored-by: Claude <noreply@anthropic.com>
…commits that decided them (objectstack-ai#20729) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the eighth stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/services/service-analytics/src/**` and nothing else. By the seat's census at the claim (`5899485578`), it is the largest package in the lane that no in-flight work holds. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 7 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as `9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`). That is **76 sites on 76 lines in 22 files, covering 14 numbers**: - 42 census sites (every census site this package has); - 34 sites in test comments, which the census defers. The raw scan found no dead site the gate's grammar cannot see (see Acceptance notes), so there is no third class this time. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what was decided: **13 distinct shas**. No number in this package has an ADR or ruling record of its own in the repository (a grep of `docs/adr/` for all 14 finds none, and a grep of the rest of `docs/` finds none either), so every anchor is a commit, per ruling C's order. No number was dropped. Only comments changed. Every touched source file keeps its line count (78 lines out, 78 in, over 22 files), so no line citation into these files moves. 2 of those 78 lines hold no dead citation: they are reflow lines, listed under Wordings below. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces: `objectstack-ai#10861` (5 lines), `objectstack-ai#12776` (3), `objectstack-ai#10413` (2), `objectstack-ai#16750` (2), and `objectstack-ai#10759`, `objectstack-ai#11152`, `objectstack-ai#5716` and the decision-batch ordinal `objectstack-ai#59` once each. Each tracker number among them resolves. Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number is the citation on an added line: the two `PR #N` spellings in scope became their pull request's squash commit, and `objectstack-ai#16750` stays only as the convenience link beside `ed7243d52`, on the line it already stood on. Eight dead sites are left on purpose, all of them test strings (see the list below). One more file: a `patch` changeset for `@objectstack/service-analytics`, because the rewritten docblocks and inline comments ship (see Changeset below). The `AnalyticsResultWithDrill` type and its four sidecar members are not touched: its docblocks carry no dead number (`objectstack-ai#20644`, `objectstack-ai#3214` and `objectstack-ai#1752` all resolve). ## Census: `service-analytics`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/services/service-analytics/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | service-analytics sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `cbaf04c1f`, run 2026-09-29T21:41:53Z to 21:45:05Z | enumerated, 186 pages, frontier objectstack-ai#20721 (newest objectstack-ai#20721 before and after), 18,548 numbers | 1,161 | **42** | 42 | 10 | 10 | | after | head `967d73531`, run 21:55:23Z to 21:58:36Z | enumerated, 186 pages, frontier objectstack-ai#20723 (newest objectstack-ai#20723 before and after), 18,550 numbers | 1,119 | **0** | 0 | 0 | 0 | The before count matches the seat's census at the claim and A1 (42 sites): the two comments PR objectstack-ai#20712 rewrote in `analytics-service.ts` did not move it. The whole-repo drop is 42, exactly this diff's census sites. The `resolves` tally is 32,991 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. The after run was taken on `967d73531`; the head `82d2b40b2` adds only the changeset. No run was truncated or discarded: both enumerations read 186 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `service-analytics/src` (162 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when that census judged it on this board anywhere (its `--list` extraction) and did not report it. The 21 numbers the census never saw, because they stand only in test files or strings here, were read one by one on the issues endpoint: 17 answer 200, and `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#16918` and `objectstack-ai#17125` answer 404. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `cbaf04c1f` | 3,514 | **84** | 42 | 34 | 0 | 8 | | after, `967d73531` | 3,438 | **8** | 0 | 0 | 0 | 8 | Its src-comment column equals the census's 42, which is the control on the second instrument. The 3,410 live citations and the 20 cross-repo citations are the same in both readings, and the drop of 76 citations is exactly the rewritten sites. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 3,598 occurrences and 84 dead before, 3,522 and 8 after; its residue equals the gate's residue site for site, and it sees no dead site beyond the gate. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject, and `git blame` at the base puts every rewritten line in its anchor commit or in a later commit that descends from it (`merge-base --is-ancestor` exit 0 for each pair). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#11461` | 20/2 | 19/1 | `399ecad58`: a cross-object leaf in one measure's own `filter` (the third producer, lowered onto `aggregations[].filter`) is refused on both ObjectQL doors with `INVALID_FIELD` / 400 naming the measure, folded into the one member view, with insertion order keeping every earlier refusal's message. The last line of its message names `objectstack-ai#11461` as the card it settles. New to the sweep | | `objectstack-ai#17130` | 17/5 | 13/4 | `54b3d1d4a` (PR objectstack-ai#17336): the row-scope resolution refusals carry `READ_SCOPE_COMPILE_FAILED` / 500 through one constructor, so `queryDataset`'s catch re-throws them instead of reading their words, every message byte-unchanged; plus the source-derived wording-collision guard. Named in its diff only (18 added lines carry the tag). New to the sweep | | `objectstack-ai#17124` | 12/8 | 10/2 | `86c505286` (PR objectstack-ai#17593): `explicitDateRangeWindow` is the one reading of `dateRange`'s array arm on all four faces, and an array that is not two string bounds is refused with `ANALYTICS_DATE_RANGE_UNRECOGNIZED` / 400. Named in its diff only (its changeset file is `17124-daterange-array-arm-arity.md`). New to the sweep | | `objectstack-ai#12209` | 10/5 | 10/0 | `017130a09` (PR objectstack-ai#12318): a custom-SQL measure is refused on the ObjectQL aggregate path with `INVALID_FIELD` / 400, keyed on the `EXPRESSION_METRIC_TYPES` partition shared with `NativeSQLStrategy`. Its message records the two failure modes the lines describe (`driver-sql` blaming a `function` key, the in-memory evaluator answering `null` per bucket). Named in its diff only. New to the sweep | | `objectstack-ai#16778` | 5/1 | 4/1 | `357f4992b`: the compile-leg refusal of an aggregate a datetime measure's field type cannot carry, scoped to temporal source fields. The squash commit of the pull request that was `objectstack-ai#16778`; its subject carries the number. New to the sweep | | `objectstack-ai#12940` | 4/2 | 4/0 | `aa16721b6` (PR objectstack-ai#13361): this package's consumer-local `executeAggregate` config mirrors (the plugin options and `AnalyticsServiceConfig`) narrow `aggregations[].method` to `AggregationFunction`, after `objectstack-ai#12776` narrowed the contract. Named in its diff only. New to the sweep | | `objectstack-ai#17015` | 4/2 | 4/0 | `0da638cd9`: the closed `dateRange` preset vocabulary is lowered once and the rest refused, the `[range, range]` fallback is removed from the faces it reached, and the shared conformance kit holds them. The squash commit of the pull request that was `objectstack-ai#17015`. New to the sweep | | `objectstack-ai#16860` | 3/1 | 3/0 | `041d9fdc6`: the object-level read grant is asked at the analytics door, and its bridge to the `security` service resolves an explicit three-way (absent admits; throwing or method-less denies at `error`, finding F3 in its message). The squash commit of the pull request that was `objectstack-ai#16860`. New to the sweep | | `objectstack-ai#12248` | 2/1 | 2/0 | `8425c17cc`: the five ruled engine members, `getDriverForObject?` and `resolveEffectiveDatasource` among them, adopted onto `IDataEngine`, and `getObject` typed. Its subject names it. Stage 5's and the spec stage's anchor | | `objectstack-ai#16685` | 2/2 | 2/0 | `ed7243d52` (PR objectstack-ai#16750): `boolean` / `toggle` accepted for `sum` / `avg` / `min` / `max` in the aggregate × field-type table, holding maintainer ruling `objectstack-ai#11152`. Its subject names it. The spec stage's anchor | | `objectstack-ai#17125` | 2/2 | 2/0 | `5d12b16e7`: the row-scope bridge tells an absent security service from a broken one, so a broken one refuses the query. The squash commit of the pull request that was `objectstack-ai#17125` (404 on the pulls endpoint too). New to the sweep | | `objectstack-ai#16918` | 1/1 | 1/0 | `5d12b16e7`: the same commit. Its changeset's headline names `objectstack-ai#16918` as the card it answers, and its diff writes the line (`admission-bridge-resolution.test.ts:120`) | | `objectstack-ai#6123` | 1/1 | 1/0 | `59d1933f9`: `err.code` lands at `error.code`, not `error.details.code`; the commit that wrote this very line. The `runtime` stage's anchor | | `objectstack-ai#13279` | 1/1 | 1/0 | `6a180e42d`: permission-store read failures fail loud, and the same commit renames `metadata/src/utils/schema-sync-errors.ts` to `packages/types/src/driver-error-classification.ts`, the move the line describes. The anchor of stages 2, 5 and 6, and of the `types`, `rest` and `runtime` stages | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 13), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; control leg: stage 1's landing `422db788a` exit 0; the history is complete, `--is-shallow-repository` false, 15,135 commits). Each of the 14 numbers answers 404 on the issues endpoint, read one by one; `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#17015` and `objectstack-ai#17125` answer 404 on the pulls endpoint too. ## Wordings to check - **Bracket tags.** `[#N]` became `[commit SHA]`, as in stage 7; `[objectstack-ai#10861 / objectstack-ai#11461]` and `[objectstack-ai#10861, objectstack-ai#11461]` keep the live `objectstack-ai#10861` beside the new sha. - **The boolean rows, `measure-result-type.ts:115-116` and `aggregate-datetime-measure-refusal.test.ts:65-66`.** 「objectstack-ai#16685 ruled A, landed as objectstack-ai#16750」 and 「objectstack-ai#16685 was ruled A and objectstack-ai#16750 added」 became 「commit ed7243d (objectstack-ai#16750) added those rows」 and 「commit ed7243d (objectstack-ai#16750) added」. 「ruled A」 named an option on the dead card; `ed7243d52`'s message records the decision itself. Line 116 of the first file and line 66 of the second are the 2 reflow lines: each keeps the `objectstack-ai#16750` it already carried. - **PR numbers, `read-scope-resolution-envelope.test.ts:25` and `refusal-wording-collision.test.ts:21`.** 「PR objectstack-ai#17125's refusal」 became 「Commit 5d12b16's refusal」, the pull request's squash commit. - **`read-scope-refusal.ts:29`.** 「objectstack-ai#17130 exists to remove it」 became 「commit 54b3d1d was made to remove it」, the form stage 6 used. - **`refusal-wording-collision.test.ts:49`.** 「the exact move objectstack-ai#17130 forbids」 became 「the exact move commit 54b3d1d ruled out」; its message says the fix is the declaration, not a luckier string. - **`read-scope-resolution-envelope.test.ts:161`.** The verb after the number moved from present to past tense with the sha. - **`measure-expression-both-strategies.test.ts:45` and `:166`.** 「deleting the objectstack-ai#12209 arm in」 became 「deleting the arm commit 017130a added in」, and 「every objectstack-ai#12209 refusal」 became 「every custom-SQL refusal (commit 017130a)」. - **`dataset-executor.ts:609`.** 「objectstack-ai#17015's kit」 became 「commit 0da638c's kit」, the conformance kit that commit built. - **`plugin.ts:116`.** 「and in objectstack-ai#12209:」 became 「and in commit 017130a:」, whose message records the two ways the engine failed. - **`analytics-service.ts:238`.** 「objectstack-ai#13279 moved it there」 became 「commit 6a180e4 moved it there」; that commit's diff is the rename. ## The 8 sites left - **Test strings, 8 sites**, left as stages 1 to 7 left theirs, all `describe` / `it` titles: - `crossobject-conjunct-refusal.test.ts:589` (`objectstack-ai#11461`); - `aggregate-nontemporal-measure-refusal.test.ts:243` (`objectstack-ai#16778`); - `date-range-array-arm-arity.test.ts:213` and `:294` (`objectstack-ai#17124`); - `read-scope-resolution-envelope.test.ts:155`, `:199` and `:226`, and `refusal-wording-collision.test.ts:336` (`objectstack-ai#17130`). - There is no operator string, generated file or quoted ruling carrying a dead number in this package. It has no generated file at all. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes never visited, base `cbaf04c1f` against head. Template literals are therefore read in context. It ran over all 22 touched `.ts` files. - Real run: 26,705 base leaf tokens, **0 files with a token change** (exit 0). - Comment control in `plugin.ts` (「refusal buys is in」 to 「refusal earns is in」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `plugin.ts` (`field: a.field,` given `as string`): DIFFER (exit 1). - Positive control, one digit changed inside a kept test title (`date-range-array-arm-arity.test.ts:213`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`ad3dc9fff4d3`, `a606ffbb6ead`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/service-analytics` (`.changeset/20596-service-analytics-provenance-anchors.md`) is included. Its body is stage 7's, word for word, with the package name changed. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build (a cache miss for this package, so `dist` is this head's source), the rewritten comments reach `dist`: `399ecad58` 6 times in each of `dist/index.js`, `index.cjs`, `index.d.ts` and `index.d.cts`; `86c505286` twice in each JS file and once in each declaration file; `54b3d1d4a` once in all four; `aa16721b6` once in each JS file and twice in each declaration file; `017130a09` once in each JS file. Positive controls: the unchanged line 「none of the coverage: a compiled measure's own」, in the same docblock as the shipped rewrite at `objectql-strategy.ts:744`, is found once in each of the four files, and the unchanged line 「back into line. Widening it here again would not be a local matter」 beside the shipped rewrite at `analytics-service.ts:559` once in each declaration file. A never-written negative phrase appears nowhere in `dist`. None of the 14 dead numbers is left anywhere in `dist`. ## Gates (head `82d2b40b2`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 11 citations across 10 files; 10 resolve and 1 resolves as a pull request (`objectstack-ai#16750`, the convenience link that already stood on its line). - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `82d2b40b2` derived 62 commands: all 56 derived at dispatch, plus `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. Each ran with its exit code captured before any pipe, and all 62 exit 0. `--ran`, fed each command with its exit code, reports 62 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/service-analytics test`: 137 files pass and 3,216 tests pass. That is every test file in the package, the 12 touched ones included. - `pnpm --filter @objectstack/service-analytics typecheck` exits 0 (`tsc --noEmit` on `tsconfig.json`). `--listFiles`: the program holds all 162 files under `src/`, the 137 test files and all 22 touched files included. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 22 touched `.ts` files gives 22 files, 0 errors and 0 warnings. All 22 are in eslint's own population (`isPathIgnored` is false for each; a `dist` file, as the control, is ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 23 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked.** `CITATION_RE` refuses a hyphen after the digits and a `/` before the `#` (objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word 「option」. In this package: - `#N-word`: 8 lines by a plain grep, and 7 once a hyphen before the `#` is excluded too, which is the claim's 7. The eighth is 「pre-objectstack-ai#10413-phase-2」 (`execution-context-bridge.test.ts:223`). The numbers, `objectstack-ai#10413`, `objectstack-ai#5298`, `objectstack-ai#13570` and `objectstack-ai#13640`, all resolve. - `#A/#B`: 29 lines, the claim's 29, over 28 distinct numbers. All resolve; `objectstack-ai#2149`, which the census never judged, was read on its own. - `option #N`: none. So nothing here needed a rewrite beyond the gate, and the raw scan agrees. - **「This card」 phrases are left.** 113 lines in 39 files of this package speak of 「this card」, 「that card」 or 「the card」. They carry no number, neither instrument sees them, and most sit in blocks whose numbers still resolve. Stage 7 rewrote two such lines as lost referents; here none is changed, because the phrase runs through the whole package and rewriting a subset would be arbitrary. - **Prose that names `queryDataset`'s catch, not changed.** Nine comment lines say `queryDataset`'s catch. Since `10c36cc43` that catch sits in the private `answerDataset`, whose docblock calls it the body of `queryDataset`, so the lines still hold at the level of the public method. This is not a dead citation, so it is outside this stage. - **The census instrument did not truncate in this stage.** Both enumerations read 186 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11461` → `399ecad58`; `objectstack-ai#17130` → `54b3d1d4a`; `objectstack-ai#17124` → `86c505286`; `objectstack-ai#12209` → `017130a09`; `objectstack-ai#16778` → `357f4992b`; `objectstack-ai#12940` → `aa16721b6`; `objectstack-ai#17015` → `0da638cd9`; `objectstack-ai#16860` → `041d9fdc6`; `objectstack-ai#17125` and `objectstack-ai#16918` → `5d12b16e7`. - **Base.** The branch is on `main` at `cbaf04c1f`. `main` has since moved four commits (`3711e0b76`, `61455de27`, `6afccda5a`, `671d4c164`). They touch `packages/spec`, `packages/metadata/package.json`, `pnpm-lock.yaml`, docs and changesets, and no file under `service-analytics` or in this diff, so no merge was taken; the merge queue rebuilds on the merged generation. One of them, `671d4c164`, declares the four drill-through sidecars on `AnalyticsResult` in the spec. This diff leaves the local `AnalyticsResultWithDrill` untouched, as the claim requires. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16685
What changed
AGGREGATE_FIELD_TYPE_COMPATIBILITY(packages/spec/src/data/aggregate-field-type-compatibility.ts) accepts the boolean class —boolean,toggle— on its four arithmetic / order rows:sum,avg,min,max. Nothing else in the table moves;count/count_distinctwere already total overFieldType.Executing the director-seat ruling of decision batch #80 (2026-09-08, comment
5577120138on the card, maintainer verbatim 「其他同意」, option A): batch #59's "every other pair: refused" was a blanket default that never named booleans; ruling #11152 (2026-08-28) — booleans aggregate as NUMBERS on every backend, no per-aggregate exception — is the specific ruling and stands. The spec's ownAGGREGATION_CASESpinssum(flag)=3,avg(flag)=0.5,min(flag)=0,max(flag)=1on six backends anddriver-sqlcasts the aggregand on Postgres (#11635) to make that hold.AGGREGATION_CASESand the driver cast are untouched (「nothing else moves」). Option B is not taken.Diff, four files:
packages/spec/src/data/aggregate-field-type-compatibility.ts— aBOOLEAN_AGGREGATE_FIELD_TYPESconstant in the file's existing spelled-out-class idiom, spread into the four rows. The module TSDoc argued the opposite of the ruling in two places (the "everything else" bullet listed booleans among the refused; a dedicated passage recorded the two-ruling collision as unresolved). Both are rewritten to the ruling: the rule table names the boolean class on each row, a boolean bullet cites [finding]AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 as the authority and batch Add modern field types (slider, qrcode, geolocation) with configuration schemas #80 as the ruling that resolved the collision, and the "overrides" passage now records only the one override that remains (the string classes onmin/max).packages/spec/src/data/aggregate-field-type-compatibility.test.ts— the literal row pins gain the boolean members; the numeric / temporal bucket pins exclude the boolean class before comparing toNUMERIC_VALUE_TYPES/ the temporal classes; a new pin holds the boolean bucket equal toBOOLEAN_VALUE_TYPESand present on all four rows; the boolean refusal pins become accept pins; and a cross-pin againstAGGREGATION_CASESasserts that every boolean case the conformance suite requires a backend to answer is a pair this table accepts — the two spec tables can no longer contradict each other..changeset/aggregate-boolean-members.md— new,@objectstack/spec: minor(reasoning below)..changeset/aggregate-field-type-compatibility.md— the table's own PENDING changeset (still in.changeset/, so not yet in any CHANGELOG) said "Booleans are refused … the row ships exactly as batch Validation Protocol: Cross-Field, Async, and Conditional validation #59 stated it" and listedsum×booleanamong the refused examples. Both would have published a falsified statement in the same release this lands in, which is the argument the card itself makes about the TSDoc. Corrected in place: the boolean sentences now point at [finding]AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 / batch Add modern field types (slider, qrcode, geolocation) with configuration schemas #80; two example pairs in the narrowing sentence are replaced by pairs that remain genuine refusals (sum×boolean→sum×text,min×text→min×json); the string-classes sentence is kept verbatim while the paragraph's lead moves from "Two refused rows…" to "One refused row…". Declared here as a deviation from the card's two-file surface; the edit is prose only.Changeset level —
minor, judged from the diffNo export is added, removed or renamed:
pnpm --filter @objectstack/spec check:generatedreports all 15 artifacts up to date at499d718(api-surface/,export-origins/,declaration-map/,authorable-surface/, docs references all unchanged) — the exported declarations are unchanged (the table's declared type is the same; only its value gains members) and the newBOOLEAN_AGGREGATE_FIELD_TYPESconstant is module-private — absent fromdist/*.d.tsand from the bundles' export lists (it does ship inside the bundles as a non-exported binding, measured at review). The shippeddist/*.d.tsis NOT byte-identical: it carries the rewritten module TSDoc (a contract-review measurement at499d718; the first version of this section claimed byte-identity on the strength ofcheck:generated, which compares export listings and signatures, not.d.tsbytes — retracted). The accept set only widens (every pair accepted before is still accepted), and widening a published accept set is the same class of change the table's introduction was (decision batch #35 puts additive widening atminor). It rides the same release as the parent changeset, so the version outcome is identical either way;minoris the honest classification. Not breaking, so no ADR-0087 marker is required (check-adr-0087-registration --base origin/mainexit 0).Clause-② —
yesAdding members widens a published accept set; the card's claim declares
Clause-②: yesand this PR carriesneeds:contract-reviewon both carriers.check-widening-tellswould not fire (declaration isyes), and the widening is exactly the ruled one: two members, four rows.Verification
Three commits on the branch:
499d718(the implementation),299ed0b(contract-review patch round 1: a changeset paragraph, a test header comment, one assertion line),bee6b0b(patch round 2: one changeset sentence). The full derivation and suite below ran at499d718; the two patch rounds ran the families that read the paths they touched, recorded here so the narrowing is visible on the PR and not only in a report.Gate narrowing on the patch rounds — declared.
dispatch-gates.mjs --commandsderives 75 families for the branch's whole change set and has no per-path narrowing flag, so the patch-round subsets were hand-selected. At299ed0b, 22 commands, all exit 0:check-adr-0087-registration.mjs,check-changeset-no-major.mjs,check-empty-changeset.mjs(each--base origin/mainand--self-test),check:changeset-gate-self-tests,check:objectui-changeset,check-closing-keyword-parity.mjs(+--self-test),check-comment-mask-adoption.mjs(+--self-test),check-comment-mask-corpus.mjs,check-keyed-text-bounds.mjs(+--self-test),check:doc-authoring,check:nul-bytes, and the test-file familiescheck:cross-package-test-inputs,check:test-source-alias,check:where-matcher,check:objectql-double-limit,check:type-source-resolution; plus the pin file (Tests 21 passed (21)) andpnpm --filter @objectstack/spec typecheck(exit 0). Atbee6b0b, the 17 of those that read.changeset/**or repo-wide text (the same list minus the five test-file families), all exit 0. The other 53 derived families are keyed on thepackages/spec/src/**export / docs / liveness / authorable surfaces, on built output (check:dts-closure,check:dual-build-cjs-loads,check:sourcemap-no-sources-content), or onpackages/**source scans — a.changeset/*.mdedit reaches none of them, and thepackages/**scans that do read a test file ran in CI: at299ed0ball six required contexts (Lint & Repo Gates,TypeScript Type Check,Test Core,Build Core,Dogfood Regression Gate,Temporal Conformance (live PG + MySQL)) aresuccess. At499d718all 75 ran: 73 exit 0, 2 NOT MEASURED (prerequisite not met, CI-owned) — itemised below.pnpm --filter @objectstack/spec build—VERDICT command-exit 0(underos-verify-lock.sh).pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/data/aggregate-field-type-compatibility.test.ts—Test Files 1 passed (1),Tests 21 passed (21).pnpm --filter @objectstack/spec test(thelocalvitest project, the package's own suite) —Test Files 465 passed (465),Tests 12955 passed (12955), 411s,VERDICT command-exit 0.pnpm --filter @objectstack/spec typecheck— exit 0 (tsc --noEmit,check:scripts-typecheck,check:test-typecheckall green; the test file is insidetsconfig.test.json's program).pnpm --filter @objectstack/spec check:generated—All 15 generated artifacts are up to date(exit 0); nothing regenerated, nothing to commit.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths — the change set is taken from the merge base, 4 paths) printed 75 commands; every one was run with the printed spelling recorded byte-for-byte;--ranreconciles75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN(exit 0). 73 exit 0. Two are NOT MEASURED, both exit 3 PREREQUISITE NOT MET and both needing the whole 87-package closure built, which is CI's run (Build Core,lint.yml):pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt(--re-measure). Two others were cleared after their prerequisite was met:check-plugin-teardown-shape.mjs --self-test(exit 1 on the shallow clone, exit 0 after fetching its pinned fixture commit621a487, 47 cases) and@objectstack/lint check:doc-formula-expressions(exit 3 before, exit 0 after buildingformula+lint).git grepfor the table and predicate overpackages/**finds only this module, its test and thedata/index.tsbarrel — the two consumer legs (No layer refuses an incoherent aggregate / field-type pair — a dataset measureavgover a datetime works on SQLite and errors on Postgres #16099, the devx lint leg) are not landed, so no downstream test is owed; the public surface listings (api-surface/,export-origins/) are unchanged (see the changeset section), so no import-side re-test is owed either.Reverse verification — the pins fail when the boolean class is emptied
Committed first, then mutated. Subject resolved through the relative
./aggregate-field-type-compatibilityimport (source, notdist/), so no rebuild is part of the loop. Mutation: theBOOLEAN_AGGREGATE_FIELD_TYPESliteral replaced with an empty array carrying anABLATION-16685marker. On-disk proof by anchored counts: removed text 1 → 0, marker 0 → 1, blob92a0a4c1…→98e329ae…. Run:Tests 6 failed | 15 passed (21)— the six failing are exactly the boolean pins (sumrow,avgrow,min/maxrow, the boolean-bucket pin, the four-aggregate accept pin, and theAGGREGATION_CASEScross-pin); the totality pins stay green as they should. Expected direction: red; observed: red. Restore:git checkout HEAD -- ABSOLUTE_PATHspelled withHEADunder atrap … EXIT INT TERM; proofgit diff HEAD --statempty,git status --porcelainempty,git hash-objectof the restored file =92a0a4c1f7aae1ec61f4b9064c90985b7a102651= the HEAD blob, marker count 0.验收备注
driver-sqlboolean cast atsql-driver.ts:8497-8516; onorigin/main5e53d73it sits at:8670-8712. Line drift only, content exactly as the card describes. 承接者:无。api-surface,export-origins,authorable-surface, docs references) would need regeneration was measured false — no export or.describe()changed;check:generatedis green with nothing to regenerate.progressstays insum, andcount_distinct×percent(isIncoherentAggregateflags it, the table accepts it — pinned as a visible divergence). No layer refuses an incoherent aggregate / field-type pair — a dataset measureavgover a datetime works on SQLite and errors on Postgres #16099 remains open; the devx lint leg remains open.packages/spec/src/data/analytics.zod.ts,runtime/src/domains/analytics.tsandpackages/spec/src/data/seed.zod.tsare untouched.🤖 Generated with Claude Code
Generated by Claude Code