Skip to content

lint: refuse a dataset measure whose aggregate its field type cannot carry - #19138

Merged
os-elon-musk merged 4 commits into
mainfrom
claude/issue-16354-dataset-measure-aggregate-field-type-lint
Sep 18, 2026
Merged

os-elon-musk merged 4 commits into
mainfrom
claude/issue-16354-dataset-measure-aggregate-field-type-lint

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #16354

Clause-②: yes (narrowing)

The lint leg of the aggregate × field-type contract (director ruling, decision batch #59, 2026-09-06, 「both legs, table in spec」). A dataset measure pairs an aggregate with a field; AGGREGATE_FIELD_TYPE_COMPATIBILITY in @objectstack/spec declares which of those pairs every backend answers the same way, and nothing in the authoring path read it. New gating rule measure-aggregate-field-type-refused in packages/lint/src/validate-dataset-measure-aggregates.ts, registered in AUTHORING_RULES, so all three commands run it. The verdict is the shared predicate's on every pair — isAggregateCompatibleWithFieldType, the same call the compile leg makes — and there is no second table in this repo.

The take-order test, as an artifact

The maintainer's test for a contract-surface card: take a piece of author-written metadata, feed it in before and after the change, and see whether accept/reject flips. Both arms below were run, not reasoned from the diff: one probe script, byte-identical in both arms, driving the real shared registry (runAuthoringRules('lint', …) — the table os lint / os validate / os build all run).

The sample (author-written metadata, three measures over one object):

objects: [{ name: 'crm_opportunity', sharingModel: 'private', fields: {
  name: { type: 'text' }, units: { type: 'number' },
  closed_at: { type: 'datetime' }, win_rate: { type: 'percent' },
} }],
datasets: [{ name: 'opportunity_metrics', object: 'crm_opportunity', dimensions: [], measures: [
  { name: 'avg_closed_at',  aggregate: 'avg', field: 'closed_at' },  // positive control
  { name: 'avg_units',      aggregate: 'avg', field: 'units' },      // negative control
  { name: 'total_win_rate', aggregate: 'sum', field: 'win_rate' },   // third control
] }]
measure BEFORE (c70581bc8, pristine worktree) AFTER (this branch)
avg(closed_at)datetime ACCEPTED, no finding REFUSED, error
avg(units)number ACCEPTED, no finding ACCEPTED, no finding (unchanged)
sum(win_rate)percent ADVISED only — measure-aggregate-incoherent, warning REFUSED, error, beside that same warning
total findings on the sample 1 3

⇒ accept/reject flips on author-written metadata. Contract surface.

BEFORE arm — taken on the pristine worktree (git status --porcelain printed 0 lines) at the branch point, through the package source, with @objectstack/spec freshly built in this worktree beforehand:

$ pnpm exec tsx probe.mjs /home/user/wt-16354/packages/lint/src/index.ts
total findings over the whole sample: 1
--- measure[0] avg(closed_at) "avg_closed_at" -> ACCEPTED (no finding)
--- measure[1] avg(units) "avg_units"         -> ACCEPTED (no finding)
--- measure[2] sum(win_rate) "total_win_rate" -> ADVISED (warning)
      [warning] measure-aggregate-incoherent @ datasets[0].measures[2]

AFTER arm — same probe, unchanged, on this branch; run twice, through the source and through a freshly built dist, with identical verdicts:

$ pnpm exec tsx probe.mjs .../packages/lint/src/index.ts     # and: node probe.mjs .../packages/lint/dist/index.js
total findings over the whole sample: 3
--- measure[0] avg(closed_at) "avg_closed_at" -> REFUSED (error)
      [error] measure-aggregate-field-type-refused @ datasets[0].measures[0].aggregate
--- measure[1] avg(units) "avg_units"         -> ACCEPTED (no finding)
--- measure[2] sum(win_rate) "total_win_rate" -> REFUSED (error)
      [warning] measure-aggregate-incoherent @ datasets[0].measures[2]
      [error]   measure-aggregate-field-type-refused @ datasets[0].measures[2].aggregate

Build provenance, because a stale dist reads exactly like a real reading. Both arms resolve @objectstack/spec through its build. packages/spec/dist was absent in this fresh worktree and was built here (pnpm --filter '@objectstack/lint^...' build, run twice: once before the BEFORE arm, once after merging main); packages/lint/dist was built for the dist arm. Nothing was served from a turbo cache: no turbo process was in either pipelinepnpm --filter … build invokes each package's own script directly, and grep -ciE 'cache hit|turbo|FULL TURBO' over the build logs returns 0. The spec dist timestamp is newer than every source it was built from.

The three controls the card names

All three are permanent tests in validate-dataset-measure-aggregates.test.ts (23 tests in the file; the whole package is 105 files / 3978 tests, green):

  1. positive controlavg over a datetime field fires: one error at datasets[0].measures[0].aggregate, message pinned to name the aggregate, the field, its type and every accepted type.
  2. negative controlavg over a number field is silent. Generalised rather than left as one case: avg/sum over the whole numeric class, all four arithmetic and order aggregates over the boolean class (maintainer ruling [finding] AGGREGATION_ROWS has no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152), min/max over the temporal class, and count/count_distinct over every declared FieldType are each asserted silent.
  3. sum over percent fires — the pair analytics-service.ts already calls incoherent. It now carries two findings: the older advisory about meaning (suppressible) and this gating refusal about the contract. Deliberate, and documented in the rule's header: the two questions disagree elsewhere — count_distinct × percent is advised and accepted by the table, avg × datetime is refused here and not advised there.

And the strongest anti-false-positive assertion, because a false positive here is worse than the gap being closed: the rule's verdict is compared against isAggregateCompatibleWithFieldType for every aggregate × every declared FieldType (6 × 44 = 264 pairs), with floors on both sides of the sweep (more than 50 refused, more than 50 accepted) so neither "stopped firing" nor "fires on everything" can satisfy the equality vacuously.

The message the rule emits, verbatim

measure "avg_closed_at" applies aggregate "avg" to field "closed_at", which object
"crm_opportunity" declares as `datetime`. That pair is refused by the aggregate ×
field-type compatibility table in @objectstack/spec, so the number a backend returns
for it is a property of the SQL dialect rather than of the data — one coerces the
stored form and answers something plausible, another has no such function and fails
at query time. "avg" accepts: number, currency, percent, rating, slider, progress,
summary, boolean, toggle.

hint:

Either point "avg" at a field of an accepted type, or aggregate "closed_at" with one
its `datetime` type accepts: count, count_distinct, min, max. `count` /
`count_distinct` accept every type because they read no arithmetic off the value; a
quantity that must be added up or averaged has to be STORED as a numeric field (a
computed column) and aggregated as one. The compile leg refuses this same pair with
`400 DATASET_INVALID` before any SQL is emitted, so this is the same repair made
earlier.

Both halves are computed from the table — the accepted set for the refused aggregate, and the aggregates that would accept this field's type — never prose restating it, so neither can drift from the rows.

Where it stands down, and where it reaches further

Silent wherever the type cannot be resolved, per the spec module's own instruction that a consumer must not hand the predicate a guess: a dataset naming no base object or one this stack does not define, an object with no readable field map, a field path that resolves to nothing (that is dataset-field-unknown's finding — one typo must not also yield a type verdict), an untyped leaf, a non-string in either position, and an aggregate outside the closed AggregationFunction vocabulary. Each is a test.

