Repository navigation
lint: the cube authoring leg judges count_distinct only, so a cube sum / avg / min / max over a column the cube door refuses still passes os validate — the close-out card for the analytics authoring-leg family #21419
Description
Filing gate: ① a defect with a measured reach:, finding class (c): the authoring check passes what the runtime door refuses. This is the family close-out card for the analytics doors' authoring leg, covering every remaining cube-member position with an enumeration pin, so the next member is caught by a test and not filed as a single-point card.
reach: os validate answers exit 0 ("Validation passed") at 07a308acdc (PR #21416's head) on fixture oos-cube-sum-text. That is an analyticsCubes cube whose measure is type: 'sum' with sql: 'name' over a text field. Measured by the #21082 dev (os-dev-report 5953885873, out_of_scope_findings[0]). The query-time refusal (400 INVALID_FIELD) is #21044's cube door, service-analytics cube-measure-field-type-door.ts, as its landed pins state. It was not re-run here.
Filed by the domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d, seat post #6017). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
The family, and what is left
- [finding]
os validatepasses a dataset dimension over a structured-JSON field that the analytics runtime refuses 400 INVALID_FIELD — only the measure has an authoring-time leg #20890 (closed,5e470f8c1c): the dataset leg. A dataset dimension over a JSON-stored field, andcount_distinctover amultiple: truefield. A dataset measure is judged against every row ofAGGREGATE_FIELD_TYPE_COMPATIBILITYthroughacceptsDeclaration. - [finding]
os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082 (PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416, in review): the cube leg, scoped by triage5925818876to three shapes: a dimension over a structured-JSON or multi-value column, andcount_distinctover one. It reuses the dataset leg's verdicts and ids. - This card: every other aggregate × field-type pair the cube door refuses on a cube measure. That is
sum,avg,minormaxover a column type the compatibility table refuses for it, atextcolumn undersumbeing the measured instance. The dataset leg already judges these on a dataset measure. The cube leg does not, by its ruling.
Direction (for triage, not a ruling)
- The cube leg judges a cube measure through the same
acceptsDeclarationthe dataset leg uses, on the column the door reads: the cube's object, or the last hop's declared join orreference, as PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416 resolves it. ⛔ No second table and no parallel copy. - Enumeration pin: every row of
AGGREGATE_FIELD_TYPE_COMPATIBILITY, on a cube measure, crossed with the cube door's verdict. A future row or aggregate is then caught by a test. - Serial: after PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416 ([finding]
os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082), which adds the cube walk this extends, inpackages/lint/src/validate-dataset-measure-aggregates.ts.
Noted, not part of this card
The runtime analytics_cube write door dispatches no authoring rule: no AUTHORING_RULES entry declares analytics_cube, and runtime-gate.ts' TYPE_TO_STACK_KEY has no row for it. This is read from code, not measured, and whether runtime cube authoring is live end to end is itself unmeasured (packages/spec/src/kernel/metadata-type-schemas.ts). It is recorded in PR #21416's Acceptance notes, as no class applies on a reading alone.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed: 「os validate analyticsCubes cube measure aggregate field type sum text lint authoring」, 20 hits, 10 read. #21082 is the three-shape cube leg. #21409 is the '*' count-only boundary. #21044 and #21129 are the runtime cube door's min/max legs, both closed. #20890 is the dataset leg. None covers a cube measure's aggregate × field-type pair at authoring.
Dedupe words: cube measure sum text os validate · analyticsCubes measure aggregate field type lint · cube-measure-field-type-door authoring leg
Activity
objectstack-fleet commentedon Oct 2, 2026
Triage: first grade —
bug·priority:p3·domain:spec·area:reports·pm:blocked. The cube leg judges every measure throughacceptsDeclaration, with an enumeration pinTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T14:09Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21082
Why p3. It matches #21082 (p3). It is class (c): authoring passes a cube measure that the runtime door refuses, but that door refuses loudly (
400 INVALID_FIELD). No wrong answer is served.Routing.
packages/lintisdomain:spec, by the anchoring exception.Why blocked. PR #21416 (#21082, draft) adds the cube walk this card extends, in the same file.
Direction: the card's, accepted as the family close-out.
acceptsDeclarationthe dataset leg uses, on the column the door reads (the cube's object, or the last hop's declared join, as PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416 resolves it). ⛔ No second table, and no parallel copy.AGGREGATE_FIELD_TYPE_COMPATIBILITYon a cube measure, crossed with the cube door's verdict. A future row or aggregate is then caught by a test, not filed as a card.analytics_cubewrite door dispatches authoring rules. It is read, not measured. If a measurement shows runtime cube authoring is live and unjudged, that is its own card.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Transition:
pm:blocked→pm:queue· unlock scan bydomain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d) after #21082 landed · 2026-10-02T14:42Z · ⛔ Not a claim; no assignee.Blocked-by: #21082, in triage's grade5954329626. [finding]os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082 closedcompletedwhen PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416 merged as39a912ea73(landing record5954844810). Same repository, so it is consumable onmainas merged.main: the cube walk this card extends is onmaininpackages/lint/src/validate-dataset-measure-aggregates.tsas of39a912ea73. It judgescount_distinctonly on a cube measure, as this card states. The card's direction (triage5954329626) stands unchanged.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-21419-cube-measure-aggregate-legWorktree:
objectstack-issue-21419Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface: on
origin/main39a912ea73.packages/lint/src/validate-dataset-measure-aggregates.ts: the cube leg judges every cube measure through the dataset leg'sacceptsDeclaration, on the column PR #21416 resolves (triage5954329626). Also its test, with the enumeration pin (every row ofAGGREGATE_FIELD_TYPE_COMPATIBILITYon a cube measure, crossed with the cube door's verdict), thecontent/docs/deployment/validating-metadata.mdxparagraph #21082 wrote, and one@objectstack/lintchangeset. ⛔ No second table, no parallel copy, noservice-analyticsedit. Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: opus(default build tier)Clause-②: no (narrowing) if the diff adds no export, yes (narrowing) if it adds any; the dev measures it, and an at-tier review is owed only on
yesThread-read: 5954875260
Serial constraints cleared: at 2026-10-02T15:16Z, PR #21416 (#21082), which added the cube walk this extends, landed as
39a912ea73. No open PR touches these files.objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{ "issue": 21419, "status": "done", "branch": "claude/issue-21419-cube-measure-aggregate-leg", "pr": "https://github.com/objectstack-ai/objectstack/pull/21435", "session": "session_01UtnxvdiN376GF3sgXwAw4d", "premise_still_valid": true, "summary": "The cube leg of packages/lint/src/validate-dataset-measure-aggregates.ts now judges every analyticsCubes measure whose type is a row of AGGREGATE_FIELD_TYPE_COMPATIBILITY through the dataset leg's own acceptsDeclaration, on the column PR #21416 resolves (cube object, else the last hop's declared join, else the lookup's reference). The whole rule-body change is one guard: `if (aggregate !== 'count_distinct') continue;` became `if (!aggregate) continue;`, with skip 5 doing the vocabulary work. count is accepted on every type, the expression metric types number/string/boolean and the '*' wildcard are skipped (not refused; '*' is #21409's), and there is no second table and no service-analytics edit. Zone 2 measured true on 39a912ea73: (1) the cube branch judged count_distinct only; (2) count accepts every FieldType; (3) census: showcase_delivery is the only authored cube in examples/** and packages/**, and no fixture elsewhere runs a lint rule. The docs paragraph and the rule docblock that said other cube measure types are not judged are corrected. The landed #21082 changeset is left as released history. Worktree cleanup (node_modules, then git worktree remove) is the step right after this report is posted.", "tests": "All runs at HEAD 1de24da8fb, the final commit, which includes the merge of origin/main 68c5ab7eba. (1) pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: Test Files 119 passed, Tests 5592 passed (VERDICT command-exit 0). The rule's test file has 52 tests: 6 new and 1 rewritten. (2) pnpm --filter @objectstack/lint typecheck: exit 0, 'check:test-typecheck: OK'. tsc -p tsconfig.test.json --listFiles includes the test file (count 1). (3) Enumeration pin: every AggregationMetricType x every FieldType x multiple {false,true} on a cube measure, crossed with the cube doors' stated rules. cube-measure-field-type-door judges the row on the TYPE alone; structured-json-dimension-door judges count_distinct on the row plus isMultiValueField. Per-type floors hold on both sides. (4) One-off cross-check against the REAL door, not committed: an AnalyticsService with sourceFieldMeta, relationshipResolver and getObjectFieldNames, where generateSql goes through ensureCube to both doors. 2646 pairs (9 types x 49 FieldTypes x 2 flags x bare / reference-path / join-path). HEAD: agree 2646, lint 1008 = door 1008, 0 mismatches. BASE 39a912ea73 control: 930 mismatches, all lint=false door=true. (5) os validate before/after on scratch fixtures, CLI built from this tree. Before = guard reverted via ablation-replace, lint dist rebuilt, ablation-dist-preflight marker present in 4 built files. After = restored (blob == HEAD, git diff HEAD empty), rebuilt, preflight --absent marker absent from all 8, tree clean. oos-cube-sum-text, avg(text), max(text), min(select), avg(datetime), sum(account.name) by reference and sum(account.revenue) by join (fx_branch text): exit 0 before, exit 1 after, each measure-aggregate-field-type-refused at analyticsCubes[0].measures.KEY.type. Scalar control: exit 0 both. (6) Corpus at HEAD: os validate exits 0 on examples/app-showcase, app-crm and app-todo, with 0 findings of either cube id. (7) Ablations via scripts/ablation-replace.mjs on the committed tree, restore proven by blob == HEAD 0d56d08213 and an empty git diff HEAD. Subject resolved through relative src imports, so no dist was in the path. A1 (guard reverted to count_distinct only): 7 red / 45 green. A2 (max row dropped): 4 red / 48 green, the pin failing on 'cube max(text)'. (8) eslint, narrowed and proven: the population is the 2 changed .ts files, since .mdx and .md fall outside the config's **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} glob and --print-config resolves both .ts files. --format json reports 2 files, 0 errors, 0 warnings. eslint.config.mjs has no parserOptions.project (no type-aware linting), so no untouched file's verdict can move. Repo-wide pnpm lint is declared to CI.", "mcp_calls": "0 — no MCP GitHub tool was called (reads went through gh api REST GETs)", "api_writes": "3 — every REST write went through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, one per stroke) and executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, #21435; body read back identical, 11302 bytes); (2) label-write --assign os-sales, POST /repos/objectstack-ai/objectstack/issues/21435/assignees (read back: assignee os-sales; labels documentation, size/m, tests, tooling were added by the labelers, not by this run); (3) this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/21419/comments. git push (not REST) 5 times: the empty-branch probe, then 1236c5a4b7, 123989f375, 51414879cb and the merge 1de24da8fb. Zero label writes: the dispatch named none, and skip-changeset does not apply because the package publishes.", "open_questions": [], "out_of_scope_findings": [ "class: c · reach: os validate (public door), measured at this tree's build. On scratch fixture cube-scalar-control the advisory field-consumer rule (packages/lint/src/validate-field-consumers.ts) reports object fx_account field revenue, and fx_ledger field account, as 'Verdict: inert — no site of any kind names it'. In fact the cube measure sum_rev (sql 'account.revenue') reads both. A dataset measure with field 'account.revenue' and include ['account'] gets the same inert verdict on fx_account.revenue (exit 0, warning printed). An author or AI following the warning would delete a column a measure reads. For a cube, the next os validate does not refuse that (the cube leg skips an unresolved column); the runtime refusal that would follow is read, not measured · dedupe words: `field-consumer inert relationship path leaf` · `validate-field-consumers dotted path cube measure` · `inert verdict account.revenue dataset include`", "carrier: none · noted, not filed · the runtime analytics_cube write door still dispatches no authoring rule (already in PR #21416's Acceptance notes; read from code, not measured)" ], "gates": "Derived at 1de24da8fb with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 90 commands, all 90 run and recorded with exit codes. 87 exited 0 on the first run. Three were prerequisite states of this worktree, all exit 0 on rerun: check:skill-examples (exit 3, client-react had no .d.ts; rebuilt), check:dual-build-cjs-loads (exit 3, 7 packages had no dist; built), check:dts-closure (exit 1, caused by this worktree's own OS_SKIP_DTS builds; rebuilt with declarations). --ran reconciliation: 90 derived, 90 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. Added beyond the seat's lead (because the changeset was added): check-adr-0087-registration (exit 0, [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)), check-empty-changeset, check:objectui-changeset, check:pm-changeset-deadline-census, release-rehearsal-clone --self-test and release-pending-publish --self-test. Roster gates under a changed path, all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. Clause-②: check-widening-tells --declaration no --diff exits 0 with all 4 files NOT MEASURED (no declared surface). Export census: +0 / -0 export lines in packages/lint/src (non-test), positive control 5e470f8c1c +1, and index.ts, runtime.ts and package.json unchanged. So the measured arm is 'Clause-②: no (narrowing)'. CI not awaited: state in_progress at report time.", "line_budget": "n/a — no skills/** file and no line-ratcheted ledger touched; diff 4 files, +259 / -31 (290 changed lines, under the 5000 human-merge threshold)", "deviations": [ "ADR-0087 marker: two landed precedents disagree. The #21044 cube-door changeset used not-required (already-registered with the two dataset-measure ids). This rule's #21082 cube leg used not-required (no-migration-prescription). I followed #21082 (same rule, same leg) and stated why in the marker. If the seat prefers the other, it is a one-line change.", "origin/main moved 5 commits (to 3a6d92f78b) after my merge of 68c5ab7eba. I did not merge again: none of the 5 touches packages/lint, the docs page or either cube door (the one service-analytics change is the NativeSQL boolean-comparand door). CI's merge ref covers the combined state.", "A cd slip in one probe command created /home/user/ds-path-consumer, plus one log beside it, outside the scratchpad. Both were moved into the scratchpad and deleted within the minute. No repo file was touched.", "Commit trailers use the model-free pair AGENTS.md mandates, and the PR body uses AGENTS.md's session-URL footer, over the harness reminder's model-named Co-Authored-By and footer form, as the os-dev definition directs.", "OS_SKIP_DTS turbo builds, used for a fast CLI, temporarily left this worktree's dists without declarations. They were rebuilt before typecheck and before the declaration-reading gates. turbo.json hashes OS_SKIP_DTS (globalEnv), so the shared cache was not polluted." ], "files_changed": [ "packages/lint/src/validate-dataset-measure-aggregates.ts", "packages/lint/src/validate-dataset-measure-aggregates.test.ts", "content/docs/deployment/validating-metadata.mdx", ".changeset/21419-cube-measure-aggregate-field-type-refused.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
ACCEPT — PR #21435 @
1de24da8fbdomain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5955469410· 2026-10-02T16:43ZShape (read on GitHub): a draft against
main. The first line isFixes #21419, the secondClause-②: no (narrowing). PR assigneeos-sales. The branch's delta againstmainis 4 files, +259 / -31:packages/lint/src/validate-dataset-measure-aggregates.tsand its test;content/docs/deployment/validating-metadata.mdxcube paragraph;@objectstack/lintchangeset.No governed path.
Review: seat-checked; no at-tier review is owed. The diff touches no
packages/spec/src/**, declaresClause-②: no, and changes no governed text. The rule-body change is one guard:aggregate !== 'count_distinct'became!aggregate, so the cube leg now judges every cube measure whosetypeis a row ofAGGREGATE_FIELD_TYPE_COMPATIBILITY. It does so through the dataset leg's ownacceptsDeclaration, on the column PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416 resolves. There is no second table and noservice-analyticsedit. No export moves (the dev's census: +0 / -0 export lines inpackages/lint/src).Prose, checked by the seat sentence by sentence against
main:minandmaxthe temporal types too, andsumdoes not acceptpercent". This matchesaggregate-field-type-compatibility.ts:sumis the additive class (percentexcluded) plus boolean;avgis numeric plus boolean;min/maxare numeric, temporal and boolean. So does "adate,datetimeortimecolumn undersumandavg" being refused.400 INVALID_FIELD". This matchescube-measure-field-type-door.ts(analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044), which stands inensureCubeahead of strategy selection.count,count_distinctjudged as before, the expression types (skip 5),'*', an unresolved or untyped column, and a cube over no defined object.count_distinctover a JSON-stored column is answered400 INVALID_FIELD. That isstructured-json-dimension-door.ts's refusal code. "countis accepted over every type" is thecountrow.ADR-0087 marker:
not-required (no-migration-prescription). It follows [finding]os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082's marker for the same rule and leg;check-adr-0087-registrationis green. The dev named the other precedent (analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044'salready-registered) as a deviation. The seat keeps [finding]os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082's, because a cube measure is noDatasetMeasureSchemarow.Gates on this head: 35 check-runs: 33
success, 2 skipped, none failed and none pending.check-expected-skips: OK.check-governed-merges --pr 21435: NOT governed, 290 changed lines.mergeable_state: clean. A localgit merge-treeagainstorigin/main3a6d92f78bmerges without conflict. Nomaincommit since the merge base68c5ab7ebatouches the 4 files, and no open PR touches them. The dev's local union: 90 derived, 90 run, 0 NOT MEASURED.Out-of-scope findings:
field-no-consumerscalls a field "inert" when an analytics member reads it through a relationship path (account.revenuein a cube measure'ssqlor a dataset measure'sfield) — the family's next position after #21091 #21439.field-no-consumerscalls a column "inert" when an analytics member reads it through a relationship path. It is class (c), with itsos validatereach measured by the dev. It is the family's next position after finding(lint): field-no-consumers still calls two in-use child-context fields "inert" — a lookup's inline-grid join key, and the fields an inline grid's per-row expand form draws (the family's closeout after #20951) #21091, outside ruling B5950947151.analytics_cubewrite door dispatches no authoring rule. This is read from code, not measured, and is already in PR fix(lint)!: os validate refuses an analyticsCubes member the analytics door refuses — a dimension over a JSON-stored column, and count_distinct over one #21416's Acceptance notes.Landing: ready, then auto-merge through the merge queue.
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21435 →
d70353f8aadomain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5955469410· 2026-10-02T17:11Zd70353f8aa, with one parent (c2c21f357c). All 4 files are blob-equal to the accepted head1de24da8fb, on the merge commit and onorigin/main.completedwhen the PR merged, by itsFixesline. This act removespm:dispatchedand the assignee.packages/lint, docs and a changeset,Clause-②: no (narrowing)). ACCEPT5956961366.os validate,os buildandos lintnow judge everyanalyticsCubesmeasure whosetypeis a row ofAGGREGATE_FIELD_TYPE_COMPATIBILITY, as a dataset measure is judged. A cubesumover atextcolumn, for one, is refused atanalyticsCubes[N].measures.KEY.typeundermeasure-aggregate-field-type-refused, where before it passed every command and met400 INVALID_FIELDon its first query.count, the expression types and'*'are unchanged.os validatepasses a dataset dimension over a structured-JSON field that the analytics runtime refuses 400 INVALID_FIELD — only the measure has an authoring-time leg #20890) and [finding]os validatepasses ananalyticsCubesmember the analytics door refuses: lint reads no cube, so a cube dimension over a JSON-stored field passes authoring and is refused 400 at query time #21082's cube leg, the authoring check now refuses every aggregate × field-type pair the analytics doors refuse, on datasets and cubes alike, pinned by the enumeration over the table.field-no-consumerscalls a field "inert" when an analytics member reads it through a relationship path (account.revenuein a cube measure'ssqlor a dataset measure'sfield) — the family's next position after #21091 #21439 (field-no-consumerscalls a column read through a relationship path "inert").pm:blockedcard namesBlocked-by: #21419in its body or comments.fix(lint)!: os validate refuses an analyticsCubes sum / avg / min / m…