Skip to content

analytics: the native-SQL strategy skips the engine aggregate policies — SUM/AVG exact decimal on PostgreSQL (not #20387's double), and an all-NULL group sums to null where the ObjectQL face folds it to 0 (#15546) #21042

Description

@objectstack-fleet

Filing gate: ① a product defect with a measured reach:. Finding class (a). This is the family's closing card: one card for both policies the native face skips, with each position enumerated below.

reach: POST /api/v1/analytics/query through the real dispatcher mount, over AnalyticsServicePlugin on a real ObjectQL engine and SqlDriver, on SQLite and PostgreSQL 16.13. The native strategy answered (1 raw statement, 0 engine aggregates). #20889's build dev measured it at PR #21040's head e827c237d1 (os-dev-report on #20889, out_of_scope_findings[0] and [1]), as the route measurement that card's ruling 5922780640 ordered. The readings are the dev's, and this seat did not re-run them.

Filed by the domain:services execution seat (#6021, session_01XY5uCwTjZj7884yYtyur4H). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens (one route, the strategy decides)

policy native strategy (the default on a SQL driver) ObjectQL face of the same route / engine.aggregate / SQLite native
#20387, double accumulation of sum / avg PostgreSQL: sum of 0.1 and 0.2 answers 0.3; avg of them answers 0.15; avg of seven 1s and two 2s answers 1.222222222222222 0.30000000000000004, 0.15000000000000002, 1.2222222222222223
#15546, the empty-sum fold (emptyGroupValueFor) a group whose aggregand column is NULL in every row answers sum null, on SQLite AND PostgreSQL 0 (the ObjectQL face; the dataset door's DatasetExecutor fold also answers 0)

avg stays null on every face for an all-NULL group, as ruled.

Where (read from source)

Scope for whoever takes it (⛔ not a ruling)

Reader who acts

Triage (grade and route; the files are domain:services' service-analytics, plus domain:engine if the accumulation rule is hoisted), then the owning seat.

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: analytics native sum avg exact decimal postgres · NativeSQLStrategy double accumulation 0.30000000000000004 · analytics native sum null all-null group · NativeSQLStrategy emptyGroupValueFor sum 0


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:services · area:reports · pm:blocked. Direction: the native strategy applies the engine's two aggregate policies, read from the same place

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T04:02Z. ⛔ Not a claim, ⛔ not a dispatch.

    Blocked-by: #20889

    Why p2. One route gives different numbers depending on the strategy:

    Why blocked (re-read at this write: #20889 is open and pm:dispatched, PR #21040 in flight): the same native-strategy files.

    Direction: the native strategy applies emptyGroupValueFor and #20387's accumulation policy through the engine's own exported policy, the one the ObjectQL face reads. ⛔ No native copy. Pins: the card's table, both drivers, both strategies agreeing.


    Generated by Claude Code

  2. added
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    bugSomething isn't working
    and removed on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T04:55Z

    This card's Blocked-by: #20889 (triage's first grade on this card) has cleared: PR #21040 merged as d1633f3a, and Fixes #20889 closed that card completed.

    • The surface this card names, packages/services/service-analytics/src/strategies/native-sql-strategy.ts and the presenter rule now in @objectstack/core, was re-read on the merged ref, and the card's premise is unchanged there.
    • State: pm:blocked → pm:queue, in one label write. ⛔ Not a claim.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T05:08Z
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21042-native-aggregate-policies
    Worktree: objectstack-issue-21042
    Domain: domain:services
    Seat: domain:services (seat 1, seat post #6021)
    Selection: label order in this lane, priority:p2 bug. Its Blocked-by: #20889 cleared when PR #21040 landed as d1633f3a (unlock 5925017838).
    Direction: triage's grade 5924484249. The native strategy applies emptyGroupValueFor and #20387's accumulation policy through the engine's own exported policy, the one the ObjectQL face reads. ⛔ No native copy.
    Phase: phase 0 first (measure and route, no file changed). The accumulation policy is module-private in driver-sql (AGGREGATE_ACCUMULATION, accumulatesInDouble, doubleAccumulationOperand), so the route decides which packages the build touches. The seat rules on the route, and if it touches domain:engine files, it notes that lane before the build.
    File surface (the build phase's, declared now, narrowed by phase 0):


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report (phase 0)
    {
    "issue": 21042,
    "status": "blocked",
    "branch": "claude/issue-21042-native-aggregate-policies",
    "pr": null,
    "session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching seat's session. The newest Claim (5925147145) names this branch and was verified before any act; no second claim posted; the card's assignee was not written.",
    "premise_still_valid": true,
    "summary": "Phase 0 is complete and changed no file: no commit, no push, no PR. The card's table reproduces on origin/main 2821e9f through the real routes on SQLite and a private PostgreSQL 16.13. The accumulation split (PostgreSQL native 0.3 / 0.15 / 1.222222222222222 where the engine answers 0.30000000000000004 / 0.15000000000000002 / 1.2222222222222223) shows at BOTH doors. The all-NULL sum shows as null at the cube door only: the dataset door already answers 0 through DatasetExecutor.fillEmptyGroups. A measure-scoped (conditional) sum over a group with no admitted row is null at the native cube door, where the ObjectQL face answers 0. A further reading in the same family: a sum / avg over a boolean field answers 500 (SQLSTATE 42883) at both doors on PostgreSQL native, because the native face also skips driver-sql's boolean-aggregand cast. The ObjectQL face and engine.aggregate answer numbers. Recommended route: (a) the hoist into @objectstack/core beside AGGREGATE_ANSWER_KIND. The boolean cast rides with it (open_questions[1]), and emptyGroupValueFor folds at the native shaping point. Expected size M; the build declares Clause-②: yes (widening), with no narrowing arm. MySQL is NOT MEASURED: there is no server binary in this container.",
    "tests": "Probe, a scratch vitest file in a detached worktree at 2821e9f, deleted after the run. Both doors are mounted for real over AnalyticsServicePlugin on a real ObjectQL engine and SqlDriver. Cube door: runtime createDispatcherPlugin, POST /api/v1/analytics/query (dispatcher-plugin.ts:1180). Dataset door: RestServer, POST /api/v1/analytics/dataset/query. The ObjectQL face was forced with the plugin's queryCapabilities, nativeSql false. The answering strategy is proven by counting engine.execute (raw SQL) against engine.aggregate calls on the object: native 1 raw / 0 aggregate at the cube door and 4 / 0 at the dataset door; ObjectQL 0 / 1 and 0 / 4. Build under the verify lock: pnpm turbo run build --filter='@objectstack/runtime^...' --concurrency=1 with NODE_OPTIONS=--max-old-space-size=3072 printed "Tasks: 29 successful, 29 total, Cached: 26 cached". Probe: pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/zz-probe-21042-scratch.test.ts printed "Test Files 1 passed (1), Tests 2 passed (2)", one cell each for SQLite and PostgreSQL. Lock verdicts: attempts 1-3 were VERDICT queue-timeout (exit 99), NOT MEASURED. Attempt 4 printed "VERDICT command-exit 0 · held the lock 43s". Direct SQL on the private PostgreSQL 16.13, over a numeric(65,30) / integer / boolean temp table: native SUM / AVG answer 0.300000000000000000000000000000, 0.150000000000000000000000000000 and 1.2222222222222222. The double operand cast(cast(x as text) as double precision) answers 0.30000000000000004, 0.15000000000000002 and 1.2222222222222223. sum(boolean) fails with "function sum(boolean) does not exist", and cast(cast(true as text) as double precision) fails with "invalid input syntax for type double precision". Cleanup: the probe file was deleted; git status --porcelain read 0 lines and git diff HEAD 0 bytes at 2821e9f; then git worktree remove without force succeeded. The private cluster was stopped with pg_ctl on its recorded pid, and its data directory was removed.",
    "mcp_calls": "0",
    "api_writes": "1 — POST /repos//issues/21042/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). Everything else was a REST GET.",
    "open_questions": [
    {
    "question": "Q1 (N3). Which route makes the native face apply #20387's accumulation, with no copy of the table, the column-class rule or the operand?",
    "options": [
    "A — Hoist into @objectstack/core, packages/core/src/utils/aggregate-answer.ts, beside AGGREGATE_ANSWER_KIND. It holds AGGREGATE_ACCUMULATION, moved with its docblock. It also holds ONE column-class predicate over the declared shape {type, multiple}: the fractional class (isFractionalNumericType, the float alias included) and the numeric-scalar-or-boolean class, both scalar only. The third piece is the double operand with the dialect as a parameter. driver-sql's two registry fills and its aggregate() call those instead of their private copies. The native compile asks declaredValueShape(object, field) and sqlDialect(object), two hooks it already holds, and wraps the column inside AGGREGATE_SQL / CONDITIONAL_AGGREGATE_SQL. It folds emptyGroupValueFor from @objectstack/spec at the shaping point. No new package edge, no spec change. Lanes: domain:engine (core, driver-sql), domain:services (service-analytics), domain:cli for the rest pin only.",
    "B — Decline: canHandle refuses a query carrying a sum over a fractional column or an avg over a numeric or boolean column, on postgres / mysql, so the ObjectQL face answers. The decline still has to READ the table and the class rule, so it needs the hoist anyway (or a copy); it only saves the operand. The empty-sum fold cannot be declined, because that would be every sum on every dialect, so it lands at the shaping point regardless. Measured cost at 2821e9f, examples/: 6 datasets, 19 aggregate measures, 11 of them (8 sum, 3 avg) over exact-decimal columns. Every dataset carries one, and so do 2 of the showcase cube's 3 measures. One statement carries every measure, so any query selecting one of them leaves native on PostgreSQL / MySQL. The ObjectQL face serves every example shape, but not every shape native serves: it refuses cross-object measures (objectql-strategy.ts:916-940, "Run this query on a native-SQL driver"), cross-object measure filters (:1026-1038) and expression metric types (resolveMeasureAggregation, :1474 onward). Those shapes would stay native and divergent, or turn into 400s. Measure-scoped filters move to the engine's in-memory lowering (sql-driver.ts:10153-10157).",
    "C — Ask the driver at runtime: a public SqlDriver.aggregateOperandSql(object, func, field, columnSql), read structurally the way dialectName is (plugin.ts:97, :1197-1207), bridged through AnalyticsServiceConfig and a strategy-context hook. The policy, the registries and the boolean cast stay in driver-sql, and nothing moves. The cost is an untyped runtime member, a hook through plugin.ts, analytics-service.ts and strategies/types.ts, and an absence path for hosts that bring their own executeRawSql. This is the shape this seat rejected as (c1) on #20987 (ruling 5923591885)."
    ],
    "recommendation": "A. 实际业务需求: the split is on the DEFAULT path. Native serves whenever executeRawSql is bridged (plugin.ts:451-506), and 11 of 19 example measures are sum / avg over exact-decimal columns. A closes every native shape, cross-object measures included, through resolveStorageTarget; B moves most dashboards off native on PostgreSQL / MySQL and still cannot close cross-object shapes. 项目长远合理性: A keeps one policy in the package both SQL faces already stand on, consistent with #20889's ruling 5922780640, the compensatedSum hoist (#20544) and this seat's #20987 ruling (json-membership-sql.ts into core, engine lane acknowledgement 5923976424). Its stated long-term cost: core carries dialect SQL text (a second such file after #20987's) and the driver's internal numeric aliases ride the predicate. C keeps the policy beside its registries but adds an untyped hook chain; B is a content-routing workaround that leaves a hidden partial gap. 防 AI 犯错: under A one predicate is called by both faces, so an edit to the policy reaches both or fails tsc (a Record over AggregationFunction). Under C a misspelled structural member reads as absent and silently restores today's arithmetic. Under B every new aggregate or column type must also be taught to canHandle. 创业阶段不扩散: A adds three or four core root exports, no spec change and no new gate. C adds one public driver member plus four files of plumbing. B is the least code with the largest behaviour change. No route warrants a new gate: recommend none."
    },
    {
    "question": "Q2 (N2, a reading the build cannot route around). PostgreSQL native sum / avg over a boolean field answers 500 (SQLSTATE 42883) at both doors, where engine.aggregate and the ObjectQL face answer numbers per #11152's ruling. The accumulation's avg class includes boolean columns, and on PostgreSQL the double operand cannot read a boolean's text. Does the PG boolean-aggregand cast (sql-driver.ts:10190-10196, #11635) ride in this build?",
    "options": [
    "A — Yes. The cast hoists with the policy (cast to int before the double operand, PostgreSQL only, for sum / avg / min / max, never the counts), and the native compile applies it. This card is the family's closing card for "the native face skips the engine's aggregate policies", so this is the third policy in the same family, with no single-point card.",
    "B — No. Boolean columns are excluded from the native accumulation, which is a second class rule, i.e. a copy, and the boolean 500 is filed separately. Without the exclusion, the hoisted operand turns the 42883 into 22P02 ("invalid input syntax for type double precision")."
    ],
    "recommendation": "A. B either restates the class rule (the copy the triage direction forbids) or emits SQL that cannot run. A is bounded: one cast in the same predicate's output, the same files and the same pins, plus a boolean row in the card's table."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/analytics/query and POST /api/v1/analytics/dataset/query on PostgreSQL 16.13, native strategy. A sum_flag / avg_flag measure over a boolean field answers 500 DATABASE_ERROR (42883, "function sum(boolean) does not exist"; statement SELECT ... SUM(flag) ... AVG(flag)). The ObjectQL face and engine.aggregate answer 1 / 0.5 for group f and 7 / 0.7777777777777778 for group i. On SQLite native the all-NULL boolean sum is null at the cube door, against 0 on the ObjectQL face. · evidence: native AGGREGATE_SQL (native-sql-strategy.ts:46-60) carries no cast; driver-sql's castBooleanAggregand does (sql-driver.ts:10190-10196). · disposition: the same family as this card (the family's closing card), so it is folded into #21042 per open_questions[1]; no single-point card. · dedupe words: analytics native sum boolean postgres 42883 · NativeSQLStrategy SUM(flag) function sum(boolean) does not exist · analytics native avg boolean 500 DATABASE_ERROR · native-sql boolean aggregand cast int",
    "carrier: 承接者:无 · noted, not filed — at the dataset door a measure-scoped avg (avg_frac_x) is ABSENT from the row, not null, for groups its supplementary query did not report (groups i and n), on BOTH strategies. The strategies agree, so it is no divergence. It belongs in the build PR's Acceptance notes."
    ],
    "phase0_readings": {
    "N1_table": {
    "base": "origin/main 2821e9f. Fixture: grp text, tag text, frac number (exact decimal), stars rating (integer column), flag boolean. Group f: frac 0.1 and 0.2, tag x. Group i: stars seven 1s and two 2s, frac 1, tag y. Group n: three rows, all aggregands NULL. Measures: count; sum / avg of frac; sum / avg of stars; count / sum / avg with filter tag = x.",
    "postgres_16_13": {
    "sum frac (f)": "engine.aggregate 0.30000000000000004 · native cube 0.3 · native dataset 0.3 · ObjectQL cube and dataset 0.30000000000000004",
    "avg frac (f)": "engine 0.15000000000000002 · native cube and dataset 0.15 · ObjectQL 0.15000000000000002",
    "avg stars, integer column (i)": "engine 1.2222222222222223 · native cube and dataset 1.222222222222222 · ObjectQL 1.2222222222222223",
    "sum frac and sum stars, all-NULL (n)": "engine 0 · native cube null · native dataset 0 (DatasetExecutor fill) · ObjectQL 0",
    "avg, all-NULL (n)": "null on every face, as ruled",
    "count control (n)": "3 on every face",
    "conditional sum, no admitted row (i)": "native cube null · native dataset 0 · ObjectQL cube and dataset 0",
    "conditional avg, no admitted row (i)": "native cube null · ObjectQL cube null · absent at the dataset door on both",
    "conditional count (i)": "0 on every face"
    },
    "sqlite": "Accumulation agrees on every face: 0.30000000000000004, 0.15000000000000002 and 1.2222222222222223. The all-NULL sum and the conditional sum are null at the native cube door and 0 on every other face, the same pattern as PostgreSQL.",
    "mysql": "NOT MEASURED, reason: no MySQL server binary in this container.",
    "dataset_door_fold": "DatasetExecutor.fillEmptyGroups (dataset-executor.ts:248-258), called at :1193-1199 over every base measure with row[column] == null, folds sum / count to 0 after any strategy. So the empty-sum row is cube-door-only, and the accumulation row shows at both doors."
    },
    "N2_what_the_native_compile_needs": {
    "policy_table": "AGGREGATE_ACCUMULATION, sql-driver.ts:1587-1597, docblock from :1537. It is module-private, so native cannot read it without a move. Home under route A: core aggregate-answer.ts beside AGGREGATE_ANSWER_KIND (:86-93).",
    "column_class": "sum reads fractionalNumericFields (sql-driver.ts:5666-5678), filled at :11623 and :11727 by isFractionalNumericType(type) && !isMultiValuedColumn. isFractionalNumericType (:366-368) is type === float || numericColumnFor(type).kind === exact: number, currency, percent, slider, progress and summary, per spec numeric-column-representation.ts:217-259; rating is integer. avg reads numericFields plus booleanFields (accumulatesInDouble, :10333-10343): NUMERIC_SCALAR_TYPES (:355-358, spec NUMERIC_VALUE_TYPES plus the integer / int / float aliases) or boolean / toggle, scalar only. The declaration is the object's fields[f].type and .multiple, on the ServiceObject the engine registry holds, which engine.syncSchemas (objectql engine.ts:17850-17860) hands to driver.syncSchema (sql-driver.ts:11074). Native reads the SAME object through declaredValueShape (strategies/types.ts:118, analytics-service.ts:1282-1286, plugin.ts:1264-1276 dataEngine().getObject(object).fields[field], engine.ts:17766). So it derives nothing a second time, provided the type-to-class rule is the one hoisted predicate. The alias half is unreachable on native: aliases come only from introspected external objects, which native declines (native-sql-strategy.ts:160-178). A dotted measure resolves its object and field through resolveStorageTarget (:1070-1093).",
    "dialect_operand": "doubleAccumulationOperand (sql-driver.ts:10352-10356) reads this.isPostgres. Native has sqlDialect(object) (types.ts:144, analytics-service.ts:1296, plugin.ts:1197-1207, answered from the driver's dialectName, sql-driver.ts:13100-13105), so the hoisted operand takes the dialect as a parameter. An undefined dialect (an embedder's own executeRawSql) keeps today's arithmetic, a residual to state.",
    "boolean_requirement": "Measured: on PostgreSQL the operand over a boolean fails (22P02) and the bare aggregate fails (42883). driver-sql casts a PG boolean aggregand to int first. See open_questions[1]."
    },
    "N3_route_files": {
    "A": "core: utils/aggregate-answer.ts and its test (index.ts:61 already has export *, so no line there). driver-sql: sql-driver.ts in four regions only: the table (:1537-1597) becomes a pointer plus import; the predicate (:362-368); the two fills (:11623, :11727); the helpers (:10325-10356). service-analytics: native-sql-strategy.ts resolveMeasureSql (:960-1040) and the execute shaping point (:494-510). Pins: service-analytics src/tests/ and rest src/analytics-*. Changeset. packages/spec/src/
    : no change.",
    "B": "service-analytics native-sql-strategy.ts canHandle (:149-280) plus a helper, the hoisted policy anyway, and pins. packages/spec/src/: no change.",
    "C": "driver-sql sql-driver.ts (a new public member); service-analytics plugin.ts, analytics-service.ts, strategies/types.ts and native-sql-strategy.ts; pins. packages/spec/src/
    : no change (structural, as dialectName)."
    },
    "N4_empty_sum_fold": "At execute (native-sql-strategy.ts:467-516, the point PR #21040 added): every measure's aggregate is known there through lookupMember(cube, member, measure) (:916-940), already read at :496-499 for the presenter. Conditional measures carry the same type: CONDITIONAL_AGGREGATE_SQL is keyed identically (:81-88, chosen at :994-996). So emptyGroupValueFor(measure.type) covers them; measured, the conditional sum is null native and 0 on ObjectQL. Expression metric types (number / string / boolean) get undefined and stay untouched. Fold only null, never undefined, before the presenter: driver order, sql-driver.ts:10321 and :10400-10409. The dataset door's fill stays and becomes idempotent for native rows.",
    "N5_clause2": "Expected for the build under route A: Clause-②: yes (widening), for the new @objectstack/core root exports (precedents: #20889 yes (widening), #20544 yes). The value change carries NO arm: it pulls the native face back to declared text. That text is emptyGroupValueFor's docblock (spec aggregation-policy.ts:40, "Counting no rows is 0 and summing them is 0") and AGGREGATE_ANSWER_KIND's one-JS-number policy (core aggregate-answer.ts:67-82). execution-duties.md:99 reads 「条款②只指已发布契约面,拉回已声明契约不触它」, and the identical value change on driver-sql's face shipped Clause-②: no (#20387 fc0db22, #15546 6d4d5d3). So it is not the claim's anticipated no (narrowing): no accept set narrows, and the boolean cast turns a 500 into the ruled answer. Changeset: core minor; driver-sql and service-analytics patch. Under B: Clause-②: no without a hoist, yes (widening) with one. Under C: yes (widening) (a public SqlDriver member), driver-sql minor. Read with scripts/pm/clause2-line.mjs readClause2Line: "yes (widening)" reads declared yes / widening; "no (widening)" reads malformed.",
    "N6_size_route_A": "core aggregate-answer.ts about +110 (about 70 of them moved) and its test about +60. driver-sql sql-driver.ts about -75 / +20, plus an optional move-proof pin of about 80 (as #20987 did). native-sql-strategy.ts about +50. service-analytics pin about +300 (SQLite and PostgreSQL cells, both strategies, the card table, the count control, conditional and boolean rows). rest dataset-door pin about +250. Changeset about +35. Total about +900 / -80, roughly three quarters tests: M. Existing emitted-SQL pins: none sets sqlDialect while asserting SUM / AVG text (0 files in service-analytics src), so no current pin flips.",
    "N7_holders": "Read at 2026-10-01T05:51Z. Open PRs: 26, each file list read. Only one touches the build's files: PR #21088 (#20986, draft) changes native-sql-strategy.ts +68/-49, analytics-service.ts +66/-26 and strategies/types.ts +18. Its native hunks include the resolveMeasureSql signature (base :961) and resolveStorageTarget (base :1060-1093), which route A's measure compile reads, so the build goes after #21088 lands or merges main. Route A touches neither analytics-service.ts nor types.ts; route C would. No open PR touches sql-driver.ts, core utils/aggregate-answer.ts, plugin.ts, dataset-executor.ts or spec aggregation-policy.ts. #21084 (#20919, draft) adds 10 lines to core/src/index.ts, and route A adds no line there. PR #20988 has merged (ceee88f) and holds nothing. In flight without a PR: #20987 (claim 5923298824) touches core/src/index.ts (+1), a new core utils/json-membership-sql.ts, sql-driver.ts JSON-membership regions only and native buildFilterClause (+19); that is merge-only. #21044 (claim 5925071499) is buildFieldMeta only and excludes AGGREGATE_SQL by name. Dispatched domain:engine cards #21007, #20914, #20821 and #19519 claim objectql files, none in A's regions."
    },
    "deviations": [
    "The worktree was created DETACHED at origin/main (no branch, no push), so the one-write budget held; it was removed afterwards and the claimed path and branch name are free for the build.",
    "The private PostgreSQL data directory lived at /tmp/pg-issue-21042, outside the scratchpad: the postgres user cannot traverse the 0700 scratchpad path. It was removed after the stop.",
    "N7 read the open-PR list twice (27 PR file lists, then 26 re-read just before this post, plus PR #21088's diff), the pm:dispatched list and 8 cards' comment threads by REST GET, plus read-only git diff --stat of the sibling worktrees for #20986, #20987 and #21044. Nothing was written in them. This is wider than the definition's no-board-reads default, as the order's N7 asks.",
    "Private evidence in the seat's scratchpad under issue-21042/: probe-out.json, n1-table.md, probe.log, build.log, lockrun1-4.log, progress.log, probe-copy.test.ts.txt."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Route ruling on phase 0 (5925588922), with the claim surface amended · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T05:53Z

    The phase-0 report is accepted. It changed no file, and its worktree and private database were removed. Its measurements, on origin/main 2821e9f15b, through the real routes:

    Q1 (the route): A, the hoist into @objectstack/core, beside AGGREGATE_ANSWER_KIND.

    Q2 (the boolean cast): A, it rides in this build.

    • The PostgreSQL boolean-aggregand cast is hoisted with the policy and applied by the native compile, for sum / avg / min / max, never the counts.
    • This card is the family's closing card ("the native face skips the engine's aggregate policies"). So this is its third position, with no single-point card. (B) would either restate the class rule (a copy) or emit SQL that cannot run.

    The empty-sum fold (N4): emptyGroupValueFor from @objectstack/spec is applied at the native shaping point PR #21040 added, for every measure, conditional ones included.

    • It folds null only, before the presenter, in driver-sql's order.
    • The dataset door's fill stays, and is idempotent for native rows.

    Clause-②: yes (widening), for the new @objectstack/core root exports. The value change carries no arm: it pulls the native face back to declared text (emptyGroupValueFor's docblock and the one-double answer policy), as the identical change on driver-sql's face shipped. The claim's anticipated no (narrowing) is withdrawn. The dev measures it; the at-tier review judges it.

    • Changeset: @objectstack/core minor; @objectstack/driver-sql and @objectstack/service-analytics patch.

    The phase-0 notes: the dataset door's measure-scoped avg is absent from a row its supplementary query did not report, on both strategies. There is no divergence: Acceptance notes for the build.

    The claim surface is amended to:

    The build waits on three conditions, recorded here:

    1. domain:engine's objection hour: the note goes on [PM seat] domain:engine — 🟢 os-project-manager #6367 in this act. The window runs from that note, or ends sooner on that lane's acknowledgement.
    2. PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088 (analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986) lands, or the build merges main after it: its hunks change resolveMeasureSql's signature and resolveStorageTarget, which this compile reads.
    3. The seat's concurrency: the maintainer has lowered it to 3 once the current work is done (「当前任务处理完,后续并发降到3」). The build is sent when a slot below that line is free.

    The card stays pm:dispatched under its claim meanwhile.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Build dispatched · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T13:07Z

    The claim 5925147145 stands, with the surface the route ruling 5925613967 amended. The build is sent now. Its three recorded conditions are met:

    1. domain:engine's objection hour passed with no objection.
    2. PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088 (analytics: on an inferred cube, a dotted path through a lookup whose name differs from its target object is never served — the door admits (and refuses) the lookup's NAME as if it were an object #20986) landed as 9b81314c.
    3. The seat's concurrency (2, per the maintainer) has a free slot.

    Selection, read fresh at this pick: the lane has no unclaimed p0 or p1 (#21079 is in the decision box; #21080 and #21154 are in flight here; seat 2 holds #21120, #21155 and #21156). Among the p2 cards, this one is the oldest bug.

    Serial constraints, re-read:

    The build also lifts #21044's named skip: PostgreSQL's native max over a boolean in cube-measure-field-type-door.test.ts (PR #21128), which this card's Q2 cast fixes.


    Generated by Claude Code

  8. added 2 commits that reference this issue on Oct 1, 2026
  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21042,
    "status": "done",
    "branch": "claude/issue-21042-native-aggregate-policies",
    "pr": "#21209",
    "session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching seat's session. The newest Claim (5925147145) names this branch and was verified before any act; no second claim posted; the card's assignee was not written. The PR assignee mirrors the card's (os-justin).",
    "premise_still_valid": true,
    "summary": "Built per the route ruling 5925613967. @objectstack/core aggregate-answer.ts now holds AGGREGATE_ACCUMULATION (moved with its docblock, byte-identical apart from export), the one column-class predicate aggregandColumnClass({type, multiple}), POSTGRES_BOOLEAN_AGGREGAND_CAST, doubleAccumulationOperand(operand, dialect) and their one composition aggregandOperandSql. driver-sql calls them in the ruled regions only, and its aggregate statements stay byte-identical: a 1668-entry dump over every function x column class x SQLite/PostgreSQL/MySQL x both registration paths had the same md5 8eee668372a28a7568f3eb1cc5a2bc9b before and after. NativeSQLStrategy's resolveMeasureSql wraps the column through aggregandOperandSql, reading the declaredValueShape and sqlDialect it already reads, and its execute shaping point folds null to emptyGroupValueFor for every measure, measure-scoped ones included, before the presenter. Measured through the cube and dataset doors on SQLite and a private PostgreSQL 16.13, the native and ObjectQL faces now differ in 0 of 144 cells. PostgreSQL native answered 0.3 / 0.15 / 1.222222222222222 and boolean sum/avg/min/max 500 before; it now answers 0.30000000000000004 / 0.15000000000000002 / 1.2222222222222223 / 7 / 0.7777777777777778 / 0 / 1. The cube-door all-NULL and measure-scoped sum went from null to 0 on both drivers. #21044's skipped PostgreSQL native boolean cell runs and passes. Draft PR #21209, Clause-②: yes (widening); changeset core minor, driver-sql and service-analytics patch.",
    "tests": "Final head ef1f9d8 (main cb45469 merged, carrying PR #21170 and PR #21173), one locked script, every exit code captured before any pipe; live PostgreSQL 16.13 at timezone Asia/Shanghai, driver-sql under TZ=America/New_York. Results: turbo build --filter=!@objectstack/docs exit 0; spec check:generated exit 0; dispatch-gates --commands derived 67 from the 10-path diff, 67/67 exit 0, and --ran printed 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.'; typecheck core / driver-sql / service-analytics / rest exit 0. Suites: core local 74 files, 2120 passed; core repo 3, 48; driver-sql 216 passed / 3 skipped files, 4300 passed / 96 skipped; service-analytics 161, 3765; rest local 256, 5027 passed / 127 skipped; rest repo 5, 179 passed / 1 skipped. Pins run explicitly: move proof 54/54, core 22/22, native-sql-aggregate-policies 49/49 (24 SQLite + 24 PG + oracle), cube-measure-field-type-door 23/23 (skip lifted), rest analytics-dataset-aggregate-policies-door 19/19 (9 + 9 + oracle). Red first at the pins commit 192fc00 (base code): core 16 red; policies red (SQLite cube-door folds; PG accumulation, boolean 500, folds); field-type PG native max(boolean) red; rest PG 7 red; move proof 54/54 green at base. Ablations, each predicted in writing first, planted by scripts/ablation-replace.mjs (anchor hit, blob changed), confirmed in dist by ablation-dist-preflight (marker present), restored by absolute path with blob == HEAD blob and git diff HEAD empty, rebuilt, marker absent, tree clean. All three matched exactly. A1 accumulation (core dialect gate): core 3, move proof 24, policies 10 PG-only, rest 3 PG-only. A2 cast: core 1, move proof 4, policies 8 PG-only, field-type 2 (native + objectql), rest 4. A3 fold (native shaping point): policies 8 (4 SQLite + 4 PG, cube door only), rest 19/19 green as predicted, because the executor fill folds the dataset door. Reverse type check: 'oracle' passed to aggregandOperandSql turned service-analytics typecheck red (TS2345 ... 'AggregandSqlDialect'), restored byte-identical. Narrowed eslint at ef1f9d8: 9 changed code files, all not ignored per ESLint.isPathIgnored; --format json 9 results, 0 errors, 0 warnings; the config has no type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is CI's. Driver-sql's four timezone non-vacuity cells failed on the first run against a UTC server, by design; green on the non-UTC rerun. MySQL NOT MEASURED, reason: no server in the container. CI on PR #21209: in_progress, not waited, per the contract.",
    "mcp_calls": "1 — mcp__github__issue_read (get_comments on #21042), read only. No write tool.",
    "api_writes": "3, each through the fleet-write relay as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectstack/pulls, pr_create draft #21209 (relay run 36894572068; read-back 17056 bytes sent, 17056 stored, identical); (2) POST /repos//issues/21209/assignees os-justin via label-write.mjs (relay run 36894665668; read-back matches); (3) POST /repos//issues/21042/comments, this os-dev-report, via post-stamped.mjs. Plus git push to the claimed branch (not REST): the empty-branch probe and every WIP, pin, fix, changeset and merge commit. Unauthenticated REST GETs: issue #21042, PR #21209.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — at the dataset door, a selection made only of measure-scoped measures reports only the groups their filter admits (measured on the native face at the base, SQLite and PostgreSQL; the ObjectQL face was not measured for that shape). It sits beside phase 0's note that a measure-scoped avg is absent, not null, from a group its supplementary query did not report. Both are in PR #21209's Acceptance notes; neither is a divergence this card covers."
    ],
    "deviations": [
    "One line outside the ruled native regions: the generateSql measure-loop call site now passes ctx to resolveMeasureSql (its signature gained ctx). Declared in the PR body.",
    "driver-sql reads the class from its registries (aggregandColumnClassOf). fractionalNumericFields is filled by the predicate; booleanFields and numericFields keep their coercion fills, whose populations equal the predicate's classes, and the move proof pins that equality per class. Asking the predicate per column through valueShapeFields would retire fractionalNumericFields, but its declaration and shard-alias regions are outside the ruled surface, so it was not done. Stated in the Acceptance notes.",
    "main was merged four times (no rebase): b9087d7, 2c1cef3, ebdb6f2, cb45469. The last one carries the same-day churn PR #21170 (canHandle) and PR #21173; the merges were clean and the readings were re-run at ef1f9d8. origin/main then moved 4 more commits (#21149, #21188, #21195, #21192), none touching core, driver-sql, service-analytics or the analytics rest tests; the one spec analytics file, ui/dataset.zod.ts, is a comment-only change. These were not merged before the PR opened.",
    "Each full gate script held the shared lock 28.9 to 35.8 minutes (four runs), and the lock flagged holder-side starvation. The runs were detached and waited on in the foreground with tail --pid.",
    "Stray files /gates.pid (6 bytes) and /progress.log (101 bytes) at the filesystem root were written at 2026-10-01T14:06Z by a redirect whose path variable lived only in the backgrounded half of an && list. Their content was copied into the scratchpad. The rm was refused by the built-in safety check and was not worked around; please remove both by hand.",
    "The first eight Bash calls got no classifier verdict, so #21042's comments were read with one MCP read call. Bash recovered on the ninth.",
    "Turbo never wrote AGENTS.md: every run ended with 0 porcelain lines. The private PostgreSQL (pid 30206, port 54921) was stopped with pg_ctl and its data dir /tmp/pg-issue-21042b removed. The worktree's node_modules were removed and the worktree removed without --force after confirming local == remote ef1f9d8.",
    "Scratchpad evidence under issue-21042b/: dump-base.json, dump-after.json, table-before.json, table-after.json, ablation-predictions.md, abl-A*.out, gates*/summary.txt, progress.log."
    ]
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: #21042 · PR #21209 at ef1f9d84 · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T16:51Z

    The dev report is 5936175525, read on GitHub together with the PR body.

    Checklist, read on GitHub rather than from the report:

    • Shape: a draft onto main, 10 files, +1309 / −139. check-governed-merges answers NOT governed. Line 1 is Fixes #21042, and line 2 is Clause-②: yes (widening). Assigned os-justin.
    • The route ruling, as built (5925613967):
      • core/src/utils/aggregate-answer.ts holds AGGREGATE_ACCUMULATION (moved with its docblock), one column-class predicate, the PostgreSQL boolean-aggregand cast and the double-accumulation operand.
      • driver-sql calls them in the ruled regions. Its aggregate statements are byte-identical: a 1668-entry dump has the same md5 before and after, and a move-proof pin passes at base and after.
      • The native compile wraps each measure through the hoisted policy. Its shaping point folds null by emptyGroupValueFor before the presenter.
    • Measured: through the cube and dataset doors, on SQLite and PostgreSQL 16.13, the native and ObjectQL faces now differ in 0 of 144 cells.
    • Pins and ablations:
      • red first at 192fc001;
      • three ablations (accumulation, cast, fold), each predicted in writing and each matched exactly, with each restore proven;
      • a reverse type check (an unsupported dialect fails tsc).
    • Gates: 67 derived, 67 exit 0, --ran 67 / 67; every touched package's projects pass. MySQL is NOT MEASURED (no server).

    Deviations, accepted:

    • The generateSql measure loop passes ctx to resolveMeasureSql: one line outside the ruled regions, declared in the PR.
    • fractionalNumericFields is kept as a registry filled by the predicate. Retiring it would touch regions outside the ruling, and the move proof pins the class equality.
    • main moved four commits after the last merge, none in these packages.

    Findings: the dataset door's measure-scoped selections answer only the groups their filters admit, beside phase 0's avg note. Both are in the PR's Acceptance notes: neither is a divergence between the faces.

    Landing waits for the at-tier contract review on ef1f9d84 and every check green on that head.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T17:37Z

    PR #21209 merged through the merge queue as 097ef802 on origin/main, and Fixes #21042 closed this card completed.

    • Review: the at-tier contract review PASS on the landed head ef1f9d84 is 5936568953.
    • Content check: the landed commit is a single-parent squash. Its git patch-id --stable equals that of the reviewed head ef1f9d84 against its base cb45469e.

    What now holds: the analytics native-SQL face applies the engine's aggregate policies through one implementation in @objectstack/core, and driver-sql emits byte-identical statements:

    • double accumulation on PostgreSQL / MySQL;
    • the PostgreSQL boolean-aggregand cast, so sum / avg / min / max over a boolean is answered instead of 500;
    • the empty-sum fold to 0 at the cube door.
      The native and ObjectQL faces agree in all 144 measured cells.

    For the release list:

    • @objectstack/core ships a minor (new root exports, Clause-②: yes (widening)); @objectstack/driver-sql and @objectstack/service-analytics ship a patch.
    • The review's note: SqlDriver's two protected accumulation helpers are replaced by one (aggregandColumnClassOf). An out-of-tree subclass that overrode them would lose that override, so the release note can say so.

    Carried elsewhere:

    In the same act, this seat removes pm:dispatched and the assignee.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions