Repository navigation
Commit 31ce45c
skills(objectstack-ui, objectstack-data): teach the aggregate × field-type rule and the multi-value dimension rule (#24439)
Fixes #24425
Clause-②: no
## 维护者速读(草稿)
**改了什么** — 对外发布的两个 skill 各补一条规则,规则只在 objectstack-ui
陈述一次,objectstack-data 只做交叉引用。① 数据集度量的 `sum`/`avg`/`min`/`max`
必须落在**已存储**的数值或布尔字段上;`formula` 字段是虚拟列(不落库),不管它声明的 `returnType` 是什么都会被拒
—— 要合计一个计算值,就把数存下来(用户录入、`summary` 汇总、或由 hook 维护的存储字段),或者把部件如实标成
`count`。② 多值字段(`multiselect` / `checkboxes` / `tags`,以及任何 `multiple:
true` 的字段)不能当分组维度,数据集维度、cube 维度、引擎 `groupBy` 一视同仁 —— 改用按成员的 `$contains`
过滤(一个成员一个部件)或换单值轴。各配一个几行的例子(请假天数合计、按标签分布)。`rules/field-types.md` 的
`formula` 行加一句指向该规则。评测夹具加两条用例,`must_not_contain` 钉住错误答案必然写出的拼法。补丁轮(裁决 A
之后):`scripts/check-skills-token-ratchet.mjs` 里该评测夹具的上限按实测值从 1102 移到
1229,行旁按文件自身惯例写入裁决原话与算术;契约复核的两处小修 ——
请假天数例子里存储字段另起名(`days_count`,公式留在表单上)并对它求和,评测 7 的 `must_not_contain` 补上
cube 维度拼法 `sql: 'tags'`。
**为什么改** — 维护者在 cloud 卡 2861 裁决 B 后的原话:「2861 这个应该是 skills
也要优化吧」。平台早已拒绝这两种写法(spec 的 aggregate × field-type 表、分析维度门与引擎 groupBy 门、以及
`os validate` 的 lint 腿),发布的 skill 却没教,AI 会先写出被拒的形状、再从拒绝里学。本 PR
把规则放到作者决定部件时已在读的位置(Dataset-Bound Widgets 的标准答案列表)。上限上移的依据是维护者 2026-10-19
对决策批次 369 第 1 项(字母 A)的裁决「其他同意」(卡上裁决评论 6259315766),按棘轮自身惯例「先付再抬」。
**风险与代价(含回滚)** — 四个文件:三个 `skills/**` 文本文件加棘轮脚本一行上限及其记录块;无平台代码,无
changeset(`skip-changeset`,`scripts/` 不发布)。token 棘轮在最终 head
全绿:`rules/dashboards.md` 6251/6252(余量 1),`rules/field-types.md`
3116/3158,`evals/analytics-inline-vs-dataset.json` 1229/1229(按实测钉死,余量
0);新增 token
全部由同文件内的无损折叠或删除他处已有陈述支付(下表逐条列出存活位置),评测夹具付到底后的余额由裁决抬上限补齐(+127)。回滚 =
revert 本 PR 的两个 commit,无数据与运行时影响。
**席位意见** —
**你要做的** — 批准并手合(Tier H,`skills/**`):本 PR 保持 draft,由你手工合并。
## Ceiling raise — letter A, paid down first, raised for the remainder
Maintainer ruling 2026-10-19, decision batch 369 item 1, letter A (the
card was presented as 「推荐:A」), verbatim and untranslated: 「其他同意」 —
ruling comment 6259315766 on the card; the director's delta 6259193569
agreed. The row
`skills/objectstack-ui/evals/analytics-inline-vs-dataset.json` in
`scripts/check-skills-token-ratchet.mjs` moves from 1102 to 1229, pinned
AT the measurement taken on the final head (zero headroom). Arithmetic,
in bytes because that is what the convention divides: on `origin/main`
(`e22defb13`) the file was 4388 bytes → 1097 tokens (headroom 5). The
existing text was folded losslessly to 3780 bytes → 945 tokens first
(the description, the five existing `expected_output` strings, eval 2's
note and prompts 1 / 3 / 4 tightened, every fact and every assertion
kept — the round-1 estimate said "about 1020"; measured, it is 945); the
two cases then add +1135 bytes, so the landed file is 4915 bytes → 1229
tokens; the 5 tokens of existing headroom are absorbed and the ceiling
moves 1102 → 1229 (+127). Reproducible without the script: `wc -c` on
the fixture answers 4915, and `ceil(4915 / 4)` is 1229. The record block
beside the row mirrors the file's 「17310 允许增加」 convention; the
shrink-only testimony above `CEILINGS` is untouched (`pnpm
check:ratchet-remedy-authority` exit 0; `node
scripts/check-skills-token-ratchet.mjs --self-test` exit 0, 79 cases).
No other ceiling moves, no new fixture, no new gate.
## What this teaches, measured against `main` at e22defb
**Rule 1 — a dataset measure's `sum` / `avg` / `min` / `max` needs a
STORED field of an accepted type; a `formula` is virtual and refused
whatever its `returnType` says.** Sources read:
`packages/spec/src/data/aggregate-field-type-compatibility.ts` (the one
table: numeric class plus booleans, `percent` not under `sum`,
`min`/`max` also the temporal class; `formula` named as VIRTUAL in SQL
storage); `packages/services/service-analytics/src/dataset-compiler.ts`
(`DATASET_INVALID` / 400 before any query is built);
`packages/lint/src/validate-dataset-measure-aggregates.ts` (the
authoring-time leg behind `os validate`);
`packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts`
(the remedy: store the value as a number and aggregate that). Worked
example: leave days — `sum` over the `formula` `days_requested` (`end -
start`) is refused; a stored `number` `days_count`, written by a
`beforeInsert` / `beforeUpdate` hook (the `defineHook` event names of
objectstack-data's data-hooks reference), is summed instead, so the
example and eval 6's prompt (the formula stays on the form) agree.
**Rule 2 — a multi-value field cannot be a group-by dimension: dataset
`dimensions`, a cube dimension and the engine's `groupBy` alike.**
Sources read:
`packages/services/service-analytics/src/structured-json-dimension-door.ts`
and `packages/objectql/src/group-by-structured-json-door.ts`
(`INVALID_FIELD` / 400 at both doors; the class is `isMultiValueField`
in `packages/spec/src/data/field-value.zod.ts`: `multiselect` /
`checkboxes` / `tags`, or `multiple: true` on `select` / `radio` /
`lookup` / `user` / `file` / `image`; no bucket-per-member grouping
exists). The route the rule names, one widget per member filtered `{
tags: { $contains: 'urgent' } }`, is a membership test on a
dataset-bound widget too:
`packages/services/service-analytics/src/contains-membership-sql.ts`
lowers `$contains` on a multi-value or JSON-stored field as membership
in the analytics where clause (`whereContainsMembershipSql` in
`strategies/native-sql-strategy.ts`) and in the read scope; the engine
path lowers it the same way.
Both rules live in `skills/objectstack-ui/rules/dashboards.md`,
Dataset-Bound Widgets, as two entries of the standardized-answers list
(right after the computed-column entry).
`skills/objectstack-data/rules/field-types.md` cross-references from the
`formula` row. `skills/objectstack-ui/SKILL.md` is untouched: its
Detailed Rules line already points at the section. No new file, no
platform code, no changeset.
## Token ratchet — every added token paid in the same file; the
fixture's remainder raised by ruling
| file | tokens before | tokens after | ceiling | headroom after |
|:--|--:|--:|--:|--:|
| `skills/objectstack-ui/rules/dashboards.md` | 6243 | 6251 | 6252 | 1 |
| `skills/objectstack-ui/SKILL.md` (untouched) | 3843 | 3843 | 3851 | 8
|
| `skills/objectstack-ui/evals/analytics-inline-vs-dataset.json` | 1097
| 1229 | 1229 (was 1102; letter A) | 0 |
| `skills/objectstack-data/rules/field-types.md` | 3157 | 3116 | 3158 |
42 |
Deletions and folds, each with the site where the fact survives:
| # | file | deleted / folded | survives at |
|:--|:--|:--|:--|
| 1 | dashboards.md | ObjectChart jsx fence under the result-row-keying
bullet (7 lines) | the bullet's own prose directly above it (rows keyed
by the raw field names; bind `xAxis.field` / `yAxis[].field` /
`series[].name` to those names; `os validate` checks both halves);
`references/react-blocks.md`, ObjectChart section (`aggregate` prop row
states the same keying); eval 5's expected output carries a worked
block; the `format: '$0,0'` axis key survives in the next bullet (axis
presentation rides on the axis) |
| 2 | dashboards.md | bullet "Dataset-bound widgets need at least one
`values` entry … `os validate` fails on an unresolved name (an empty
chart otherwise)" | the section's opening paragraph (a widget requires
`dataset` + `values`; one lacking `dataset` fails `os validate`) and
`SKILL.md` → Verify your work → Dashboard widget binding (an unresolved
`dataset` / `dimensions` / `values` renders an empty chart) |
| 3 | dashboards.md | child-count entry: dropped the quoted restatement
"on `project`, count related tasks" and "to-many rollup: not
dataset-expressible — use a" | the Level A table's right column (a
to-many rollup onto the parent → escalate) and the iron rule (escalate
to a stored field / a `summary` rollup) |
| 4 | dashboards.md | computed-column entry: dropped the pseudo-example
`margin = difference(revenue, cost)`; "Formatting/currency → a
measure's" folded to "Format/currency → the measure's"; patch round:
"arithmetic over declared measures" → "arithmetic over measures" | the
Level A table (derived measures — `ratio/sum/difference/product` of
other measures) |
| 5 | dashboards.md | dashboard-filter entry: dropped "(date/region)"
and "a dashboard variable + per-chart `filterBindings` broadcast into
each chart's WHERE"; now points at `globalFilters` + `filterBindings`
(below); patch round: "reshapes the query (grain/window/join)" →
"reshapes grain/window/join" | the Dashboard filters section of the same
file (broadcast into every widget's query; the `filterBindings` table) |
| 6 | dashboards.md | to-one-enrichment and filter-by-parent entries
re-phrased ("is fine and does not change grain" → "keeps grain"; "model
it as a dimension" → "a dimension") | same entries, same facts |
| 7 | field-types.md | the blockquote "`multiple: true` lookup ≠
junction object …" after the Relational table | objectstack-data
`SKILL.md` → Relationship Patterns: the same blockquote, a superset (it
adds the positional `{record.tags.0}` reading), plus the Many-to-Many
(simple) table row |
| 8 | evals json | `description` and the five existing `expected_output`
strings, eval 2's `note`, and prompts 1 / 3 / 4 shortened (4388 → 3780
bytes) | every fact and every assertion-bearing clause kept; the caveat
in eval 2's note kept (no other home) |
## Line budget
| file | lines before | lines after |
|:--|--:|--:|
| `skills/objectstack-ui/rules/dashboards.md` | 468 | 465 |
| `skills/objectstack-ui/SKILL.md` | 308 | 308 |
| `skills/objectstack-ui/evals/analytics-inline-vs-dataset.json` | 57 |
77 |
| `skills/objectstack-data/rules/field-types.md` | 327 | 322 |
| `scripts/check-skills-token-ratchet.mjs` (the ceiling row + its record
block; unpublished) | 1456 | 1477 |
Net +12 lines across the four `skills/**` files (budget ≤ +30); +21 in
the ratchet script (the record block, in the file's own convention).
`git diff --numstat e22defb...HEAD`: 4 files, 77 insertions / 44
deletions; 52 of the insertions are new lines (two entries 11, two eval
cases 20, the record block 21), the other 25 are in-place folds or edits
of existing lines (the token payment, the two nits, the ceiling row) —
the gross-additions line (≤ 45) is exceeded by those folds and the
record block and is reported as a deviation. Bundle readings: sum of all
`SKILL.md` lines 4394 → 4394 (unchanged); whole `skills/` tree 13338 →
13350 lines over 65 files; ratcheted authored tokens 144061 → 144160 of
155015 (the ceiling sum moved by the +127; shipped tree 155247 →
155346).
## Evals
No in-repo eval runner exists: `git grep -l must_not_contain origin/main
-- scripts packages/spec/scripts packages/lint` answers 0 files, no
`package.json` script is named `eval`, and
`skills/objectstack-ui/evals/README.md` describes the fixtures as scored
by an assistant harness. The card's acceptance line "the eval cases pass
with the skill's eval runner" is therefore unmet by construction; the
fixture's validity is JSON validity (parsed with the ids 1–7 read back),
the token ratchet and `check:skill-docs`.
- Case 6 (leave-days total): the prompt keeps the `formula` on the form,
so a right answer stores the number under another name (the rule's
example now does the same: `days_count`); `must_not_contain` `field:
'days_requested'` (a dataset measure or an inline aggregate over the
formula) and `sql: 'days_requested'` (a cube measure over it) are what
the wrong answer must spell; `must_contain` `number`.
- Case 7 (by-tag breakdown): `must_not_contain` `field: 'tags'` (a
dataset dimension declared over the multiselect), `groupBy: 'tags'` (an
inline aggregate or an engine query) and, since the patch round, `sql:
'tags'` (a cube dimension, `analytics.zod.ts`) are what the wrong answer
must spell; a right answer filters `{ tags: { $contains: TAG } }` and
never declares a dimension on `tags`; `must_contain` `$contains`.
## Verification — head `6acc9e915`
- Derivation at `6acc9e915`: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no paths; change set from
the merge base `e22defb13`, 4 files) names 50 families (the ratchet
script adds 19 to round 1's 31). `--ran` reconciliation: 50 derived, 46
run, 4 NOT MEASURED, 0 UNRUN. The 4 NOT MEASURED answered exit 3
(PREREQUISITE NOT MET: the tree is not built, and the diff touches no
package): `check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content` — CI's
`Lint & Repo Gates` carries them (success on this head).
- Exit codes captured before any pipe, all 0 unless named: `node
scripts/check-skills-token-ratchet.mjs` 0 — GREEN at the final head ("54
authored bundle file(s) within their ceilings"; the eval file "is 1229
tokens (ceiling 1229; headroom 0)", dashboards.md "6251 tokens (ceiling
6252; headroom 1)"); its `--self-test` 0 (79 cases); `pnpm
check:ratchet-remedy-authority` 0 (275 scripts swept); `pnpm
check:pm-dispatch-gates` 0 (the battery: 1913 fast-tier cases, 280 s,
run via nohup and `tail --pid`); `check:corpus-claim-drift`,
`check:skill-identifier-liveness`, `check:doc-authoring`,
`check:pm-skill-id-lint`, `check:nul-bytes`, `check:skill-frame-sync`,
`check:skill-compatibility`, `check:skill-docs`,
`check:doc-formula-expressions` (after building `@objectstack/formula` +
`@objectstack/lint` under the verify lock), the 19 added families
(`check-declaration-mirrors`, `check-position-name-fold-loaders`,
`check-scripts-symbol-anchors`, `check-self-test-wired`,
`check-self-test-workflow-commands`, `check-whole-set-label-write` and
their self-tests, `bare-root-worklist --self-test`,
`check:bash32-floor`, `check:cli-command-ids`, `check:entry-guard`,
`check:parse-guard`, `check:pnpm-filter-targets`,
`check:pnpm-flag-spellings`) and every other derived family: 0.
- No package is touched, so no build closure or package test is owed;
`test:repo` is declared to CI. No ablation: no tested behaviour changed
(published skill text, an eval fixture and a ceiling row).
- The published commit `1b0c5012b` is untouched (no rebase, no amend, no
force-push); the patch round is one commit on top, `6acc9e915`.
## Acceptance notes
- PM assumption falsified: the card's
`validate-dataset-measure-aggregates.ts` does exist on `main` — `git
cat-file -e
e22defb:packages/lint/src/validate-dataset-measure-aggregates.ts`
succeeds. It is the authoring-time leg of the aggregate × field-type
contract and also carries the dimension leg
(`dimension-json-stored-field-refused`, a dataset dimension over a
JSON-stored or multi-value field, and a cube dimension over such a
column), so `os validate` refuses both shapes at authoring time. Cited
above by its real path.
- Observation, not filed (a stale code comment, no runtime effect;
carrier: none): the module comment of
`structured-json-dimension-door.ts` ("The multi-value route is a RECORD
query's membership filter … It is not this service's own `where`")
predates `contains-membership-sql.ts`, which now lowers `$contains` as
membership in `NativeSQLStrategy.buildFilterClause` as well.
- The harness's commit-attribution reminder names a model; the
repository's pre-push hook refused that trailer on the first
(unpublished) push of round 1, and the commit was amended to the
model-free pair before anything reached the remote — no published
history was rewritten.
- Labels other than `skip-changeset` on this PR (`documentation`,
`size/m`) are the labeler's, not set by the dev, left alone.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01V1upgJbYCZ5xed9pEQF4Tz)_
---------
Co-authored-by: Claude Code <noreply@anthropic.com>1 parent 83e31cd commit 31ce45c
4 files changed
Lines changed: 77 additions & 44 deletions
File tree
- scripts
- skills
- objectstack-data/rules
- objectstack-ui
- evals
- rules
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
573 | 573 | | |
574 | 574 | | |
575 | 575 | | |
576 | | - | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
577 | 598 | | |
578 | 599 | | |
579 | 600 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | 100 | | |
106 | 101 | | |
107 | 102 | | |
| |||
126 | 121 | | |
127 | 122 | | |
128 | 123 | | |
129 | | - | |
| 124 | + | |
130 | 125 | | |
131 | 126 | | |
132 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
| 7 | + | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
| 18 | + | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
29 | | - | |
| 28 | + | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
55 | 75 | | |
56 | 76 | | |
57 | 77 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
101 | 109 | | |
102 | 110 | | |
103 | 111 | | |
| |||
106 | 114 | | |
107 | 115 | | |
108 | 116 | | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
119 | 124 | | |
120 | 125 | | |
121 | 126 | | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | | - | |
130 | 127 | | |
131 | 128 | | |
132 | 129 | | |
| |||
0 commit comments