Skip to content

Commit 40264be

Browse files
docs(skills): objectstack-formula evals exercise label(record.kind), and refuse it in a predicate (#26810)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi91AMradEff12Xm38NWmyrwr)_ Fixes #26760 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`objectstack-formula` 技能的 eval 文件新增两例 —— 正例(用 select 字段拼记录名,须写 `label(record.kind)`,禁止硬编码标签的三元)、反例(校验规则与 `visibleWhen` 里禁止出现 `label(`,它在谓词里会报错并中止写入);同文件删掉 4 句只是复述自身 `must_not_contain` 断言的话(212 字节),token 棘轮行按实测 1046 → 1352 上调,附一条 `@objectstack/skills` patch changeset。 - **为什么改**:#26755 在 SKILL.md 教了 `label(record.f)`,但没有任何 eval 检查 agent 真会用它,也没有 eval 拦住把它写进校验谓词(校验谓词不可求值 = 每次写入都失败)。 - **风险与回滚**:只动已发布技能包的 eval 数据与棘轮上限,无运行时代码;回滚即 revert 本 PR。 - **先看**:`skills/objectstack-formula/evals/cel-predicates-formulas.json`(case 6、7 与 case 1/2/3/5 的删句),其次 `scripts/check-skills-token-ratchet.mjs` 的新行注释(算术),最后 changeset。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs`,预期看到 `cel-predicates-formulas.json is 1352 tokens (ceiling 1352; headroom 0)` 与整体 ✓。 - **需要你做的**:批准本 PR(`skills/**` 为 Tier H,需 GOVERNED_APPROVERS 批准后由席位落地)。 ## What changes - **`skills/objectstack-formula/evals/cel-predicates-formulas.json`** — two cases join the five: - **Case 6 (positive).** Prompt: name `contract_version` records by version number and the `kind` select so a zh-CN reader sees `v2 · 清稿` and an en-US reader `v2 · Clean`. Expected: a `type: 'formula'` `nameField` spelled `F`'v' + string(record.version_no) + ' · ' + label(record.kind)``; `label()` takes exactly `record.FIELD` of a select field, reads the option label in the reader's locale, `''` for null. `must_contain: ["label(record.kind)", "F`"]`; `must_not_contain: ["== 'clean' ?", "{record."]` — the hard-coded label ternary the card describes. - **Case 7 (negative, per the seat note on the card).** Prompt: refuse a `contract_version` save unless `kind` is the clean copy, and show `signed_at` only once `status` is signed. Expected: predicates over the STORED value — a `script` validation `P`record.kind != 'clean'`` (it fails when true) and `visibleWhen: P`record.status == 'signed'``; the `label` macro evaluates only in a formula field and faults in a validation, `visibleWhen` or hook predicate, aborting every write. `must_contain: ["record.kind", "visibleWhen", "P`"]`; `must_not_contain: ["label(", "{record."]`. - **Paid down first.** Four sentences in cases 1, 2, 3 and 5 restated their own `must_not_contain` assertions and are cut, 212 bytes = 53 units: case 1 "Field references are bare `record.FIELD`, never brace-wrapped." (asserted by `{record.`), case 2 "Uses `'draft'` single quotes and bare `record.` references." (`{record.`; the `'draft'` spelling stays in the expression), case 3 "No `ISCHANGED()` function." (`ISCHANGED(`), case 5 "Canonical property names only — never `conditionalRequired`." (`conditionalRequired:`). Every other sentence carries a fact (why a guard is needed, what a shape stores, which gate refuses) and stays; every assertion stays. - **`scripts/check-skills-token-ratchet.mjs`** — the file's row rises AT the measurement, 1046 → 1352, headroom 0, with the arithmetic in its comment under the standing ruling 「A:先削重复,余额按实测提高」: 1046 − 53 (the cut) = 993; + 1438 bytes = 359 units (the two cases) = 1352. - **`.changeset/26760-formula-label-eval.md`** — `@objectstack/skills` patch (the package's `dist` is built from `skills/**`, so the bundle publishes). **Behaviour the cases pin is read off `origin/main` `8b276385`, not invented.** `packages/formula/src/stdlib.ts` `bindOptionLabel`: `label` is a macro taking exactly `record.FIELD`; `optionLabelOf` answers `''` for null and THROWS when the evaluation context carries no `optionLabels` — which is what a validation, visibility or hook predicate's context is (only the data engine's formula-field reads pin the map, per the #26755 changeset), so the engine reports `{ ok: false, kind: 'runtime' }` and the write is aborted. `packages/formula/src/option-label.test.ts` pins both ("refuses at run time when the context carries no option labels — never the stored value"; "compiles the one argument shape and refuses every other"). `content/docs/data-modeling/formulas.mdx` § "Record name from an option label" says the same for predicates: "fails loudly instead of comparing against a label; compare the stored value there". ### Readings (`skills/**` touched) | Reading | Before | After | |:--|--:|--:| | `evals/cel-predicates-formulas.json` lines | 55 | 75 | | `evals/cel-predicates-formulas.json` tokens (`ceil(utf8 bytes / 4)`) | 1046 (4181 B) | 1352 (5407 B) | | whole package, sum of all `skills/*/SKILL.md` lines | 4606 | 4606 | | whole package, sum of all `skills/*/SKILL.md` tokens | 56132 (224526 B) | 56132 (224526 B) | | ratchet row for the file | 1046 | 1352 (headroom 0) | No `SKILL.md` moves; the net addition is +306 units in one eval file, paid first by the 53-unit cut and then raised at the measurement. ## Tests No runtime code changes. The eval file is read by no runner in this repository; `scripts/check-skills-token-ratchet.mjs` is its only reader and is in the gate list below. JSON parses (`JSON.parse` over the file at head, 7 cases); every `must_contain` string is absent from the same case's `must_not_contain` and present in its `expected_output`; in cases 6 and 7 no `must_not_contain` string appears in the `expected_output`. ## Acceptance notes - The card's "hard-coded label ternary" is pinned as the literal `== 'clean' ?` (the stored value the prompt names, compared and branched on); a ternary over another spelling would pass `must_not_contain`, and the `must_contain` of `label(record.kind)` is what carries the teaching. - Case 7's expected output spells the macro as "the `label` macro" rather than `label()` so the ideal answer itself does not carry the string the case refuses (the same contradiction case 3 had on `main`, `ISCHANGED()` inside its own refusal, is gone at this head: the paid-down cut removed that sentence). - `check:skill-docs` / `check:skill-refs` / `check:skill-examples` read `SKILL.md`, `references/` and rule code fences, not eval JSON; the derived gate list below does not name them, consistent with that. ## Verification Gate list derived at head with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (60 commands; the model tier line reads MANDATORY, `skills/**` clause ①, no one-line exit), each run with stdin closed, reconciled with `--ran`. Readings follow in the os-dev-report comment on the card. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPm7NR24Xypi61eyg8)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent bdb00ff commit 40264be

3 files changed

Lines changed: 48 additions & 5 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-formula evals check that a record name built from a select field reaches for `label(record.kind)`, and that a predicate never calls `label()`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `skills/objectstack-formula/SKILL.md` teaches `label(record.f)`, but no eval in `evals/cel-predicates-formulas.json` exercised it: an agent authoring a record name from a select field could hand-roll a ternary over the stored value with hard-coded English or Chinese strings, and nothing caught it; nor did anything catch `label()` written into a validation rule, where it faults and aborts every write.
11+
- **Now.** Eval 6 asks for a `contract_version` name built from its `kind` select and expects `label(record.kind)` in an `F` formula, refusing a `== 'clean' ?` ternary. Eval 7 asks for a validation rule and a `visibleWhen` on the same object and refuses `label(` in either, expecting predicates over the stored value. Four sentences in evals 1, 2, 3 and 5 that restated their own `must_not_contain` assertions are gone; every fact and assertion stays.
12+
- No schema, key or behaviour changes.

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,18 @@ export const CEILINGS = new Map([
882882
// Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
883883
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
884884
['skills/objectstack-data/evals/objects-fields-relationships.json', 1050],
885-
['skills/objectstack-formula/evals/cel-predicates-formulas.json', 1046],
885+
// 1046 -> 1352 (card #26760): two eval cases for the `label(record.FIELD)`
886+
// teaching — case 6 asks for a record name built from a select field and
887+
// expects `label(record.kind)`, never a hard-coded label ternary; case 7
888+
// asks for a validation and a `visibleWhen` and refuses `label(` there,
889+
// where it faults and aborts the write. Maintainer ruling 2026-10-27 on the
890+
// decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A,
891+
// verbatim and untranslated: 「A:先削重复,余额按实测提高」. PAID DOWN FIRST:
892+
// four sentences in cases 1, 2, 3 and 5 that restated their own
893+
// `must_not_contain` assertions, 212 bytes = 53 units (1046 -> 993); the two
894+
// cases add 1438 bytes = 359 units (993 -> 1352); pinned AT the measurement,
895+
// zero headroom. The raising PR's body carries the same arithmetic.
896+
['skills/objectstack-formula/evals/cel-predicates-formulas.json', 1352],
886897
// 1004 -> 1451 (2026-10-27, the objectstack-i18n fix card of the skills gap
887898
// audit): two eval prompts for the teaching that card adds — the `reports`,
888899
// `positions`, `permissions` and `picklists` groups; a form view `title`, a

‎skills/objectstack-formula/evals/cel-predicates-formulas.json‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{
55
"id": 1,
66
"prompt": "Add a `full_name` formula on `contact` built from salutation, first name and last name, and a `margin_pct` formula on `deal` = (revenue - cost) / cost * 100.",
7-
"expected_output": "Two `type: 'formula'` fields whose `expression` is CEL via the `F` tag. The text formula coalesces every nullable operand — `F`coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '') + ' ' + coalesce(record.last_name, '')`` — because `null + 'foo'` throws. The numeric one guards the divisor: `F`coalesce(record.cost, 0) > 0 ? ((coalesce(record.revenue, 0) - record.cost) * 100) / record.cost : 0.0``. Field references are bare `record.<field>`, never brace-wrapped.",
7+
"expected_output": "Two `type: 'formula'` fields whose `expression` is CEL via the `F` tag. The text formula coalesces every nullable operand — `F`coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '') + ' ' + coalesce(record.last_name, '')`` — because `null + 'foo'` throws. The numeric one guards the divisor: `F`coalesce(record.cost, 0) > 0 ? ((coalesce(record.revenue, 0) - record.cost) * 100) / record.cost : 0.0``.",
88
"files": [],
99
"assertions": {
1010
"must_contain": ["F`", "coalesce(", "record."],
@@ -14,7 +14,7 @@
1414
{
1515
"id": 2,
1616
"prompt": "Translate this Salesforce validation rule into ObjectStack: AND(ISBLANK(po_number), amount > 10000, NOT(ISPICKVAL(status, 'draft'))).",
17-
"expected_output": "Applies the mechanical legacy → CEL table: `P`isBlank(record.po_number) && record.amount > 10000 && record.status != 'draft'``, noting that a `script` validation is inverted (it FAILS when the expression is true, so keep the failing condition) and that `record.amount > 10000` on a declared-nullable field needs an explicit `record.amount != null &&` guard or the publish gate rejects it. Uses `'draft'` single quotes and bare `record.` references.",
17+
"expected_output": "Applies the mechanical legacy → CEL table: `P`isBlank(record.po_number) && record.amount > 10000 && record.status != 'draft'``, noting that a `script` validation is inverted (it FAILS when the expression is true, so keep the failing condition) and that `record.amount > 10000` on a declared-nullable field needs an explicit `record.amount != null &&` guard or the publish gate rejects it.",
1818
"files": [],
1919
"assertions": {
2020
"must_contain": ["isBlank(", "&&", "record.", "P`"],
@@ -24,7 +24,7 @@
2424
{
2525
"id": 3,
2626
"prompt": "Run an automation only when a support case has just become `escalated` — on single updates and on bulk updates alike.",
27-
"expected_output": "Writes it once as `P`previous.status != 'escalated' && record.status == 'escalated'``: `record` is the post-write state (true on every later update too), so only `previous` says \"just became\"; `previous` is bound per row on `before*`/`after*` update events, bulk too. On insert `previous` is `null`: the `previous.status` read faults and aborts the write, so to run on inserts guard with `previous != null &&`. No `ISCHANGED()` function.",
27+
"expected_output": "Writes it once as `P`previous.status != 'escalated' && record.status == 'escalated'``: `record` is the post-write state (true on every later update too), so only `previous` says \"just became\"; `previous` is bound per row on `before*`/`after*` update events, bulk too. On insert `previous` is `null`: the `previous.status` read faults and aborts the write, so to run on inserts guard with `previous != null &&`.",
2828
"files": [],
2929
"assertions": {
3030
"must_contain": ["previous.status", "record.status", "!="],
@@ -44,12 +44,32 @@
4444
{
4545
"id": 5,
4646
"prompt": "Hide `discount_reason` unless a discount amount was entered, and reject any `end_date` earlier than `start_date`.",
47-
"expected_output": "`visibleWhen: P`record.discount != null`` (or `!isBlank(record.discount)`) on the field — `has(record.discount)` is not a null check: every declared field exists, so it is always true. The date rule is a `script` validation whose predicate dominates the comparison with explicit null tests: `record.start_date != null && record.end_date != null && record.end_date < record.start_date`; a `has()` guard does not satisfy the publish-time gate. Canonical property names only — never `conditionalRequired`.",
47+
"expected_output": "`visibleWhen: P`record.discount != null`` (or `!isBlank(record.discount)`) on the field — `has(record.discount)` is not a null check: every declared field exists, so it is always true. The date rule is a `script` validation whose predicate dominates the comparison with explicit null tests: `record.start_date != null && record.end_date != null && record.end_date < record.start_date`; a `has()` guard does not satisfy the publish-time gate.",
4848
"files": [],
4949
"assertions": {
5050
"must_contain": ["visibleWhen", "!= null", "record.end_date"],
5151
"must_not_contain": ["conditionalRequired:", "{record."]
5252
}
53+
},
54+
{
55+
"id": 6,
56+
"prompt": "Name `contract_version` records by their version number and the `kind` select, so a zh-CN reader sees `v2 · 清稿` and an en-US reader `v2 · Clean`.",
57+
"expected_output": "A `type: 'formula'` `nameField`: `F`'v' + string(record.version_no) + ' · ' + label(record.kind)``. `label()` takes exactly `record.<field>` of a select field and reads its option label in the reader's locale (`''` for null). The stored value is the same in every locale, so a ternary over it with hard-coded strings cannot follow the reader.",
58+
"files": [],
59+
"assertions": {
60+
"must_contain": ["label(record.kind)", "F`"],
61+
"must_not_contain": ["== 'clean' ?", "{record."]
62+
}
63+
},
64+
{
65+
"id": 7,
66+
"prompt": "Refuse a `contract_version` save unless its `kind` is the clean copy, and show `signed_at` only once `status` is signed.",
67+
"expected_output": "Both are predicates over the STORED value: a `script` validation `P`record.kind != 'clean'`` (it fails when true) and `visibleWhen: P`record.status == 'signed'``. The `label` macro evaluates only in a formula field; in a validation, `visibleWhen` or hook predicate it faults and aborts every write, so compare the stored value, never its option label.",
68+
"files": [],
69+
"assertions": {
70+
"must_contain": ["record.kind", "visibleWhen", "P`"],
71+
"must_not_contain": ["label(", "{record."]
72+
}
5373
}
5474
]
5575
}

0 commit comments

Comments
 (0)