Repository navigation
[finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and a count_distinct over a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912
Description
Filing gate: ① a defect with a named landing site: packages/services/service-analytics, beside structured-json-dimension-door.ts (#20807's door). Finding class (a). reach: AnalyticsService.query / queryDataset, wired with AnalyticsServicePlugin's own bridges, which POST /api/v1/analytics/query and /api/v1/analytics/dataset/query relay verbatim.
The measurement is #20808's dev's, at the service (not over HTTP), at 42d78b97f and after merging 00a92e18d (#20807's door), on SQLite and a live PostgreSQL 16.13 (os-dev-report 5916514958 on #20808, out_of_scope_findings[0]).
Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
PR #20911 (#20808) refuses both shapes at the engine aggregate door with INVALID_FIELD / 400: a groupBy on a multi-value field, and count_distinct over a JSON-stored field. The analytics ObjectQL strategy reaches that door and answers the 400. The NativeSQL strategy compiles its own SQL on SQL drivers and bypasses the door:
| analytics query | SQLite | PostgreSQL 16 |
|---|---|---|
a cube or dataset dimension on a multi-value field (a multiple: true select or lookup, tags, …) |
one group per serialized array | 500 DATABASE_ERROR |
an inferred cube measure FIELD_count_distinct over a JSON-stored field |
2 | 500 |
a dataset measure count_distinct over a select with multiple: true (the compatibility table sees only the type select) |
200 | 500 |
A dataset measure count_distinct over a JSON-stored type is already refused DATASET_INVALID on every driver, by the table row #20808 narrows.
Scope for whoever takes it (⛔ not a ruling)
- The NativeSQL strategy refuses the same shapes the engine door refuses, in the engine door's words. It asks
isMultiValueFieldand the table'scount_distinctrow, beside [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's structured-JSON dimension door. - The dataset measure door reads the declaration (
multiple: true), not only the type. - Pins on SQLite and PostgreSQL, with a scalar control.
- The [finding] two more JSON-stored columns as a group or distinct key answer 500 on PostgreSQL:
groupByon amultiple: trueselect, andcount_distincton ajsonfield #20808 dev also noted that a refusal relayed through the ObjectQL strategy names the engine's position (groupBy[0]), not the cube member the caller wrote. It is fair to fold that in. - Serial: after PR fix(objectql,spec)!: a groupBy on a multi-value field and a count_distinct on a JSON-stored field are refused INVALID_FIELD / 400 at the engine aggregate door, on every driver (#20808) #20911.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
- 「analytics native sql dimension multi-value field postgres 500 cube measure count_distinct json NativeSQLStrategy」: 15 hits.
- [finding] analytics: a cube / dataset dimension on a
jsonfield, 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 (closed) is thejsondimension, the structured-JSON half this card extends. - [finding] analytics: on PostgreSQL the native-SQL path answers a measure the response declares
numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889 (open) is a numeric string on PostgreSQL. [finding]os validatepasses a dataset dimension over a structured-JSON field that the analytics runtime refuses 400 INVALID_FIELD — only the measure has an authoring-time leg #20890 (open) isos validatefor a structured-JSON dimension. - [finding] two more JSON-stored columns as a group or distinct key answer 500 on PostgreSQL:
groupByon amultiple: trueselect, andcount_distincton ajsonfield #20808 (open, in flight) is the engine door. [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783, driver-sql on PostgreSQL: the nativeaggregatereturnscount/sum/avgas strings ("n":"1","total":"20.000…"), sohaving { n: { $in: [2] } }keeps no group on PostgreSQL alone, where memory, SQLite and PG's rows path keep c1, c2 #20335, service-analytics: the NativeSQL read-scope compiler and the/analytics/sqlecho compile two scope shapes the shared comparand faces refuse (plain-object comparand under$eq, null member in$in): one scope, two answers across faces #20018 and [spec/drivers]AggregationNode.distinctis honoured by the in-memory fallback and ignored by every SQL face — one query, two numbers (ADR-0049) #6815 are closed neighbours. - None covers a multi-value dimension or
count_distincton the NativeSQL path.
- [finding] analytics: a cube / dataset dimension on a
Dedupe words: 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
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
bug·priority:p2·domain:services·area:reports·pm:blockedon #20808. Direction: NativeSQL refuses what the engine door refuses, in its wordsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T17:58Z. ⛔ Not a claim, ⛔ not a dispatch.Triage:
packages/services/service-analytics(besidestructured-json-dimension-door.ts) ⇒domain:services.Why p2. It takes #20807's and #20808's grade: a SQLite answer and a PostgreSQL
500on the path that bypasses the engine door.Direction.
isMultiValueField, and the table'scount_distinctrow), beside [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's door, and refuses with the engine door's code and words. ⛔ No second copy of the rule.multiple: true), not the type alone. Amultipleselect is JSON-stored whatever its type name.Serial. After PR #20911 (#20808), whose door it mirrors.
State
Blocked-by: #20808
objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 · 2026-09-30T23:03Z
Session:
session_01XY5uCwTjZj7884yYtyur4HAccount:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20912-native-multivalue-doorWorktree:
objectstack-issue-20912Domain:
domain:servicesSeat:
domain:services(seat 1, seat post #6021)Unlock:
Blocked-by: #20808is met. #20808 is closed, and its PR #20911 landed as975b2481onmain. This act moves the card frompm:blockedstraight topm:dispatched.File surface:
packages/services/service-analytics/src/structured-json-dimension-door.ts([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's door, beside which triage puts this card), anddataset-compiler.ts(the dataset measure door that reads the declaration,multiple: true), with their pins inservice-analytics/src/__tests__/.packages/rest/src/analytics-*.test.ts).packages/client/src/envelope-caller-census.test.ts(domain:cli): ledger rows only, if a new pin adds a censused call site..changeset/20912-*.md.analytics-service.ts,strategies/native-sql-strategy.ts,strategies/objectql-strategy.ts,strategies/filter-normalizer.tsandread-scope-sql.ts(security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933, p0, and PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 / #20802 analytics half (domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887), norfield-read-admission.tsandplugin.ts(security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935, p0, thedomain:specseat). If the fix needs one of them, the dev stops and reports, and this card serializes behind that holder.packages/objectql/src/**and notpackages/spec/src/**: the engine's predicates and the compatibility table are called, not changed. If one is not exported, the dev stops and reports.isMultiValueField, the table'scount_distinctrow).(stop on a breach outside these; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The door is a mirror of one that has already landed, so the default tier fits. The contract review runs atCONTRACT_REVIEW_TIER.Clause-②: no
no: the native path answers today what the engine refuses (a wrong group on SQLite, a500on PostgreSQL). Refusing it widens nothing. The expected line 2 isno (narrowing), measured by the dev.Thread-read: 5916800604
Serial constraints cleared:
domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887, this seat) hold the service, the strategies, the normalizer and the read scope. security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935 (p0, the spec seat) holds the gate and the plugin wiring. None of those files is on this surface; the exclusion above holds them out.area:reportsaxis: [finding] service-analytics still spells a year below 1000 its own way:formatDateBucketlabels bucket key0050as 1970 and0050-06as50-06, and the draft-previewbucketDatewrites unpadded keys the published path no longer writes #20867 (claimed in the same act) holdsdimension-labels.tsandpreview-evaluator.ts, which are disjoint by file.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 20912, "status": "blocked", "branch": "claude/issue-20912-native-multivalue-door", "pr": null, "session": "session_01XY5uCwTjZj7884yYtyur4H (dispatching PM seat's session; this executor ran as its subagent)", "premise_still_valid": true, "summary": "Stopped at F2, before any edit, on the order's own stop clause: the `multiple: true` half of all three rows needs a new input from the held `analytics-service.ts`. Both doors this card may touch receive the declared TYPE only: `assertDimensionsGroupScalarColumns` (analytics-service.ts at f6ccca4a44, lines 2714-2730) hands `assertNoStructuredJsonDimension` a lambda answering `fieldMeta(o, field)?.type`, and `compile()` (lines 1813-1828) hands `compileDataset` a `declaredFieldType` lambda answering `sourceFieldMeta(object, field)?.type`; `multiple` reaches only the strategy context's `declaredValueShape` (line 1250), which neither door reads. With today's inputs the door could judge row 1 only for `tags`/`multiselect`/`checkboxes` and row 2 only for JSON-stored TYPES, and row 3 not at all (it is exactly the declaration half). A type-only door would refuse `tags` and serve `select` with `multiple: true`, the split triage's direction rules out ('reads the declaration, not the type alone'), so no partial door was written. What only the PM can settle: (A) serialize behind the holders of analytics-service.ts, PR #20962 (#20933, open draft, base f6ccca4a44, hunks at old lines 1443-1681) and PR #20916 (#20887, open draft, base 975b2481cd, hunks at 61, 2775 [a where-gate doc comment], 3093, 3106), or (B) grant a two-call-site surface extension: both lambdas pass `{ type, multiple }` (the shape `declaredValueShape` already builds at line 1250) instead of `?.type`. Neither holder's hunks cover those two call sites textually (read from the PR file patches), but the hold is by file. Premise holds on SQLite (readings in `tests`). The claim's remote branch is at BASE f6ccca4a44, with no commits; the worktree is removed, and the local branch ref remains for a resume (`git worktree add ../objectstack-issue-20912 claude/issue-20912-native-multivalue-door`, then merge origin/main).", "tests": "No repo diff, so no gate, test or typecheck of the package was owed or run; `node scripts/pm/dispatch-gates.mjs --commands` on the branch: exit 2, 'this branch changes nothing against origin/main (merge base f6ccca4a4) - nothing to derive'. For reference, the hypothetical surface (door + dataset-compiler + analytics-service.ts) derives 49 commands and adds `pnpm check:dispatcher-error-vocabulary` over the 61-command dispatch list. MEASUREMENT (F1 + premise), tree f6ccca4a44, scratch vitest file inside the package (deleted afterwards and never committed; `git status --porcelain` empty before the worktree was removed), run under os-verify-lock (build closure `pnpm turbo run build --filter=@objectstack/service-analytics^... --concurrency=1`, 15 packages: VERDICT command-exit 0; measure run: VERDICT command-exit 0). Composition: `AnalyticsServicePlugin` initialised over a real ObjectQL engine + SqlDriver better-sqlite3 in memory, with its own auto-bridges and sourceFieldMeta. The ObjectQL face uses the same plugin with `queryCapabilities` forcing objectqlAggregate. Object mv_ledger, 3 rows: status a,a,b (single select, the scalar control); tags_f (tags), ms (multiselect) and multi_sel (select, multiple: true) each hold [a,b],[a,b],[b]; meta (json) holds {a:1},{a:1},{b:1}. NATIVE face (today's default on a SQL driver): row 1 cube dimension tags_f / ms / multi_sel answers 200, groups '[\"a\",\"b\"]':2 and '[\"b\"]':1 (one group per serialized array); dataset dimension on tags_f / multi_sel answers the same 200; control status answers 200 a:2 b:1. Row 2 `meta_count_distinct` / `tags_f_count_distinct` / `multi_sel_count_distinct`, on the authored cube and on the inferred ad-hoc cube alike answers 200, 2; control `status_count_distinct` answers 2. Row 3 dataset measure count_distinct over multi_sel registers, then answers 200, 2; over tags_f answers DATASET_INVALID 400 at registration (the existing type row); control over status answers 200, 2. OBJECTQL face (F1, the reference): every row answers INVALID_FIELD 400 with the engine's words. Dimension: \"aggregate('mv_ledger'): groupBy[0] names 'multi_sel', a declared select field with multiple: true - a multi-value field, which the engine does not group by. The query was NOT run. Filter by one member instead: where { \\\"multi_sel\\\": { \\\"$contains\\\": VALUE } } counts or lists the records that hold VALUE, one query per member.\" (tags_f and ms read 'a declared tags field' / 'a declared multiselect field'). Measure over json: \"aggregate('mv_ledger'): aggregations[0].field counts distinct 'meta', a declared json field - a structured-JSON value, which the engine does not count distinct. The query was NOT run. Count distinct values of a field that stores one scalar value: ...\"; over tags_f / multi_sel: '... a multi-value field, which the engine does not count distinct. The query was NOT run. Count the records that hold one member instead: count with where { FIELD: { $contains: VALUE } }, one query per member.' Row 3 over multi_sel on this face: registers, then the same engine count_distinct refusal at query time. Every control 200. The engine names its own position (groupBy[0], aggregations[0].field), never the member the caller wrote, which is the card's fold-in note. POSTGRESQL: NOT MEASURED, reason: OS_TEST_POSTGRES_URL unset and no server running here (a PG 16 binary is installed but was not started for a blocked stop); the PG 500 in the card is the card's own measurement. No ablation was run: there is no fix to ablate.", "mcp_calls": "0", "api_writes": "2 - `git push -u origin claude/issue-20912-native-multivalue-door` (empty branch at BASE, the write-route probe: accepted), and this `os-dev-report` comment on #20912 via scripts/pm/post-stamped.mjs. No pr_create and no label-write, because there is no PR. Reads only otherwise: REST GET of issue 20912, its comments, pulls 20962 and 20916 and their file lists.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Serial order ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T23:16ZThe dev stopped at F2 before any edit (report on this card), on the order's own stop clause.
multiple: truehalf of all three rows needs the declaration,{ type, multiple }. Both doors this card may touch receive the declared TYPE only, from two lambdas inanalytics-service.ts: the one feedingassertNoStructuredJsonDimension, andcompile()'sdeclaredFieldType. That file is held by PR fix(service-analytics)!: an object read through a relationship path joins the one admitted and scoped object set (#20933) #20962 (security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933, p0, in review) and PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 (#20802 analytics half (domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887, reviewed, waiting).tagsand serve aselectwithmultiple: true, which is the split triage's direction rules out.{ type, multiple }(the shapedeclaredValueShapealready builds) instead of?.type. The branchclaude/issue-20912-native-multivalue-dooris at base, with no commits.INVALID_FIELD/ 400 in the engine's words on the ObjectQL path. PostgreSQL is not measured.The claim
5921222049stands; the card stayspm:dispatchedand assigned. It waits from this comment's stamp.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Resume ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T01:10ZThe serial wait in
5921389276is over:5f6b63a6;domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887) landed as8d329f02.No other open PR or in-flight claim holds
analytics-service.ts,structured-json-dimension-door.tsordataset-compiler.tsat this reading.The surface in effect: the claim
5921222049, plus exactly the twoanalytics-service.tscall sites that serial order named:assertNoStructuredJsonDimension;compile()'sdeclaredFieldType.Each passes
{ type, multiple }instead of?.type. Nothing else in that file is added.Held out:
numberas a string (count: "2"), where SQLite answers 2 — the class #20335 closed at the engine door #20889's surface: this seat's measure-first claim5922551152, covering the native path's row shaping point and the number presenter's home.The order: the dev resumes on the claim's branch, which is at base with no commits, and merges
origin/mainfirst.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20912, "status": "done", "branch": "claude/issue-20912-native-multivalue-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/21019", "session": "session_01XY5uCwTjZj7884yYtyur4H (dispatching PM seat's session; this executor ran as its subagent)", "premise_still_valid": true, "summary": "Resumed on the claim's branch (remote at BASE f6ccca4a44, no commits), fast-forwarded to origin/main 8d329f02 (PR #20916 and PR #20962 landed), later merged origin/main f8178ffece (no overlap with the surface); PR head d53f230cdb. The analytics door (structured-json-dimension-door.ts) now refuses, ahead of both strategies and naming the member the caller wrote, a grouped dimension on a multi-value field and a count_distinct measure over a JSON-stored field, INVALID_FIELD / 400, asking the spec predicates the engine's two doors ask (isMultiValueField, STRUCTURED_JSON_TYPES, isAggregateCompatibleWithFieldType('count_distinct', ...)); the dataset compiler's measure door reads the declaration and refuses a count_distinct over a multiple: true field DATASET_INVALID / 400 at compile time. analytics-service.ts changes at exactly the two granted call sites, each now handing { type, multiple } instead of ?.type. Order measurements: F1 recorded (engine words in `tests`); F2' holds, the two call sites are the only inputs (the door already receives query and cube, so the measure walk needs nothing new), with one consequence the order did not name: the compile probe is a member of the PUBLISHED DatasetCompileOptions interface, so it is renamed declaredFieldType -> declaredValueShape (the name and ValueShapeFieldDef type the strategies' context already uses) and the changeset carries it as BREAKING with a prose 'Your fix'; its ADR-0087 disposition is not-required (runtime-interface-only ...dataset-compiler.ts#DatasetCompileOptions), verified by the gate, rather than no-migration-prescription. F3 holds (both exported from @objectstack/spec/data). F4: PostgreSQL MEASURED, not skipped: a PostgreSQL 16.13 server started by this run on 127.0.0.1 (own data dir, stopped and removed at the end). F5: Clause-② measured both directions, no (narrowing); changeset minor with the BREAKING banner. One deviation from 'the engine door's words': the multi-value route names a record query on the declaring object with where { FIELD: { $contains: VALUE } }, not the analytics where, because the analytics native strategy compiles $contains over a JSON-stored column to LIKE (measured: SQLite 3 instead of 2, PostgreSQL 500); that defect is out_of_scope_findings[0].", "tests": "All local readings at HEAD d53f230cdb unless named; every heavy run under scripts/pm/os-verify-lock.sh (slot issue-20912-dev), NODE_OPTIONS=--max-old-space-size=3072, turbo --concurrency=1, vitest --maxWorkers=2, OS_TEST_POSTGRES_URL pointing at the run's own PostgreSQL 16.13. BUILD: pnpm turbo run build --filter='@objectstack/rest^...' (24 tasks, VERDICT command-exit 0, after the merge). F1 + PREMISE (scratch vitest file in the package, never committed, deleted before the PR; readings saved in scratch measure-r-before.{sqlite,pg}.json at 8d329f02): NativeSQL face, SQLite: cube dimension tags_f / ms / multi_sel 200 with groups '[\"a\",\"b\"]', '[\"ab\"]', '[\"b\"]' (one per serialized array); dataset dimension the same; tags_f_count_distinct 3, meta_count_distinct 2, multi_sel_count_distinct 2; dataset count_distinct over multi_sel registers then 200, 2. PostgreSQL 16.13: every one of those 500 DATABASE_ERROR; controls (status dimension, status_count_distinct, dataset count_distinct over status) 200 on both. ObjectQL face (F1, the reference), both dialects: INVALID_FIELD 400 in the engine's words, e.g. \"aggregate('mv_ledger_scr'): groupBy[0] names 'multi_sel', a declared select field with multiple: true — a multi-value field, which the engine does not group by. The query was NOT run. Filter by one member instead: ...\" and \"aggregate('mv_ledger_scr'): aggregations[0].field counts distinct 'meta', a declared json field — a structured-JSON value, which the engine does not count distinct. The query was NOT run. ...\"; row 3 on that face registered, then the same count_distinct refusal at query time. AFTER (same scratch at 2c1b093a66, both faces, both dialects): every row 400, INVALID_FIELD naming the member for rows 1-2, DATASET_INVALID at registration for row 3; controls unchanged. PINS RED (f88977bf4e, before the fix): multi-value-json-stored-door 16/21 failed (5 controls green); json-stored-door-live-drivers 6/10 failed (3 refusal cases x SQLite + PostgreSQL); dimension-structured-json-door GUARD 1/13 failed; REST analytics-dataset-multi-value-door: both refusal cases red on both dialects (that run also exposed a wrong control expectation of mine, option labels A/B, corrected before the pins commit). GREEN: service-analytics vitest run 152 files / 3489 tests passed (PostgreSQL cell on); service-analytics typecheck exit 0; rest new pin 8/8; rest typecheck exit 0 (check:test-typecheck OK). ABLATION (from the committed fix, scripts/ablation-replace.mjs per leg: anchor x1 -> x0, blob changed; outer trap restores by absolute path; each restore proven blob == HEAD and git diff HEAD empty; suites = 3 pin files + aggregate-nontemporal-measure-refusal, 92 cases): M1a door call site back to fieldMeta(o, field)?.type -> 27 red; M1b door call site drops multiple (type-only shape) -> 16 red; M2a compile call site back to ?.type -> 33 red; M2b compile call site drops multiple -> 3 red (exactly row 3); M3 door measure walk removed -> 9 red; M4 door multi-value grouped class removed -> 10 red; M5 compiler declaration half removed -> 4 red; control on the restored tree 92/92 green, status clean. DIST LEG for the REST pin (it resolves service-analytics through dist): M1a, pnpm --filter @objectstack/service-analytics build (JS emitted; DTS step failed TS2345 on the ?.type revert), ablation-dist-preflight marker present in dist/index.js and dist/index.cjs, REST pin 2 red (dimension rows, SQLite + PostgreSQL); restored (blob == HEAD), rebuilt exit 0, preflight --absent and tree clean, REST pin 8/8 green. GATES: node scripts/pm/dispatch-gates.mjs --commands (tree d53f230cdb) derived 62; all 62 run sequentially under the lock plus the 4 roster families it marks (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity): 64 exit 0; 2 exit 3 PREREQUISITE NOT MET = NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, reason: both need a whole-workspace build and the order builds only the tested closure. --ran reconciliation with recorded exit codes: '62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN'. check-adr-0087-registration: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... not-required (runtime-interface-only) -- verified'. check-changeset-no-major: 'This diff introduces no major bump'. clause2-line readClause2Line on the PR body: {kind: declared, value: no, arm: narrowing}. NOT MEASURED: MySQL (the refusal precedes any SQL); downstream consumer suites (runtime, dogfood) declared to CI. CI at report time (one read, 02:00Z): 31 check runs on d53f230cdb, 11 success, 3 skipped, 17 in_progress, 0 failed — in_progress.", "mcp_calls": "0", "api_writes": "3 relay writes as objectstack-fleet[bot]: pr_create POST /repos/objectstack-ai/objectstack/pulls (draft, #21019; run 36803629625, body read back 11383 bytes identical); label-write.mjs --issue 21019 --assign os-justin, POST /repos/objectstack-ai/objectstack/issues/21019/assignees (run 36803683107, read back matches); this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20912/comments via post-stamped.mjs. Plus 2 git pushes of claude/issue-20912-native-multivalue-door (f6ccca4a44..2c1b093a66, then ..d53f230cdb). Reads only otherwise: REST GET of issue 20912 and its comments, PR 21019 and its check runs.", "open_questions": [], "out_of_scope_findings": [ "class: a + b · reach: AnalyticsService.query (what POST /api/v1/analytics/query relays) as AnalyticsServicePlugin composes it on a real engine, NativeSQL face: where { tags_f: { $contains: 'a' } } over tags rows [a,b], [a,b], [b], [ab] answers count 3 on SQLite (membership answer is 2; the [ab] row matches by substring) and 500 DATABASE_ERROR on PostgreSQL 16.13 (statement WHERE tags_f LIKE ? ESCAPE ?, 'operator does not exist: json ~~ text'); ms (multiselect) and multi_sel (select, multiple: true) also 500 on PostgreSQL; the ObjectQL face and engine.aggregate answer 2 on both dialects. Measured at 8d329f02 and again at 2c1b093a66. · contract: packages/spec/src/data/filter.zod.ts, 'A JSON-stored column changes what $contains ASKS': 'On a multiple: true field or a JSON_COLUMN_TYPES member, $contains: v is a MEMBERSHIP test ... answered identically on every SQL dialect and by driver-memory.' · Seam: spec:filter.zod.ts $contains over a JSON-stored column → runtime:NativeSQLStrategy filter compilation (strategies/native-sql-strategy.ts, text-match-sql.ts; the dataset measure filter and the read-scope compiler likely share it, not measured) · dedupe words: analytics native sql contains json multi-value LIKE substring · analytics where contains tags postgres 500 json ~~ text · native sql strategy contains membership jsonMembershipPredicate", "class: b · reach: @objectstack/lint's exported validateDatasetMeasureAggregates (rule measure-aggregate-field-type-refused, the rule os validate runs per the 20808 changeset), dist built at d53f230cdb, on a one-object stack: a dataset count_distinct over tags_f (tags) gives 1 finding, over multi_sel (select, multiple: true) gives 0; this PR's compile leg answers 400 DATASET_INVALID for that pair, and the engine door refused it at query time already. os validate itself was not driven. · contract: packages/spec/src/data/aggregate-field-type-compatibility.ts: 'this table is the contract both consumer legs execute — the compile-time refusal in the dataset compiler (#16099) and the authoring-time lint rule — so the two cannot drift into two accounts of one pair' · Seam: spec:isMultiValueField + AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct → lint:packages/lint/src/validate-dataset-measure-aggregates.ts (reads verdict.meta?.type only) · dedupe words: lint dataset measure count_distinct multiple select · measure-aggregate-field-type-refused multiple true declaration · os validate count_distinct multi-value", "carrier: none · noted, not filed: assertNoStructuredJsonDimension and structured-json-dimension-door.ts now also judge multi-value grouping and count_distinct measures; not renamed, because a rename moves the call in analytics-service.ts beyond the two granted call sites (in the PR's Acceptance notes)", "carrier: none · noted, not filed: the comments beside the two analytics-service.ts call sites still say 'declared type' (in the PR's Acceptance notes)" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT: #20912 · PR #21019 at
d53f230c·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T02:04ZThe dev report is
5923231598, read on GitHub.Checklist, read on GitHub rather than from the report:
main, 8 files, +1,175 / −53.structured-json-dimension-door.ts,dataset-compiler.tsandanalytics-service.ts.service-analytics/src/__tests__/, one re-judged there, and one REST route pin.Fixes #20912, and line 2 isClause-②: no (narrowing), measured.analytics-service.tschanges at exactly the two call sites the serial order5921389276granted. Each now hands{ type, multiple }instead of?.type. Read in the diff.dataset-compiler.ts's compile probe is a member of the publishedDatasetCompileOptions. It is renameddeclaredFieldType→declaredValueShape, the name andValueShapeFieldDeftype the strategies' context already uses. The rename is in a file on the surface, and the changeset carries it as BREAKING with a prose remedy and the ADR-0087not-required (runtime-interface-only …)disposition, which the gate verified. Its grade and that disposition are the at-tier review's to judge.500on PostgreSQL;400in the engine door's class, naming the member the caller wrote, at the door or at dataset compile;service-analytics: 152 files, 3,489 tests passed, typecheck exit 0;?.type, and one dropsmultiple. Each restore was proven, and a dist leg covers the REST pin.One deviation from "the engine door's words", accepted: the multi-value refusal's remedy names a record query with
$contains, not an analyticswhere. The analytics native$containsover a JSON-stored column is itself wrong today (below), so prescribing it would send authors into a second defect.Findings, line by line:
$containsover a JSON-stored column (substring rather than membership: an over-count on SQLite,500on PostgreSQL). Already carried: [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 (p1, this lane, thewhereface). The reading is added there as a measurement of a face that was "read, not measured".measure-aggregate-field-type-refusedreads the type only, so acount_distinctover amultiple: trueselect passes authoring while the compiler refuses it. Same family as [finding]os validatepasses a dataset dimension over a structured-JSON field that the analytics runtime refuses 400 INVALID_FIELD — only the measure has an authoring-time leg #20890 (the authoring-time leg for the analytics doors,domain:spec). A pointer goes there; no new card.assertNoStructuredJsonDimension,structured-json-dimension-door.ts) now covers multi-value grouping andcount_distincttoo. Acceptance notes: a rename would moveanalytics-service.tspast the granted call sites.Landing waits for every check green on the head and the at-tier contract review. After the merge, this seat checks that
Fixes #20912closed the card.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T03:53ZPR #21019 merged through the merge queue as
bb2eccf7onorigin/main, andFixes #20912closed this cardcompleted.d53f230cis5923426452.git patch-id --stableequals that of the reviewed headd53f230cagainst its basef8178ffe.What now holds: the analytics door refuses, ahead of both strategies,
INVALID_FIELD/400, naming the member the caller wrote. It asks the same spec predicates the engine's two doors ask.count_distinctmeasure over a JSON-stored field is refused.multiple: true) and refuses acount_distinctover such a field,DATASET_INVALID/400.For the release list:
@objectstack/service-analyticsships aminorwith the BREAKING banner (Clause-②: no (narrowing)). The publishedDatasetCompileOptions.declaredFieldTypeis renameddeclaredValueShape({ type, multiple }), and the changeset carries the one-line fix.Carried elsewhere:
$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987: the native$containsover a JSON-stored column (this card's dev measured thewhereface).os validatepasses a dataset dimension over a structured-JSON field that the analytics runtime refuses 400 INVALID_FIELD — only the measure has an authoring-time leg #20890: lint'smeasure-aggregate-field-type-refusedreads the type only.In the same act, this seat removes
pm:dispatchedand the assignee.Generated by Claude Code
fix(service-analytics)!: the analytics door refuses a multi-value gro…
fix(service-analytics): the ObjectQL strategy hands the engine its nu…
fix(service-analytics)!: the cube door asks the aggregate × field-typ…
fix(service-analytics)!: judge and present a relationship-path cube m…