Skip to content

fix(objectql)!: engine aggregate judges the sum row too — sum over a refused type answers INVALID_FIELD / 400 on every driver - #21103

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20914-release-sum-row
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20914-release-sum-row

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20914
Clause-②: no (narrowing)

The follow-up round of this card, after PR #21037 landed as a75311dd2 (Part of, the sum row held). It executes triage's answer 5924445782 (A), whose execution line reads: "'sum' leaves ROWS_HELD_FOR_TRIAGE, and the held pins flip. If the constant is then empty, delete it." With this PR the engine's aggregate door asks every row of AGGREGATE_FIELD_TYPE_COMPATIBILITY, so the card's direction (the whole table) is complete.

What this changes

FROM → TO, measured on three drivers (H2)

Through engine.aggregate, two rows, on the real InMemoryDriver, on SqlDriver over SQLite, and on a private PostgreSQL 16.14. Before = base 2821e9f15; after = this branch's built dist.

sum over before: memory / SQLite / PostgreSQL after, all three
json 0 / 0 / 500 function sum(json) does not exist 400 INVALID_FIELD
text 0 / 0 / 500 function sum(text) does not exist 400 INVALID_FIELD
single-value select 0 / 0 / 500 function sum(character varying) does not exist 400 INVALID_FIELD
formula 0 / 400 INVALID_FIELD (driver: no column) / 400 (driver: no column) 400 INVALID_FIELD, the engine's words
tags 0 / 0 / 500 400 INVALID_FIELD
datetime 0 / 4052 (the years added) / 500 400 INVALID_FIELD
percent (refused by the table: a rate does not add) 30 / 30 / 30 400 INVALID_FIELD
controls: currency, number, boolean; max number; count json 30, 3, 1, 2, 2 on all three unchanged

The percent row is the one whose old answer agreed across drivers. The table refuses it on the semantic ground its TSDoc names (isIncoherentAggregate), and it is in the changeset's FROM → TO with its route (avg).

The census, re-run before the flip (H1)

