Skip to content

Commit f6e1db1

Browse files
docs(skills): objectstack-formula eval for a mail amount in each record's own currency (currency:@path) (#27922)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DXVvNqWc3NAaChEvSCx3DJX)_ domain:skills#2 · os-elon-musk · session_016WV4d1BfaajCZsT5GLe2M7 Fixes #26940 Ruling-ref: 6512920986 ## What changes - `skills/objectstack-formula/evals/cel-predicates-formulas.json`: case 8 asks for an `email_template` that mails one overdue notice per `contract`, the amount shown in that contract's own currency. The reference answer is `{{ amount | currency:@currency_code }}`: only a mail template may read the code from the send's data; the stored code is three capitals (`EUR`), exactly as stored; when the template declares `variables`, the path must be one of them or lie under one. `must_contain`: `currency:@`. `must_not_contain`: the bare `currency:currency_code`, `currency:USD`, `currency:CNY` and the no-argument `| currency }}`. Six restatements in the reference answers of cases 1, 2, 4, 5 and 7 are shortened (-160 bytes); every assertion and every fact is kept. - `scripts/check-skills-token-ratchet.mjs`: that file's row moves 1352 → 1506, the row comment quoting the ruling below. - `.changeset/26940-formula-eval-currency-at-path.md`: `@objectstack/skills` patch. The eval case, the six trims and the changeset are the blocked round's commit `1c67d742`, cherry-picked unchanged (it keeps its own trailer); the ratchet row is the only new change, as the ruling binds. ## Why PR #26937 (#26660) added `currency:@path` to mail templates, and `skills/objectstack-formula/SKILL.md:450` teaches it, but no eval scored it: `grep -rn 'currency:@' skills/objectstack-formula/evals` on `origin/main` `88d60203c` has 0 hits. An AI author writing a per-record overdue mail could write a bare path after the colon (refused at authoring, `currency-code-literal`), hard-code one code for every record, or leave the argument out, which formats every record in the organization's default currency (`packages/formula/src/template-currency-tenant-default.test.ts`; the card first said US dollars, the reference answer follows the implementation). Nothing caught any of the three. The file sat at its ceiling: 5407 bytes of 5408, 1352 tokens. The case costs 194 tokens, the lossless trims paid 40, and no further deletion inside the file pays the remaining 154 (the arithmetic is in the blocked report 6488686813). The maintainer ruled on the director's brief, ruling record 6512920986, letter A, verbatim and untranslated: > 「26940同意,并继续帮我评估这种天花板的价值是什么,为什么要卡住车队的开发进度」 同意 is letter A: raise the row to 1506. The second clause asks the director seat for a separate evaluation of the ceiling and is not part of this PR. ## Verification Token measurements (`node scripts/check-skills-token-ratchet.mjs`, `ceil(utf8 bytes / 4)`): | Reading | Before (`origin/main` `88d60203c`) | After (`ee7516b7`) | Ceiling | |:--|--:|--:|:--| | `skills/objectstack-formula/evals/cel-predicates-formulas.json` | 75 lines · 5407 B · 1352 | 85 lines · 6021 B · 1506 | 1352 → 1506, headroom 0 | | All `skills/*/SKILL.md` | 4610 lines · 226161 B | unchanged | — | | Bundle total, whole shipped tree | 219245 | 219399 (+154) | — | Facts in the reference answer re-read on `origin/main` `88d60203c`: `skills/objectstack-formula/SKILL.md:448-450`; `CURRENCY_ARGUMENT_RULE` and the mail-dialect refusal of a bare argument in `packages/formula/src/template-engine.ts`; `isDeclaredPath` and `currency-code-path-undeclared` in `packages/formula/src/mail-hole-grammar.ts`; the tenant-default test above. Eval self-consistency (every `must_contain` string in its own `expected_output`, no `must_not_contain` string in its own prompt or answer): `cases=8 failures=0`. JSON parses; no control bytes in the three files; `eslint --no-inline-config` on the edited script is clean; `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` passes. Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 60 families on `ee7516b7` (after the last commit, in a fresh worktree after `pnpm install --frozen-lockfile --ignore-scripts`); `--ran` reconciliation: 60 accounted for, 58 run and exit 0, 2 NOT MEASURED (exit 3, prerequisite: whole-tree `dist/`): `check:dual-build-cjs-loads`, `check:lean-entry-closure`. Among the 58: `check-skills-token-ratchet` and its `--self-test` (79 cases), `check:skill-compatibility`, `check:skill-identifier-liveness`, `check:skill-frame-sync`, `check:published-files`, `check:pm-governed-merges`, `check:nul-bytes`, `check-empty-changeset` and `check-adr-0087-registration` against `origin/main`, and `check:doc-formula-expressions` after building `@objectstack/formula` and `@objectstack/lint`. `check:dts-closure` and `check:sourcemap-no-sources-content` ran green over the 4 packages built here only; CI sweeps the whole tree. No package source changed, so no package test or typecheck is owed; `ui/scripts/check-skill-eval-tokens.mjs` grades `skills/objectui` only, so it does not read this file. Clause-②: no Metadata-protocol: none ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No spec change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; eslint on the edited script passes (full `pnpm lint` left to CI). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. ## 维护者速读 - 改了什么:objectstack-formula 技能新增第 8 条 eval:按合同各自的货币发逾期邮件,期望写 `{{ amount | currency:@currency_code }}`,拒收裸路径、写死 `USD`/`CNY` 与不带参数三种写法。 - 为什么改:PR #26937 已让邮件模板支持 `currency:@path`,技能也在教,但没有任何 eval 计分;AI 写错了没人发现。 - 风险与回滚:只动 eval 文件、ratchet 一行与 changeset,无 schema、键或运行时行为变化;回滚即 revert 本 PR。 - 先看:`scripts/check-skills-token-ratchet.mjs` 的 1352 → 1506 行注释(引裁决 6512920986);再看 eval 第 8 条。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期该文件一行显示 `1506 tokens (ceiling 1506; headroom 0)`,整体 exit 0。 - 需要你做的:欠 CONTRACT_REVIEW_TIER 契约复审;落地为 Tier H,待受管批准人批准。 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4d1BfaajCZsT5GLe2M7 --------- Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 427c943 commit f6e1db1