It reaches further than the compile leg in exactly one direction: a dotted relationship.field reference. The compile leg returns early on those because its declared-type source answers for the base object only; authoring time has the whole object graph, so the leaf's declared type is a read rather than an inference, and the refusal names the object the leaf lives on. Registry-injected columns are judged on the same axis as authored ones.

Verification

  • pnpm --filter @objectstack/lint test — 105 files, 3978 passed, 0 skipped. Re-run after merging current main into this branch (the merge landed sibling work inside this package), and again after the last commit.
  • pnpm --filter @objectstack/lint typecheck — clean, including check:test-typecheck over the test layer.
  • pnpm --filter '@objectstack/lint^...' build and pnpm --filter @objectstack/lint build — clean, dts emitted.
  • Gates run locally, each exit code captured before any pipe: check:nul-bytes, check-empty-changeset --base origin/main, check-adr-0087-registration --base origin/main (judges this changeset: [BREAKING+clause-②-narrowing] not-required (already-registered)), check-changeset-no-major, check:changeset-gate-self-tests, check-changeset-fixed, check:doc-anchors, check-doc-frontmatter, check:docs-single-h1, check-docs-section-name, check:doc-authoring, check:docs-transcript-drift, check:docs-spec-enumerations, check:docs-redirects, check:docs-audit-scope, check:published-files, check:published-readme-links, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check-undeclared-dep-imports, check-comment-mask-adoption, check-comment-mask-corpus, check-keyed-text-bounds, check-section-landing-index, check-doc-route-spelling --advisory, docs-audit/check-affected-docs, docs-audit/check-drift-comment, check:cli-examples-parity, check:corpus-claim-drift, spec check:docs and check:yaml-examples, and the lint package's own two doc gates. All green.
  • Two of those gates went red on my first pass and drove real corrections, both mechanical consequences of registering a rule: check:doc-authoring refused a tracker id inside the new entry's runtime surfaceReason string, and check:docs-transcript-drift derives the author-time rule count from the registry and found four CLI transcripts still printing the old one (45 → 46).
  • ESLint, as a declared narrowing rather than a farm run: eslint --no-inline-config --format json over the four touched TypeScript files — 4 files linted, 0 errors, 0 warnings. The invariance that makes the narrowing a measurement rather than a skipped check is stated by the config itself: this repo runs one eslint.config.mjs, which "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not" — so no verdict on an untouched file can move on account of this diff, which changes no config and no shared roster. The population is that config's own files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] blocks minus their ignores; the repo-wide run belongs to CI.
  • check:type-check-debt reported PREREQUISITE NOT MET (exit 3) — the whole-workspace closure is not built here, and it says in its own words that this "is NOT a pass and NOT a finding". Recorded as not measured, not as green. Its sibling check:type-check-coverage did run and passed.
  • The gate derivation (scripts/pm/dispatch-gates.mjs --commands) names 86 families for this change set; 26 were run locally and the rest — repo-wide scans and populations CI owns — are left to CI, as a declared narrowing.