Query: every line carrying a 'sum' / "sum" literal, plus the unquoted and backtick spellings. Each hit was resolved by hand to its object and the field's declared type, then asked of the table.

  • examples/** at 2821e9f15: 17 lines, 0 refused pairs. They are dataset measures, roll-up summaryOperations, a cube measure, a kpi metric and an ObjectChart aggregate. Every operand is a currency, number or summary field (11 distinct object.field pairs).
  • hotcrm (objectstack-ai/hotcrm cloned at its head fb408a73): 33 lines in src. One is a comment (a derived op), which leaves 32 authored sum aggregations over 18 distinct object.field pairs. 31 are over currency or number. 1 refused pair: src/sales/views/forecast.view.ts:28, crm_forecast.expected_amount (Field.formula), the list-column summary: 'sum'. That is the known hit: triage-answered and carried by crm_forecast: the all_forecasts list view authors summary: 'sum' on the formula field expected_amount, a pair objectstack's aggregate compatibility table refuses (objectstack-ai/objectstack#20914) hotcrm#1980, which has not changed it yet. No other hit, so the flip stands. Outside src: 2 lines in test, and 3 in docs and the changelog (prose).
  • The query is its own positive control: it finds the known hotcrm hit.

Pins: the held sum pins flipped, ⛔ not deleted

  • packages/objectql/src/engine-aggregate-field-type-door.test.ts:
    • The CONTROL's two held cases (sum meta, sum title, which reached the driver while the row was held) move into a new refusal test. It covers sum over json, text, select, formula, tags, percent, datetime and a select with multiple: true, with envelope INVALID_FIELD / 400 / httpStatus 400, field / fields / object / param, the message head and the route, and no read.
    • The CONTROL gains sum over a currency and a boolean.
    • The GUARD that asserted "no verdict for sum × every type" now walks every row read off the table, so a row the door skipped would turn it red. It asserts the key set is the six functions, with a sum floor beside the others.
    • The fail-closed GUARD covers sum too.
  • packages/rest/src/data-aggregate-field-type-door.test.ts: a sum refusal cell over json, text, select, tags and formula at POST /api/v1/data/:object/query (SQLite always; PostgreSQL where OS_TEST_POSTGRES_URL is set). The CONTROL gains sum over a currency (60) and a boolean (2). ⚠️ As before, no CI job provisions the PostgreSQL / MySQL cells; the PostgreSQL evidence is the local run below.
  • packages/objectql/src/engine-json-stored-group-distinct-door.test.ts, its GUARD clause: sum moves from "passes, held for triage" to the refused set beside avg / min / max over json, in the same envelope at the same position. count still passes.

Ablation (from committed code, 1519ed6bd)

  • Mutate. node scripts/ablation-replace.mjs (WRAP, trap-restored) put sum back in the held set: the table-vocabulary continue became … || (fn === 'sum' ? 'ablation-20914-sum-A1' : '')) continue;. Anchor 1 → 0, blob 9a2bc89178af → adcfb213d670. objectql was rebuilt, and ablation-dist-preflight.mjs found the marker in 4 built files.
  • Predicted: red on the flipped sum pins only.
  • Observed, objectql: 3 failed / 28 passed — the new sum refusal test, the every-row GUARD (sum × text: expected null), and the group-distinct GUARD clause. Every other pin was green.
  • Observed, REST: 2 failed / 14 passed / 8 skipped (MySQL) — the sum cell on SQLite and on PostgreSQL. The controls and the min / max / avg / count_distinct cells were green.
  • Restore. Blob == HEAD (9a2bc89178af), git diff HEAD 0 bytes, rebuilt. --absent found the marker in 0 of 14 built files and the tree clean. The same pins were green again: objectql 31 / 31, REST 16 passed + 8 skipped.

Tests (at 1519ed6bd, the merge of origin/main b3d7a7086; all with a private PostgreSQL 16.14 where a cell reads it)

  • objectql full: 353 files, 6889 passed. pnpm --filter @objectstack/objectql typecheck exit 0; test layer 40 files / 234 errors / 65 pinned, held.
  • rest full (OS_TEST_POSTGRES_URL set): 259 files, 5124 passed / 52 skipped. typecheck exit 0; test layer 0 / 0 / 0.
  • metadata-protocol full: 196 files passed, 3 skipped; 2930 tests passed, 19 skipped.
  • service-analytics full: 154 files, 3498 passed / 10 skipped.
  • Every other test file in the workspace that mentions a sum aggregation and reaches the engine: runtime 4 files / 25 tests, plugin-security 2 / 297, platform-objects 2 / 56, mcp 1 / 17, dogfood 2 / 12. All passed.
  • Driver conformance before and after: 50 covered, 0 DEBT, 0 exempt; the two readings are identical.
  • Lint, narrowed and proven at 1519ed6bd:
    • eslint --no-inline-config --format json over the 6 changed .ts files: 6 results, 0 errors, 0 warnings;
    • ESLint isPathIgnored is false for all 6;
    • calculateConfigForFile shows no parserOptions.project / projectService on any of them. No type-aware lint is on, so this diff cannot move a verdict on an untouched file.
    • Repo-wide pnpm lint is CI's.
  • pnpm --filter @objectstack/spec check:generated: all 15 generated artifacts up to date.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run with no paths, derived 88 commands at 1519ed6bd. All 88 were run, each exit code recorded before any pipe, and all exited 0. --ran reconciles: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN. Among them: check:adr-0087-registration (accepted, not-required (already-registered)), check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:nul-bytes, check:engine-double-contract and check:api-surface.

NOT MEASURED locally (CI-owned): the 5 path-scheduled CI jobs, the workspace type-check lanes beyond objectql and rest, the MySQL cells (no server here), and repo-wide pnpm lint.

origin/main moved 13 commits after this merge. A local git merge-tree of the head with it is clean. One of them, 88b484e00, touches engine.ts where metadata collections load (picklists), not at the aggregate call site. The merge queue rebuilds on current main.

Deviations from the claim's file surface, each named

  • packages/objectql/src/engine.ts, one comment at the aggregate door's call site. "The sum row is held back by that card's census (see the door's header)" would have been false after this PR. It now says every row is asked, and adds the measured sum answer. No code line moved.
  • radio added to two comment lists of the multi-capable types: the door's DECLARATION paragraph, and the spec TSDoc's JSON-stored paragraph. This is the same omission review ① 3 named in the landed changeset. The door's own behaviour already held radio (it asks isMultiValueField).

Acceptance notes


Generated by Claude Code

claude added 2 commits October 1, 2026 05:10
…refused type answers INVALID_FIELD / 400 on every driver

The aggregate x field-type door held the table's sum row back for a census
answer; triage released it. ROWS_HELD_FOR_TRIAGE is deleted (it would be
empty), the held sum pins flip to refusal pins, and the spec TSDoc states
that the door asks every row.

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/aggregate-field-type-compatibility.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/aggregate-field-type-compatibility.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 138 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 5e470f8c1c6ac2ef5f05c4d65a7ab0b0ac388c9f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5e470f8c1c6ac2ef5f05c4d65a7ab0b0ac388c9f

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1519ed6bdd2a958501c84b4c3241c79bf6961fb9
Local-runs: none

PR #21103 (claude/issue-20914-release-sum-row) on card #20914, the follow-up round after PR #21037 landed as a75311dd2. Inputs: the card's body and its ten comments, the PR body and file list, the net diff against the merge-base b3d7a708 (7 files, +183 / −62), and the check-runs on the head. Governing items: triage's answer 5924445782 (A), the claim amendment 5924989981, and the owed items ① 3 (radio) and ① 7 (the .d.ts clause) of the prior review 5924038187 as the card's ACCEPT 5924213998 and landing 5924984282 restate them. Nothing was built, run or re-run; every source reading below is of the head ref.

① Derived judgments

  1. The constant is deleted, not emptied — right. 'sum' left ROWS_HELD_FOR_TRIAGE; the set was then empty, and the constant and its one continue in refusedAggregations are gone. At the head ROWS_HELD_FOR_TRIAGE occurs nowhere outside CHANGELOG.md. This is triage's execution line, verbatim.
  2. No second table, no per-pair exemption — right. refusedAggregations has no function-specific skip: the verdict is isAggregateCompatibleWithFieldType(fn, type) on the row and rowAcceptsMultiValue(fn) for the declaration half, both read off AGGREGATE_FIELD_TYPE_COMPATIBILITY. The only fn-specific branch left in the door is routeAndReason's count_distinct wording, which decides no verdict. The route is acceptedTypesOf(fn), read off the table, so the door carries no accept list of its own.
  3. The accept set sum now refuses — right, the table as ruled. The sum row is ADDITIVE_AGGREGATE_FIELD_TYPES plus the boolean class: number, currency, rating, slider, progress, summary, boolean, toggle. The other 41 of the 49 declared FieldTypes are refused INVALID_FIELD / 400 before any driver — percent included, on the table's own ground (isIncoherentAggregate: a rate does not add), and a multi-capable type declared multiple: true by the declaration half as well as by its type's row. The door applies the table; it does not re-rule it. The route string both suites pin (sum accepts a field of type number, currency, rating, slider, progress, summary, boolean or toggle: aggregate a field of one of those types, or count the rows with count.) is the row in the table's order.
  4. The table and the predicate are byte-identical — right. Every changed line in packages/spec/src/data/aggregate-field-type-compatibility.ts is a TSDoc continuation line: checked mechanically over the net diff, zero non-comment added or removed lines in that file. The table literal and isAggregateCompatibleWithFieldType are untouched. The dev's comment-stripped sha256 is their evidence; the diff is mine.
  5. Public surface — right, nothing moves. ROWS_HELD_FOR_TRIAGE was a module-private const, never exported; the door is internal and not re-exported. The published text that moves is the spec .d.ts TSDoc: the module header now says the engine door refuses "on every row: count_distinct since [finding] two more JSON-stored columns as a group or distinct key answer 500 on PostgreSQL: groupBy on a multiple: true select, and count_distinct on a json field #20808 …, and sum, avg, min and max since [finding] max / min over a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only its count_distinct row #20914 (count refuses no type)"; the predicate's TSDoc says "on every row of the table"; the "does NOT do" section says "at query time, on every row too". "the other rows since [finding] max / min over a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only its count_distinct row #20914" occurs nowhere in the file at the head, and the pointer to an objectql source file a spec consumer cannot see is gone. That is the prior review's ① 7 clause, discharged.
  6. The flipped sum pins were flipped, not deleted — right. (a) objectql suite: the CONTROL's sum meta / sum title, which reached the driver while the row was held, move into a new refusal test over json, text, select, formula, tags, percent, datetime and a select with multiple: true — envelope (INVALID_FIELD, 400, httpStatus 400), field / fields / object / param, message head and route, zero reads; the CONTROL gains sum over a currency and a boolean. (b) The GUARD's "no verdict for sum × every FieldType" clause becomes sum in an every-row walk read off the table's keys, with the key set pinned to the six functions and a sum floor above 30 beside max's (41 and 37 refused). (c) The group-distinct GUARD moves sum from the not.toThrow pair into the refused loop beside avg / min / max, same envelope at the same position; count still passes. (d) REST: a new sum refusal cell over json, text, select, tags and formula at POST /api/v1/data/:object/query (SQLite; PostgreSQL env-gated), and the CONTROL gains sum price = 60 and sum flag = 2. No pin was deleted; the REST file held no sum pin before, so there it is an addition.
  7. The pinned words match the door's — right. declared and kind compose as the door builds them: select field with multiple: true — a multi-value field for picks, tags field — a multi-value field for labels (multi-value is tested before structured-JSON), json field — a structured-JSON value for meta, bare formula field, percent field, datetime field, text field, select field for the rest; does not sum / sums are FUNCTION_WORDS.sum, already in the door.
  8. Surface deviations, both named in the PR body and the report — accepted. engine.ts: two comment lines at the aggregate door's call site, which this diff would otherwise have made false ("The sum row is held back…"); no code line moved, read at the head. radio in two docblocks (the door's DECLARATION paragraph and the spec TSDoc's JSON-stored paragraph): MULTI_CAPABLE_TYPES at the head is select, radio, lookup, user, file, image, so both lists are now the constant's; this is the prior review's ① 3, and the door's behaviour already held radio through isMultiValueField. Neither is a behaviour change.
  9. Fail-closed tiers kept — right. A fieldless aggregation, a function outside the table's vocabulary, an undeclared name or path, a registry-less host, and a declared type outside FieldType still pass on as they came; the fail-closed GUARD now covers sum too.
  10. sum × percent — right in verdict and route; generic in reason. The refusal's route names the accepted set read off the table; its reason is the door's generic sentence, not the table's "a rate does not add". The changeset carries the avg route for a rate. A rate-specific reason is a words change outside this round's claim (see ③).

② Semver level

  • .changeset/20914-release-sum-row.md: @objectstack/objectql: minor with a BREAKING banner, @objectstack/spec: patch. Right. The objectql diff narrows aggregate's accept set on every driver — an accept-set narrowing is BREAKING, shipped as minor under the launch-window convention check-changeset-no-major.mjs enforces. The spec diff ships TSDoc into the published .d.ts, so it publishes something and takes patch, never skip-changeset. No export, type or stored row moves, so nothing asks for more.
  • Clause-②: no (narrowing) — in the changeset body and the PR body, and matching the claim amendment. Right: no plus the (narrowing) arm is the valid breaking spelling, and nothing is widened.
  • Migration carried: FROM → TO per type class (memory 0; SQLite 0, or the years added for a datetime; PostgreSQL 500, or a driver 400 for a formula; the rates added for a percent on all three → 400 INVALID_FIELD), what to write instead (the eight accepted types; avg for a rate; a numeric field for a formula's value; count for a count in disguise), who is affected (the metadata that lowers onto engine.aggregate included), and what is unchanged. The earlier entry's "Not judged yet: sum" paragraph is declared superseded and the landed changeset is not edited. Right.
  • ADR-0087: exactly one marker, not-required (already-registered dataset-measure-aggregate-field-type-refused). Right: the id resolves at the head (packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts), predates this diff, and its own text records that a later change widened its prescription to sum / avg over every field class; its replacement names the routes this changeset gives. The selecting id covers min / max only, which this diff does not touch, so naming the one id is right. The Check Changeset run on the head concluded success.
  • The radio correction to the earlier entry is carried in this entry's text, not by editing the landed changeset. Right: a merged changeset is a record.

③ Boundary flags

  • open_questions: [] in report 5926229168. Nothing to answer.
  • Deviation, engine.ts comment only — answered, accepted (① 8): outside the claim amendment's file surface, comment-only, named in the report and the PR body, and needed for truth after this diff.
  • Deviation, radio in two docblocks — answered, accepted (① 8): the prior review's owed ① 3, closed.
  • Deviation, fixture gains (price, status, expected in LEDGER and PROBE; sum controls) — answered, accepted: inside the claim's pin files, carrying the flipped pins and their controls.
  • Deviation, the head merges origin/main at b3d7a7086 and main moved 13 commits after — answered: the check-runs on this head are the verdicts on this head; the queue rebuilds on current main; the dev's merge-tree reading is theirs, not a gate.
  • Deviations, commit trailers and the cleanup of the private PostgreSQL and the scratch clone — not contract matters; noted.
  • Out-of-scope finding, sum × percent generic reason (carrier none) — answered, accepted as an Acceptance note: the verdict and the route are the table's; a rate-specific reason is a words change on the door, not owed by triage's one-entry execution line. Not escalated.
  • PR flag, no CI job provisions the PostgreSQL / MySQL cells — answered: as in the prior round, the door answers before any driver, so the in-memory pin is the verdict's cell by construction and the SQLite cell runs in CI; the PostgreSQL cell is the dev's local evidence, not a gate.
  • The census re-run (H1) is dev evidence, consistent with triage's answer: 0 refused sum pairs in examples/**, and the one hotcrm hit is the client-side footer carried by crm_forecast: the all_forecasts list view authors summary: 'sum' on the formula field expected_amount, a pair objectstack's aggregate compatibility table refuses (objectstack-ai/objectstack#20914) hotcrm#1980, which does not block this card. Not re-run here.
  • The PR body opens Fixes #20914 — right: triage's "On A, the card closes with the follow-up". The card this PR closes must claim this branch and Part-of PR must not also close its card concluded success.
  • Governed surfaces: none in the file list; Governed Surface Queue Guard concluded success.
  • Check-runs on the head, 32, read once at 2026-10-01T06:50Z and not polled again:
    • success (13): Auto Label, Check Changeset, Check Documentation Links, Check PR Size, filter, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates.
    • skipped (3): Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in).
    • in_progress — not verdicts (16): Build Core; Dogfood Regression Gate 1/3, 2/3, 3/3; Dogfood Verify CLI; Lint & Repo Gates; Temporal Conformance (live PG + MySQL); Test Core 1/6, 2/6, 3/6, 4/6, 5/6, 6/6; Type Check · consumer gates; Type Check · debt ledger; Type Check · workspace. Each answers its own family when it concludes; this record states no conclusion for any of them, and the landing waits on every one of them being green. No check had failed at the read.
    • The Docs Drift Check comment is informational: no hand-written page names a changed anchor, and a grep of content/docs at the head finds no prose on a held sum row.

Implemented-by: claude/issue-20914-release-sum-row
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 07:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 07:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit d98bf24 Oct 1, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20914-release-sum-row branch October 1, 2026 07:41
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…e table for every measure (objectstack-ai#21044) (objectstack-ai#21128)

Fixes objectstack-ai#21044
Clause-②: no (narrowing)

## What changed

A configured cube's `max` over a `text` column was served at the cube
door (`POST /api/v1/analytics/query`) by the native-SQL strategy, with
the column's text, while the same response's `fields[]` declared the
measure `number`. The dataset door refuses that pair at compile by the
spec table `AGGREGATE_FIELD_TYPE_COMPATIBILITY`, and the cube door
consulted nothing. Triage's direction (`5924500811`) is carried out as
ruled: the cube door asks the one table, and no second table exists.

- **The judgment**
(`packages/services/service-analytics/src/cube-measure-field-type-door.ts`,
new, beside `measure-result-type.ts`):
`assertCubeMeasureFieldTypesAccepted` refuses the first `measures` entry
whose aggregate the table refuses for its column's declared type,
`INVALID_FIELD` / 400 through `invalidMemberError`, with `member`,
`param: 'measures'`, `cube`, `field` and `object` on the error. The
verdict is `isAggregateCompatibleWithFieldType`'s; the accepted set the
words name is read off the exported table. It judges every row of the
table, as the dataset door does since decision batch objectstack-ai#127, with no scope
condition on top of it. `count_distinct` keeps its own door (objectstack-ai#20912,
`structured-json-dimension-door.ts`), which asks the same row plus the
`multiple: true` declaration, so one pair has one verdict and one
wording.
- **Where it runs** (`analytics-service.ts`): `assertMeasureFieldTypes`,
called in `ensureCube` on all three paths (inferred cube, augmented
cube, declared cube), right after the objectstack-ai#20807 / objectstack-ai#20912 door and before
the `where` gate. That is ahead of `callCtx` and strategy selection, so
both strategies see it once and nothing is read before it answers. The
same placement covers the dry run (`POST /api/v1/analytics/sql`) and
every query `DatasetExecutor` runs through `queryIn`.
- **The column description** (`analytics-service.ts`):
`withMeasureResultTypes`, applied at the result seam in `queryIn` beside
`withDeclaredMeasureFormats`, asks the dataset door's one rule,
`measureResultType`, with the cube measure's aggregate and the declared
type of the column it reads, and writes only what the rule answers. A
`min` / `max` over `date` / `datetime` / `time` is now described `time`.
⛔ No copy of the rule in `buildFieldMeta`: both strategies are
untouched.
- **`measure-result-type.ts`**: TSDoc only, one paragraph naming the
cube door as the rule's second reader.
- **`.changeset/21044-cube-measure-field-type-table.md`**:
`@objectstack/service-analytics` minor, BREAKING banner, `Clause-②: no
(narrowing)`, ADR-0087 `not-required (already-registered
dataset-measure-selecting-aggregate-field-type-refused,
dataset-measure-aggregate-field-type-refused)`.
`check:adr-0087-registration` accepts it.

### The four measured questions of the dispatch

- **H1, the card's reading on `main`.** Confirmed through the real
dispatcher route at base `2821e9f15b`, SQLite and PostgreSQL 16.13, both
strategies (table below). The native face served every refused `min` /
`max` pair with the column's text under `fields[]` `number`. Since PR
objectstack-ai#21037 the ObjectQL face already answered `400 INVALID_FIELD`, from the
engine's aggregate door, after the strategy had begun: the words name
the engine's position (`aggregate('…'): aggregations[0].field takes the
max of 'note', a declared text field…`), and the error carries no
`member`. `sum` over the same text column answered `0` on SQLite on both
faces and `500 DATABASE_ERROR` on PostgreSQL; `avg` answered `0` / `500`
on the native face and `400` on the ObjectQL face.
- **H2, where the door learns the source field type.** From the resolver
this file already uses for measure columns: `declaredMemberEntry(cube,
member, 'measure')` (the one `withDeclaredMeasureFormats` reads) gives
the cube measure, its `sql` is the column when it is a bare identifier,
and the type is `sourceFieldMeta(object, column).type`, the base-object
declaration the objectstack-ai#20807 / objectstack-ai#20912 door and `compile()` already read. ⛔ No
second resolution of a member to a field. A relationship-path column is
not judged (the declaration read is the base object's), which is the
dataset door's tier.
- **H3, where the refusal goes and its code.** In `ensureCube`, as
above. The code is `INVALID_FIELD` / 400, not the dataset door's
`DATASET_INVALID` / 400, for the reason `dataset-refusal.ts`'s header
gives: `DATASET_INVALID` is a verdict about a dataset document, and
`/analytics/query` carries none; a verdict about one member the request
named is the `INVALID_FIELD` family. It is also the code the cube door's
three source-field gates and its objectstack-ai#20912 `count_distinct` door answer,
and the code the engine's door already answered for this very pair on
the ObjectQL face, so that face's wire code does not move. The dataset
door keeps `DATASET_INVALID` at compile and never reaches this door for
a pair it refuses.
- **H4, `fields[]` for an accepted non-numeric pair.** By
`measureResultType`, read at the cube door's result seam without
widening the claimed surface: `min` / `max` over a temporal column is
`time`; over a `boolean` column the rule declines (three readings
disagree), so the producer's `number` stands, which is what SQLite (`1`)
and the ObjectQL face on PostgreSQL (`1`) answer.

Triage's second sentence, "`buildFieldMeta` stops minting `number` for a
non-numeric `min` / `max`", is delivered at the seam both strategies'
results leave through rather than inside `buildFieldMeta`, which has no
field types to read: a refused pair never reaches a descriptor, a
temporal one is re-described `time` by the one rule, and a boolean one
keeps `number` by that rule's own verdict.

## Per-row readings, before and after

| driver | strategy | measure | pair | before (`2821e9f15b`): status,
value, `fields[]` type | after (`d85b700030`) |
|:--|:--|:--|:--|:--|:--|
| SQLite | native SQL | config `max_note` | `max` over note (text) |
200, `"y"`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | config `min_note` | `min` over note (text) |
200, `"x"`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | config `max_status` | `max` over status (select)
| 200, `"won"`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | config `max_opened` | `max` over opened_at
(datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` | 200,
`"2026-03-04T05:06:07.000Z"`, `time` |
| SQLite | native SQL | config `min_due` | `min` over due_on (date) |
200, `"2026-01-15"`, `number` | 200, `"2026-01-15"`, `time` |
| SQLite | native SQL | config `max_flag` | `max` over flag (boolean) |
200, `1`, `number` | 200, `1`, `number` |
| SQLite | native SQL | config `max_amount` | `max` over amount (number)
control | 200, `32`, `number` | 200, `32`, `number` |
| SQLite | native SQL | config `sum_note` | `sum` over note (text) |
200, `0`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | config `avg_note` | `avg` over note (text) |
200, `0`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | config `cd_note` | `count_distinct` over note
(text) control | 200, `2`, `number` | 200, `2`, `number` |
| SQLite | native SQL | inferred `note_max` | `max` over note (text) |
200, `"y"`, `number` | 400 `INVALID_FIELD` |
| SQLite | native SQL | inferred `amount_max` | `max` over amount
(number) control | 200, `32`, `number` | 200, `32`, `number` |
| SQLite | native SQL | inferred `opened_at_max` | `max` over opened_at
(datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` | 200,
`"2026-03-04T05:06:07.000Z"`, `time` |
| SQLite | native SQL | augmented `note_max` | `max` over note (text) |
200, `"y"`, `number` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `max_note` | `max` over note (text) | 400
`INVALID_FIELD` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `min_note` | `min` over note (text) | 400
`INVALID_FIELD` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `max_status` | `max` over status (select) |
400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `max_opened` | `max` over opened_at
(datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` | 200,
`"2026-03-04T05:06:07.000Z"`, `time` |
| SQLite | ObjectQL | config `min_due` | `min` over due_on (date) | 200,
`"2026-01-15"`, `number` | 200, `"2026-01-15"`, `time` |
| SQLite | ObjectQL | config `max_flag` | `max` over flag (boolean) |
200, `1`, `number` | 200, `1`, `number` |
| SQLite | ObjectQL | config `max_amount` | `max` over amount (number)
control | 200, `32`, `number` | 200, `32`, `number` |
| SQLite | ObjectQL | config `sum_note` | `sum` over note (text) | 200,
`0`, `number` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `avg_note` | `avg` over note (text) | 400
`INVALID_FIELD` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | config `cd_note` | `count_distinct` over note
(text) control | 200, `2`, `number` | 200, `2`, `number` |
| SQLite | ObjectQL | inferred `note_max` | `max` over note (text) | 400
`INVALID_FIELD` | 400 `INVALID_FIELD` |
| SQLite | ObjectQL | inferred `amount_max` | `max` over amount (number)
control | 200, `32`, `number` | 200, `32`, `number` |
| SQLite | ObjectQL | inferred `opened_at_max` | `max` over opened_at
(datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` | 200,
`"2026-03-04T05:06:07.000Z"`, `time` |
| SQLite | ObjectQL | augmented `note_max` | `max` over note (text) |
400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `max_note` | `max` over note
(text) | 200, `"y"`, `number` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `min_note` | `min` over note
(text) | 200, `"x"`, `number` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `max_status` | `max` over
status (select) | 200, `"won"`, `number` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `max_opened` | `max` over
opened_at (datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` |
200, `"2026-03-04T05:06:07.000Z"`, `time` |
| PostgreSQL 16.13 | native SQL | config `min_due` | `min` over due_on
(date) | 200, `"2026-01-15"`, `number` | 200, `"2026-01-15"`, `time` |
| PostgreSQL 16.13 | native SQL | config `max_flag` | `max` over flag
(boolean) | 500 `DATABASE_ERROR` | 500 `DATABASE_ERROR` |
| PostgreSQL 16.13 | native SQL | config `max_amount` | `max` over
amount (number) control | 200, `32`, `number` | 200, `32`, `number` |
| PostgreSQL 16.13 | native SQL | config `sum_note` | `sum` over note
(text) | 500 `DATABASE_ERROR` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `avg_note` | `avg` over note
(text) | 500 `DATABASE_ERROR` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | config `cd_note` | `count_distinct`
over note (text) control | 200, `2`, `number` | 200, `2`, `number` |
| PostgreSQL 16.13 | native SQL | inferred `note_max` | `max` over note
(text) | 200, `"y"`, `number` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | native SQL | inferred `amount_max` | `max` over
amount (number) control | 200, `32`, `number` | 200, `32`, `number` |
| PostgreSQL 16.13 | native SQL | inferred `opened_at_max` | `max` over
opened_at (datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` |
200, `"2026-03-04T05:06:07.000Z"`, `time` |
| PostgreSQL 16.13 | native SQL | augmented `note_max` | `max` over note
(text) | 200, `"y"`, `number` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `max_note` | `max` over note
(text) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `min_note` | `min` over note
(text) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `max_status` | `max` over status
(select) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `max_opened` | `max` over
opened_at (datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` |
200, `"2026-03-04T05:06:07.000Z"`, `time` |
| PostgreSQL 16.13 | ObjectQL | config `min_due` | `min` over due_on
(date) | 200, `"2026-01-15"`, `number` | 200, `"2026-01-15"`, `time` |
| PostgreSQL 16.13 | ObjectQL | config `max_flag` | `max` over flag
(boolean) | 200, `1`, `number` | 200, `1`, `number` |
| PostgreSQL 16.13 | ObjectQL | config `max_amount` | `max` over amount
(number) control | 200, `32`, `number` | 200, `32`, `number` |
| PostgreSQL 16.13 | ObjectQL | config `sum_note` | `sum` over note
(text) | 500 `DATABASE_ERROR` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `avg_note` | `avg` over note
(text) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | config `cd_note` | `count_distinct` over
note (text) control | 200, `2`, `number` | 200, `2`, `number` |
| PostgreSQL 16.13 | ObjectQL | inferred `note_max` | `max` over note
(text) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |
| PostgreSQL 16.13 | ObjectQL | inferred `amount_max` | `max` over
amount (number) control | 200, `32`, `number` | 200, `32`, `number` |
| PostgreSQL 16.13 | ObjectQL | inferred `opened_at_max` | `max` over
opened_at (datetime) | 200, `"2026-03-04T05:06:07.000Z"`, `number` |
200, `"2026-03-04T05:06:07.000Z"`, `time` |
| PostgreSQL 16.13 | ObjectQL | augmented `note_max` | `max` over note
(text) | 400 `INVALID_FIELD` | 400 `INVALID_FIELD` |

"Before" is base `2821e9f15b`; "after" is this branch's head
`d85b700030` (two merges of `main` in, the second carrying objectstack-ai#21098's 401
for an anonymous analytics caller, so the probe signs its caller in).
Both were read through the real `dispatcher-plugin` mount of `POST
/api/v1/analytics/query`, over `AnalyticsServicePlugin` composed on a
real `ObjectQL` engine and `SqlDriver`, by a scratch probe that was
deleted after each run. Two rows of a ledger: `note` `x` / `y` (text),
`status` `open` / `won` (select), two instants and two days, `flag`
`true` / `false`, `amount` `10` / `32`. "Inferred" is an unregistered
cube name (the object's), "augmented" a suffix-inferred measure on the
configured cube. The same 56 readings were taken again after the first
merge of `main` (`0abe2120fb`): no row differs from the head's. Since
objectstack-ai#21103 landed (in the second merge) the engine's door also refuses `sum`
over a refused type, so on the ObjectQL face the `sum` rows would answer
`400` without this change too; this door answers first.

## Pins (red first), the ablation

- **Red, on the tree committed as `be5c1a69b2`** (pins only, no fix;
base `2821e9f15b`), SQLite and a private PostgreSQL 16.13:
- `service-analytics`
`src/__tests__/cube-measure-field-type-door.test.ts` (new) and the
flipped `native-sql-measure-number-presentation.test.ts`: 16 failed, 16
passed, 1 skipped. Failures: `max_note must not be served: expected {
rows: [ { max_note: 'y' } ], …(1) } to be undefined` (native),
`max_note: expected undefined to be 'max_note'` (ObjectQL: the engine's
refusal carries no `member`), `max_opened is described time: expected
'number' to be 'time'`, `expected the query to be refused, but it
resolved` (dry run).
- `runtime` `src/analytics-cube-measure-field-type-door.test.ts` (new):
6 failed, 6 passed. Native `max_note` answered `200`; both faces
described `max_opened` `number`. The ObjectQL face's `400 INVALID_FIELD`
and both faces' `number` controls were green already.
- **Fixture triage.** `native-sql-measure-number-presentation.test.ts`
(objectstack-ai#20889) read a configured cube's `max` over its `code` text column back
as text, as the second half of its "keyed on the declared function,
never on the value" control. That pin held exactly the served pair this
card refuses, so it is flipped, not deleted: the case now asserts
`INVALID_FIELD` / 400 with no statement run, keeps its text-dimension
half, and the cube read above it no longer asks for `max_code`.
- **Green, at `d85b700030`:** the same files 32 passed, 1 skipped (the
PostgreSQL native `max` over `boolean` cell, a named skip: an accepted
pair that is a 500 there, see Acceptance notes) and 12 passed.
- **Ablation** (the cube door's table check removed), from committed
code at `5d69394a60`. Predicted before running, in `progress.log`:
service-analytics 12 red (per dialect: the native refusal, the ObjectQL
refusal, both inferred-measure cases, the dry run and the flipped objectstack-ai#20889
case), 20 green, 1 skipped; runtime 2 red (the native refusal on both
dialects), 10 green, because the engine's door still answers the
ObjectQL face's `400 INVALID_FIELD` on the wire.
- The mutation went through `scripts/ablation-replace.mjs` (WRAP mode,
trap-restored, absolute path): `if
(isAggregateCompatibleWithFieldType(aggregate, declared)) continue;`
gained `|| String(aggregate) !== 'ABLATION-21044'`, so every pair
passes. Anchor 1 to 0, marker 0 to 1, blob `6c7bdce48e43` to
`f49be3058c84`. A second `trap` in the outer script restored by absolute
path too.
- `service-analytics` was rebuilt, and `ablation-dist-preflight.mjs`
found the marker in 2 built files (`dist/index.cjs`, `dist/index.js`).
- Observed: service-analytics 12 failed, 20 passed, 1 skipped; runtime 2
failed, 10 passed. Exactly as predicted.
- Restore: blob after restore `6c7bdce48e43` equals HEAD, `git diff
HEAD` empty, whole-tree porcelain 0 lines. Then rebuilt, and
`ablation-dist-preflight.mjs --absent`: marker absent from all 6 built
files and the tree clean. The pins are green again at both later heads.

## Tests (at `d85b700030`, after `pnpm install --frozen-lockfile` and a
full `turbo run build`, 73 tasks)

- `@objectstack/service-analytics`, full suite with
`OS_TEST_POSTGRES_URL` set (no PostgreSQL cell skipped): 156 files, 3558
passed, 1 skipped (the named cell above). `typecheck` exit 0; `tsc
--noEmit --listFiles` lists both touched test files.
- `@objectstack/rest`, `src/analytics-*` and
`rest-hook-refusal-message-parity`, with PostgreSQL: 20 files, 279
passed.
- `@objectstack/runtime`, `src/analytics-*`, `src/dispatcher*`,
`src/http-dispatcher*`, `src/domains/analytics*` and the other
analytics-route suites: 51 files, 843 passed. `typecheck` exit 0,
`check:test-typecheck` holds its ledger (27 files, 190 errors, 68
signatures; the new file adds none).
- Not run locally, declared to CI: the whole `rest` and `runtime`
suites, and `packages/qa/dogfood` (`Dogfood Regression Gate`).

## Gates (at `d85b700030`, as ONE sequential script under the shared
verify lock, each exit code captured before any pipe)

- **Derived families:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` names 62. `--ran` reconciles: `62
derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED
zero — all 62 recorded an exit code and none of them is 3)`. All 62 exit
0. `check:dual-build-cjs-loads` and `check:type-check-debt` answered
`PREREQUISITE NOT MET` (exit 3) on the first sweep at `0abe2120fb`,
which had built only the dependency closure; after a full `turbo run
build` (73 tasks) both exit 0, and both are 0 in the sweep at this head.
- **The five roster families the derivation marks ⛔ (a roster in a
directory this diff touches):** `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing`, `pnpm check:filter-alias-parity`, `pnpm
check:route-ledger-census`. All exit 0.
- `check:adr-0087-registration` reads the changeset as
`[BREAKING+bang+clause-②-narrowing] not-required (already-registered)`.
`check-changeset-no-major`: no `major` bump. `check:nul-bytes`: 9734
text files, no raw control bytes.

## ESLint, a declared narrowing (the repo-wide `pnpm lint` is CI's)

- Touched files: `eslint --no-inline-config --format json` over the 6
touched `.ts` files at `0abe2120fb` (the two later commits are a merge
of `main` and a test-only harness change of 7 lines). From the JSON: 6
files, 0 errors, 0 warnings, 0 ignored.
- Population: `eslint.config.mjs`'s `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` minus `NEVER_LINTED`, which
contains all 6 (none came back ignored).
- Invariance: the config enables no type-aware linting (no
`parserOptions.project` or `projectService` in any block), and this diff
does not touch the config, so no untouched file's verdict can move.

## Beyond the claimed file surface

- **The route pin lives in `packages/runtime/src/`, not beside
`packages/rest/src/analytics-*.test.ts`.** The cube door, `POST
/api/v1/analytics/query`, is mounted by `@objectstack/runtime`'s
`dispatcher-plugin`; `RestServer` mounts only
`/analytics/dataset/query`, so a pin in `rest` cannot reach this route.
The new file is test-only and sits beside the sibling analytics route
pins there. `rest`'s analytics suites were run as well (above).
- **`sum` / `avg` are judged by the same door.** The claim priced the
narrowing as `min` / `max`; the door asks the table for every row, as
the dataset door has since decision batch objectstack-ai#127, because a `min` /
`max`-only condition would be a second scope on top of the one table,
the shape `dataset-compiler.ts` records retiring. Measured, those rows
answered `0` on SQLite and `500` on PostgreSQL before (table above), and
the changeset prices them. No shipped cube authors either.

## Acceptance notes

- **NOT MEASURED: MySQL** (no server here). The door runs before any
driver, so its verdict cannot depend on the dialect; the `time`
description reads metadata only.
- **NOT MEASURED locally: `packages/qa/dogfood`** (`Dogfood Regression
Gate` is CI's). The one shipped cube, `examples/app-showcase`'s
`showcase_delivery`, declares `count` over `*` and `sum` / `avg` over
`estimate_hours` (`Field.number`): every pair accepted. Every other
`min` / `max` / `sum` / `avg` in `examples/**` is a dataset measure,
which the dataset door already judged.
- **NOT MEASURED: the console.** A console widget that sends a
suffix-inferred `FIELD_max` / `FIELD_sum` to `/analytics/query` over a
refused field now gets `400 INVALID_FIELD`; the sibling repository was
not read (dispatch order).
- **Relationship-path measures are not judged here, measured at the
head:** a configured measure `{ type: 'max', sql: 'account.name' }` over
a related `text` field is still served on the native face (`200`,
`"zeta"`, `fields[]` `number`, SQLite and PostgreSQL), and `{ type:
'max', sql: 'account.revenue' }` over a related `number` field answers
the string `"250.000000000000000000000000000000"` on PostgreSQL's native
face under `fields[]` `number`. The ObjectQL face refuses both as a
cross-object measure (`400 INVALID_FIELD`). Judging them needs the
declaration on the hop's object, which is the hop-object resolution this
dispatch fences off (objectstack-ai#20986's sites); the door stands down on a dotted
column rather than guess, the dataset door's tier. This is the second
position the seat's comment `5924234751` names; reported to the seat,
not fixed here.
- **PostgreSQL's native face answers `500 DATABASE_ERROR` for `max` over
a `boolean` column**, a pair the table ACCEPTS (`function max(boolean)
does not exist`); SQLite and the ObjectQL face on PostgreSQL answer `1`.
Unchanged by this PR (measured before and after); the boolean pin skips
that one cell by name. Reported to the seat.
- `main` advanced three commits after the second merge (`a11faeecb3`,
`99398542b3`, `53ed3d1093`: objectql's aggregation-filter door,
driver-mongodb and the showcase's security set). None touches
`service-analytics`, the dispatcher or the spec table; CI and the merge
queue read the merged generation.
- The private PostgreSQL 16.13 cluster used for every live cell runs on
`127.0.0.1` from `/tmp`; it is stopped and its directory removed with
this delivery.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 protocol:data size/m tests tooling

Projects

None yet

2 participants