2 files changed

Lines changed: 27 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 gains an eval for a mail amount shown in each record's own currency (`currency:@path`)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The skill taught `{{ amount | currency:@currency_code }}` for a mail template that shows each record's amount in that record's own currency, but no eval checked that an AI author reaches for it. An author could write a bare path after the colon (refused at authoring), hard-code one currency code for every record, or leave the argument out (the organization's default currency for every record), and nothing scored that wrong.
11+
- **Now.** Case 8 of `evals/cel-predicates-formulas.json` asks for an `email_template` that mails one overdue notice per contract with the amount in that contract's own currency. Its reference answer names the marked form, that only a mail template may read the code from the send's data, that the stored code is three capitals exactly as stored, and that a declared `variables` list must name the path or a root above it; the assertions require `currency:@` and refuse the bare path, a hard-coded code and the no-argument form. Six reference answers of existing cases are shortened where they restated a point they already make, with every assertion and fact kept.
12+
- No schema, key or behaviour changes.

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

Lines changed: 15 additions & 5 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``.",
7+
"expected_output": "Two `type: 'formula'` fields, `expression` under the `F` tag. The text one 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.",
17+
"expected_output": "By the legacy → CEL table, `P`isBlank(record.po_number) && record.amount > 10000 && record.status != 'draft'``; a `script` validation is inverted (it FAILS when true, so keep the failing condition), and `record.amount > 10000` on a 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`"],
@@ -34,7 +34,7 @@
3434
{
3535
"id": 4,
3636
"prompt": "Demo opportunities should close 45 days after the package is installed, and a formula should show how many days remain until close.",
37-
"expected_output": "Seed value as a CEL envelope, `close_date: cel`daysFromNow(45)``, never a JS `Date` / `Date.now()` (the determinism gate: two `os build` runs must be byte-identical). Days remaining is `daysBetween(today(), record.close_date)` — never `record.close_date - today()` (a duration stored as `{}`) and never date arithmetic on `today()` (it faults).",
37+
"expected_output": "Seed value as a CEL envelope, `close_date: cel`daysFromNow(45)``, never a JS `Date` / `Date.now()` (two `os build` runs must be byte-identical). Days remaining is `daysBetween(today(), record.close_date)` — never `record.close_date - today()` (a duration stored as `{}`) and never date arithmetic on `today()` (it faults).",
3838
"files": [],
3939
"assertions": {
4040
"must_contain": ["cel`", "daysFromNow(", "daysBetween("],
@@ -44,7 +44,7 @@
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.",
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 with explicit null tests ahead of the comparison: `record.start_date != null && record.end_date != null && record.end_date < record.start_date`; a `has()` guard does not satisfy the publish gate.",
4848
"files": [],
4949
"assertions": {
5050
"must_contain": ["visibleWhen", "!= null", "record.end_date"],
@@ -64,12 +64,22 @@
6464
{
6565
"id": 7,
6666
"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.",
67+
"expected_output": "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.",
6868
"files": [],
6969
"assertions": {
7070
"must_contain": ["record.kind", "visibleWhen", "P`"],
7171
"must_not_contain": ["label(", "{record."]
7272
}
73+
},
74+
{
75+
"id": 8,
76+
"prompt": "An `email_template` that mails one overdue notice per `contract`, the amount shown in that contract's own currency.",
77+
"expected_output": "`{{ amount | currency:@currency_code }}` — only a mail template may read the code from the send's data. The stored code is three capitals (`EUR`), exactly as stored; when the template declares `variables`, the path must be one of them or lie under one. A bare path is refused, a literal code fixes one currency for every record, and no argument means the organization's default currency.",
78+
"files": [],
79+
"assertions": {
80+
"must_contain": ["currency:@"],
81+
"must_not_contain": ["currency:currency_code", "currency:USD", "currency:CNY", "| currency }}"]
82+
}
7383
}
7484
]
7585
}

0 commit comments

Comments
 (0)