Repository navigation
Commit d70353f
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)
Fixes #21419
Clause-②: no (narrowing)
## What changes
`@objectstack/lint`'s dataset-member rule
(`packages/lint/src/validate-dataset-measure-aggregates.ts`) now judges
**every** `analyticsCubes` measure through `acceptsDeclaration`, the
verdict a dataset measure already gets. Until now the cube leg judged
`count_distinct` only, so a cube `sum` / `avg` / `min` / `max` over a
column the cube door refuses passed `os validate`.
- A cube measure's `type` is its aggregate. Every `type` that is a row
of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` is 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's `reference` (the
resolution PR #21416 added, unchanged).
- `count` reads no value and is accepted on every type, as on a dataset.
- A `type` outside the table (`number` / `string` / `boolean`, the
expression metric types) is skip 5, as on a dataset. Neither cube door
judges it either.
- The row wildcard `'*'` is still skipped. Whether it belongs on a
non-count measure is the question #21409 owns; nothing here refuses it.
- **No second table and no parallel copy.** The change in the rule body
is one guard: `if (aggregate !== 'count_distinct') continue;` becomes
`if (!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 cube `count_distinct` leg already emits.
**Text this PR makes false, corrected in it:** the rule's module note
and the `validate-dataset-measure-aggregates.ts` docblock, and the cube
paragraph in §6 of `content/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.md` is 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.json` and
`service-analytics` are 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 every
`FieldType`, flagged `multiple: true` and 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 the `multiple` flag is not read.
- `count_distinct`: `structured-json-dimension-door.ts`. The row is
checked, and so is `isMultiValueField`.
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 `AnalyticsService` with `sourceFieldMeta`, `relationshipResolver`
and `getObjectFieldNames` wired. Then it called `generateSql` on one
measure, which goes through `ensureCube` to both doors. It compared the
refusal (`INVALID_FIELD` with the door's own words) against the lint
verdict. The cases were every `AggregationMetricType` × every
`FieldType` × `multiple` {false, true} × three column positions: bare,
reference-tier path and join-tier path.
| lint rule | pairs | agree | lint refuses | door refuses | mismatches |
|---|---|---|---|---|---|
| HEAD `1de24da8fb` (the rule's blob is `0d56d08213` since `1236c5a4b7`)
| 2646 | 2646 | 1008 | 1008 | **0** |
| BASE `39a912ea73` (control) | 2646 | 1716 | 78 | 1008 | 930, all
lint=false door=true |
No pair diverges, so there is nothing to report as a door/table
disagreement.
## `os validate`, before and after
Fixtures `fx_ledger` + one cube `fx_cube` were measured with the CLI
built from this tree. **Before** is the same tree with the guard
reverted through `scripts/ablation-replace.mjs`. The lint `dist/` was
rebuilt, and `ablation-dist-preflight` confirmed 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.
| fixture | before | after |
|---|---|---|
| `oos-cube-sum-text` (`sum` over `name`, `text`), the card's measured
instance | exit 0 | **exit 1** at
`analyticsCubes[0].measures.sum_name.type` |
| `avg` over `text` | exit 0 | **exit 1** |
| `max` over `text` | exit 0 | **exit 1** |
| `min` over a single `select` | exit 0 | **exit 1** |
| `avg` over `datetime` | exit 0 | **exit 1** |
| `sum` over `account.name`, reference tier (`fx_account`, `text`) |
exit 0 | **exit 1** |
| `sum` over `account.revenue`, join tier (`joins.account` reaches
`fx_branch`, where it is `text`) | exit 0 | **exit 1**, naming
`fx_branch` |
| scalar control | exit 0 | exit 0 |
The scalar control holds `sum` and `avg` over `number`, `max` and `min`
over `datetime`, `count` and `count_distinct` over `text`, `sum` over
`account.revenue` by reference (`number`), and `sum` over `'*'`.
## Census
The census covers `examples/**`, `packages/**` (fixtures included) and
the platform objects. It found **one** authored cube in the shipped
corpus: showcase's `showcase_delivery`, with `count` over `'*'` and
`sum` / `avg` over `estimate_hours` (`number`). It stays clean. Every
other `analyticsCubes` or `defineCube` site is a spec,
service-analytics, runtime, objectql or driver test, and none of them
runs a lint rule.
At HEAD `1de24da8fb`, `os validate` exits 0 on `examples/app-showcase`,
`app-crm` and `app-todo`, with 0 findings of either cube id.
## Clause-②: the measured arm
- `node scripts/pm/check-widening-tells.mjs --declaration no --diff` on
the final diff (`git diff 39a912e...HEAD`) exits 0. All 4 files are
NOT MEASURED (no declared surface covers them), so it decided nothing.
- **Export census:** the diff adds 0 and removes 0 `export` lines in
`packages/lint/src`, tests excluded. The positive control `5e470f8c1c`
adds 1. `index.ts`, `runtime.ts` and `package.json` are unchanged.
- So no export is added, and the arm is `no (narrowing)`. The changeset
is still BREAKING, because metadata that passed `os validate` can now
fail.
## Changeset
`.changeset/21419-cube-measure-aggregate-field-type-refused.md`:
`@objectstack/lint` `minor`, `fix(lint)!`, a **BREAKING** banner,
`Clause-②: no (narrowing)`, and exactly one ADR-0087 marker:
`not-required (no-migration-prescription)`.
`check-adr-0087-registration` accepts it and reads it as
`[BREAKING+bang+clause-②-narrowing]`.
## Tests
`src/validate-dataset-measure-aggregates.test.ts` gains 6 tests in a new
block, and 1 test is rewritten.
- The rewritten test is the #21082 test that pinned "judges only
`count_distinct`". It now pins that `count` over a `json` column is
accepted and `sum` / `avg` / `min` / `max` over it are refused.
- The 6 new tests cover:
- the measured instance and its control;
- per-row refusals and acceptances over `text` and `datetime`;
- the enumeration pin;
- the reference tier and the join tier giving opposite verdicts on one
path;
- the skips: expression types, a prototype key, no or non-string `type`,
`'*'`, an unresolved column, an unfollowable hop, an untyped column, a
cube naming no object;
- report order, and `runAuthoringRules` on all three commands.
Runs at `1de24da8fb`, the final commit, which includes the merge of
`origin/main` `68c5ab7eba`:
- `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 --listFiles` includes the test file.
- eslint, narrowed and proven: the population is the 2 changed `.ts`
files. The `.mdx` and `.md` fall outside the config's
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` glob, and `--print-config`
resolves a config for both `.ts` files. `--format json` reports 2 files,
0 errors and 0 warnings. `eslint.config.mjs` enables no type-aware
linting (no `parserOptions.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 empty `git diff HEAD`.
| ablation | result |
|---|---|
| A1: the guard reverted to `count_distinct` only | 7 red / 45 green:
the 6 new tests and the rewritten one |
| A2: the `max` row dropped on cube measures | 4 red / 48 green,
including the enumeration pin on `cube max(text)` |
## Gates
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at `1de24da8fb` derived 90 commands, and all 90 were run.
- `check:skill-examples` first exited 3: PREREQUISITE NOT MET, because
`client-react` had no `.d.ts`.
- `check:dual-build-cjs-loads` first exited 3: 7 packages had no `dist`.
- `check:dts-closure` first exited 1, because of this worktree's own
`OS_SKIP_DTS` builds.
- After those packages were built with declarations, all three exit 0.
- `--ran` reconciliation: 90 derived, 90 run, 0 NOT-MEASURED (a derived
zero: every line recorded its exit code), 0 UNRUN.
- Also run, as the roster gates under a changed path:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`. All exit 0.
- `main` moved 5 commits after the merge (`3a6d92f78b`). None of them
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 tests the combined state.
## Acceptance notes
- **ADR-0087 disposition: the two landed precedents disagree.** The
query-time cube door's changeset declared `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 declared `no-migration-prescription`, on the
ground that those two entries describe `DatasetMeasureSchema` rows. 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.
- **A field type outside `FieldType`.** `acceptsDeclaration` is
fail-closed on vocabulary, so the lint would refuse even `count` over
such a column, where both cube doors stand down. `os validate` cannot
reach that case: `defineStack` refuses 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.
- **The field-consumer advisory does not credit a relationship-path
leaf** (`validate-field-consumers.ts`). This was seen while measuring
and is reported to the seat, not fixed here. `os validate` on the scalar
control flags `fx_account.revenue` and `fx_ledger.account` as "inert —
no site of any kind names it". That is wrong: the cube measure `sum_rev`
reads `account.revenue`. A dataset measure over `account.revenue` with
`include: ['account']` gets the same inert verdict on
`fx_account.revenue`.
- **Runtime write door, unchanged.** The runtime `analytics_cube` write
door still dispatches no authoring rule, as PR #21416 recorded. Read
from code, not measured. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c2c21f3 commit d70353f
4 files changed
Lines changed: 259 additions & 31 deletions
File tree
- .changeset
- content/docs/deployment
- packages/lint/src
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
236 | 236 | | |
237 | 237 | | |
238 | 238 | | |
239 | | - | |
240 | | - | |
241 | | - | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
242 | 247 | | |
243 | 248 | | |
244 | | - | |
245 | | - | |
| 249 | + | |
| 250 | + | |
246 | 251 | | |
247 | 252 | | |
248 | 253 | | |
| |||
Lines changed: 199 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
656 | 657 | | |
657 | 658 | | |
658 | 659 | | |
659 | | - | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
660 | 663 | | |
661 | 664 | | |
662 | 665 | | |
663 | 666 | | |
664 | | - | |
| 667 | + | |
665 | 668 | | |
666 | 669 | | |
667 | 670 | | |
| |||
741 | 744 | | |
742 | 745 | | |
743 | 746 | | |
744 | | - | |
745 | | - | |
746 | | - | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
747 | 757 | | |
748 | 758 | | |
749 | 759 | | |
| |||
859 | 869 | | |
860 | 870 | | |
861 | 871 | | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
0 commit comments