Commit 39a912e
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>1 parent 22c2d6f commit 39a912e
5 files changed
Lines changed: 621 additions & 86 deletions
File tree
- .changeset
- content/docs/deployment
- packages/lint/src
| 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 | |
|---|---|---|---|
| |||
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
235 | 247 | | |
236 | 248 | | |
237 | 249 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
718 | 718 | | |
719 | 719 | | |
720 | 720 | | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
721 | 727 | | |
722 | 728 | | |
723 | 729 | | |
| |||
Lines changed: 240 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
619 | 619 | | |
620 | 620 | | |
621 | 621 | | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
0 commit comments