Acceptance notes

Out of scope here, filed nowhere by this PR, recorded so they are not rediscovered:

  • The dataset metadata type is not gated at the runtime publish door at all. dataset is a registered type with allowRuntimeCreate: true and supportsOverlay: true, but TYPE_TO_STACK_KEY in runtime-gate.ts maps no dataset row, so a dataset write builds no per-write snapshot and zero author-time rules dispatch for it — including the existence rules whose whole failure mode is a chart that renders empty. That is why this rule declares surfaces: cli with a reason naming the type axis rather than the snapshot's contents: both collections it reads are carried, so the usual reason does not apply. Mapping that type is a card of its own, and it would hand the door every rule reading stack.datasets at once.
  • The refusal's divergence and remedy prose lives only in service-analytics. The compile leg builds its message from two local helpers; @objectstack/lint cannot import them (its dependency direction is lint → spec, never a service), so this rule states the same two facts in its own words. Two accounts of one refusal can drift. Moving that prose into @objectstack/spec beside the table would make both legs read one sentence.
  • One pair now yields two findings. sum × percent is reported by both this rule (error, contract) and measure-aggregate-incoherent (warning, semantics, suppressible). Consolidating them is a decision about a published rule id's severity and suppressibility, which is its own PR by this repo's own convention.

Note on the declaration — it diverges from the claim comment, deliberately. The claim carries Clause-②: no, which is right about the refusal on its own: narrowing an accept set is a semantic surface and does not by itself touch clause ②. But this diff also adds published exports from @objectstack/lint (validateDatasetMeasureAggregates, MEASURE_AGGREGATE_FIELD_TYPE_REFUSED, DatasetMeasureAggregateFinding) and an AUTHORING_RULES entry — both widening tells against a no declaration — so the honest value is yes, and yes (narrowing) is the spelling for a diff that widens and narrows. Body and changeset carry the identical line. The changeset additionally carries the migration per refused class and the ADR-0087 disposition not-required (already-registered …): the two semantic entries registering this exact surface already exist, and the compile-time leg declared the same disposition against the first of them. The claim comment is the seat's to amend, not this PR's.

This PR opens as a draft and stays draft.


Generated by Claude Code

… cannot carry

