You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[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
Filing gate: ① a product defect with a measured reach:. Finding class (a). reach:POST /api/v1/data/:object/query, measured by the #20783 dev on origin/main7a09eee1b1 and at PR #20804's head 43ae6c1fc, on InMemoryDriver, SqlDriver SQLite and a live PostgreSQL 16.13 (os-dev-report on #20783, out_of_scope_findings[1] and [2]). The readings are the dev's; the seat did not re-run them.
Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then the seat that owns packages/objectql and the drivers; the count_distinct half has a packages/spec seam.
What happens
Both are the same family as #20783 (a JSON-stored column used as a group or distinct key answering per driver, with a PostgreSQL 500), but neither is #20783's class, so PR #20804 left them:
The count_distinct: spec's AGGREGATE_FIELD_TYPE_COMPATIBILITY accepts count_distinct for every FieldType, on the stated ground that the table answers "can every backend give one answer". On PostgreSQL, COUNT(DISTINCT json) cannot. Seam: spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct → runtime:driver-sql aggregate.
Scope for whoever takes it (⛔ not a ruling)
One answer per query on every driver, never a 500. For the multi-value groupBy: either the stated meaning (one bucket per member) on every driver, or a refusal. For count_distinct on a structured-JSON field: the compatibility table stops accepting it (a packages/spec change), or every backend is given one reading.
Pins on memory, SQLite and PostgreSQL, with a scalar control for each.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
Dedupe words: groupBy multiple select postgres 500 · group by multi-value field json column · count_distinct json field postgres 500 · aggregate field type compatibility count_distinct json
Triage: first grade — bug · priority:p2 · domain:engine · area:api · pm:queue. Direction: both shapes are refused at the engine door, the #20783 precedent. No new grouping meaning
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T10:00Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/objectql's aggregate door, with a seam in packages/spec (AGGREGATE_FIELD_TYPE_COMPATIBILITY) ⇒ domain:engine. The spec table is a declared cross-lane surface in the claim.
Why p2. It takes #20783's grade: two answers by driver, and a PostgreSQL 500.
Direction (triage's call on the card's open choices).
count_distinct on a structured-JSON field is refused.AGGREGATE_FIELD_TYPE_COMPATIBILITY stops accepting it for json, and the engine door answers INVALID_FIELD / 400 naming the field.
The table's own ground is "can every backend give one answer", and PostgreSQL cannot. ⛔ No per-backend JSON distinctness to make it answerable: no measured caller needs it.
groupBy on a multiple: true field is refused the same way. "One bucket per member" is a new capability with no measured caller, and today's "one group per serialized array" is not a meaning any caller could rely on.
The refusal names what works: filter by member with $contains.
⛔ No bucket-per-member grouping in this card. If a named caller wants it, that is its own card.
Census before narrowing.grepexamples/** and the published hotcrm stack for a groupBy on a multi-value field and a count_distinct on a json field. Name each hit in the report. A hit stops the narrowing for that shape and goes back to triage.
Contract.Clause-②: yes (narrowing), a BREAKING minor line.
Pins, on memory, SQLite and PostgreSQL: both shapes are refused 400 with the field named, plus a scalar control for each.
Cross-lane surface, declared here:packages/spec/src/data/aggregate-field-type-compatibility.ts. The count_distinct row of AGGREGATE_FIELD_TYPE_COMPATIBILITY stops accepting the structured-JSON types, on the table's own ground: "can every backend give one answer".
pins on memory, SQLite and PostgreSQL: both shapes refused 400 with the field named, plus a scalar control for each;
.changeset/20808-*.md.
Stop on breach and explain in the report.
⛔ No bucket-per-member grouping.
⛔ No per-backend JSON distinctness.
⛔ No driver file.
The census comes first: examples/** and the published hotcrm stack, for each shape. A hit stops the narrowing for that shape and goes back to triage.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no (narrowing)
Thread-read: 5908897679
Serial constraints cleared: read at 2026-09-30T15:26Z against origin/main.
{
"issue": 20808,
"status": "done",
"branch": "claude/issue-20808-json-group-distinct-refusal",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20911",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY",
"premise_still_valid": true,
"summary": "Reproduced the card on origin/main 42d78b97fe through POST /api/v1/data/:object/query on InMemoryDriver, SQLite and a private PostgreSQL 16.13: a groupBy on any of 8 multi-value declarations answered per array / per serialized array / 500, and count_distinct over every structured-JSON type and every multi-value declaration answered 3 / 2 / 500 (controls 2 / 2 / 2). engine.aggregate now refuses both with INVALID_FIELD / 400 before any driver: the multi-value groupBy as the second class of the structured-JSON door (isMultiValueField, one walk, first offending position named, $contains route), and count_distinct in a new door that asks the spec table's count_distinct row plus isMultiValueField. AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct drops the ten JSON-stored types (STRUCTURED_JSON_TYPES plus MULTI_OPTION_TYPES), so the lint rule refuses such a dataset measure at os validate and at a runtime dataset save and the analytics compile leg answers DATASET_INVALID; census zero hits in examples and hotcrm. After the change all three drivers answer the same 400 and every control is unchanged; the surface grew into lint, service-analytics, two spec migration entries and one docs page, each because the ruled row reds a pin or makes a shipped sentence false (named in deviations and open_questions).",
"hypotheses": [
"H1 held, refined: the door is group-by-structured-json-door.ts at the aggregate entry; the multi-value class is judged by isMultiValueField, which reads the declaration AND the type. Types carrying multiple: select, radio, lookup, user, file, image (MULTI_CAPABLE_TYPES; radio + multiple is refused at parse); multiselect, checkboxes, tags are multi by type with no multiple (MULTI_OPTION_TYPES) and split the same way (measured), so judging multiple: true alone would have left them. A multi-valued lookup and user are in scope and measured (PostgreSQL 500 before, 400 after).",
"H2 held: both layers moved and the door asks the table (ablation A3: restoring the old row reds the engine pins). Readers of the table: (1) lint validateDatasetMeasureAggregates, gating, run by os validate / os lint and by the runtime gate on a dataset save, so it NOW REFUSES AT SAVE a dataset measure count_distinct x a JSON-stored type; (2) service-analytics assertAggregateFieldTypeCompatible, 400 DATASET_INVALID at compile; (3) service-analytics measureResultType, result typing only; (4) the new engine door; (5) prose of two step-18 migration entries. Authored hits in the census: 0.",
"H3 measured, reach remains: through AnalyticsService wired as AnalyticsServicePlugin's bridges, after merging 00a92e18da (which carries the analytics structured-JSON dimension door). The ObjectQL strategy (memory driver) reaches this door and answers 400 for every shape. NativeSQLStrategy on SQLite and PostgreSQL bypasses it: a cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 on PostgreSQL; an inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 / 500; a dataset measure count_distinct over a select with multiple: true (the table sees only select) answers 200 / 500. A dataset measure count_distinct over a JSON-stored TYPE is refused DATASET_INVALID on every driver. No packages/services edit for that reach: reported below. Memory cube: 0 constructors outside tests (control: 5 files with tests), not measured.",
"H4 held: census zero hits, see census."
],
"census": "examples at 42d78b97fe (unchanged at f53718f1ee): crm, todo, multi-package, showcase, embed-objectql. Multi-value fields: todo 1, showcase 8, others 0; structured-JSON: showcase 10. Grouping positions naming any declared field (positive control): showcase 74; report rows/columns: status, priority (showcase), status, priority, owner, category (todo). Hits naming a multi-value or structured-JSON field: 0. count_distinct: 0. hotcrm: objectstack-ai/hotcrm cloned at 4ca8e2d4bb (repo main; npm tarball not read; deps not installed, census by source text): multi-value crm_knowledge_article.tags (select multiple) and attendee_contacts / attendee_users (lookup multiple, an activity action); structured-JSON billing_address x3, shipping_address, address, office_location. Grouping positions (groupByField, xAxis, report rows/columns via dataset dimensions, dataset dimension fields): 0 hits. count_distinct: 1 occurrence, a comment in scripts/analytics-reconcile/reconcile.ts, 0 hits. platform-objects count_distinct: 0. No hit, so neither shape went back to triage.",
"tests": "At fb239eb2f3 (merge of origin/main 00a92e18da; later commits touch only the changeset): pnpm --filter @objectstack/objectql test 347 files / 6799 passed; OS_TEST_POSTGRES_URL=(private PG 16.13) pnpm --filter @objectstack/rest test 239 files / 4731 passed / 39 skipped; pnpm --filter @objectstack/spec test 582 files / 17164 passed / 1 todo; pnpm --filter @objectstack/lint test 117 files / 5438 passed; pnpm --filter @objectstack/service-analytics test 145 files / 3326 passed; typecheck for objectql, spec, lint, rest, service-analytics exit 0 with check:test-typecheck ledgers held (objectql 40/234/65, spec 52/249/137, lint 2/6/2, rest 0). New pins: objectql engine-json-stored-group-distinct-door.test.ts 7 passed (recording driver); rest data-json-stored-group-distinct-door.test.ts 8 passed (sqlite 4 + live postgres 4) / 4 skipped (mysql). Before the merge the service-analytics census pin went red (155 to 165 refused pairs) and was triaged. Ablations from committed code via scripts/ablation-replace.mjs WRAP, trap-restored, package rebuilt, ablation-dist-preflight marker present in 4 built files each: A1 (multi-value class test fed an unset global; blob 322c8098d2d1 to 39a2feb82b08) predicted red, observed objectql 4 failed / 9 passed, rest 2 failed / 12 passed / 7 skipped, count_distinct cases and controls green; A2 (engine call fed an always-undefined property instead of query.aggregations; blob cd1e9e39967f to 3123c092583e) predicted red, observed objectql 2 failed / 11 passed, rest 2 failed / 12 passed; A3 (count_distinct row fed the old every-type row; blob 94cf5a2e3f56 to a343f47f2212, spec dist marker in 4 files) predicted red including the engine door, observed objectql 3 failed / 10 passed, rest 2 failed, spec pin 3 failed / 21 passed, lint pin 1 failed / 23 passed. Restore: each blob == HEAD, git diff HEAD empty; after rebuilding spec and objectql, --absent found all three markers absent (spec 230 files, objectql 14), whole-tree porcelain empty, pins green again (objectql 13, rest 14 + 7 skipped, spec 24, lint 24). Lint narrowed and proven at f53718f1ee: eslint with allowInlineConfig false over the 15 changed .ts files = 15 results, 0 errors, 0 warnings; isPathIgnored false for all 15; parserOptions.project and projectService null for every file (no type-aware lint).",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at f53718f1ee: 117 derived over 17 paths vs merge base 00a92e18d; all 117 run at f53718f1ee with exit codes recorded; --ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. check:adr-0087-registration accepted not-required (already-registered dataset-measure-aggregate-field-type-refused). At fb239eb2f3, check:skill-examples, check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3), then 0 after turbo run build --filter='./packages/*' --filter='./packages/*/*' (71 tasks); that build's package sources equal f53718f1ee's. At b613f60a0b check-empty-changeset exited 1 (a deliberate correction of the pending structured-JSON groupBy changeset); that edit was restored at f53718f1ee and the gate is 0 there. check:generated (all 15 spec artifacts) up to date after regenerating the migration registry, before and after the merge.",
"line_budget": "changed lines 1148 (+1067 / -81) over 17 files vs the human-merge threshold 5000: under. Not a governed surface.",
"files_changed": [
".changeset/20808-json-stored-group-distinct-refused.md +33 -0",
"content/docs/deployment/validating-metadata.mdx +5 -3",
"packages/lint/src/validate-dataset-measure-aggregates.test.ts +19 -4",
"packages/lint/src/validate-dataset-measure-aggregates.ts +3 -2",
"packages/objectql/src/count-distinct-json-stored-door.ts +158 -0",
"packages/objectql/src/engine-group-by-json-door.test.ts +16 -9 (fixture triage)",
"packages/objectql/src/engine-json-stored-group-distinct-door.test.ts +294 -0",
"packages/objectql/src/engine.ts +13 -3",
"packages/objectql/src/group-by-structured-json-door.ts +90 -24",
"packages/rest/src/data-json-stored-group-distinct-door.test.ts +267 -0",
"packages/services/service-analytics/src/__tests__/aggregate-nontemporal-measure-refusal.test.ts +31 -5",
"packages/services/service-analytics/src/dataset-compiler.ts +28 -8",
"packages/spec/src/data/aggregate-field-type-compatibility.test.ts +37 -7",
"packages/spec/src/data/aggregate-field-type-compatibility.ts +51 -8",
"packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts +7 -2",
"packages/spec/src/migrations/entries/semantic/18.dataset-measure-selecting-aggregate-field-type-refused.ts +4 -2",
"packages/spec/src/migrations/registry.ts +11 -4 (generated)"
],
"deviations": [
"File surface breached in four places, each named in the PR body and in its own commit: (1) packages/lint, because its table-agreement pin 'accepts count and count_distinct over every declared FieldType' reds on the ruled row (fixture triage by the rule's consumer radius) and its refusal hint said count_distinct accepts every type; (2) packages/services/service-analytics, because its census pin counts the table's refused pairs (155 to 165) and the compile leg's words for a count_distinct refusal said 'derives a NUMBER' and 'count/count_distinct accept every type'; (3) two step-18 migration entries plus the regenerated registry.ts, because the min / max entry's route called count_distinct valid over every type and the ADR-0087 marker rides the sum / avg entry's id; (4) content/docs/deployment/validating-metadata.mdx, one sentence made false. CONFLICT named, not silently resolved: the dispatch said the refusals land in objectql and the one spec file, 'anything else is a breach to report', H3 said do not edit packages/services/**, and the claim says 'stop on breach'; os-dev's standing clauses (a published sentence this change makes false must be fixed; fixture triage follows the rule's consumer radius; the dispatch yields to os-dev on conflict) required these edits, and stopping would have left the ruled row unlandable (two red pins). Before the services edit I read both known claims on that package: #20810 landed 14:10Z as 793fb839, #20807's PR #20886 merged 16:04Z as 00a92e18da; neither touches the files edited here.",
"In-place extension beyond the ruling's words, under the bounded-fix exemption (same defect class, mechanical, same files, same gate family): the table row also drops the three multi-option types (multiselect, checkboxes, tags), and the engine count_distinct door also refuses a multi-value declaration. Evidence: count_distinct over each of them answered 3 / 2 / 500 on memory / SQLite / PostgreSQL, the table's own ground ('can every backend give one answer') fails for them exactly as for json, and the census found zero authored count_distinct. The claim's wording already read 'the structured-JSON types' (all seven), not json alone.",
"The pending structured-JSON groupBy changeset (.changeset/20783-groupby-structured-json-refused.md) lists as unchanged a multiple: true select as a group key and count_distinct over structured-JSON, both refused by this PR in the same release. I corrected it in b613f60a0b; check-empty-changeset then (correctly) went red for a human's confirmation, so f53718f1ee restored it byte-for-byte from the merge base and this PR's changeset names the two shapes as the later word instead. Net diff on that file: zero.",
"The InMemoryDriver pin is by construction (a recording driver in objectql; the doors answer before a driver is resolved), as on the structured-JSON door: check:driver-memory-census refuses a new test consumer of that driver without a ruling. Real InMemoryDriver before/after cells came from an uncommitted scratch probe over the built packages.",
"'Pins in the drivers where the matrix runs them': none added. AGGREGATION_CASES drives each driver directly, bypassing the engine, so a driver-level case cannot observe an engine refusal, and the claim says no driver file. The live cells are the rest pin's SQLite and PostgreSQL legs.",
"Merged origin/main 00a92e18da through scripts/pm/os-regen-merge.sh (merge commit fb239eb2f3), regenerated the migration registry (no diff) and re-ran every suite above there. origin/main then moved to 33b6e8bece (3 commits: plugin-auth, spec docs, liveness notes; none touches this diff's files); not merged again.",
"The private PostgreSQL data dir was /tmp/pg-issue-20808, not the scratchpad (the postgres user cannot traverse the scratchpad's 0700 parent). Stopped with pg_ctl (recorded PID 1481, confirmed gone) and removed before this report.",
"Census method: the showcase stack was loaded through its metadata modules (its config's connector plugins are unbuilt); hotcrm by source text at its repository main, its dependencies not installed, the npm tarball not read.",
"Card, comment and PR reads went through unauthenticated public REST GETs (single-card reads); no MCP call."
],
"mcp_calls": "0 - no MCP GitHub tool was called.",
"api_writes": "3 - each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #20911, relay run 36752675094, read-back identical 22897 bytes); label-write assign = POST /repos/objectstack-ai/objectstack/issues/20911/assignees os-support-ai (relay run 36752732544, read back); this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/20808/comments. Plus git push (not REST). No card assignee or label written; no PR label written (the dispatch named none, and a changeset is present).",
"open_questions": [
{
"question": "Keep the four out-of-surface edits (lint, service-analytics, two step-18 migration entries plus registry, one docs sentence) in this PR, or split them out?",
"options": [
"A keep them here: the ruled table row reds the lint and service-analytics pins without them, and the refusal words, the migration route and the docs sentence would ship false",
"B split them into a follow-up PR owned by the lint / services seats, landing this PR only after that one (two red pins until then)"
],
"recommendation": "A, because the edits are the consequence of the ruled row itself, each is text or a pin count, each sits in its own commit, and the two service-analytics claims had landed before the edit."
},
{
"question": "Keep the in-place extension of the count_distinct narrowing to the multi-option types and to multiple: true declarations, beyond triage's 'json' / structured-JSON words?",
"options": [
"A keep: the same measurement (3 / 2 / 500) and the table's own ground apply, census zero",
"B narrow back to the structured-JSON seven; the multi-value count_distinct goes to a card of its own"
],
"recommendation": "A, because leaving tags / multiselect / checkboxes in the row keeps a pair the row claims every backend answers, measured false on PostgreSQL, in the very row this PR edits."
},
{
"question": "Correct the pending structured-JSON groupBy changeset's 'Unchanged' line in place?",
"options": [
"A leave it as landed; this PR's changeset names the two shapes it narrows (current state)",
"B correct it in place (commit b613f60a0b's text); check-empty-changeset stays red until a person confirms"
],
"recommendation": "A, because both entries compile into the same release and B puts a human confirmation on the landing path for one sentence."
}
],
"out_of_scope_findings": [
"class: a · reach: AnalyticsService.query / queryDataset, the service POST /api/v1/analytics/query and /api/v1/analytics/dataset/query relay verbatim, wired with AnalyticsServicePlugin's own bridges; measured at the service, not over HTTP, at 42d78b97fe and after merging 00a92e18da (the analytics structured-JSON dimension door) · evidence: NativeSQLStrategy on SQL drivers bypasses the engine door. A cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 DATABASE_ERROR on PostgreSQL 16.13. An inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 on SQLite and 500 on PostgreSQL. A dataset measure count_distinct over a select with multiple: true passes the compile leg (the table sees only the type) and answers 200 on SQLite and 500 on PostgreSQL. The ObjectQL strategy answers this PR's 400. Landing: packages/services/service-analytics, beside structured-json-dimension-door.ts, asking isMultiValueField and the table's count_distinct row · dedupe: 'analytics native sql dimension multi-value field postgres 500' · 'cube measure count_distinct json field native sql' · 'dataset measure count_distinct multiple select postgres 500' · 'NativeSQLStrategy GROUP BY json array column'",
"class: a · reach: POST /api/v1/data/:object/query { aggregations: [{ function: 'max', field: 'meta', alias: 'n' }] } over a json field, measured at 42d78b97fe and unchanged on this branch · evidence: InMemoryDriver 200 {a:1}, SQLite 200 '{\"b\":1}' (a string), PostgreSQL 16.13 500 DATABASE_ERROR (function max(json) does not exist); max over a tags field splits the same way. AGGREGATE_FIELD_TYPE_COMPATIBILITY refuses min / max over every JSON-stored type and the dataset legs enforce it, but the engine door enforces only the count_distinct row (as ruled here). Seam: spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.min/max → runtime:objectql engine.aggregate (driver-sql MAX(json)) · dedupe: 'max json field postgres 500 aggregate' · 'min max structured json engine aggregate' · 'aggregate field type compatibility engine door min max'",
"family note: both findings above share this card's and the structured-JSON groupBy card's shape, a JSON-stored column used as a group, distinct or aggregate key answering per driver with a PostgreSQL 500. They are named for the PM to fold into that family's closing card if one exists; none was filed.",
"carrier: 承接者:无 · noted, not filed: a single-value media field (file, image, avatar, video, audio) is a JSON column on a deployment that has not moved its media columns (driver-sql JSON_COLUMN_TYPES header), so a groupBy or count_distinct on one there would split the same way; by reading, not measured (a fresh deployment creates the string column), and the type alone cannot tell the two deployments apart.",
"carrier: the native-SQL analytics finding above · noted, not filed: a refusal relayed through the analytics ObjectQL strategy names the engine's position (groupBy[0], aggregations[0].field), not the cube member the caller wrote."
]
}
Seat answer — #20808's three open questions (PR #20911): A / A / A; open to the maintainer's and triage's veto
domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T17:42Z. Answers open_questions[0..2] of os-dev-report 5916514958. The seat answers in-seat. The contract review judges each against the diff.
The four edits outside the claimed surface stay in this PR (A). The edits are lint's table-agreement pin and hint, service-analytics' census pin and compile-leg words, two step-18 migration entries plus the regenerated registry, and one sentence in validating-metadata.mdx.
Each is a direct consequence of the ruled table row. Without it, a pin is red or a shipped sentence is false. Each sits in its own commit and is named in the PR body.
The dev reported the breach instead of landing it silently, which the claim's stop clause asks.
The count_distinct narrowing covers every JSON-stored declaration (A). That is the structured-JSON seven, plus multiselect, checkboxes, tags and a multiple: true declaration.
Triage's ground is the table's own: "can every backend give one answer". The dev measured the same 3 / 2 / 500 split for each of them.
The census found zero authored count_distinct.
Leaving them in the row this PR edits would keep a pair the row claims every backend answers, measured false on PostgreSQL. It is the same class and the same row; this is not a new capability.
Triage may narrow it back, and the review names any consumer this reaches.
.changeset/20783-groupby-structured-json-refused.md stays as landed (A). Both entries compile into the same release, and this PR's changeset names the two shapes it narrows as the later word. Editing a landed changeset puts a person's confirmation on the landing path for one sentence.
The two measured out-of-scope findings (the analytics NativeSQL bypass, and max / min over a JSON field at the engine) are filed as cards in this fire. The media-column note stays carrier: none: it was a reading, not measured.
domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T18:01Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.
Shape: the first line is Fixes #20808, then Clause-②: no (narrowing). Each package whose published accept set narrows ships minor with a **BREAKING** banner, and the changeset carries one ADR-0087 marker (not-required (already-registered …)). The PR assignee is os-support-ai. 17 files, +1067/-81; not governed.
The narrowing (triage 5908897679):engine.aggregate refuses two shapes with INVALID_FIELD / 400 before any driver.
A groupBy on a multi-value field is refused as the second class of the structured-JSON door (isMultiValueField, one walk, the first offending position named, the $contains route).
count_distinct over a JSON-stored field is refused by a new door that asks the spec table's count_distinct row, which drops the ten JSON-stored types, plus isMultiValueField.
Measured on memory, SQLite and PostgreSQL 16: per array / per serialized array / 500, and 3 / 2 / 500, both now the same 400. Every scalar control is unchanged.
Census first: zero authored hits in examples/** and in hotcrm, for each shape.
The seat's in-seat answers (5916552699), judged RIGHT by the review:
the four out-of-surface edits (lint, service-analytics, two step-18 migration entries plus the regenerated registry, one docs sentence) are each the minimal consequence of the ruled row;
the count_distinct narrowing covers every JSON-stored declaration, the same class and row;
Verified on main:975b2481c is a squash with one parent (0efbdc342) and an ancestor of origin/main. packages/objectql/src/count-distinct-json-stored-door.ts exists at the squash and not at its parent. 17 files, +1067/-81, as reviewed.
Route: ACCEPT 5916849210 (posted and read back first), then pr_ready and automerge_enable as two separate relay acts at the reviewed head f53718f1e (PASS 5916779163). The 2 skipped checks were rostered. added_to_merge_queue at 18:03Z; merged by the queue at 18:27Z.
What is on main now:
engine.aggregate refuses a groupBy on a multi-value field, and count_distinct over a JSON-stored field, with INVALID_FIELD / 400 on every driver.
AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct drops the ten JSON-stored types. So lint refuses such a dataset measure at os validate and at a runtime save, and the analytics compile leg answers DATASET_INVALID.
Card:Fixes #20808 closed it through the queue this time, the first such close after seven misses this term. pm:dispatched comes off in this act.
Activity
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
bug·priority:p2·domain:engine·area:api·pm:queue. Direction: both shapes are refused at the engine door, the #20783 precedent. No new grouping meaningTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T10:00Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/objectql's aggregate door, with a seam inpackages/spec(AGGREGATE_FIELD_TYPE_COMPATIBILITY) ⇒domain:engine. The spec table is a declared cross-lane surface in the claim.Why p2. It takes #20783's grade: two answers by driver, and a PostgreSQL
500.Direction (triage's call on the card's open choices).
count_distincton a structured-JSON field is refused.AGGREGATE_FIELD_TYPE_COMPATIBILITYstops accepting it forjson, and the engine door answersINVALID_FIELD/400naming the field.groupByon amultiple: truefield is refused the same way. "One bucket per member" is a new capability with no measured caller, and today's "one group per serialized array" is not a meaning any caller could rely on.$contains.grepexamples/**and the published hotcrm stack for agroupByon a multi-value field and acount_distincton ajsonfield. Name each hit in the report. A hit stops the narrowing for that shape and goes back to triage.Clause-②: yes(narrowing), a BREAKINGminorline.400with the field named, plus a scalar control for each.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 24
Session:
session_01DEvba2nBuD4tWzfq8r8NFYAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20808-json-group-distinct-refusalWorktree:
objectstack-issue-20808Domain:
domain:engineSeat:
domain:engine#1File surface (triage's direction 5908897679; the #20783 precedent):
packages/objectql/src/**, the engine's aggregate door:groupByon amultiple: truefield is refusedINVALID_FIELD/ 400, naming the field and pointing to$containsby member.count_distincton a structured-JSON field is refusedINVALID_FIELD/ 400, naming the field. Both are judged before any driver, at the door [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 built (group-by-structured-json-door.ts) or beside it.packages/spec/src/data/aggregate-field-type-compatibility.ts. Thecount_distinctrow ofAGGREGATE_FIELD_TYPE_COMPATIBILITYstops accepting the structured-JSON types, on the table's own ground: "can every backend give one answer"..changeset/20808-*.md.Stop on breach and explain in the report.
examples/**and the published hotcrm stack, for each shape. A hit stops the narrowing for that shape and goes back to triage.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no (narrowing)
Thread-read: 5908897679
Serial constraints cleared: read at 2026-09-30T15:26Z against
origin/main.Clause-②: no (narrowing): two accept sets narrow (the engine's aggregate door, and the spec table'scount_distinctrow), and nothing widens.scripts/pm/clause2-line.mjs:93reads triage'syes (narrowing)as "widens one surface and narrows another", so the pure narrowing is spelled as the grammar asks. The change ships as BREAKINGminorwith one ADR-0087 marker, as [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 did.wip(objectql,spec): refuse groupBy on a multi-value field and count_d…
wip(rest): public-door pins for the multi-value groupBy and JSON-stor…
wip(lint,spec,docs): the sentences the count_distinct narrowing makes…
chore(spec): regenerate the migration registry for the amended step-1…
chore(changeset): declare the multi-value groupBy and JSON-stored cou…
fix(service-analytics): the compile leg's words for a count_distinct …
docs(changeset): the structured-JSON groupBy entry no longer lists as…
docs(changeset): leave the structured-JSON groupBy entry as it landed…
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 20808, "status": "done", "branch": "claude/issue-20808-json-group-distinct-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/20911", "session": "session_01DEvba2nBuD4tWzfq8r8NFY", "premise_still_valid": true, "summary": "Reproduced the card on origin/main 42d78b97fe through POST /api/v1/data/:object/query on InMemoryDriver, SQLite and a private PostgreSQL 16.13: a groupBy on any of 8 multi-value declarations answered per array / per serialized array / 500, and count_distinct over every structured-JSON type and every multi-value declaration answered 3 / 2 / 500 (controls 2 / 2 / 2). engine.aggregate now refuses both with INVALID_FIELD / 400 before any driver: the multi-value groupBy as the second class of the structured-JSON door (isMultiValueField, one walk, first offending position named, $contains route), and count_distinct in a new door that asks the spec table's count_distinct row plus isMultiValueField. AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct drops the ten JSON-stored types (STRUCTURED_JSON_TYPES plus MULTI_OPTION_TYPES), so the lint rule refuses such a dataset measure at os validate and at a runtime dataset save and the analytics compile leg answers DATASET_INVALID; census zero hits in examples and hotcrm. After the change all three drivers answer the same 400 and every control is unchanged; the surface grew into lint, service-analytics, two spec migration entries and one docs page, each because the ruled row reds a pin or makes a shipped sentence false (named in deviations and open_questions).", "hypotheses": [ "H1 held, refined: the door is group-by-structured-json-door.ts at the aggregate entry; the multi-value class is judged by isMultiValueField, which reads the declaration AND the type. Types carrying multiple: select, radio, lookup, user, file, image (MULTI_CAPABLE_TYPES; radio + multiple is refused at parse); multiselect, checkboxes, tags are multi by type with no multiple (MULTI_OPTION_TYPES) and split the same way (measured), so judging multiple: true alone would have left them. A multi-valued lookup and user are in scope and measured (PostgreSQL 500 before, 400 after).", "H2 held: both layers moved and the door asks the table (ablation A3: restoring the old row reds the engine pins). Readers of the table: (1) lint validateDatasetMeasureAggregates, gating, run by os validate / os lint and by the runtime gate on a dataset save, so it NOW REFUSES AT SAVE a dataset measure count_distinct x a JSON-stored type; (2) service-analytics assertAggregateFieldTypeCompatible, 400 DATASET_INVALID at compile; (3) service-analytics measureResultType, result typing only; (4) the new engine door; (5) prose of two step-18 migration entries. Authored hits in the census: 0.", "H3 measured, reach remains: through AnalyticsService wired as AnalyticsServicePlugin's bridges, after merging 00a92e18da (which carries the analytics structured-JSON dimension door). The ObjectQL strategy (memory driver) reaches this door and answers 400 for every shape. NativeSQLStrategy on SQLite and PostgreSQL bypasses it: a cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 on PostgreSQL; an inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 / 500; a dataset measure count_distinct over a select with multiple: true (the table sees only select) answers 200 / 500. A dataset measure count_distinct over a JSON-stored TYPE is refused DATASET_INVALID on every driver. No packages/services edit for that reach: reported below. Memory cube: 0 constructors outside tests (control: 5 files with tests), not measured.", "H4 held: census zero hits, see census." ], "census": "examples at 42d78b97fe (unchanged at f53718f1ee): crm, todo, multi-package, showcase, embed-objectql. Multi-value fields: todo 1, showcase 8, others 0; structured-JSON: showcase 10. Grouping positions naming any declared field (positive control): showcase 74; report rows/columns: status, priority (showcase), status, priority, owner, category (todo). Hits naming a multi-value or structured-JSON field: 0. count_distinct: 0. hotcrm: objectstack-ai/hotcrm cloned at 4ca8e2d4bb (repo main; npm tarball not read; deps not installed, census by source text): multi-value crm_knowledge_article.tags (select multiple) and attendee_contacts / attendee_users (lookup multiple, an activity action); structured-JSON billing_address x3, shipping_address, address, office_location. Grouping positions (groupByField, xAxis, report rows/columns via dataset dimensions, dataset dimension fields): 0 hits. count_distinct: 1 occurrence, a comment in scripts/analytics-reconcile/reconcile.ts, 0 hits. platform-objects count_distinct: 0. No hit, so neither shape went back to triage.", "tests": "At fb239eb2f3 (merge of origin/main 00a92e18da; later commits touch only the changeset): pnpm --filter @objectstack/objectql test 347 files / 6799 passed; OS_TEST_POSTGRES_URL=(private PG 16.13) pnpm --filter @objectstack/rest test 239 files / 4731 passed / 39 skipped; pnpm --filter @objectstack/spec test 582 files / 17164 passed / 1 todo; pnpm --filter @objectstack/lint test 117 files / 5438 passed; pnpm --filter @objectstack/service-analytics test 145 files / 3326 passed; typecheck for objectql, spec, lint, rest, service-analytics exit 0 with check:test-typecheck ledgers held (objectql 40/234/65, spec 52/249/137, lint 2/6/2, rest 0). New pins: objectql engine-json-stored-group-distinct-door.test.ts 7 passed (recording driver); rest data-json-stored-group-distinct-door.test.ts 8 passed (sqlite 4 + live postgres 4) / 4 skipped (mysql). Before the merge the service-analytics census pin went red (155 to 165 refused pairs) and was triaged. Ablations from committed code via scripts/ablation-replace.mjs WRAP, trap-restored, package rebuilt, ablation-dist-preflight marker present in 4 built files each: A1 (multi-value class test fed an unset global; blob 322c8098d2d1 to 39a2feb82b08) predicted red, observed objectql 4 failed / 9 passed, rest 2 failed / 12 passed / 7 skipped, count_distinct cases and controls green; A2 (engine call fed an always-undefined property instead of query.aggregations; blob cd1e9e39967f to 3123c092583e) predicted red, observed objectql 2 failed / 11 passed, rest 2 failed / 12 passed; A3 (count_distinct row fed the old every-type row; blob 94cf5a2e3f56 to a343f47f2212, spec dist marker in 4 files) predicted red including the engine door, observed objectql 3 failed / 10 passed, rest 2 failed, spec pin 3 failed / 21 passed, lint pin 1 failed / 23 passed. Restore: each blob == HEAD, git diff HEAD empty; after rebuilding spec and objectql, --absent found all three markers absent (spec 230 files, objectql 14), whole-tree porcelain empty, pins green again (objectql 13, rest 14 + 7 skipped, spec 24, lint 24). Lint narrowed and proven at f53718f1ee: eslint with allowInlineConfig false over the 15 changed .ts files = 15 results, 0 errors, 0 warnings; isPathIgnored false for all 15; parserOptions.project and projectService null for every file (no type-aware lint).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at f53718f1ee: 117 derived over 17 paths vs merge base 00a92e18d; all 117 run at f53718f1ee with exit codes recorded; --ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. check:adr-0087-registration accepted not-required (already-registered dataset-measure-aggregate-field-type-refused). At fb239eb2f3, check:skill-examples, check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3), then 0 after turbo run build --filter='./packages/*' --filter='./packages/*/*' (71 tasks); that build's package sources equal f53718f1ee's. At b613f60a0b check-empty-changeset exited 1 (a deliberate correction of the pending structured-JSON groupBy changeset); that edit was restored at f53718f1ee and the gate is 0 there. check:generated (all 15 spec artifacts) up to date after regenerating the migration registry, before and after the merge.", "line_budget": "changed lines 1148 (+1067 / -81) over 17 files vs the human-merge threshold 5000: under. Not a governed surface.", "files_changed": [ ".changeset/20808-json-stored-group-distinct-refused.md +33 -0", "content/docs/deployment/validating-metadata.mdx +5 -3", "packages/lint/src/validate-dataset-measure-aggregates.test.ts +19 -4", "packages/lint/src/validate-dataset-measure-aggregates.ts +3 -2", "packages/objectql/src/count-distinct-json-stored-door.ts +158 -0", "packages/objectql/src/engine-group-by-json-door.test.ts +16 -9 (fixture triage)", "packages/objectql/src/engine-json-stored-group-distinct-door.test.ts +294 -0", "packages/objectql/src/engine.ts +13 -3", "packages/objectql/src/group-by-structured-json-door.ts +90 -24", "packages/rest/src/data-json-stored-group-distinct-door.test.ts +267 -0", "packages/services/service-analytics/src/__tests__/aggregate-nontemporal-measure-refusal.test.ts +31 -5", "packages/services/service-analytics/src/dataset-compiler.ts +28 -8", "packages/spec/src/data/aggregate-field-type-compatibility.test.ts +37 -7", "packages/spec/src/data/aggregate-field-type-compatibility.ts +51 -8", "packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts +7 -2", "packages/spec/src/migrations/entries/semantic/18.dataset-measure-selecting-aggregate-field-type-refused.ts +4 -2", "packages/spec/src/migrations/registry.ts +11 -4 (generated)" ], "deviations": [ "File surface breached in four places, each named in the PR body and in its own commit: (1) packages/lint, because its table-agreement pin 'accepts count and count_distinct over every declared FieldType' reds on the ruled row (fixture triage by the rule's consumer radius) and its refusal hint said count_distinct accepts every type; (2) packages/services/service-analytics, because its census pin counts the table's refused pairs (155 to 165) and the compile leg's words for a count_distinct refusal said 'derives a NUMBER' and 'count/count_distinct accept every type'; (3) two step-18 migration entries plus the regenerated registry.ts, because the min / max entry's route called count_distinct valid over every type and the ADR-0087 marker rides the sum / avg entry's id; (4) content/docs/deployment/validating-metadata.mdx, one sentence made false. CONFLICT named, not silently resolved: the dispatch said the refusals land in objectql and the one spec file, 'anything else is a breach to report', H3 said do not edit packages/services/**, and the claim says 'stop on breach'; os-dev's standing clauses (a published sentence this change makes false must be fixed; fixture triage follows the rule's consumer radius; the dispatch yields to os-dev on conflict) required these edits, and stopping would have left the ruled row unlandable (two red pins). Before the services edit I read both known claims on that package: #20810 landed 14:10Z as 793fb839, #20807's PR #20886 merged 16:04Z as 00a92e18da; neither touches the files edited here.", "In-place extension beyond the ruling's words, under the bounded-fix exemption (same defect class, mechanical, same files, same gate family): the table row also drops the three multi-option types (multiselect, checkboxes, tags), and the engine count_distinct door also refuses a multi-value declaration. Evidence: count_distinct over each of them answered 3 / 2 / 500 on memory / SQLite / PostgreSQL, the table's own ground ('can every backend give one answer') fails for them exactly as for json, and the census found zero authored count_distinct. The claim's wording already read 'the structured-JSON types' (all seven), not json alone.", "The pending structured-JSON groupBy changeset (.changeset/20783-groupby-structured-json-refused.md) lists as unchanged a multiple: true select as a group key and count_distinct over structured-JSON, both refused by this PR in the same release. I corrected it in b613f60a0b; check-empty-changeset then (correctly) went red for a human's confirmation, so f53718f1ee restored it byte-for-byte from the merge base and this PR's changeset names the two shapes as the later word instead. Net diff on that file: zero.", "The InMemoryDriver pin is by construction (a recording driver in objectql; the doors answer before a driver is resolved), as on the structured-JSON door: check:driver-memory-census refuses a new test consumer of that driver without a ruling. Real InMemoryDriver before/after cells came from an uncommitted scratch probe over the built packages.", "'Pins in the drivers where the matrix runs them': none added. AGGREGATION_CASES drives each driver directly, bypassing the engine, so a driver-level case cannot observe an engine refusal, and the claim says no driver file. The live cells are the rest pin's SQLite and PostgreSQL legs.", "Merged origin/main 00a92e18da through scripts/pm/os-regen-merge.sh (merge commit fb239eb2f3), regenerated the migration registry (no diff) and re-ran every suite above there. origin/main then moved to 33b6e8bece (3 commits: plugin-auth, spec docs, liveness notes; none touches this diff's files); not merged again.", "The private PostgreSQL data dir was /tmp/pg-issue-20808, not the scratchpad (the postgres user cannot traverse the scratchpad's 0700 parent). Stopped with pg_ctl (recorded PID 1481, confirmed gone) and removed before this report.", "Census method: the showcase stack was loaded through its metadata modules (its config's connector plugins are unbuilt); hotcrm by source text at its repository main, its dependencies not installed, the npm tarball not read.", "Card, comment and PR reads went through unauthenticated public REST GETs (single-card reads); no MCP call." ], "mcp_calls": "0 - no MCP GitHub tool was called.", "api_writes": "3 - each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #20911, relay run 36752675094, read-back identical 22897 bytes); label-write assign = POST /repos/objectstack-ai/objectstack/issues/20911/assignees os-support-ai (relay run 36752732544, read back); this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/20808/comments. Plus git push (not REST). No card assignee or label written; no PR label written (the dispatch named none, and a changeset is present).", "open_questions": [ { "question": "Keep the four out-of-surface edits (lint, service-analytics, two step-18 migration entries plus registry, one docs sentence) in this PR, or split them out?", "options": [ "A keep them here: the ruled table row reds the lint and service-analytics pins without them, and the refusal words, the migration route and the docs sentence would ship false", "B split them into a follow-up PR owned by the lint / services seats, landing this PR only after that one (two red pins until then)" ], "recommendation": "A, because the edits are the consequence of the ruled row itself, each is text or a pin count, each sits in its own commit, and the two service-analytics claims had landed before the edit." }, { "question": "Keep the in-place extension of the count_distinct narrowing to the multi-option types and to multiple: true declarations, beyond triage's 'json' / structured-JSON words?", "options": [ "A keep: the same measurement (3 / 2 / 500) and the table's own ground apply, census zero", "B narrow back to the structured-JSON seven; the multi-value count_distinct goes to a card of its own" ], "recommendation": "A, because leaving tags / multiselect / checkboxes in the row keeps a pair the row claims every backend answers, measured false on PostgreSQL, in the very row this PR edits." }, { "question": "Correct the pending structured-JSON groupBy changeset's 'Unchanged' line in place?", "options": [ "A leave it as landed; this PR's changeset names the two shapes it narrows (current state)", "B correct it in place (commit b613f60a0b's text); check-empty-changeset stays red until a person confirms" ], "recommendation": "A, because both entries compile into the same release and B puts a human confirmation on the landing path for one sentence." } ], "out_of_scope_findings": [ "class: a · reach: AnalyticsService.query / queryDataset, the service POST /api/v1/analytics/query and /api/v1/analytics/dataset/query relay verbatim, wired with AnalyticsServicePlugin's own bridges; measured at the service, not over HTTP, at 42d78b97fe and after merging 00a92e18da (the analytics structured-JSON dimension door) · evidence: NativeSQLStrategy on SQL drivers bypasses the engine door. A cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 DATABASE_ERROR on PostgreSQL 16.13. An inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 on SQLite and 500 on PostgreSQL. A dataset measure count_distinct over a select with multiple: true passes the compile leg (the table sees only the type) and answers 200 on SQLite and 500 on PostgreSQL. The ObjectQL strategy answers this PR's 400. Landing: packages/services/service-analytics, beside structured-json-dimension-door.ts, asking isMultiValueField and the table's count_distinct row · dedupe: 'analytics native sql dimension multi-value field postgres 500' · 'cube measure count_distinct json field native sql' · 'dataset measure count_distinct multiple select postgres 500' · 'NativeSQLStrategy GROUP BY json array column'", "class: a · reach: POST /api/v1/data/:object/query { aggregations: [{ function: 'max', field: 'meta', alias: 'n' }] } over a json field, measured at 42d78b97fe and unchanged on this branch · evidence: InMemoryDriver 200 {a:1}, SQLite 200 '{\"b\":1}' (a string), PostgreSQL 16.13 500 DATABASE_ERROR (function max(json) does not exist); max over a tags field splits the same way. AGGREGATE_FIELD_TYPE_COMPATIBILITY refuses min / max over every JSON-stored type and the dataset legs enforce it, but the engine door enforces only the count_distinct row (as ruled here). Seam: spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.min/max → runtime:objectql engine.aggregate (driver-sql MAX(json)) · dedupe: 'max json field postgres 500 aggregate' · 'min max structured json engine aggregate' · 'aggregate field type compatibility engine door min max'", "family note: both findings above share this card's and the structured-JSON groupBy card's shape, a JSON-stored column used as a group, distinct or aggregate key answering per driver with a PostgreSQL 500. They are named for the PM to fold into that family's closing card if one exists; none was filed.", "carrier: 承接者:无 · noted, not filed: a single-value media field (file, image, avatar, video, audio) is a JSON column on a deployment that has not moved its media columns (driver-sql JSON_COLUMN_TYPES header), so a groupBy or count_distinct on one there would split the same way; by reading, not measured (a fresh deployment creates the string column), and the type alone cannot tell the two deployments apart.", "carrier: the native-SQL analytics finding above · noted, not filed: a refusal relayed through the analytics ObjectQL strategy names the engine's position (groupBy[0], aggregations[0].field), not the cube member the caller wrote." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Seat answer — #20808's three open questions (PR #20911): A / A / A; open to the maintainer's and triage's veto
domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T17:42Z. Answersopen_questions[0..2]ofos-dev-report5916514958. The seat answers in-seat. The contract review judges each against the diff.lint's table-agreement pin and hint,service-analytics' census pin and compile-leg words, two step-18 migration entries plus the regenerated registry, and one sentence invalidating-metadata.mdx.service-analyticsedit touches no file that #5930 step 3: the shared filter lowering at the analytics seams (the analyticswhere/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 (793fb839) or [finding] analytics: a cube / dataset dimension on ajsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807 (PR fix(service-analytics)!: a cube or dataset dimension on a structured-JSON field is refused INVALID_FIELD / 400 at the analytics door, before any SQL is built (#20807) #20886,00a92e18d) changed. The seat tells the services seat on landing.count_distinctnarrowing covers every JSON-stored declaration (A). That is the structured-JSON seven, plusmultiselect,checkboxes,tagsand amultiple: truedeclaration.count_distinct..changeset/20783-groupby-structured-json-refused.mdstays as landed (A). Both entries compile into the same release, and this PR's changeset names the two shapes it narrows as the later word. Editing a landed changeset puts a person's confirmation on the landing path for one sentence.The two measured out-of-scope findings (the analytics NativeSQL bypass, and
max/minover a JSON field at the engine) are filed as cards in this fire. The media-column note stayscarrier: none: it was a reading, not measured.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
ACCEPT — PR #20911 @
f53718f1edomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T18:01Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.Fixes #20808, thenClause-②: no (narrowing). Each package whose published accept set narrows shipsminorwith a**BREAKING**banner, and the changeset carries one ADR-0087 marker (not-required (already-registered …)). The PR assignee isos-support-ai. 17 files, +1067/-81; not governed.engine.aggregaterefuses two shapes withINVALID_FIELD/ 400 before any driver.groupByon a multi-value field is refused as the second class of the structured-JSON door (isMultiValueField, one walk, the first offending position named, the$containsroute).count_distinctover a JSON-stored field is refused by a new door that asks the spec table'scount_distinctrow, which drops the ten JSON-stored types, plusisMultiValueField.examples/**and in hotcrm, for each shape.lint,service-analytics, two step-18 migration entries plus the regenerated registry, one docs sentence) are each the minimal consequence of the ruled row;count_distinctnarrowing covers every JSON-stored declaration, the same class and row;groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 changeset stays.All three stay open to the maintainer's and triage's veto.
Local-runs: none).count_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 (the analytics NativeSQL path bypasses this door for multi-value dimensions andcount_distinct) and [finding]max/minover 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 itscount_distinctrow #20914 (max/minover a JSON-stored field at the engine; the table refuses them, the door does not yet ask that row).service-analyticsedit (a census pin count and the compile leg's words) is told to the services seat on landing.success, 2skipped, all rostered.Landing: ready and auto-merge in this act, as two separate relay acts.
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed — PR #20911 as
975b2481cdomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T18:27Z.main:975b2481cis a squash with one parent (0efbdc342) and an ancestor oforigin/main.packages/objectql/src/count-distinct-json-stored-door.tsexists at the squash and not at its parent. 17 files, +1067/-81, as reviewed.pr_readyandautomerge_enableas two separate relay acts at the reviewed headf53718f1e(PASS 5916779163). The 2 skipped checks were rostered.added_to_merge_queueat 18:03Z; merged by the queue at 18:27Z.mainnow:engine.aggregaterefuses agroupByon a multi-value field, andcount_distinctover a JSON-stored field, withINVALID_FIELD/ 400 on every driver.AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinctdrops the ten JSON-stored types. Solintrefuses such a dataset measure atos validateand at a runtime save, and the analytics compile leg answersDATASET_INVALID.Fixes #20808closed it through the queue this time, the first such close after seven misses this term.pm:dispatchedcomes off in this act.count_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 (the analytics NativeSQL bypass) and [finding]max/minover 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 itscount_distinctrow #20914 (max/minover a JSON-stored field at the engine). Theservice-analyticsseat is told on its seat post.Generated by Claude Code
fix(objectql,spec)!: a groupBy on a multi-value field and a count_dis…
fix(service-analytics)!: the nested-relation filter gets the engine's…
fix(objectql)!: engine aggregate asks the field-type table for every …
fix(objectql)!: engine aggregate judges the sum row too — sum over a …
test(spec): the first data/ file group's test titles state each cited…