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
Commit 39a912e
Browse filesBrowse the repository at this point in the historyBrowse files
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)
Fixes#21082
Clause-②: no (narrowing)
## What changes
`@objectstack/lint`'s dataset-member rule
(`packages/lint/src/validate-dataset-measure-aggregates.ts`) gains a
cube leg. It walks `analyticsCubes` and refuses the three shapes that
the analytics door (`service-analytics`
`structured-json-dimension-door.ts`) refuses with `400 INVALID_FIELD`:
1. a cube dimension whose `sql` column is structured-JSON
(`STRUCTURED_JSON_TYPES`);
2. a cube dimension whose `sql` column is multi-value
(`isMultiValueField`, `multiple: true` included);
3. a cube `count_distinct` measure whose `sql` column is JSON-stored
(the table's `count_distinct` row AND `isMultiValueField`).
**No parallel copy.** The verdicts are the dataset leg's own helpers,
`groupKeyClassOf` and `acceptsDeclaration`. The findings carry the
dataset leg's two ids:
- `dimension-json-stored-field-refused`, at
`analyticsCubes[N].dimensions.KEY.sql`;
- `measure-aggregate-field-type-refused`, at
`analyticsCubes[N].measures.KEY.type`.
The dataset and cube legs now share two builders for the finding text.
The dataset findings stay byte-identical (measured below).
The column is read the way the door reads it (`analytics-service.ts`,
`hop-object.ts`):
- **The cube's object** is its `sql`, trimmed, when it names an object
the stack defines with a field map.
- **A member's `sql`** is either a column of that object, or a
relationship path whose leaf is read on the object its last hop reaches.
- A hop the cube declares a join for reaches that join's `name`. This is
the door's first tier.
- Every other hop is walked by the shared `resolveFieldPath`. This is
the door's second tier.
- A hop the graph cannot follow is skipped.
- **`'*'`** resolves to nothing and is skipped on both member kinds.
Whether `'*'` belongs on a dimension or on a non-count measure is
#21000's question, and it is not refused here.
**Not judged here:** cube `sum` / `avg` / `min` / `max`, which are the
#21044 door's pairs. See Acceptance notes.
**Runtime door.** The registry entry is unchanged (`runtimeTypes:
['dataset']`). A `dataset` write's per-write snapshot carries no
`analyticsCubes`, so the cube leg reads nothing at the runtime door.
`authoring-rules.ts` gains a comment that says so.
**Files changed:**
- the rule and its tests;
- that comment in `authoring-rules.ts`;
- a cube paragraph in §6 of
`content/docs/deployment/validating-metadata.mdx`;
- one changeset.
`index.ts` is untouched.
## Clause-②: the measured arm
`node scripts/pm/check-widening-tells.mjs --declaration no --diff` on
the final diff exits 0. It also reports all 5 files as NOT MEASURED,
because no declared surface covers `packages/lint`, so it decided
nothing.
The arm therefore comes from the dispatch's criterion:
- The diff adds 0 `export` lines in `packages/lint/src` (tests
excluded). The positive control, the model commit `5e470f8c1c`, adds 1.
- `index.ts`, `runtime.ts` and `package.json` are unchanged.
So the diff adds no symbol and no listing row, and the arm is `no
(narrowing)`. The changeset is still BREAKING because metadata that used
to pass `os validate` can now fail. It carries `!`, the banner, `minor`,
and one ADR-0087 marker: `not-required (no-migration-prescription)`.
## `os validate`, before (`4b20c84748`) and after (`07a308acdc`)
Each fixture is `fx_ledger` plus one cube, `fx_cube`.
| fixture | before | after |
|---|---|---|
| `cube-json-dim` (dimension `meta`, `sql: 'meta'`, over `json`), the
card's measured instance | exit 0 | **exit 1**
(`dimension-json-stored-field-refused`) |
| dimension over `tags` | exit 0 | **exit 1** |
| dimension over `select` with `multiple: true` | exit 0 | **exit 1** |
| dimension over `account.hq` (lookup to `fx_account`, where `hq` is
`json`) | exit 0 | **exit 1** |
| `count_distinct` over `json` | exit 0 | **exit 1**
(`measure-aggregate-field-type-refused`) |
| `count_distinct` over `select` with `multiple: true` | exit 0 | **exit
1** |
| scalar control (see below) | exit 0 | exit 0 |
The scalar control has dimensions over `text`, a single `select` and
`account.name`, and `count_distinct` over `text` and a single `select`.
**Corpus after:** `os validate` passes on `examples/app-showcase`,
`app-crm` and `app-todo`, with 0 findings of either id. App-showcase
holds `showcase_delivery`, the repo's one real cube.
## Tests
`src/validate-dataset-measure-aggregates.test.ts` gains 11 cube tests:
- each shape, paired with a scalar control;
- a sweep of every `FieldType`, flagged and not, against the door's
predicates;
- the reference tier and the declared-join tier;
- the skips and the `'*'` wildcard;
- report order;
- `runAuthoringRules` on all three commands.
Runs at `07a308acdc`:
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: 119
files, 5586 tests passed.
- `pnpm --filter @objectstack/lint typecheck`: exit 0. `--listFiles`
shows the test file is in the `tsconfig.test.json` program.
**Byte identity of the dataset leg.** I ran BASE's rule and HEAD's rule
over 98 stacks: every aggregate × every `FieldType` × the `multiple`
flag, plus joined paths and missing names. They produced 920 findings
with 0 mismatches. As a control, a perturbed copy of the old rule
produced 26 mismatches.
**Ablations.** Each one went through `scripts/ablation-replace.mjs` on
the committed tree. Every restore was proven by its blob hash matching
HEAD and by an empty `git diff HEAD`.
| ablation | result |
|---|---|
| A1: cube walk dropped | 8 red / 37 green |
| A2: declared-join tier disabled | 1 red (the join test: without the
tier, the lookup's target is judged instead of the join's) |
| A3: cube `count_distinct` leg disabled | 5 red |
| A4: cube measure leg widened to every aggregate | 1 red (`judges only
count_distinct`) |
## Gates
All gates ran at `07a308acdc`:
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 90 commands. I ran all 90.
- I also ran the 4 roster gates whose roster sits under a changed path:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`.
- All 94 exit 0.
- `check:skill-examples` and `check:dual-build-cjs-loads` first exited 3
(PREREQUISITE NOT MET: 8 packages had no `dist`). After those packages
were built, both exit 0.
- `--ran` reconciliation: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN.
`main` has moved 6 commits since the branch point, and the branch is not
merged with it. None of those commits touches `packages/lint` or the
docs page. CI's merge ref tests the combined state.
## Changeset
`.changeset/21082-cube-member-json-stored-refused.md`:
`@objectstack/lint` `minor`, BREAKING. It states what is refused, what
an author now sees, and what is unchanged.
## Acceptance notes
- **Cube `sum` / `avg` / `min` / `max` are not judged at authoring.**
The cube door from #21044 refuses those pairs at query time. Measured at
`07a308acdc`: a cube `sum` over a `text` column passes `os validate`
with exit 0. This is the same family as this card. It is reported to the
seat, not fixed here.
- **The runtime `analytics_cube` write door runs no authoring rule.** No
rule declares that type, and `TYPE_TO_STACK_KEY` has no row for it. This
was read from code, not measured. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` dimension over a JSON-stored column, and a cube `count_distinct` measure over one, which the analytics door already refuses at query time
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at authoring of two cube member TARGETS the analytics door already refuses with 400 INVALID_FIELD at query time: an analyticsCubes dimension whose sql column is declared structured-JSON (json, composite, repeater, record, location, address, vector) or multi-value (multiselect, checkboxes, tags, or a select, radio, lookup, user, file or image declared multiple: true), and a count_distinct measure whose sql column is either. It is the cube face of the dataset dimension refusal, which declared this category for the same door. No authorable key, spelling, export or stored shape moves: CubeSchema keeps parsing every member, no stored row is read or rewritten, and which scalar part of a document, or which member of a list, an author meant to group on or count is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); dataset-measure-aggregate-field-type-refused scopes itself to DatasetMeasureSchema rows and no ADR-0087 id covers a cube member (not already-registered); and the change is a rule verdict, not a declaration (not runtime-interface-only or type-surface-only). -->
10
+
11
+
**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. An authored analytics cube (`defineStack({ analyticsCubes })`) is queried through the same analytics door as a compiled dataset, and that door refuses a query that groups by a JSON-stored column, or counts its distinct values, with `400 INVALID_FIELD` before any SQL is built. So such a member could be declared but never served, and until now no authoring rule read `analyticsCubes` at all. The dataset rule's two ids now judge cube members as well: `dimension-json-stored-field-refused` and `measure-aggregate-field-type-refused` (gating, `error`). It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed.
12
+
13
+
**What is refused.** On a cube whose `sql` names an object the stack defines: a `dimensions` entry whose `sql` column is declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`); and a `measures` entry of `type: 'count_distinct'` whose `sql` column is either. The column is the member's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). The classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, `isMultiValueField` and the `count_distinct` row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY`, the predicates the door reads.
14
+
15
+
**What an author sees now.** The finding names the cube, the member, the column, the object that declares it and its declaration, and says the analytics door refuses it with `400 INVALID_FIELD`. It names the route: group by, or count the distinct values of, a field that stores one scalar value; for a multi-value field, filter by one member with `$contains` in a record query. It is located at `analyticsCubes[N].dimensions.KEY.sql` or `analyticsCubes[N].measures.KEY.type`, where `KEY` is the member's key.
16
+
17
+
**Unchanged.** Every dataset finding, word for word. A cube member over any other column, a single-value `select` or `lookup` included; a `count` measure, and a `sum`, `avg`, `min` or `max` measure, which this check does not judge; the row wildcard `'*'`; a member whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save, and a `dataset` save's snapshot carries no cubes.
0 commit comments