The authoring-time leg of the aggregate x field-type contract (director ruling,
decision batch #59: "both legs, table in spec"). A dataset measure pairs an
`aggregate` with a `field`; `AGGREGATE_FIELD_TYPE_COMPATIBILITY` in
`@objectstack/spec` says which pairs every backend can answer identically, and
until now nothing in the authoring path read it: `avg` over a `datetime` field
validated clean, shipped, and became either a plausible wrong number (SQLite
coerces the canonical UTC text and returns the average YEAR) or a query-time
failure (PostgreSQL has no such function), decided by the deployment rather
than by the document.

`validateDatasetMeasureAggregates` walks `datasets[].measures[]`, resolves the
field's declared type on the object graph lint already indexes, and refuses the
pair when `isAggregateCompatibleWithFieldType` says no. The verdict is the
shared predicate's on every pair — no second table here — and the message names
the aggregate, the field, its declared type and the accepted set, with the way
out computed from the same table.

Silent wherever the type cannot be resolved rather than guessing: an
unresolvable base object, a dangling field path (that is
`dataset-field-unknown`'s finding), an untyped leaf, a non-string in either
position, and an aggregate outside the table's own vocabulary. It reaches
further than the compile leg in one direction only: a dotted
`relationship.field` reference, whose leaf type authoring time can read and the
compile leg's base-object field metadata cannot.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
… with its changeset

The rule reference gains the row and the worked example beside the dataset-axis
section that already covers measures, so an author reading about chart axes
finds the one about the measure itself. The changeset carries the migration the
refusal prescribes, per refused class, and the ADR-0087 disposition: the two
semantic entries that register this surface already exist.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
…nd re-derive the printed rule count

Two obligations the local gates named, both mechanical consequences of the new
registry entry:

- `check:doc-authoring` refuses a tracker id inside a runtime string, since the
  operators and generated surfaces that read one cannot resolve it. The reason
  string says what the differential does instead of citing where it was ruled;
  the adjacent comment, which only a source reader sees, keeps the citation.
- `check:docs-transcript-drift` derives the author-time rule count from the
  registry and found four CLI transcripts printing the old one. A registry entry
  moves that number, so the four move with it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
@os-elon-musk os-elon-musk added documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 14 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), avg_closed (literal, a string literal in a comment in DatasetMeasureAggregateFinding))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ee5812a5e3931b64037cb46e255649a9da0e7b74packageMentionDocs.

Which tree this was computed on

This run read content/docs from 16e494b74487164bce01fad4504386b24488d8a9 — the merge of head e1851367078379be69a0b80d82f2f25fe69c534d into base ee5812a5e3931b64037cb46e255649a9da0e7b74, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 16e494b74487164bce01fad4504386b24488d8a9 && git checkout 16e494b74487164bce01fad4504386b24488d8a9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee5812a5e3931b64037cb46e255649a9da0e7b74 e1851367078379be69a0b80d82f2f25fe69c534d && git checkout -B drift-repro ee5812a5e3931b64037cb46e255649a9da0e7b74 && git merge --no-ff e1851367078379be69a0b80d82f2f25fe69c534d

node scripts/docs-audit/affected-docs.mjs --json ee5812a5e3931b64037cb46e255649a9da0e7b74

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

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 75/75 CONTRACT_REVIEW_TIER
Head-sha: e1851367078379be69a0b80d82f2f25fe69c534d

Isolated at-tier reviewer for PR #19138 / card #16354. Tier reading: grep of my OWN transcript (agent-a5bac29d297fdc9c9) at 2026-09-18T22:59:23Z — 75 assistant-message LINES, 75 at the constant's value, 0 off-tier, across 12 distinct request ids (lines are the honest grain; the request count is stated beside it, not as it). Diff read as git diff merge-base b4b83b3b2 (with main at aadea24b8) to e18513670, fetched into refs I own: 9 files, +744/−4, identical to the PR's file list. ⛔ No node_modules on this checkout: the package suite, typecheck, build and eslint were NOT re-run here and every such figure below is the dev's claim, marked as such. What I ran myself is named as mine.

① Derived judgments

