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
analytics: '*' runs only under count, but a cube measure's or dimension's sql and a dataset measure's field admit it under any aggregate — a summed '*' answers 500 at the dataset door (split from #21000) #21409
Split from #21000 by the triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U, answering the pm:retriage question 5952467081 with B. ⛔ Not a claim.
#21000 retires three meaningless enum values. It is p3, with the full retirement kit. This is a measured defect of class (a): an authoring shape the contract admits answers a server fault at a public door.
A dataset dimension's field already refuses '*' (PR #21240), so it is the control.
Direction
'*' is admitted only where a count consumes it. Elsewhere it is refused at the authoring parse, naming the field and prescribing either count or a column. That is one cross-field rule per position, sharing one predicate. ⛔ No second spelling of the rule per position.
The refinement is not expressible in the published JSON Schema, so it is ledgered as a dropped-refinement gap, the way the gate requires.
It is a narrowing of a published accept set: a BREAKING minor, Clause-②: no (narrowing). No producer is known to send such a shape. Census the repository's examples and objectui's mirror first.
Pins
Each position refuses its non-count '*' at parse.
count over '*' is still admitted and still runs, on both strategies.
The measured door cell answers 400, not 500.
Serial
#21000 (the enum retirement) and #21365 (in flight, AnalyticsQuerySchema) edit the same analytics.zod.ts. Whichever lands later merges main.
Claim: PM loop round 1
Session: session_01UtnxvdiN376GF3sgXwAw4d
Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21409-star-count-only
Worktree: objectstack-issue-21409
Domain: domain:spec
Seat: domain:spec#1 (seat post #6017)
File surface: on origin/mainceb4a939b4. packages/spec: one shared predicate (beside src/data/analytics-column-reference.ts) and the three cross-field refinements it backs. They are MetricSchema's sql under a non-count aggregate and DimensionSchema's sql (src/data/analytics.zod.ts), and DatasetMeasureSchema's field under a non-count aggregate (src/ui/dataset.zod.ts), with their tests. Also the dropped-refinements.baseline.json gap entries the gate requires, the liveness/analytics_cube.json note re-pointed here (and liveness/dataset.json if it names the boundary), one ADR-0087 semantic entry with its generated migrations/registry.ts region, the regenerated reference pages, and one @objectstack/spec changeset. Declared cross-lane test-only surface: one door cell pinning the measured 500 → 400 (service-analytics or rest tests). ⛔ No strategy code edit. Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (default build tier; packages/spec/src/** non-test, so the contract review runs at CONTRACT_REVIEW_TIER through an isolated subagent)
Clause-②: no (narrowing) per triage's card; the dev confirms the arm with check-widening-tells and switches to yes (narrowing) only if an export-listing row is added
Thread-read: none
Serial constraints cleared: at 2026-10-02T13:55Z, PR #21413 (#20274, spec seat 2) edits dropped-refinements.baseline.json and migrations/registry.ts, and this seat's #21293 (in flight) adds a registry.ts entry. Each generated region merges through scripts/pm/os-regen-merge.sh by whichever lands later. #21000 (pm:queue) and #21365 (domain:services) also edit analytics.zod.ts in other regions; whichever lands later merges main. This seat's PR #21416 (#21082) deliberately leaves '*' unrefused in lint.
{
"issue": 21409,
"status": "done",
"branch": "claude/issue-21409-star-count-only",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21431",
"session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session's id (subagent run; Claude-Session trailer on every non-merge commit)",
"premise_still_valid": true,
"summary": "The analytics row wildcard '*' is now admitted only where a count consumes it. Cube measure MetricSchema.sql and dataset measure DatasetMeasureSchema.field refuse '*' under any aggregate but count (code custom, at sql / field) through ONE shared predicate, rowWildcardOutsideCount, plus one shared refusal builder, both in the non-public packages/spec/src/data/analytics-column-reference.ts and called from a superRefine on each measure schema. Cube dimension DimensionSchema.sql takes ANALYTICS_COLUMN_PATH (code invalid_format), the dataset dimension's own pattern. That is a flagged reading: a dimension has no aggregate, so its rule cannot be cross-field, and Zone 2 item 2 said to follow the control's spelling, which is a pattern. The measured door cell (dataset sum over '*' at POST /api/v1/analytics/dataset/query) answered 500 DATABASE_ERROR on both strategies at the base build and answers 400 VALIDATION_FAILED at measures.N.field after the fix. count over '*' still answers the row count on both strategies. Kit: dropped-refinement ledger rows (data/Metric and ui/DatasetMeasure roots plus 10 embedded sites, measured 217 / 652); one D3 entry, analytics-row-wildcard-outside-count-refused (registry regenerated, no D2, no retired-key row, no STEP18 fragment); liveness analytics_cube measures.sql / dimensions.sql re-pointed from #21000 to here, and dataset measures.field noted; ui/dataset reference regenerated; changeset minor + BREAKING + (narrowing) + registered marker. Runtime and service-analytics strategy code untouched. Census: zero producers in this repo or objectui at the pin.",
"tests": "Final union at HEAD 60644d73d9 (after the main merge): pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 gave 'Test Files 601 passed (601) / Tests 17647 passed | 1 todo'; pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/analytics-dataset-row-wildcard-door.test.ts src/analytics-16019-driver-declared-fault.test.ts gave 'Tests 42 passed (42)'. Before the merge, at 934b70a2db: rest --project local 'Test Files 256 passed (256) / Tests 4848 passed | 322 skipped'; service-analytics 'Test Files 168 passed (168) / Tests 3794 passed | 89 skipped'; spec repo-project subset of 5 files '131 passed'; spec + rest typecheck exit 0. The whole spec repo project is NOT MEASURED (one run exceeded the 570 s cap; declared to CI). Reverse reading: the door test against the BASE spec dist (ceb4a939b4, dist grep-verified free of the predicate) gave 'Tests 20 failed | 4 passed (24)'. The inline cells answered '{\"error\":\"Internal server error\",\"code\":\"DATABASE_ERROR\"}: expected 500 to be 400' for sum/avg/min/max/count_distinct over '*' on native and ObjectQL; the saved dataset with only its count selected answered 200; the 4 count controls were green. The saved-wildcard-selected cells were added later, so their base answer is NOT MEASURED. Ablation, from the committed fix through scripts/ablation-replace.mjs: rowWildcardOutsideCount made to return false (anchor x1 to x0, marker x0 to x1, blob 2bb692602dc8 to 4bdfba658269). The new spec file went '19 failed | 12 passed (31)': predicate, positions 1 and 3, and door cases red; position 2 pattern, controls, JSON-Schema and D3 pins green, as predicted. Restored via git checkout HEAD: blob == HEAD blob and git diff HEAD empty, under trap EXIT INT TERM. The spec suite imports source by relative path, so there is no dist on its resolution path. Read-path probe: computeMetadataDiagnostics on the built spec reads a stored dataset back as valid:false at measures.1.field/custom, and a stored cube at measures.total.sql/custom plus dimensions.everything.sql/invalid_format.",
"gates": "dispatch-gates --repo objectstack-ai/objectstack --commands (no paths, merge base 39a912ea7, tree 60644d73d9) derived 115 families, all run. --ran reconciliation (each line recorded with its exit code): 115 derived, 113 run green, 2 NOT MEASURED (exit 3 prerequisite), 0 UNRUN. NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: needs every package's dist (whole-repo build). NOT MEASURED: pnpm check:type-check-debt, reason: its --re-measure builds the ledgered closure itself, unlocked, and that build passed the 300 s per-gate cap (killed, exit 124, gate exit 3). check:skill-examples first exited 3 (client-react unbuilt); after building client + client-react it exited 0. pnpm --filter @objectstack/spec check:generated: all 15 artifacts green after gen:docs (only content/docs/references was stale). check-widening-tells --declaration no --diff (merge-base diff): exit 0, no tell; 2 stated silences on the imported refusal factory. ESLint, narrowed and measured: the config population **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED covers all 8 changed .ts files; --format json gave 8 files, 0 errors, 0 warnings; the config never enables type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is CI's. CI on PR #21431: in_progress (not awaited).",
"line_budget": "n/a — no skills/** or line-ratcheted ledger touched",
"files_changed": [
".changeset/21409-analytics-row-wildcard-count-only.md",
"content/docs/references/ui/dataset.mdx",
"packages/rest/src/analytics-dataset-row-wildcard-door.test.ts",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/liveness/analytics_cube.json",
"packages/spec/liveness/dataset.json",
"packages/spec/src/data/analytics-column-reference.ts",
"packages/spec/src/data/analytics-row-wildcard-count-only.test.ts",
"packages/spec/src/data/analytics.zod.ts",
"packages/spec/src/data/cube-member-sql-column-reference.test.ts",
"packages/spec/src/migrations/entries/semantic/18.analytics-row-wildcard-outside-count-refused.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/ui/dataset.zod.ts"
],
"deviations": [
"Position 2 (cube dimension sql) is spelled as the ANALYTICS_COLUMN_PATH pattern, not a cross-field refinement. Zone 1's 'one cross-field rule per position' cannot hold for a slot with no aggregate. Zone 2 item 2 (follow the control's spelling) points to the pattern, which the published JSON Schema carries, so no ledger row is needed for it. The measure positions share the one predicate.",
"Zone 2 item 2 partly falsified: the control (PR #21240) is a pattern, not a refinement.",
"No STEP18_RATIONALE fragment: optional, and it would be a hand edit to registry.ts outside the claimed generated region.",
"verifiedAt bumped to 2026-10-02 on the three liveness rows whose notes changed (evidence anchors re-read).",
"The main merge commit 60644d73d9 carries git's default merge message without the Claude-Session / Co-authored-by pair; not amended after push.",
"dropped-refinements.baseline.json conflicted on its measured counts at the main merge. Resolved by taking main's file (MERGE_HEAD blob) and re-declaring this branch's 12 sites with counts recomputed from the entries, never a text merge; gen:schema then validated the ledger against the tree.",
"A throwaway read-path probe file was placed briefly in packages/metadata-protocol so its imports resolved, then deleted; git status was verified clean before the next commit.",
"Two gates NOT MEASURED (whole-repo build prerequisites), and the whole spec repo project NOT MEASURED (subset run), all declared to CI."
],
"mcp_calls": "0",
"api_writes": "3 — all through the fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #21431, body read back identical, 14670 bytes); (2) label-write --assign os-sales → POST /repos/objectstack-ai/objectstack/issues/21431/assignees (read back: assignee os-sales, label size/xl from another actor left untouched); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/21409/comments. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:services lane (no carrier named) · noted, not filed — service-analytics inferMeasure turns a caller-named measure with an empty prefix (_sum, _avg, _min, _max, _count_distinct) into type sum etc. with sql '*' (key.slice(0, -suffix.length) || '*'), and the caller-measure gate admits inferredSql === '*'; read from code only, NOT MEASURED at a door (read-only inference), outside this card's no-strategy-edit surface · dedupe words: inferMeasure, caller-named measure, empty prefix, row wildcard, SUM(*)",
"carrier: none named · noted, not filed — a derived dataset measure's field is read by nothing (the compiler skips it), so any column value there parses inert; this card refuses only '*' there; no producer found (observation) · dedupe words: derived measure, field ignored, dataset compiler, inert key"
]
}
{
"issue": 21409,
"status": "done",
"round": "patch round 2 (contract review 5956317829 ②, plus the escalated door probe)",
"branch": "claude/issue-21409-star-count-only",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21431",
"head": "b79d1be80d2a37297f27f7c84716ca4b7001fcfc (was 60644d73d9; one commit, .changeset/21409-analytics-row-wildcard-count-only.md only, 8 insertions / 4 deletions)",
"session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session id (subagent run)",
"premise_still_valid": true,
"summary": "The changeset's dataset FROM/TO block attributed 'parsed' and the ZodError to defineDataset, which is an identity function. It now names the doors that parse: DatasetSchema.parse (custom at measures.0.field), defineStack (422 STACK_SCHEMA_INVALID at datasets.N.measures.0.field) and the dataset query route (400 VALIDATION_FAILED). FROM is a plain dataset literal. Both door outcomes for this exact literal were re-measured from source before the commit: DatasetSchema issues [[custom,[measures,0,field]]]; defineStack STACK_SCHEMA_INVALID 422 at [datasets,0,measures,0,field]. The cube half (defineCube parses) is unchanged. Every other changeset sentence was re-read, and no other defineDataset attribution exists there, in the PR body (live body grep: 0 hits) or in the D3 entry, so the PR body was not touched (no pr_update). The escalated finding A is now measured at a public door: a caller-named '_sum' / '_avg' measure answers 500 DATABASE_ERROR at POST /api/v1/analytics/query on both strategies, on an ad-hoc and on an authored cube, while the named-column controls answer 200 (table below).",
"corrected_sentences": [
{
"file": ".changeset/21409-analytics-row-wildcard-count-only.md (FROM → TO block, dataset half)",
"before": "FROM defineDataset({ name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',\n dimensions: [{ name: 'stage', field: 'stage' }],\n measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] })\n -> parsed; every query selecting `deals` answered 500 DATABASE_ERROR\nTO -> ZodError at measures.0.field (custom):\n `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …",
"after": "FROM { name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',\n dimensions: [{ name: 'stage', field: 'stage' }],\n measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] }\n -> DatasetSchema.parse accepted it; a dataset query selecting `deals`\n answered 500 DATABASE_ERROR\nTO -> DatasetSchema.parse throws a ZodError at measures.0.field (custom):\n `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …\n defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422\n STACK_SCHEMA_INVALID), and POST /api/v1/analytics/dataset/query answers\n 400 VALIDATION_FAILED for an inline or a saved copy"
}
],
"merge": "No main merge: git merge-tree --write-tree HEAD origin/main (origin/main 3bddd4a6b5) exited 0, tree 8d09b677bf, no conflict.",
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"Verdict lines: check-adr-0087-registration --base origin/main: 1 declared-breaking changeset, [BREAKING+bang+clause-②-narrowing] registered analytics-row-wildcard-outside-count-refused; check-changeset-no-major: no major bump; check-empty-changeset: no empty-frontmatter changeset introduced, no merge-base changeset modified or deleted.",
"Derived by dispatch-gates --repo objectstack-ai/objectstack --commands at b79d1be80d (no paths); the 12 lines above are every changeset-driven family it named (the ADR-0087 registration, no-major, empty-changeset, closing-keyword parity, changeset-gate self-tests, nul-bytes, objectui-changeset, changeset-deadline census), each exit captured before any pipe."
],
"probe": {
"route": "POST /api/v1/analytics/query, served by the runtime dispatcher (createDispatcherPlugin), not by @objectstack/rest. The rest package mounts only /analytics/dataset/query, so the probe sat beside the existing runtime door test packages/runtime/src/analytics-json-dimension-door.test.ts and copied its harness: a real ObjectQL engine, a better-sqlite3 SqlDriver, AnalyticsServicePlugin composition, read counters. No new harness.",
"file": "packages/runtime/src/zz-probe-21409-inferred-wildcard.test.ts (throwaway; deleted, never staged or committed)",
"fixture": "object probe_wildcard_ledger {category text, amount number}, 3 rows, amounts 100/400/900; authored cube probe_wildcard_cube {measures: count over '*', amount_total sum over amount; dimension category}",
"table_columns": "strategy | cell | request body | status | reads | response body (verbatim)",
"rows": [
"native | adhoc _sum | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_sum\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"native | adhoc _avg | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_avg\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"native | adhoc amount_sum (control) | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"amount_sum\"]} | status 200 | rawSql+1 aggregate+0 | {\"success\":true,\"data\":{\"rows\":[{\"amount_sum\":1400}],\"fields\":[{\"name\":\"amount_sum\",\"type\":\"number\"}]}}",
"native | authored _sum | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"_sum\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"native | authored amount_total (control) | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"amount_total\"]} | status 200 | rawSql+1 aggregate+0 | {\"success\":true,\"data\":{\"rows\":[{\"amount_total\":1400}],\"fields\":[{\"name\":\"amount_total\",\"type\":\"number\"}]}}",
"objectql | adhoc _sum | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_sum\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"objectql | adhoc _avg | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_avg\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"objectql | adhoc amount_sum (control) | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"amount_sum\"]} | status 200 | rawSql+0 aggregate+1 | {\"success\":true,\"data\":{\"rows\":[{\"amount_sum\":1400}],\"fields\":[{\"name\":\"amount_sum\",\"type\":\"number\"}]}}",
"objectql | authored _sum | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"_sum\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}",
"objectql | authored amount_total (control) | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"amount_total\"]} | status 200 | rawSql+0 aggregate+1 | {\"success\":true,\"data\":{\"rows\":[{\"amount_total\":1400}],\"fields\":[{\"name\":\"amount_total\",\"type\":\"number\"}]}}"
],
"reading": "Every caller-named '_sum' / '_avg' cell answered 500 DATABASE_ERROR after a statement reached the engine (rawSql+1 on native SQL, aggregate+1 on ObjectQL): the minted SUM(*) / AVG(*) reached the database. That holds on an ad-hoc cube (cube = the object name) and on an authored cube that does not declare the member. Both named-column controls answered 200 with the true sum, 1400. This is the one measured wrong answer at a public door the seat asked for.",
"clean_status_proof": "After deleting the probe: git status --porcelain printed 0 lines; HEAD b79d1be80d2a37297f27f7c84716ca4b7001fcfc equals origin/claude/issue-21409-star-count-only (git ls-remote), and HEAD's only file is the changeset. Order, stated: the changeset commit was made and pushed BEFORE the probe was written, so the probe never existed at any commit; the clean status was proven after its deletion, with no commit since."
},
"tests": "Changeset-only round: no code or test touched. Probe run: pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/zz-probe-21409-inferred-wildcard.test.ts gave Tests 10 passed (10). These are observation cells with no assertions on the status; the readings are the PROBE lines. The runtime dependency closure was rebuilt by turbo (29/29 cached); the spec dist was grep-verified to carry the predicate.",
"deviations": [
"The probe lives beside the runtime door test, not the rest one: POST /api/v1/analytics/query is the runtime dispatcher route, and @objectstack/rest does not mount it.",
"The changeset commit came before the probe rather than after; the probe was never committed, and the clean status is proven after its deletion."
],
"mcp_calls": "0",
"api_writes": "1 this round — this os-dev-report comment, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/21409/comments as objectstack-fleet[bot]). No pr_update: the PR body had no defineDataset attribution. git push is not a REST write. Reads only otherwise: the review comment and the PR body, by REST GET.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/analytics/query answers 500 DATABASE_ERROR to a caller-named measure '_sum' / '_avg' on both strategies, on an ad-hoc and on an authored cube; the named-column controls answer 200 (probe table above) · evidence: service-analytics inferMeasure mints { type: 'sum', sql: '*' } via key.slice(0, -suffix.length) || '*'; assertCallerMembersResolvable admits inferredSql === '*'; the strategies emit the operand verbatim · dedupe words: inferMeasure, caller-named measure, empty prefix, row wildcard, SUM(*) 500",
"carrier: seat's filing decision (the review escalated it as a candidate spec card) · noted, not filed — a derived dataset measure's field (and aggregate) is read by nothing (dataset-compiler continues before m.field), so any value parses inert · dedupe words: derived measure, field ignored, aggregate ignored, dataset compiler"
]
}
{
"issue": 21409,
"status": "done",
"round": "patch round 3 (re-review 5956873511 ②: one changeset sentence; plus the owed regen merge of main)",
"branch": "claude/issue-21409-star-count-only",
"pr": "https://github.com/objectstack-ai/objectstack/pull/21431",
"head": "30437505b8b1cbcff045b29fcc3d6eaccdc9e64c",
"commits_since_b79d1be80d": [
"fce0c9a042 chore(changeset): os migrate meta lists the entry as a manual change; it rewrites nothing — .changeset/21409-analytics-row-wildcard-count-only.md only, +3 / -1",
"30437505b8 Merge remote-tracking branch 'origin/main' into claude/issue-21409-star-count-only — origin/main 3a6d92f78b through scripts/pm/os-regen-merge.sh; pure merge, no regeneration commit needed (step 4 regenerated byte-identically)"
],
"session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session id (subagent run)",
"premise_still_valid": true,
"summary": "The one-line-fix paragraph's last sentence claimed os migrate meta lists nothing for this narrowing. It lists the D3 entry as a manual change (chain.ts maps every step.semantic entry to a MigrationTodo; meta.ts prints them under 'manual change(s) require your judgment'), and only rewrites nothing. Respelled in its own commit. The D3 entry ('so the sweep is mechanical — parse each document') and the PR body ('has no mechanical rewrite') make no such tool claim and are true, so neither was changed and there was no pr_update. Then origin/main was merged through os-regen-merge.sh: no conflict; registry.ts auto-merged and gen:migration-registry reproduced it byte-identically; check:generated passed 15/15 on the rebuilt spec; every changeset family exited 0.",
"corrected_sentences": [
{
"file": ".changeset/21409-analytics-row-wildcard-count-only.md (the one-line-fix paragraph, last sentence)",
"before": "There is no mechanical rewrite, so `os migrate meta` lists nothing for it.",
"after": "There is no mechanical rewrite: `os migrate meta` rewrites nothing for it, and\nlists the entry `analytics-row-wildcard-outside-count-refused` as a manual\nchange that requires your judgment."
}
],
"merge_readings": {
"tool": "bash scripts/pm/os-regen-merge.sh: exit 0. Step 1 merged origin/main 3a6d92f78b at merge base 39a912ea73, 45 files, with no conflict; packages/spec/src/migrations/registry.ts auto-merged. Step 2 took main's side of the generated artifacts main moved, and kept the branch's content/docs/references/ui/dataset.mdx (branch-only change). Step 3: the merge left nothing further to commit.",
"registry_byte_identity": "merged blob a49da25ca28fe75334d02ee7243cd82f6e8b31b4 == HEAD blob a49da25ca28f…; after pnpm --filter @objectstack/spec gen:migration-registry the blob is still a49da25ca28f… and git diff HEAD on registry.ts is 0 lines. Byte-identical, so no regeneration commit.",
"semantic_entry_counts": "before the merge (fce0c9a042, gen:migration-registry): 347 semantic, 244 retired-key, 212 retired-def, entries/semantic 347 files. After the merge (30437505b8): 348 semantic, 244 retired-key, 212 retired-def, entries/semantic 348 files. The +1 is main's dashboard-widget-single-series-multi-measure-refused (#21293 via 32d5769080 / #21425): present once at HEAD and once on origin/main. This branch's analytics-row-wildcard-outside-count-refused is present once at HEAD; #21413's agent-memory-store-retired-and-limits-required is still present.",
"rebuild": "pnpm --filter @objectstack/spec build (gen:schema validated dropped-refinements.baseline.json against the merged tree; main did not touch the ledger) then check:generated, both under the verify lock: VERDICT command-exit 0, all 15 artifacts green (check:migration-registry, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:api-surface, check:docs, check:liveness among them)."
},
"gates": [
"pnpm --filter @objectstack/spec check:migration-registry :: exit 0",
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated :: exit 0 (15/15 artifacts green)",
"Verdict lines: check:migration-registry: 'src/migrations/registry.ts is current (348 semantic, 244 retired-key, 212 retired-def)'; check-adr-0087-registration --base origin/main: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.'",
"The changeset families are every one dispatch-gates --repo objectstack-ai/objectstack --commands named at 30437505b8 (no paths); each exit was captured before any pipe."
],
"tests": "Spec pins on the merged tree: vitest --project local on analytics-row-wildcard-count-only, cube-member-sql-column-reference and dataset-field-column-reference gave Test Files 3 passed (3), Tests 57 passed (57). The rest door test was not re-run locally on the merged tree; main's merge brought native-sql-strategy changes (#21376), so CI Test Core measures it. The Test Core (4/6) red on b79d1be80d is the known #21428 flake (import-template-route.test.ts timing out at 5000 ms), recorded by the coordinator in 5956763546; not re-run or touched here.",
"deviations": [
"The merge commit 30437505b8 again carries git's default merge message without the trailer pair. It was not amended, to avoid re-entering the pre-commit os-regen deferral on a merge commit; the repository squash-merges with the PR body's message."
],
"mcp_calls": "0",
"api_writes": "1 this round — this os-dev-report comment through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/21409/comments as objectstack-fleet[bot]). No pr_update: the PR body carries no os-migrate-meta claim. git push (once) is not a REST write. Reads: the re-review comment and the PR body, by REST GET.",
"open_questions": [],
"out_of_scope_findings": []
}
domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5953981594 · 2026-10-02T17:19Z
Shape (read on GitHub): a draft against main. The first line is Fixes #21409, then Clause-②: no (narrowing). PR assignee os-sales. The branch's delta against main is 13 files, +986 / -59:
the shared predicate and refusal in packages/spec/src/data/analytics-column-reference.ts;
the three positions in analytics.zod.ts and ui/dataset.zod.ts, with their tests;
the dropped-refinements.baseline.json rows and the two liveness notes;
one ADR-0087 D3 entry with its generated migrations/registry.ts region, and the regenerated ui/dataset.mdx;
the packages/rest door test, and one @objectstack/spec changeset.
No governed path.
Review history (all at CONTRACT_REVIEW_TIER, isolated):
PASS 5956317829 on 60644d73d9. Each position's accept-set delta is exactly '*' outside count. The cube dimension's pattern (ANALYTICS_COLUMN_PATH) is the right form: it is the card's own control's spelling, and it reaches the published JSON Schema. No export is added, the ledger rows match the sites, and the D3 entry is right. Its ② found one changeset sentence untrue: defineDataset parses nothing.
Patch round 2 → narrow PASS 5956873511 on b79d1be80d. The dataset FROM → TO block now names the doors that parse (DatasetSchema.parse, defineStack 422, the dataset query route 400), each judged true. Its ② found one more sentence untrue: os migrate meta does list the D3 entry.
Prose (the seat's check): each changeset sentence was judged true by the records above, the two last ones after correction. The two hand-written pages the drift bot names stay true. content/docs/data-modeling/analytics.mdx says a dataset measure's field "may also be '*' (or be omitted) for a count". content/docs/api/data-api.mdx says an inferred measure's prefix is the object's own field name.
Card: closed completed when the PR merged, by its Fixes line. This act removes pm:dispatched and the assignee.
Review: PASS 5956317829 → narrow PASS 5956873511 → narrow PASS 5957579031, over two changeset corrections and a regen merge; ACCEPT 5957602318.
What changes: the row wildcard '*' is admitted only where a count consumes it, at three positions:
a cube measure's sql and a dataset measure's field, through one shared predicate, as a custom issue under any other aggregate;
a cube dimension's sql, which now takes ANALYTICS_COLUMN_PATH.
A summed '*' at the dataset door answers 400 VALIDATION_FAILED where it answered 500 DATABASE_ERROR. This is a BREAKING minor under ADR-0087 entry analytics-row-wildcard-outside-count-refused.
Activity
objectstack-fleet commentedon Oct 2, 2026
Claim: PM loop round 1
Session:
session_01UtnxvdiN376GF3sgXwAw4dAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21409-star-count-onlyWorktree:
objectstack-issue-21409Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface: on
origin/mainceb4a939b4.packages/spec: one shared predicate (besidesrc/data/analytics-column-reference.ts) and the three cross-field refinements it backs. They areMetricSchema'ssqlunder a non-countaggregate andDimensionSchema'ssql(src/data/analytics.zod.ts), andDatasetMeasureSchema'sfieldunder a non-countaggregate (src/ui/dataset.zod.ts), with their tests. Also thedropped-refinements.baseline.jsongap entries the gate requires, theliveness/analytics_cube.jsonnote re-pointed here (andliveness/dataset.jsonif it names the boundary), one ADR-0087 semantic entry with its generatedmigrations/registry.tsregion, the regenerated reference pages, and one@objectstack/specchangeset. Declared cross-lane test-only surface: one door cell pinning the measured 500 → 400 (service-analyticsorresttests). ⛔ No strategy code edit. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(default build tier;packages/spec/src/**non-test, so the contract review runs atCONTRACT_REVIEW_TIERthrough an isolated subagent)Clause-②: no (narrowing) per triage's card; the dev confirms the arm with
check-widening-tellsand switches to yes (narrowing) only if an export-listing row is addedThread-read: none
Serial constraints cleared: at 2026-10-02T13:55Z, PR #21413 (#20274, spec seat 2) edits
dropped-refinements.baseline.jsonandmigrations/registry.ts, and this seat's #21293 (in flight) adds aregistry.tsentry. Each generated region merges throughscripts/pm/os-regen-merge.shby whichever lands later. #21000 (pm:queue) and #21365 (domain:services) also editanalytics.zod.tsin other regions; whichever lands later mergesmain. This seat's PR #21416 (#21082) deliberately leaves'*'unrefused in lint.objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{ "issue": 21409, "status": "done", "branch": "claude/issue-21409-star-count-only", "pr": "https://github.com/objectstack-ai/objectstack/pull/21431", "session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session's id (subagent run; Claude-Session trailer on every non-merge commit)", "premise_still_valid": true, "summary": "The analytics row wildcard '*' is now admitted only where a count consumes it. Cube measure MetricSchema.sql and dataset measure DatasetMeasureSchema.field refuse '*' under any aggregate but count (code custom, at sql / field) through ONE shared predicate, rowWildcardOutsideCount, plus one shared refusal builder, both in the non-public packages/spec/src/data/analytics-column-reference.ts and called from a superRefine on each measure schema. Cube dimension DimensionSchema.sql takes ANALYTICS_COLUMN_PATH (code invalid_format), the dataset dimension's own pattern. That is a flagged reading: a dimension has no aggregate, so its rule cannot be cross-field, and Zone 2 item 2 said to follow the control's spelling, which is a pattern. The measured door cell (dataset sum over '*' at POST /api/v1/analytics/dataset/query) answered 500 DATABASE_ERROR on both strategies at the base build and answers 400 VALIDATION_FAILED at measures.N.field after the fix. count over '*' still answers the row count on both strategies. Kit: dropped-refinement ledger rows (data/Metric and ui/DatasetMeasure roots plus 10 embedded sites, measured 217 / 652); one D3 entry, analytics-row-wildcard-outside-count-refused (registry regenerated, no D2, no retired-key row, no STEP18 fragment); liveness analytics_cube measures.sql / dimensions.sql re-pointed from #21000 to here, and dataset measures.field noted; ui/dataset reference regenerated; changeset minor + BREAKING + (narrowing) + registered marker. Runtime and service-analytics strategy code untouched. Census: zero producers in this repo or objectui at the pin.", "tests": "Final union at HEAD 60644d73d9 (after the main merge): pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 gave 'Test Files 601 passed (601) / Tests 17647 passed | 1 todo'; pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/analytics-dataset-row-wildcard-door.test.ts src/analytics-16019-driver-declared-fault.test.ts gave 'Tests 42 passed (42)'. Before the merge, at 934b70a2db: rest --project local 'Test Files 256 passed (256) / Tests 4848 passed | 322 skipped'; service-analytics 'Test Files 168 passed (168) / Tests 3794 passed | 89 skipped'; spec repo-project subset of 5 files '131 passed'; spec + rest typecheck exit 0. The whole spec repo project is NOT MEASURED (one run exceeded the 570 s cap; declared to CI). Reverse reading: the door test against the BASE spec dist (ceb4a939b4, dist grep-verified free of the predicate) gave 'Tests 20 failed | 4 passed (24)'. The inline cells answered '{\"error\":\"Internal server error\",\"code\":\"DATABASE_ERROR\"}: expected 500 to be 400' for sum/avg/min/max/count_distinct over '*' on native and ObjectQL; the saved dataset with only its count selected answered 200; the 4 count controls were green. The saved-wildcard-selected cells were added later, so their base answer is NOT MEASURED. Ablation, from the committed fix through scripts/ablation-replace.mjs: rowWildcardOutsideCount made to return false (anchor x1 to x0, marker x0 to x1, blob 2bb692602dc8 to 4bdfba658269). The new spec file went '19 failed | 12 passed (31)': predicate, positions 1 and 3, and door cases red; position 2 pattern, controls, JSON-Schema and D3 pins green, as predicted. Restored via git checkout HEAD: blob == HEAD blob and git diff HEAD empty, under trap EXIT INT TERM. The spec suite imports source by relative path, so there is no dist on its resolution path. Read-path probe: computeMetadataDiagnostics on the built spec reads a stored dataset back as valid:false at measures.1.field/custom, and a stored cube at measures.total.sql/custom plus dimensions.everything.sql/invalid_format.", "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands (no paths, merge base 39a912ea7, tree 60644d73d9) derived 115 families, all run. --ran reconciliation (each line recorded with its exit code): 115 derived, 113 run green, 2 NOT MEASURED (exit 3 prerequisite), 0 UNRUN. NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: needs every package's dist (whole-repo build). NOT MEASURED: pnpm check:type-check-debt, reason: its --re-measure builds the ledgered closure itself, unlocked, and that build passed the 300 s per-gate cap (killed, exit 124, gate exit 3). check:skill-examples first exited 3 (client-react unbuilt); after building client + client-react it exited 0. pnpm --filter @objectstack/spec check:generated: all 15 artifacts green after gen:docs (only content/docs/references was stale). check-widening-tells --declaration no --diff (merge-base diff): exit 0, no tell; 2 stated silences on the imported refusal factory. ESLint, narrowed and measured: the config population **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED covers all 8 changed .ts files; --format json gave 8 files, 0 errors, 0 warnings; the config never enables type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is CI's. CI on PR #21431: in_progress (not awaited).", "line_budget": "n/a — no skills/** or line-ratcheted ledger touched", "files_changed": [ ".changeset/21409-analytics-row-wildcard-count-only.md", "content/docs/references/ui/dataset.mdx", "packages/rest/src/analytics-dataset-row-wildcard-door.test.ts", "packages/spec/dropped-refinements.baseline.json", "packages/spec/liveness/analytics_cube.json", "packages/spec/liveness/dataset.json", "packages/spec/src/data/analytics-column-reference.ts", "packages/spec/src/data/analytics-row-wildcard-count-only.test.ts", "packages/spec/src/data/analytics.zod.ts", "packages/spec/src/data/cube-member-sql-column-reference.test.ts", "packages/spec/src/migrations/entries/semantic/18.analytics-row-wildcard-outside-count-refused.ts", "packages/spec/src/migrations/registry.ts", "packages/spec/src/ui/dataset.zod.ts" ], "deviations": [ "Position 2 (cube dimension sql) is spelled as the ANALYTICS_COLUMN_PATH pattern, not a cross-field refinement. Zone 1's 'one cross-field rule per position' cannot hold for a slot with no aggregate. Zone 2 item 2 (follow the control's spelling) points to the pattern, which the published JSON Schema carries, so no ledger row is needed for it. The measure positions share the one predicate.", "Zone 2 item 2 partly falsified: the control (PR #21240) is a pattern, not a refinement.", "No STEP18_RATIONALE fragment: optional, and it would be a hand edit to registry.ts outside the claimed generated region.", "verifiedAt bumped to 2026-10-02 on the three liveness rows whose notes changed (evidence anchors re-read).", "The main merge commit 60644d73d9 carries git's default merge message without the Claude-Session / Co-authored-by pair; not amended after push.", "dropped-refinements.baseline.json conflicted on its measured counts at the main merge. Resolved by taking main's file (MERGE_HEAD blob) and re-declaring this branch's 12 sites with counts recomputed from the entries, never a text merge; gen:schema then validated the ledger against the tree.", "A throwaway read-path probe file was placed briefly in packages/metadata-protocol so its imports resolved, then deleted; git status was verified clean before the next commit.", "Two gates NOT MEASURED (whole-repo build prerequisites), and the whole spec repo project NOT MEASURED (subset run), all declared to CI." ], "mcp_calls": "0", "api_writes": "3 — all through the fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #21431, body read back identical, 14670 bytes); (2) label-write --assign os-sales → POST /repos/objectstack-ai/objectstack/issues/21431/assignees (read back: assignee os-sales, label size/xl from another actor left untouched); (3) this os-dev-report comment → POST /repos/objectstack-ai/objectstack/issues/21409/comments. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: domain:services lane (no carrier named) · noted, not filed — service-analytics inferMeasure turns a caller-named measure with an empty prefix (_sum, _avg, _min, _max, _count_distinct) into type sum etc. with sql '*' (key.slice(0, -suffix.length) || '*'), and the caller-measure gate admits inferredSql === '*'; read from code only, NOT MEASURED at a door (read-only inference), outside this card's no-strategy-edit surface · dedupe words: inferMeasure, caller-named measure, empty prefix, row wildcard, SUM(*)", "carrier: none named · noted, not filed — a derived dataset measure's field is read by nothing (the compiler skips it), so any column value there parses inert; this card refuses only '*' there; no producer found (observation) · dedupe words: derived measure, field ignored, dataset compiler, inert key" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{ "issue": 21409, "status": "done", "round": "patch round 2 (contract review 5956317829 ②, plus the escalated door probe)", "branch": "claude/issue-21409-star-count-only", "pr": "https://github.com/objectstack-ai/objectstack/pull/21431", "head": "b79d1be80d2a37297f27f7c84716ca4b7001fcfc (was 60644d73d9; one commit, .changeset/21409-analytics-row-wildcard-count-only.md only, 8 insertions / 4 deletions)", "session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session id (subagent run)", "premise_still_valid": true, "summary": "The changeset's dataset FROM/TO block attributed 'parsed' and the ZodError to defineDataset, which is an identity function. It now names the doors that parse: DatasetSchema.parse (custom at measures.0.field), defineStack (422 STACK_SCHEMA_INVALID at datasets.N.measures.0.field) and the dataset query route (400 VALIDATION_FAILED). FROM is a plain dataset literal. Both door outcomes for this exact literal were re-measured from source before the commit: DatasetSchema issues [[custom,[measures,0,field]]]; defineStack STACK_SCHEMA_INVALID 422 at [datasets,0,measures,0,field]. The cube half (defineCube parses) is unchanged. Every other changeset sentence was re-read, and no other defineDataset attribution exists there, in the PR body (live body grep: 0 hits) or in the D3 entry, so the PR body was not touched (no pr_update). The escalated finding A is now measured at a public door: a caller-named '_sum' / '_avg' measure answers 500 DATABASE_ERROR at POST /api/v1/analytics/query on both strategies, on an ad-hoc and on an authored cube, while the named-column controls answer 200 (table below).", "corrected_sentences": [ { "file": ".changeset/21409-analytics-row-wildcard-count-only.md (FROM → TO block, dataset half)", "before": "FROM defineDataset({ name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',\n dimensions: [{ name: 'stage', field: 'stage' }],\n measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] })\n -> parsed; every query selecting `deals` answered 500 DATABASE_ERROR\nTO -> ZodError at measures.0.field (custom):\n `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …", "after": "FROM { name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',\n dimensions: [{ name: 'stage', field: 'stage' }],\n measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] }\n -> DatasetSchema.parse accepted it; a dataset query selecting `deals`\n answered 500 DATABASE_ERROR\nTO -> DatasetSchema.parse throws a ZodError at measures.0.field (custom):\n `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …\n defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422\n STACK_SCHEMA_INVALID), and POST /api/v1/analytics/dataset/query answers\n 400 VALIDATION_FAILED for an inline or a saved copy" } ], "merge": "No main merge: git merge-tree --write-tree HEAD origin/main (origin/main 3bddd4a6b5) exited 0, tree 8d09b677bf, no conflict.", "gates": [ "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0", "node scripts/check-closing-keyword-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0", "node scripts/check-empty-changeset.mjs --self-test :: exit 0", "pnpm check:changeset-gate-self-tests :: exit 0", "pnpm check:nul-bytes :: exit 0", "pnpm check:objectui-changeset :: exit 0", "pnpm check:pm-changeset-deadline-census :: exit 0", "Verdict lines: check-adr-0087-registration --base origin/main: 1 declared-breaking changeset, [BREAKING+bang+clause-②-narrowing] registered analytics-row-wildcard-outside-count-refused; check-changeset-no-major: no major bump; check-empty-changeset: no empty-frontmatter changeset introduced, no merge-base changeset modified or deleted.", "Derived by dispatch-gates --repo objectstack-ai/objectstack --commands at b79d1be80d (no paths); the 12 lines above are every changeset-driven family it named (the ADR-0087 registration, no-major, empty-changeset, closing-keyword parity, changeset-gate self-tests, nul-bytes, objectui-changeset, changeset-deadline census), each exit captured before any pipe." ], "probe": { "route": "POST /api/v1/analytics/query, served by the runtime dispatcher (createDispatcherPlugin), not by @objectstack/rest. The rest package mounts only /analytics/dataset/query, so the probe sat beside the existing runtime door test packages/runtime/src/analytics-json-dimension-door.test.ts and copied its harness: a real ObjectQL engine, a better-sqlite3 SqlDriver, AnalyticsServicePlugin composition, read counters. No new harness.", "file": "packages/runtime/src/zz-probe-21409-inferred-wildcard.test.ts (throwaway; deleted, never staged or committed)", "fixture": "object probe_wildcard_ledger {category text, amount number}, 3 rows, amounts 100/400/900; authored cube probe_wildcard_cube {measures: count over '*', amount_total sum over amount; dimension category}", "table_columns": "strategy | cell | request body | status | reads | response body (verbatim)", "rows": [ "native | adhoc _sum | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_sum\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "native | adhoc _avg | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_avg\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "native | adhoc amount_sum (control) | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"amount_sum\"]} | status 200 | rawSql+1 aggregate+0 | {\"success\":true,\"data\":{\"rows\":[{\"amount_sum\":1400}],\"fields\":[{\"name\":\"amount_sum\",\"type\":\"number\"}]}}", "native | authored _sum | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"_sum\"]} | status 500 | rawSql+1 aggregate+0 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "native | authored amount_total (control) | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"amount_total\"]} | status 200 | rawSql+1 aggregate+0 | {\"success\":true,\"data\":{\"rows\":[{\"amount_total\":1400}],\"fields\":[{\"name\":\"amount_total\",\"type\":\"number\"}]}}", "objectql | adhoc _sum | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_sum\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "objectql | adhoc _avg | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"_avg\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "objectql | adhoc amount_sum (control) | {\"cube\":\"probe_wildcard_ledger\",\"measures\":[\"amount_sum\"]} | status 200 | rawSql+0 aggregate+1 | {\"success\":true,\"data\":{\"rows\":[{\"amount_sum\":1400}],\"fields\":[{\"name\":\"amount_sum\",\"type\":\"number\"}]}}", "objectql | authored _sum | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"_sum\"]} | status 500 | rawSql+0 aggregate+1 | {\"success\":false,\"error\":{\"code\":\"DATABASE_ERROR\",\"message\":\"Internal server error\",\"httpStatus\":500}}", "objectql | authored amount_total (control) | {\"cube\":\"probe_wildcard_cube\",\"measures\":[\"amount_total\"]} | status 200 | rawSql+0 aggregate+1 | {\"success\":true,\"data\":{\"rows\":[{\"amount_total\":1400}],\"fields\":[{\"name\":\"amount_total\",\"type\":\"number\"}]}}" ], "reading": "Every caller-named '_sum' / '_avg' cell answered 500 DATABASE_ERROR after a statement reached the engine (rawSql+1 on native SQL, aggregate+1 on ObjectQL): the minted SUM(*) / AVG(*) reached the database. That holds on an ad-hoc cube (cube = the object name) and on an authored cube that does not declare the member. Both named-column controls answered 200 with the true sum, 1400. This is the one measured wrong answer at a public door the seat asked for.", "clean_status_proof": "After deleting the probe: git status --porcelain printed 0 lines; HEAD b79d1be80d2a37297f27f7c84716ca4b7001fcfc equals origin/claude/issue-21409-star-count-only (git ls-remote), and HEAD's only file is the changeset. Order, stated: the changeset commit was made and pushed BEFORE the probe was written, so the probe never existed at any commit; the clean status was proven after its deletion, with no commit since." }, "tests": "Changeset-only round: no code or test touched. Probe run: pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/zz-probe-21409-inferred-wildcard.test.ts gave Tests 10 passed (10). These are observation cells with no assertions on the status; the readings are the PROBE lines. The runtime dependency closure was rebuilt by turbo (29/29 cached); the spec dist was grep-verified to carry the predicate.", "deviations": [ "The probe lives beside the runtime door test, not the rest one: POST /api/v1/analytics/query is the runtime dispatcher route, and @objectstack/rest does not mount it.", "The changeset commit came before the probe rather than after; the probe was never committed, and the clean status is proven after its deletion." ], "mcp_calls": "0", "api_writes": "1 this round — this os-dev-report comment, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/21409/comments as objectstack-fleet[bot]). No pr_update: the PR body had no defineDataset attribution. git push is not a REST write. Reads only otherwise: the review comment and the PR body, by REST GET.", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: POST /api/v1/analytics/query answers 500 DATABASE_ERROR to a caller-named measure '_sum' / '_avg' on both strategies, on an ad-hoc and on an authored cube; the named-column controls answer 200 (probe table above) · evidence: service-analytics inferMeasure mints { type: 'sum', sql: '*' } via key.slice(0, -suffix.length) || '*'; assertCallerMembersResolvable admits inferredSql === '*'; the strategies emit the operand verbatim · dedupe words: inferMeasure, caller-named measure, empty prefix, row wildcard, SUM(*) 500", "carrier: seat's filing decision (the review escalated it as a candidate spec card) · noted, not filed — a derived dataset measure's field (and aggregate) is read by nothing (dataset-compiler continues before m.field), so any value parses inert · dedupe words: derived measure, field ignored, aggregate ignored, dataset compiler" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{ "issue": 21409, "status": "done", "round": "patch round 3 (re-review 5956873511 ②: one changeset sentence; plus the owed regen merge of main)", "branch": "claude/issue-21409-star-count-only", "pr": "https://github.com/objectstack-ai/objectstack/pull/21431", "head": "30437505b8b1cbcff045b29fcc3d6eaccdc9e64c", "commits_since_b79d1be80d": [ "fce0c9a042 chore(changeset): os migrate meta lists the entry as a manual change; it rewrites nothing — .changeset/21409-analytics-row-wildcard-count-only.md only, +3 / -1", "30437505b8 Merge remote-tracking branch 'origin/main' into claude/issue-21409-star-count-only — origin/main 3a6d92f78b through scripts/pm/os-regen-merge.sh; pure merge, no regeneration commit needed (step 4 regenerated byte-identically)" ], "session": "session_01UtnxvdiN376GF3sgXwAw4d — the dispatching PM session id (subagent run)", "premise_still_valid": true, "summary": "The one-line-fix paragraph's last sentence claimed os migrate meta lists nothing for this narrowing. It lists the D3 entry as a manual change (chain.ts maps every step.semantic entry to a MigrationTodo; meta.ts prints them under 'manual change(s) require your judgment'), and only rewrites nothing. Respelled in its own commit. The D3 entry ('so the sweep is mechanical — parse each document') and the PR body ('has no mechanical rewrite') make no such tool claim and are true, so neither was changed and there was no pr_update. Then origin/main was merged through os-regen-merge.sh: no conflict; registry.ts auto-merged and gen:migration-registry reproduced it byte-identically; check:generated passed 15/15 on the rebuilt spec; every changeset family exited 0.", "corrected_sentences": [ { "file": ".changeset/21409-analytics-row-wildcard-count-only.md (the one-line-fix paragraph, last sentence)", "before": "There is no mechanical rewrite, so `os migrate meta` lists nothing for it.", "after": "There is no mechanical rewrite: `os migrate meta` rewrites nothing for it, and\nlists the entry `analytics-row-wildcard-outside-count-refused` as a manual\nchange that requires your judgment." } ], "merge_readings": { "tool": "bash scripts/pm/os-regen-merge.sh: exit 0. Step 1 merged origin/main 3a6d92f78b at merge base 39a912ea73, 45 files, with no conflict; packages/spec/src/migrations/registry.ts auto-merged. Step 2 took main's side of the generated artifacts main moved, and kept the branch's content/docs/references/ui/dataset.mdx (branch-only change). Step 3: the merge left nothing further to commit.", "registry_byte_identity": "merged blob a49da25ca28fe75334d02ee7243cd82f6e8b31b4 == HEAD blob a49da25ca28f…; after pnpm --filter @objectstack/spec gen:migration-registry the blob is still a49da25ca28f… and git diff HEAD on registry.ts is 0 lines. Byte-identical, so no regeneration commit.", "semantic_entry_counts": "before the merge (fce0c9a042, gen:migration-registry): 347 semantic, 244 retired-key, 212 retired-def, entries/semantic 347 files. After the merge (30437505b8): 348 semantic, 244 retired-key, 212 retired-def, entries/semantic 348 files. The +1 is main's dashboard-widget-single-series-multi-measure-refused (#21293 via 32d5769080 / #21425): present once at HEAD and once on origin/main. This branch's analytics-row-wildcard-outside-count-refused is present once at HEAD; #21413's agent-memory-store-retired-and-limits-required is still present.", "rebuild": "pnpm --filter @objectstack/spec build (gen:schema validated dropped-refinements.baseline.json against the merged tree; main did not touch the ledger) then check:generated, both under the verify lock: VERDICT command-exit 0, all 15 artifacts green (check:migration-registry, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:api-surface, check:docs, check:liveness among them)." }, "gates": [ "pnpm --filter @objectstack/spec check:migration-registry :: exit 0", "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0", "node scripts/check-closing-keyword-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0", "node scripts/check-empty-changeset.mjs --self-test :: exit 0", "pnpm check:changeset-gate-self-tests :: exit 0", "pnpm check:nul-bytes :: exit 0", "pnpm check:objectui-changeset :: exit 0", "pnpm check:pm-changeset-deadline-census :: exit 0", "pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated :: exit 0 (15/15 artifacts green)", "Verdict lines: check:migration-registry: 'src/migrations/registry.ts is current (348 semantic, 244 retired-key, 212 retired-def)'; check-adr-0087-registration --base origin/main: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.'", "The changeset families are every one dispatch-gates --repo objectstack-ai/objectstack --commands named at 30437505b8 (no paths); each exit was captured before any pipe." ], "tests": "Spec pins on the merged tree: vitest --project local on analytics-row-wildcard-count-only, cube-member-sql-column-reference and dataset-field-column-reference gave Test Files 3 passed (3), Tests 57 passed (57). The rest door test was not re-run locally on the merged tree; main's merge brought native-sql-strategy changes (#21376), so CI Test Core measures it. The Test Core (4/6) red on b79d1be80d is the known #21428 flake (import-template-route.test.ts timing out at 5000 ms), recorded by the coordinator in 5956763546; not re-run or touched here.", "deviations": [ "The merge commit 30437505b8 again carries git's default merge message without the trailer pair. It was not amended, to avoid re-entering the pre-commit os-regen deferral on a merge commit; the repository squash-merges with the PR body's message." ], "mcp_calls": "0", "api_writes": "1 this round — this os-dev-report comment through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/21409/comments as objectstack-fleet[bot]). No pr_update: the PR body carries no os-migrate-meta claim. git push (once) is not a REST write. Reads: the re-review comment and the PR body, by REST GET.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
ACCEPT — PR #21431 @
30437505b8domain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5953981594· 2026-10-02T17:19ZShape (read on GitHub): a draft against
main. The first line isFixes #21409, thenClause-②: no (narrowing). PR assigneeos-sales. The branch's delta againstmainis 13 files, +986 / -59:packages/spec/src/data/analytics-column-reference.ts;analytics.zod.tsandui/dataset.zod.ts, with their tests;dropped-refinements.baseline.jsonrows and the two liveness notes;migrations/registry.tsregion, and the regeneratedui/dataset.mdx;packages/restdoor test, and one@objectstack/specchangeset.No governed path.
Review history (all at
CONTRACT_REVIEW_TIER, isolated):5956317829on60644d73d9. Each position's accept-set delta is exactly'*'outsidecount. The cube dimension's pattern (ANALYTICS_COLUMN_PATH) is the right form: it is the card's own control's spelling, and it reaches the published JSON Schema. No export is added, the ledger rows match the sites, and the D3 entry is right. Its ② found one changeset sentence untrue:defineDatasetparses nothing.5956873511onb79d1be80d. The dataset FROM → TO block now names the doors that parse (DatasetSchema.parse,defineStack422, the dataset query route 400), each judged true. Its ② found one more sentence untrue:os migrate metadoes list the D3 entry.5957579031on this head. The sentence is respelled and judged true againstchain.ts,meta.tsandprintMigrationReport. The merge ofmain3a6d92f78b(throughos-regen-merge.sh) is pure: the registry region holds this branch's entry and spec(ui): refuse two or more measures on a dimensionedpie/donut/funnel/treemap/sankeywidget too — the single-series types bind one measure whatever the dimension (extends #20958) #21293's once each, andgen:migration-registryreproduces it byte for byte.Prose (the seat's check): each changeset sentence was judged true by the records above, the two last ones after correction. The two hand-written pages the drift bot names stay true.
content/docs/data-modeling/analytics.mdxsays a dataset measure'sfield"may also be'*'(or be omitted) for a count".content/docs/api/data-api.mdxsays an inferred measure's prefix is the object's own field name.Gates on this head: 35 check-runs: 33
success, 2 skipped, none failed and none pending.Test Core (4/6)was red onb79d1be80dwith the Flaky on Test Core:packages/restimport-template-route.test.ts› "the*agrees with the engine … for every shape of default the engine reads" times out at 5000ms on PRs that touch nopackages/restfile #21428 flake (import-template-route.test.ts, 5000ms timeout; comment5956763546). It is green on this head, and so is theTest Corerollup.check-expected-skips: OK.check-governed-merges --pr 21431: NOT governed, 1045 changed lines.mergeable_state: clean. A localgit merge-treeagainstorigin/maind7d5b4f96amerges without conflict. Nomaincommit since the merge base3a6d92f78btouches the 13 files, and no open PR touches them.Serial: spec+service-analytics: retire the cube metric types
number/string/boolean— they existed to carry a SQL expression, which #20943 retired from a member'ssql#21000 (this seat, in flight) also editspackages/spec/src/data/analytics.zod.ts, in another region. It mergesmainafter this lands.Out-of-scope findings:
_sum,_avg, …) mints the row wildcard under a non-count aggregate and answers 500 DATABASE_ERROR — the runtime position of #21409's family #21437. A caller-named_sum/_avgmeasure mints'*'under a non-countaggregate and answers500 DATABASE_ERRORatPOST /api/v1/analytics/query, on both strategies. Class (a); the reach was measured by the round-2 probe. It is the runtime position of this card's family, with an enumeration pin.deriveddataset measure'sfield/aggregateis read by nothing. There is no measured reach and no producer, so it is not filed.STEP18_RATIONALEfragment, following spec(ui): refuse two or more measures on a dimensionless dashboard widget whose type declares no multi-measure rendering (pie, donut, funnel, scatter, radar, treemap, sankey): ruling D's principle, objectui#8894 Q1 #20958's and spec(ui): refuse two or more measures on a dimensionedpie/donut/funnel/treemap/sankeywidget too — the single-series types bind one measure whatever the dimension (extends #20958) #21293's precedent.Landing: ready, then auto-merge through the merge queue.
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21431 →
b79301000cdomain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5953981594· 2026-10-02T17:48ZLanded: PR feat(spec)!: the analytics row wildcard '*' is admitted only where a count consumes it (#21409) #21431 went through the merge queue as
b79301000c, with one parent (535d1d25ab). All 13 files are blob-equal to the accepted head30437505b8, on the merge commit and onorigin/main, the regeneratedmigrations/registry.tsincluded.Card: closed
completedwhen the PR merged, by itsFixesline. This act removespm:dispatchedand the assignee.Review: PASS
5956317829→ narrow PASS5956873511→ narrow PASS5957579031, over two changeset corrections and a regen merge; ACCEPT5957602318.What changes: the row wildcard
'*'is admitted only where acountconsumes it, at three positions:sqland a dataset measure'sfield, through one shared predicate, as acustomissue under any other aggregate;sql, which now takesANALYTICS_COLUMN_PATH.A summed
'*'at the dataset door answers400 VALIDATION_FAILEDwhere it answered500 DATABASE_ERROR. This is a BREAKINGminorunder ADR-0087 entryanalytics-row-wildcard-outside-count-refused.Filed from this PR: analytics query: a caller-named measure with an empty prefix (
_sum,_avg, …) mints the row wildcard under a non-count aggregate and answers 500 DATABASE_ERROR — the runtime position of #21409's family #21437 (a caller-named_sum/_avgmints'*'atPOST /api/v1/analytics/query, the runtime position of this family).Unlock scan: one card names
Blocked-by: #21409: analytics query: a caller-named measure with an empty prefix (_sum,_avg, …) mints the row wildcard under a non-count aggregate and answers 500 DATABASE_ERROR — the runtime position of #21409's family #21437, in triage's grade5957198328. It moved topm:queuein5958035591, withpm:retriageon one routing question. The predicate its ruling reads is not exported from@objectstack/spec/dataonmain, so the export half needs a lane. No other openpm:blockedcard names analytics:'*'runs only undercount, but a cube measure's or dimension'ssqland a dataset measure'sfieldadmit it under any aggregate — a summed'*'answers 500 at the dataset door (split from #21000) #21409; all 25 were read.fix(lint)!: os validate refuses an analyticsCubes sum / avg / min / m…
feat(spec)!: the analytics row wildcard '*' is admitted only where a …
feat(spec,service-analytics)!: retire the cube metric types number / …