Skip to content

feat(service-analytics)!: refuse an aggregate a datetime measure's field type cannot carry, and reconcile the storage-form annotations to one measured statement - #16778

Draft
os-trump wants to merge 4 commits into
mainfrom
claude/issue-16737-avg-datetime-measure
Draft

feat(service-analytics)!: refuse an aggregate a datetime measure's field type cannot carry, and reconcile the storage-form annotations to one measured statement#16778
os-trump wants to merge 4 commits into
mainfrom
claude/issue-16737-avg-datetime-measure

Conversation

@os-trump

@os-trump os-trump commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16737

Also implements the compile leg described on #16099 — that card stays open and its
disposition is the PM's; nothing here is a verdict about it.

1. The storage reality, established first — and it is a THIRD answer

The card said the package states two incompatible answers. Measured on origin/main,
neither is current. A SQLite Field.datetime column has ONE storage form:
canonical UTC TEXT, YYYY-MM-DDTHH:MM:SS.sssZ (#3912/#3928).

How it was measured, not recalled:

  • SqlDriver.storageDatetimeValue canonicalises on the write path, and
    sql-driver-datetime-canonical-storage.test.ts pins every accepted input shape
    folding onto the same stored string — a Date, ISO …Z, ISO with an offset, a naive
    wall clock, an epoch number, an epoch string, a bare calendar day. The NOW() column
    default writes the same strftime('%Y-%m-%dT%H:%M:%fZ', 'now') bytes.
  • The INTEGER epoch survives only in a database written before the convention.
    initObjects runs backfillCanonicalDatetimes at schema sync;
    needsLegacyDatetimeRepair is the one predicate for "might this column still hold a
    pre-canonical value". Two cases keep it true: a table not yet backfilled, and an
    external / unmanaged object (registerExternalObject never marks its datetime columns
    canonical).
  • Postgres and MySQL never enter the question: the DDL gives them timestamptz /
    DATETIME(3), one on-disk shape by construction.

Re-driven live, in this session, on a fresh SQLite:

select typeof(dt), dt from t limit 1   => {"t":"text","v":"2026-05-19T00:00:00.000Z"}
select avg(dt) from t                  => 2025.5      <- the average YEAR
select sum(dt) from t                  => 4051
select avg(d)  from t   -- a `date`    => 2025.5
select avg(tm) from t   -- a `time`    => 13          <- the average HOUR
select avg(n)  from t   -- a `number`  => 15          <- the control: correct
select min(dt), max(dt) from t         => the two real instants

So the filer's text|2026-05-19T00:00:00.000Z reading is exactly what the current
driver produces. ⭐ The mixed INTEGER/TEXT column is the transitional state, and the
flat "a SQLite Field.datetime IS an INTEGER epoch" has been wrong since #3912.

2. coerceTemporal — the thing triage named as unmeasured

It is correct for the mixed-storage reality, and it is correct for the canonical one.
Its docblocks were the stale part.

  • The comparand half (NativeSQLStrategy.coerceTemporalctx.coerceTemporalFilterValue
    SqlDriver.temporalFilterValuecoerceFilterValue) canonicalises to the same
    function the write path uses, so an ISO or bare-day comparand becomes the stored form.
    Pinned in sql-driver-temporal-dialect.test.ts and sql-driver-analytics-datetime.test.ts.
  • The column half (temporalColumntemporalFilterColumnSql) emits the repair CASE
    only while needsLegacyDatetimeRepair holds, and the bare column otherwise —
    pinned by sql-driver-datetime-canonical-storage.test.ts
    (expect(driver.temporalFilterColumnSql('evt','at','"at"')).toBe('"at"') after backfill).

No code change was needed there. A test case was added on the analytics side
(native-sql-datetime-filter.test.ts) driving the strategy against a hook that returns
today's canonical UTC text, beside the epoch hook the file was written with — so the
suite covers the live storage form as well as the legacy one.

3. The annotations reconciled — seven sites, not the one the filer quoted

The card named four. A sweep for the claim found seven in source plus two test
narratives, all making the same stale statement. All are now consistent, and the fact is
stated once:

site was now
analytics-service.ts coerceTemporalFilterValue "SQLite Field.datetime → epoch ms" the single canonical statement — measured, with the legacy/external tiers named
analytics-service.ts coerceTemporalFilterColumn (:493) "holds BOTH storage forms … at once" links to the block above; states that the column half is now CONDITIONAL
plugin.ts (:640) "becomes epoch ms on SQLite" links; no restatement
plugin.ts (:662) "holds BOTH storage forms" links; names the mixed form as transitional
native-sql-strategy.ts temporalColumn (:963) "carries an INTEGER epoch and ISO TEXT at the SAME time" links; keeps what is still true of the method
native-sql-strategy.ts buildFilterClause (:1190) "converted to its INTEGER epoch storage form" links
objectql-strategy.ts dateRangeBounds (:1669) "a SQLite Field.datetime IS an INTEGER epoch (#2034)" links; keeps the real reason NativeSQLStrategy needs the coercion
__tests__/native-sql-datetime-filter.test.ts narrative asserted the epoch as current narrative corrected; epoch fixture KEPT and re-labelled (it is the only hook that changes value AND JS type, which is what makes "bound verbatim" decidable)
__tests__/native-sql-datetime-filter-column.test.ts narrative asserted the mixed column as steady state narrative corrected; EPOCH_MS(...) re-labelled as a marker, not a claim about emitted SQL

4. Refusal, not a definition — and where it stops

Refusal. A definition would have to pick one of two dialect answers and ship it as
the meaning of a number nobody can check. The refusal moves the failure to the person
writing the dashboard, which is the PM direction and the card's own danger analysis.

It is not a new rule. The contract already exists: AGGREGATE_FIELD_TYPE_COMPATIBILITY
in @objectstack/spec (#16353, landed as 6ba0db4e0), declared under the director
ruling of decision batch #59. Until this PR it had zero consumers. compileDataset
now reads the shipped predicate and refuses with DATASET_INVALID / 400 before any
query is built, using the declared type the host already supplies through
AnalyticsServiceConfig.sourceFieldMeta (threaded as a new optional
DatasetCompileOptions.declaredFieldType probe, tiered "cannot answer, do not block").

⚠️ The compile leg lands SCOPED to temporal source fields — the dispatch's stop condition, honoured

The dispatch said: stop and report if refusing would break a use that currently works
and is legitimate
. Executing every row of the table does exactly that, in two places,
and both are already recorded in the spec module's own header as overrides of existing
opinions rather than agreement
:

⇒ The gate judges the temporal class only (TEMPORAL_SOURCE_FIELD_TYPES, this
package's own shipped statement of it). The verdict is still the table's — nothing is
restated, and min/max over a temporal field stay accepted because the table accepts
them. The temporal rows carry no collision: measured on both dialects, no shipped dataset
in this repo pairs them, and there is no reading on which the mean of a set of instants is
a duration. Whether the string and boolean rows are enforced is a product judgement, and
it is the blocker for the full-table leg.
See the acceptance notes below.

5. derived is covered by construction

A derived measure's of operands are base measures of the same dataset, and the executor
expands a selected derived measure into them before querying. Both are downstream of the
compile loop, so a dataset carrying a refused base measure never finishes compiling and no
derived op can be handed its output — including when the selection names only the
derived measure
, which is the filer's exact shape. Pinned as two cases, one of them
selecting only cycle_delta.

6. Second dialect — measured on real PostgreSQL 16.13

select avg(submitted_at) from t16737;   -- timestamptz
  ERROR:  function avg(timestamp with time zone) does not exist   -- SQLSTATE 42883
select sum(submitted_at) from t16737;   -- same
select avg('2026-05-19'::date);         -- ERROR: function avg(date) does not exist
select min(submitted_at), max(submitted_at) from t16737;
  2025-01-19 00:00:00+00 | 2026-05-19 00:00:00+00                 -- correct

Which half is dialect-specific: the SILENT half is SQLite's. SQLite has no temporal
type, so the stored text is coerced to a number by its leading digits and the call
succeeds. Postgres has a real timestamptz and no avg over it, so it refuses loudly.
The MEANINGLESS half is not dialect-specific — there is no backend on which the mean
of a set of instants is a duration; Postgres simply says so out loud.

⚠️ One asymmetry worth recording: select avg('12:00:00'::time) succeeds on Postgres
and returns a clock time, while SQLite silently answers 13 (the average hour) for the
same data. So avg over a Field.time is a genuine per-dialect split. The ruled table
refuses it (time appears only in the min/max rows), and that ruling is executed here
rather than re-litigated.

7. Negative controls — this card is aggregation only

All pinned in aggregate-datetime-measure-refusal.test.ts:

  • avg over a number measure, and over a currency measure — still work end to end,
    one statement emitted, AVG in the SQL.
  • A datetime used as a dimension — month grouping, and a dateRange window — both
    reach the engine unchanged (the window asserted present in the engine call).
  • min / max over a datetime — still accepted, still return real instants.
  • count over a datetime — still accepted.
  • A derived measure over numeric operands — untouched.
  • min over a text field — still compiles here, with the table's contrary verdict
    asserted directly, so the scope boundary reads as "the contract says no, this gate does
    not act on it" and cannot widen unnoticed.

Tests

pnpm --filter @objectstack/service-analytics test
  Test Files  97 passed (97)        Tests  2187 passed (2187)
pnpm --filter @objectstack/service-analytics typecheck   -> exit 0
pnpm --filter @objectstack/spec typecheck                -> exit 0
  check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s) held

Dissolution verification — direction predicted before running, mutation proved on disk

Deleting the assertAggregateFieldTypeCompatible call from the measure loop:

HEAD blob          = d4570fabe20285c57a3873233519a38089fae902
worktree (pre)     = d4570fabe20285c57a3873233519a38089fae902   (identical)
anchor occurrences BEFORE = 1  AFTER = 0    injected marker = 1
worktree (mutated) = a4ea0d805f086dd1b1f8932e47f52bedf2074e78   (changed)
ABLATED RUN EXIT = 1 — Tests  7 failed | 16 passed (23)
restore: git diff HEAD for target = []                          (empty: restored)

The 7 red are exactly the refusal cases (both derived cases among them); the 16 green
are the negative controls, the tiering tiers, the scope boundary, the contract-table
assertions and the live SQLite measurement suite. No case can pass vacuously: each refusal
case asserts the envelope AND that no SQL was emitted, and with the gate gone the compile
succeeds and SQL IS emitted. Restored from HEAD (not a bare checkout), proved by an empty
git diff HEAD rather than by an exit code.

Gates

node scripts/pm/dispatch-gates.mjs --commands derived the family; every command was run
and reconciled with --ran. Green, with three NOT MEASURED that are CI's:

  • pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET (needs a whole-farm
    pnpm build). Declared to CI.
  • pnpm check:type-check-debt — exit 3, PREREQUISITE NOT MET (--re-measure needs the
    built closure of 23 workspace deps). Its first half, check:type-check-coverage, passed.
  • pnpm --filter @objectstack/lint check:doc-formula-expressions — exit 3, PREREQUISITE
    NOT MET (@objectstack/lint not built here); not a package this diff touches.

ADR-0087 and the ledger:

node scripts/check-adr-0087-registration.mjs   -> exit 0
  1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
  [BREAKING+bang] registered dataset-measure-aggregate-field-type-refused (new here)
pnpm --filter @objectstack/spec check:migration-registry / check:spec-changes / check:upgrade-guide -> exit 0

Repo-wide pnpm lint was not narrowed and not run locally; it is CI's whole-root run.

Clause-② and the changeset

Clause-②: yes. The dispatch's provisional read was no; the diff overturns it, on
both the mechanical floor and the gate:

node scripts/pm/check-widening-tells.mjs --declaration no  -> exit 4
  T2 packages/spec/src/migrations/registry.ts — a new member of a closed set
node scripts/pm/check-widening-tells.mjs --declaration yes -> exit 0

The mechanical floor also applies on its own: DatasetCompileOptions.declaredFieldType is
a new key on a published exported type (packages/services/service-analytics/src/index.ts
re-exports DatasetCompileOptions). needs:contract-review is carried on this PR.

Changeset: minor for both packages, declared BREAKING with a ! title. It is an
accept-set narrowing on a published authoring surface, which is breaking — and #16353's own
changeset says in writing that the consumer legs "carry the breaking declaration, its
migration prescription and the ADR-0087 disposition". minor rather than major is the
repo's launch-window convention for accept-set narrowings, stated verbatim in the protocol
18 step's header ("The enforcement itself ships earlier on the 17.x line … this step is
where migrate meta users are told"). ⛔ Not patch: nothing here is "just a bug fix" —
authoring shapes that parsed and ran now fail. The FROM → TO table is in the changeset body.

验收备注

Reported here for the PM, not filed as issues:

  1. The full-table compile leg (No layer refuses an incoherent aggregate / field-type pair — a dataset measure avg over a datetime works on SQLite and errors on Postgres #16099) is blocked on a ruling collision, not on
    engineering.
    Enforcing the table's string rows contradicts A dataset measure over a datetime is typed number in the analytics response, and a metric tile ignores the format style it is handed — measured on 17.3.0 #15768 (measured: 15 red
    cases); enforcing its boolean rows contradicts 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. The spec
    module's header already records both as overrides referred onward. Somebody has to
    rule them before the leg can be widened past temporal.
  2. No layer refuses an incoherent aggregate / field-type pair — a dataset measure avg over a datetime works on SQLite and errors on Postgres #16099 is still labelled pm:blocked with Blocked-by: #16353, and spec: declare the aggregate × field-type compatibility matrix (AggregationFunction × FieldType) that dataset measures are refused against (spec half of #16099) #16353 has
    landed
    (6ba0db4e0, PR feat(spec): declare the aggregate × field-type compatibility matrix (AggregationFunction × FieldType) dataset measures are refused against (#16353) #16684). The blocker is stale; the label is the PM's to move.
  3. /analytics/query (the non-dataset face) is not covered by this leg. An
    auto-inferred submitted_at_avg measure on a Cube still reaches the driver — the
    ruling names two legs (lint + compile) and neither is that face. Observation, not a
    defect this PR introduces.
  4. The authoring-time lint leg has not landed; packages/lint still carries no rule
    pairing a measure's aggregate with its field type. That is No layer refuses an incoherent aggregate / field-type pair — a dataset measure avg over a datetime works on SQLite and errors on Postgres #16099's sibling devx card.
  5. avg over a Field.time is a real per-dialect split (Postgres answers a clock
    time; SQLite answers the average hour). The table refuses it and this PR executes that;
    worth a maintainer eye if anyone was relying on the Postgres behaviour.

None of these were filed: a duplicate-search would be needed for each, and items 1–2 are
dispositions on an existing card rather than new work.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37


Generated by Claude Code

…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
…rce 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 (#15768,
maintainer ruling #11152). Executing those is a product judgement that belongs to
#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
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-analytics, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/measure-result-type.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/plugins/packages.mdx (via AnalyticsServicePlugin (symbol, a top-level class))

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

  • content/docs/releases/v17.mdx (via ObjectQLStrategy (symbol, a top-level class))

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/measure-result-type.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 130 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 941232040d94eccc3d86204c7e96c84b775a1000packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7be3862e7cb967d7e4a4497d383290c8c356c614 — the merge of head 80ec9f2b6a7e076b9c9683334224f62234979ac1 into base 941232040d94eccc3d86204c7e96c84b775a1000, 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 7be3862e7cb967d7e4a4497d383290c8c356c614 && git checkout 7be3862e7cb967d7e4a4497d383290c8c356c614
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 941232040d94eccc3d86204c7e96c84b775a1000 80ec9f2b6a7e076b9c9683334224f62234979ac1 && git checkout -B drift-repro 941232040d94eccc3d86204c7e96c84b775a1000 && git merge --no-ff 80ec9f2b6a7e076b9c9683334224f62234979ac1

node scripts/docs-audit/affected-docs.mjs --json 941232040d94eccc3d86204c7e96c84b775a1000

⚠️ 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 941232040d94eccc3d86204c7e96c84b775a1000 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16778 @ 80ec9f2

Verdict: CHANGES REQUIRED — the code implements the ruling exactly; the breaking declaration (changeset + ADR-0087 ledger entry) declares a refusal the code does not perform (F1). Text-only fix, no behaviour change needed.

Ruling implemented: YES. The compile gate judges only fields in TEMPORAL_SOURCE_FIELD_TYPES (date / datetime / time); string and boolean fields are never handed to the predicate. Verified in code, not from the body: dataset-compiler.ts assertAggregateFieldTypeCompatible returns before calling isAggregateCompatibleWithFieldType unless TEMPORAL_SOURCE_FIELD_TYPES.has(fieldType); the scope-boundary test pins min × text still compiling. Nothing enforces the string rows (would pre-empt #16785 C) or the boolean rows (would contradict #11152 / #16685).

Rulings, quoted

#16785, maintainer via director seat, comment 5580219196 (2026-09-08 06:16Z):

Ruled: C, with one correction to the card's framing: the boolean half is already settled — #16685 was ruled A and PR #16750 (merged 2026-09-08T05:12Z) added boolean / toggle to sum / avg / min / max; only the string rows remain. […] The table therefore accepts min / max for the string-typed field types #15768 types 'string'; the temporal refusals stay as PR #16778 executes them. […] @objectstack/spec minor; ⛔ no ADR text moves.

#16737, PM seat, comment 5579707565: 「问题一:已裁的兼容表无法整表执行 —— 裁 A(维持温度类范围)」 / 「A = 本 PR 已做的:只执行温度类,把 string 与 boolean 两组另行送裁。」

⇒ Both rulings converge on: this PR enforces the temporal rows only. It does.

Verification (independent, against refs/review/16778 vs merge-base 8b37a0973)

  1. Scope of enforcement. Refused at compile time: avg / sum over date, datetime, time (any aggregate the table's row lacks for a temporal field). Accepted and pinned: count / min / max over datetime, avg over number / currency, datetime as a grouping dimension and as a dateRange window, derived over numeric operands, min over text. Boolean fields: never judged (not in the temporal set). Consistent with both rulings.
  2. Files (13, none governed). .changeset/dataset-measure-aggregate-field-type-refused.md (A); service-analytics/src/: dataset-compiler.ts, analytics-service.ts, plugin.ts, measure-result-type.ts, strategies/native-sql-strategy.ts, strategies/objectql-strategy.ts (M); __tests__/aggregate-datetime-measure-refusal.test.ts (A), __tests__/measure-result-type.test.ts, __tests__/native-sql-datetime-filter.test.ts, __tests__/native-sql-datetime-filter-column.test.ts (M); packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts (A), packages/spec/src/migrations/registry.ts (M, the generated concatenation). Checked against GOVERNED_SURFACES (docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md) and CODEOWNERS (docs/adr/, CODEOWNERS, check-governed-merges.mjs): no governed path. content/docs/releases/ untouched.
  3. The refusal. Compile-time, in compileDataset's measure loop, after assertDeclared; throws datasetInvalidError(...)DATASET_INVALID / 400. The code is pre-existing and registered: packages/spec/src/api/error-code-ledger.zod.ts:219,898 (ADR-0112 D3 ledger, RegisteredErrorCode-typed at dataset-refusal.ts:124). No new code introduced; packages/runtime/src/dispatcher-error-vocabulary.ts is not the relevant ledger for this service-level code. Host wiring: analytics-service.ts threads declaredFieldType from the existing sourceFieldMeta(...)?.type. Measured premise: the SQLite half (typeof = text, avg = 2025.5, min/max = real instants, numeric control = 20) is pinned by a live sql.js suite in the new test file (sql.js is a declared devDependency, already used by five sibling suites). The Postgres half (42883) is asserted only — body, compiler docblock, ledger reason, test header — no test drives it and the live PG conformance job is untouched (F3).
  4. Annotation reconciliation — no behaviour hides in it. Seven source sites moved, all comments/docblocks: analytics-service.ts coerceTemporalFilterValue (new single statement) and coerceTemporalFilterColumn; plugin.ts ×2 (the two hook bridges); native-sql-strategy.ts temporalColumn and buildFilterClause; objectql-strategy.ts dateRangeBounds; plus measure-result-type.ts's module TSDoc. Diff hunks in those files are comment-only except the declaredFieldType: line in analytics-service.ts (the wiring, part of the refusal half). Test edits: two narratives reworded, one additive case (canonicalTextHook, binds '2025-06-18T00:00:00.000Z'), and measure-result-type.test.ts's summed_touches / avg_touch re-pointed from last_update_at to estimate_hours (necessary: the old fixture would now be refused; the pin — sum/avg say number — is unchanged).
  5. Changeset. @objectstack/service-analytics: minor, @objectstack/spec: minor — correct per batch [WIP] Add query enhancements and advanced validation features #35 WHICH LEVEL (accept-set narrowing on a published surface, launch-window minor; spec grows a registry entry). **BREAKING** banner present. <!-- adr-0087: registered dataset-measure-aggregate-field-type-refused --> present; ran node scripts/check-adr-0087-registration.mjs --base origin/main --head refs/review/16778 → exit 0, [BREAKING+bang] registered … (new here); check-changeset-no-major.mjs → no major. FROM/TO table present — but two of its rows and the ledger entry's surface / acceptanceCriteria describe the pre-scoping full-table gate (F1).
  6. Tests. Revert-sensitive: 7 refusal cases assert code + status + message + sqls === [], so a removed gate turns them red (SQL is emitted). Negative controls present for numeric avg (number, currency), datetime count/min/max, datetime dimension (group + dateRange), numeric derived, and the three stand-down tiers. derived-only selection pinned (the filer's shape). No .skip / .only / .todo in the diff. tsconfig.json includes src and excludes only node_modules/dist, so __tests__ is typechecked.
  7. CI @ 80ec9f2: 38 check runs — 32 success, 6 skipped, 0 failure; mergeable_state: clean; draft. Head is 11 commits behind main; ed7243d52 (feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (#16685) #16750, boolean rows) is not in the PR's ancestry (merge-base --is-ancestor → no), and CI ran on a merge into 941232040, which also predates feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (#16685) #16750. Predicted post-merge: no red (the gate never judges boolean fields; the accepted-set message assertion iterates the live table rather than a hard-coded list).

Findings

Not done by this seat

No approval, no request-changes, no label, no edit, no merge action. Throwaway ref refs/review/16778 deleted after review.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants