diff --git a/.changeset/30824-formula-no-group-key.md b/.changeset/30824-formula-no-group-key.md new file mode 100644 index 00000000000..494149e6917 --- /dev/null +++ b/.changeset/30824-formula-no-group-key.md @@ -0,0 +1,13 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the `objectstack-data` field-types formula row says a formula is no dataset or cube dimension (group key) either + +Clause-②: no +Metadata-protocol: none + +- **Before.** The `formula` row of `skills/objectstack-data/rules/field-types.md` said a formula is virtual and not aggregatable, and that a dataset measure needs a stored field. It said nothing about grouping by one, so an AI author reading the row had nothing telling it not to declare a dataset or `analyticsCubes` dimension over a formula. +- **Now.** The row says a formula is neither aggregatable nor a dataset or cube dimension (group key), and that both need a stored field — what the platform refuses today: the authoring rule `dimension-virtual-field-refused`, the dataset compile's `400 DATASET_INVALID`, and every analytics query grouping by one, `400 INVALID_FIELD`. +- `skills/objectstack-ui/rules/dashboards.md` (Dataset-Bound Widgets, the row's pointer): the multi-value breakdown bullet now names a `formula` beside the multi-value types as refused for a dataset or cube dimension and `groupBy`, so the reader lands on the dimension refusal too. +- No schema, key or behaviour changes; the skill text only states refusals that already ship. diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 2d2a9b19146..298cc761aa7 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -697,7 +697,10 @@ export const CEILINGS = new Map([ // headroom, ceiling +49, pinned AT the measurement. Standing ruling (2026-10-27, tap A, // verbatim and untranslated): 「A:先削重复,余额按实测提高」; recognised path per ruling // record 6513306068. - ['skills/objectstack-data/rules/field-types.md', 3207], + // 3207 -> 3199 (#30824): RE-LOCK at the landed count. The formula row now also says + // a formula is no dataset or cube dimension (group key), paid by the Formula example's + // remaining `// see the Calculated table` pointer; 12794 bytes -> 3199 tokens, zero headroom. + ['skills/objectstack-data/rules/field-types.md', 3199], ['skills/objectstack-data/rules/indexing.md', 3241], ['skills/objectstack-data/rules/lifecycle.md', 1590], ['skills/objectstack-data/rules/naming.md', 773], @@ -853,7 +856,12 @@ export const CEILINGS = new Map([ // The file is 25047 bytes -> 6262 tokens, zero headroom, ceiling +41, pinned AT the // measurement. Standing ruling (2026-10-27, tap A, verbatim and untranslated): // 「A:先削重复,余额按实测提高」; recognised path per ruling record 6513306068. - ['skills/objectstack-ui/rules/dashboards.md', 6262], + // 6262 -> 6232 (#30824): RE-LOCK at the landed count. The multi-value breakdown bullet + // adds a `formula` as a refused dimension and `groupBy` (a dataset dimension over one is + // `DATASET_INVALID` at compile); paid by the `compareTo` paragraph's restated "no second + // widget-side vocabulary" sentence and the `DatasetSchema` source pointer that + // `references/_index.md` already carries; 24926 bytes -> 6232 tokens, zero headroom. + ['skills/objectstack-ui/rules/dashboards.md', 6232], // 3011 -> 3141 (#19715, card item 4): a 5-line table of contents at the top, // anchors to the file's existing headings, nothing else changed. +132 tokens, // of which the row's 2 tokens of headroom absorb 2; the CEILING moves 130. diff --git a/skills/objectstack-data/rules/field-types.md b/skills/objectstack-data/rules/field-types.md index 36a333d3bc0..626cbff0d11 100644 --- a/skills/objectstack-data/rules/field-types.md +++ b/skills/objectstack-data/rules/field-types.md @@ -128,7 +128,7 @@ Stored as JSON on the parent row — no separate table / FK: | Type | When to Use | Config | |:-----|:------------|:-------| -| `formula` | Computed from an expression referencing other fields — virtual: not aggregatable, a dataset measure needs a stored field (objectstack-ui → Dashboards → Dataset-Bound Widgets) | `expression` (CEL, `record.` prefixes), `returnType` (`'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'`); `'currency'` carries its own `currencyConfig` | +| `formula` | Computed from an expression referencing other fields — virtual: neither aggregatable nor a dataset or cube dimension (group key); both need a stored field (objectstack-ui → Dashboards → Dataset-Bound Widgets) | `expression` (CEL, `record.` prefixes), `returnType` (`'number' \| 'text' \| 'boolean' \| 'date' \| 'currency'`); `'currency'` carries its own `currencyConfig` | | `summary` | Roll-up aggregation from child records | `summaryOperations` ({ object, field? (omit for `count`), function, relationshipField?, filter? }) | | `autonumber` | Auto-incrementing display format ({0000} counter + optional date / {field} tokens, resets per scope) | `format` (shorthand) / `autonumberFormat` (canonical) — e.g., `"CASE-{0000}"`, `"AD{YYYYMMDD}{0000}"` | @@ -201,7 +201,7 @@ import { F } from '@objectstack/spec'; const total = { type: 'formula', expression: F`record.amount * record.tax_rate`, - returnType: 'number', // see the Calculated table + returnType: 'number', }; ``` diff --git a/skills/objectstack-ui/rules/dashboards.md b/skills/objectstack-ui/rules/dashboards.md index d7165d45aaf..c6f898a9b97 100644 --- a/skills/objectstack-ui/rules/dashboards.md +++ b/skills/objectstack-ui/rules/dashboards.md @@ -38,8 +38,7 @@ and select named `dimensions` + `values`**; the dataset owns the base object, allowed joins, intrinsic filter, dimensions, and certified measures. A widget **requires `dataset` + `values`**; the closed schema rejects `object` / `categoryField` / `valueField` / `aggregate` by name, and one lacking `dataset` -fails `os validate`. The dataset shape is -`DatasetSchema` — see `node_modules/@objectstack/spec/src/ui/dataset.zod.ts`. +fails `os validate`. A widget's presentation-scope `filter` flows into the query as the runtime filter; keep `filter` on the widget when binding a dataset. @@ -102,8 +101,9 @@ Standardized answers to the recurring ambiguous cases: `beforeInsert`/`beforeUpdate` hook writes, then `{ name: 'days_sum', aggregate: 'sum', field: 'days_count' }`. - **Breakdown by a multi-value field.** `multiselect`/`checkboxes`/`tags` and any `multiple: true` field is refused as a dataset or cube dimension and as - `groupBy` (`INVALID_FIELD`; no per-member buckets). One widget per member, - filtered `{ tags: { $contains: 'urgent' } }`, or a single-value axis. + `groupBy` (`INVALID_FIELD`; no per-member buckets); so is a `formula` (at a + dataset's compile, `DATASET_INVALID`). One widget per member, filtered + `{ tags: { $contains: 'urgent' } }`, or a single-value axis. - **Filter by a parent's attribute** → a **dimension** (guaranteed to join); a lookup-path *filter* is not a reliable analytics-path construct. - **A dashboard filter driving several charts** → **not** a dataset: `globalFilters` @@ -228,7 +228,7 @@ no second `filter` is required. `compareTo` is `{ kind, dimension? }` — the same shape the analytics executor reads (`DatasetSelection.compareTo`), so what a widget declares is exactly what -runs. There is no second widget-side vocabulary. +runs. | Key | Value | Behaviour | |:--|:--|:--|