fix(lint)!: os validate refuses an analyticsCubes sum / avg / min / max measure over a column the cube door refuses — every cube measure is judged by the aggregate × field-type table - #21435
Conversation
… field-type table WIP: rule and tests; docs and changeset follow. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…he aggregate x field-type table Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…s judged on every row of the table Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 37796ee7bf9b75f04bf074c0a009545bd361ca42 && git checkout 37796ee7bf9b75f04bf074c0a009545bd361ca42
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 1de24da8fb3447429901f228dde1673c505e5325 && git checkout -B drift-repro 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 && git merge --no-ff 1de24da8fb3447429901f228dde1673c505e5325
node scripts/docs-audit/affected-docs.mjs --json 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2
|
Fixes #21419
Clause-②: no (narrowing)
What changes
@objectstack/lint's dataset-member rule (packages/lint/src/validate-dataset-measure-aggregates.ts) now judges everyanalyticsCubesmeasure throughacceptsDeclaration, the verdict a dataset measure already gets. Until now the cube leg judgedcount_distinctonly, so a cubesum/avg/min/maxover a column the cube door refuses passedos validate.typeis its aggregate. Everytypethat is a row ofAGGREGATE_FIELD_TYPE_COMPATIBILITYis judged on the column the door reads. That column is the cube's object, or, for a relationship path, the last hop's declared join, else the lookup'sreference(the resolution 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 added, unchanged).countreads no value and is accepted on every type, as on a dataset.typeoutside the table (number/string/boolean, the expression metric types) is skip 5, as on a dataset. Neither cube door judges it either.'*'is still skipped. Whether it belongs on a non-count measure is the question 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 owns; nothing here refuses it.if (aggregate !== 'count_distinct') continue;becomesif (!aggregate) continue;, and the existing skip 5 (ACCEPTED_TYPES_BY_AGGREGATE.get) now does the vocabulary work it does for a dataset. The finding id (measure-aggregate-field-type-refused), its location (analyticsCubes[N].measures.KEY.type) and its words are the ones the cubecount_distinctleg already emits.Text this PR makes false, corrected in it: the rule's module note and the
validate-dataset-measure-aggregates.tsdocblock, and the cube paragraph in §6 ofcontent/docs/deployment/validating-metadata.mdx. Both said a cube's other measure types were not judged. The landed.changeset/21082-cube-member-json-stored-refused.mdis released history and is left as it is.Files changed (4): the rule, its test file, the docs paragraph, one changeset.
index.ts,runtime.ts,package.jsonandservice-analyticsare untouched.The cube door's verdict, pair for pair
The enumeration pin in the test file crosses every cube measure
type(AggregationMetricType, which holds every row of the table) with everyFieldType, flaggedmultiple: trueand not. It checks each pair against the two cube doors' rules as their modules state them:count/sum/avg/min/max:cube-measure-field-type-door.ts. The row is checked on the declared TYPE, and themultipleflag is not read.count_distinct:structured-json-dimension-door.ts. The row is checked, and so isisMultiValueField.Per-type floors hold on both sides.
As a one-off proof against the real door rather than its stated rule, a scratch script was run and not committed. For each case it built a real
AnalyticsServicewithsourceFieldMeta,relationshipResolverandgetObjectFieldNameswired. Then it calledgenerateSqlon one measure, which goes throughensureCubeto both doors. It compared the refusal (INVALID_FIELDwith the door's own words) against the lint verdict. The cases were everyAggregationMetricType× everyFieldType×multiple{false, true} × three column positions: bare, reference-tier path and join-tier path.1de24da8fb(the rule's blob is0d56d08213since1236c5a4b7)39a912ea73(control)No pair diverges, so there is nothing to report as a door/table disagreement.
os validate, before and afterFixtures
fx_ledger+ one cubefx_cubewere measured with the CLI built from this tree. Before is the same tree with the guard reverted throughscripts/ablation-replace.mjs. The lintdist/was rebuilt, andablation-dist-preflightconfirmed the marker present in 4 built files. After is the restored tree, rebuilt, with the marker confirmed absent from all 8 built files and the tree clean against HEAD.oos-cube-sum-text(sumovername,text), the card's measured instanceanalyticsCubes[0].measures.sum_name.typeavgovertextmaxovertextminover a singleselectavgoverdatetimesumoveraccount.name, reference tier (fx_account,text)sumoveraccount.revenue, join tier (joins.accountreachesfx_branch, where it istext)fx_branchThe scalar control holds
sumandavgovernumber,maxandminoverdatetime,countandcount_distinctovertext,sumoveraccount.revenueby reference (number), andsumover'*'.Census
The census covers
examples/**,packages/**(fixtures included) and the platform objects. It found one authored cube in the shipped corpus: showcase'sshowcase_delivery, withcountover'*'andsum/avgoverestimate_hours(number). It stays clean. Every otheranalyticsCubesordefineCubesite is a spec, service-analytics, runtime, objectql or driver test, and none of them runs a lint rule.At HEAD
1de24da8fb,os validateexits 0 onexamples/app-showcase,app-crmandapp-todo, with 0 findings of either cube id.Clause-②: the measured arm
node scripts/pm/check-widening-tells.mjs --declaration no --diffon the final diff (git diff 39a912ea73...HEAD) exits 0. All 4 files are NOT MEASURED (no declared surface covers them), so it decided nothing.exportlines inpackages/lint/src, tests excluded. The positive control5e470f8c1cadds 1.index.ts,runtime.tsandpackage.jsonare unchanged.no (narrowing). The changeset is still BREAKING, because metadata that passedos validatecan now fail.Changeset
.changeset/21419-cube-measure-aggregate-field-type-refused.md:@objectstack/lintminor,fix(lint)!, a BREAKING banner,Clause-②: no (narrowing), and exactly one ADR-0087 marker:not-required (no-migration-prescription).check-adr-0087-registrationaccepts it and reads it as[BREAKING+bang+clause-②-narrowing].Tests
src/validate-dataset-measure-aggregates.test.tsgains 6 tests in a new block, and 1 test is rewritten.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 test that pinned "judges onlycount_distinct". It now pins thatcountover ajsoncolumn is accepted andsum/avg/min/maxover it are refused.textanddatetime;type,'*', an unresolved column, an unfollowable hop, an untyped column, a cube naming no object;runAuthoringRuleson all three commands.Runs at
1de24da8fb, the final commit, which includes the merge oforigin/main68c5ab7eba:pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 119 files, 5592 tests passed.pnpm --filter @objectstack/lint typecheck: exit 0.tsc -p tsconfig.test.json --listFilesincludes the test file..tsfiles. The.mdxand.mdfall outside the config's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}glob, and--print-configresolves a config for both.tsfiles.--format jsonreports 2 files, 0 errors and 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot move a verdict on an untouched file.Ablations ran on the committed tree through
scripts/ablation-replace.mjs. Each restore was proven by its blob hash matching HEAD and an emptygit diff HEAD.count_distinctonlymaxrow dropped on cube measurescube max(text)Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat1de24da8fbderived 90 commands, and all 90 were run.check:skill-examplesfirst exited 3: PREREQUISITE NOT MET, becauseclient-reacthad no.d.ts.check:dual-build-cjs-loadsfirst exited 3: 7 packages had nodist.check:dts-closurefirst exited 1, because of this worktree's ownOS_SKIP_DTSbuilds.--ranreconciliation: 90 derived, 90 run, 0 NOT-MEASURED (a derived zero: every line recorded its exit code), 0 UNRUN.check-changeset-fixed,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity. All exit 0.mainmoved 5 commits after the merge (3a6d92f78b). None of them touchespackages/lint, the docs page or either cube door. The oneservice-analyticschange is the NativeSQL boolean-comparand door. CI's merge ref tests the combined state.Acceptance notes
not-required (already-registered dataset-measure-selecting-aggregate-field-type-refused, dataset-measure-aggregate-field-type-refused)for these same pairs. This rule's own cube leg declaredno-migration-prescription, on the ground that those two entries describeDatasetMeasureSchemarows. This PR follows the rule's own leg. Either marker passes the gate; if the seat prefers the other, the change is the one comment line.FieldType.acceptsDeclarationis fail-closed on vocabulary, so the lint would refuse evencountover such a column, where both cube doors stand down.os validatecannot reach that case:defineStackrefuses the field first (measured:objects.0.fields.amount.type: Invalid field type 'integer', exit 1, no author-time rule runs). The dataset leg has the same shape.validate-field-consumers.ts). This was seen while measuring and is reported to the seat, not fixed here.os validateon the scalar control flagsfx_account.revenueandfx_ledger.accountas "inert — no site of any kind names it". That is wrong: the cube measuresum_revreadsaccount.revenue. A dataset measure overaccount.revenuewithinclude: ['account']gets the same inert verdict onfx_account.revenue.analytics_cubewrite door still dispatches no authoring rule, 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 recorded. Read from code, not measured. Carrier: none.Generated by Claude Code