Declaration Clause-②: yes (narrowing) is right, for the right reasons — both limbs verified on the diff, not the prose.

  • Narrowing limb (accept set). At the merge base, isAggregateCompatibleWithFieldType has 1 mention in packages/lint non-test source and it is a doc comment (data-model-rules.ts:116) — 0 calls; lit control: 5 mentions in the package's test files at the same tree. At head: 2 calls, both in the new rule (validate-dataset-measure-aggregates.ts:166, :212). The rule is tier: 'gating', commands: ALL (= validate/build/lint, AUTHORING_COMMANDS:153), severity always error. Read path of the three controls against the spec table (49-member FieldType enum, unchanged by the diff): avg row = 7 numeric + 2 boolean, no datetime ⇒ predicate false ⇒ error at datasets[i].measures[k].aggregate (positive control fires); number is in the avg row ⇒ silent (negative control is a real negative); sum row omits percent ⇒ error, beside measure-aggregate-incoherent (warning, validate-widget-bindings.ts:720) ⇒ two findings. Accept/reject flips on author-written metadata; the dev's BEFORE/AFTER probe was not re-run here and is consistent with this reading.
  • Widening limb (public surface). packages/lint/src/index.ts adds 3 barrel exports (validateDatasetMeasureAggregates, MEASURE_AGGREGATE_FIELD_TYPE_REFUSED, type DatasetMeasureAggregateFinding) from @objectstack/lint 17.4.0 — not private, one of the 70 members of the changeset fixed group; AUTHORING_RULES grows 45 → 46 rows (run: lines counted at base and head). ⚠️ check-widening-tells (my run, --declaration no control) answers 9 files NOT MEASURED — packages/lint is under no declared tell surface — so this limb is a human reading of the diff, which is exactly what the seat's correction 5737112323 claimed. yes (narrowing) is the gate's spelling for widen-and-narrow; readClause2Line reads the arm as narrowing (the ADR-0087 gate's MUST_MATCH_BREAKING fixture pins that exact spelling).
  • The 294-pair sweep, judged by reading. It iterates Object.keys(AGGREGATE_FIELD_TYPE_COMPATIBILITY) × FieldType.options, asserts fires === !tableAccepts per pair, then refused greater than 50, accepted greater than 50, and refused + accepted === aggregates × types. Every fixture declares measured: { type: fieldType } on a single-segment path, so resolveFieldPath answers kind: 'ok' with meta.type = fieldType for every member (graphObjectOf keeps any string type; no hop is traversed) — a skip on a refused pair would FAIL the equality, never satisfy it vacuously. Floors are asserted, both sides.
  • ⚠️ Corrected figure. FieldType has 49 members at both the merge base and the head (enum literal parsed with comments stripped; cross-check: the second ADR-0087 entry's own "37 × 2 = 74" arithmetic is 49 − 12), not 44. The sweep is therefore 6 × 49 = 294 pairs — 139 accepted (49 + 49 + 8 + 9 + 12 + 12), 155 refused — both floors hold with margin. The "6 × 44 = 264" in the PR body and the os-dev-report is a prose error; the test hardcodes neither number, so no code moves.
  • Dev's counts: 23 tests in the file — taken (23 it( lines, control 6 describe(). 105 files / 3978 tests — NOT measured here; CI Test Core 6/6 shards concluded success by 2026-09-18T22:58:03Z.

② Semver level

  • .changeset/lint-dataset-measure-aggregate-field-type.md: '@objectstack/lint': minor, a **BREAKING** banner, Clause-②: yes (narrowing), a per-class FROM → TO migration, and the adr-0087 marker not-required (already-registered dataset-measure-aggregate-field-type-refused, dataset-measure-selecting-aggregate-field-type-refused).
  • Level: right. yes takes at least minor; (narrowing) is BREAKING; the launch-window guard ships breaking as minor with the banner and the ADR-0087 disposition as the carriers, and no .changeset/pre.json exists in the tree, so major would be refused. My run: check-changeset-no-major --base refs/review/16354-main --head refs/review/16354-head → exit 0.
  • Sentence: describes what shipped. "Refuse a dataset measure whose aggregate the field's declared type cannot carry, at authoring time" — gating error on all three commands, dotted relationship.field leaves judged, silence on unresolvable types: each verified in the rule source. Gap, non-blocking: the body names the rule id but not the three barrel exports that carry the yes half; one line would make the CHANGELOG state both halves.
  • "Already registered" — tested, and it holds. Both ids exist as id: rows in packages/spec/src/migrations/registry.ts at the merge-base tree AND at the head tree (:6953, :7020 in both), each with its own entry file under migrations/entries/semantic/18.*; lit control: a made-up id returns 0 hits (grep exit 1). The diff touches nothing under packages/spec, so neither id is fresh and the gate's R5 refusal cannot fire. Coverage: entry 1 registers the avg/sum × temporal rows and carries the No layer refuses an incoherent aggregate / field-type pair — a dataset measure avg over a datetime works on SQLite and errors on Postgres #16099 widening to every class (the compile-leg PR e66da5c91 declared already-registered against this same id — read from that commit's changeset); entry 2 registers min/max × the 37 refused types (74 aggregate x field-type pairs the table refuses are enforced by nothing — min/max over every class, and the 42 string rows cannot be ruled apart from the other 32 #17560). Together they cover every pair the table refuses, and the lint leg adds no pair — same predicate, same table, second door. My run: check-adr-0087-registration --base refs/review/16354-main --head refs/review/16354-head → exit 0, reading [BREAKING+clause-②-narrowing] not-required (already-registered) — the dev's reading, taken by me on the fetched refs.

③ Boundary flags

  • Governed / generated / other lanes: none of the 9 paths lies under a governed surface. No api-surface artefact exists for packages/lint (only packages/spec has one), so nothing generated is owed. The 4 content/docs edits are the rule-reference paragraph the card demanded plus the transcript count 45 → 46; whole-tree check at head: 0 residual "(45)", 4 "(46)".
  • Confirmed: refusal prose exists twice. DIVERGENCE_BY_AGGREGATE / REMEDY_BY_SOURCE_CLASS live only in packages/services/service-analytics/src/dataset-compiler.ts; lint depends on spec alone, so the rule restates the two facts. Drift risk, noted, not blocking; the dev's "move it into spec beside the table" is the right successor card.
  • Confirmed: sum × percent yields two findings from two rule idsmeasure-aggregate-incoherent (warning, suppressible) and the new one (error, not) — documented in the rule header; consolidation changes a published id's severity and is its own PR.
  • Confirmed: TYPE_TO_STACK_KEY (runtime-gate.ts) maps flow/object/view/action/page/dashboard/agent/hook/seed/permission/book and no dataset row, so the rule is CLI_ONLY with a reason; the runtime-door gap is the card the dev says to file — filed by nobody yet.
  • Shared-file overlap: sibling open lint PRs Refuse an unreadable reference carrier at the ten residual readers (ruling E item 2 residue) #19080 (18 files), feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319 (92 files), fix(lint): translation-target-unknown resolves the two declared contribution surfaces #19060 (3 files) touch neither authoring-rules.ts nor index.ts nor the two docs pages; CI single-writer check success.
  • Main moved since the merge base (19 files, including migrations/registry.ts); both ids remain at aadea24b8. The queue's merge re-runs the gates.
  • Prose correction owed: the "264 pairs" figure (PR body + os-dev-report) should read 294 / 49 field types; a follow-up comment, not a body PATCH (the body already carries its footer), and no code change.
  • CI as seen at 2026-09-18T22:58:03Z: 36 success, 14 skipped, 1 in_progress (Lint & Repo Gates — the job that owns the 60 families the dev left to CI). Not green yet; this verdict is on the contract, not on CI, and the seat does not flip ready before that job concludes.
  • Not measured here: package tests, typecheck, build, eslint, check:type-check-debt (the dev recorded exit 3 PREREQUISITE NOT MET as not measured — correct handling).

Implemented-by: claude/issue-16354-dataset-measure-aggregate-field-type-lint
Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh

VERDICT: PASS

Record written 2026-09-18T23:00Z; every reading above carries its own time or tree.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Seat disposition — contract review PASS, both carriers cleared, and one number in the body is wrong

Seat domain:spec#3, session_019srGWGCBBCBHqcDoRZpQRh, 2026-09-18T23:03Z.

Review of record: comment 5737203251, **VERDICT: PASS** on head e18513670, taken by an isolated at-tier review subagent — this seat measured BELOW CONTRACT_REVIEW_TIER and so may not issue a clause-② verdict itself. Its tier reading, re-taken on its own turn: 75/75 at the granularity of assistant-message lines in its own transcript over 12 distinct request ids — not 75 requests.

Carriers cleared, with provenance. This seat ran check-clause2-carriers.mjs --pair 19138 (a space, never =) and captured the code BEFORE any pipe: exit 0. C2-CORRECTION reads the declaration as Clause-②: yes off correction comment 5737112323, which supersedes the claim's own no without editing it; C6-RECORD names 5737203251 on this exact head with a Reviewed-by: line and an at-tier Served-tier:. ⇒ needs:contract-review removed from this PR and from card #16354 in one four-step write each, read back: PR documentation, size/l, tests, tooling; card enhancement, tooling, priority:p2, pm:dispatched, domain:spec with the assignee kept.

⚠️ Correction to this PR's own text: the sweep is 294 pairs over 49 field types, not 264 over 44

The body and the dev report both say 6 × 44 = 264. The reviewer flagged it and this seat re-measured it independently rather than relaying the flag:

git show origin/main:packages/spec/src/data/field.zod.ts
  | awk '/const FieldType = z.enum\(\[/,/\]\)/' | sed 's|//.*||'
  | grep -oE "'[a-z_]+'" | sort -u | wc -l     ->  49

⭐ Two controls on the same subject: (i) stripping comment text first changes nothing — 49 either way, so no quoted word from a comment inflated it; (ii) the second ADR-0087 entry's own arithmetic (37 × 2 = 74) reconciles with 49 − 12 = 37. ⇒ 6 aggregates × 49 field types = 294 pairs, which the reviewer read as 139 accepted / 155 refused on the head.

No code change follows, and none is owed. The sweep asserts fires === !tableAccepts per pair and asserts a floor on BOTH sides; it hard-codes no total, so the population it walks is 294 whatever the prose says, and both floors keep the equality from holding vacuously. What is wrong is a sentence, not a test. ⛔ This seat does not PATCH the PR body — it already carries the platform footer in its session-URL form, so a PATCH would append a second one (AGENTS.md:432); the correction lives here instead, which is the same remedy this seat used on PR #19122 earlier today.

The reviewer's non-blocking gaps, recorded so they are not rediscovered

  • check-widening-tells answers NOT MEASURED for all 9 files — packages/lint sits under no declared surface — so the widening limb (3 new barrel exports on a published package, AUTHORING_RULES 45 → 46) is a human diff reading, not a tool's. That is exactly what correction 5737112323 asserted, and the reviewer confirmed it by reading the diff.
  • The changeset names the rule id but not those 3 barrel exports, which are the carrier of the yes limb. Non-blocking.
  • The dataset runtime-publish gap the dev reported (TYPE_TO_STACK_KEY carries no dataset row) — the reviewer noted no card existed yet. It does now: [finding] a runtime-created dataset reaches ZERO author-time rules — the type declares allowRuntimeCreate: true while nothing declares it in runtimeTypes and TYPE_TO_STACK_KEY has no row #19143, filed by this seat with both legs measured and a lit control.
  • Not measured by the reviewer, stated rather than implied: the package test suite, typecheck, build, eslint, and check:type-check-debt (which the dev correctly recorded as exit 3 PREREQUISITE NOT MET rather than green).

What is left before this can land

CI on e18513670, read by this seat at 2026-09-18T23:02Z: 52 check runs — 37 success · 14 skipped · 1 QUEUED (Check Changeset) · 0 failed. ⛔ A queued run is not green, so this PR stays a draft for now. When that run completes clean, the seat flips it ready through POST .../ccr/ready_for_review (⛔ a bare PATCH {"draft": false} answers 200 and changes nothing — reproduced again today) and arms the queue; if this seat's shift closes first, the PR is handed over green-pending, carriers clear, PASS on record and the successor needs only a fresh CI read before flipping it.


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 23:06
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 362dcc3 Sep 18, 2026
55 of 56 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-16354-dataset-measure-aggregate-field-type-lint branch September 18, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: refuse an incoherent dataset measure (aggregate × field type) at authoring time, from the spec's compatibility matrix (lint leg of #16099)

2 participants