Skip to content

Commit 5fe0b96

Browse files
skills(objectstack-data): an eval asks for a hook's refusals catalogue instead of a thrown English sentence (#27088)
Fixes #26834 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`objectstack-data` 技能的 eval 文件 `evals/hooks-security-seeds-datasources.json` 新增第 7 题:多语言应用里一个 hook 拒绝保存并给用户看一句话,期望答案在 hook 上声明 `refusals: { CREDIT_LIMIT: { message } }`、沙箱 `js` body 只抛 `{ code: 'CREDIT_LIMIT' }`、中文句子放进翻译包的 `hooks` 组;禁止 body 里抛英文句子(`new Error('`)和用 `handler:`(没有 `js` body 的目录会在保存时被拒)。同文件先删掉 5 句只是复述自身 `must_not_contain` 断言的话(453 字节 = 114 token),再按实测把该文件的 token 上限 1404 → 1573(净 +169,席位预算 ≤ +180);`@objectstack/skills` 补一条 patch changeset。 - 为什么改:PR #26831 给 hook 加了 `refusals` 目录并在 `references/data-hooks.md` 教了它,但没有任何 eval 检查 AI 作者真会用它;照旧习惯直接 `throw new Error('...')` 的英文句子会原样到达 zh-CN 用户,而且没有东西在测,技能会悄悄漂移。 - 风险与回滚:只改一个已发布 eval 夹具、棘轮脚本的一行上限(及其注释)和一条 changeset,不动 schema、键或运行时;回滚即 revert 本 PR(上限随之回落)。删掉的 5 句话每一句都仍由同一题的 `must_not_contain` 机械断言覆盖,事实与断言一个不少。 - 先看:① `skills/objectstack-data/evals/hooks-security-seeds-datasources.json` 第 7 题(题面、期望、六条 must_contain、两条 must_not_contain)与第 1–5 题的删句;② `scripts/check-skills-token-ratchet.mjs` 该行注释里的算术;③ `.changeset/26834-hook-refusals-eval.md`。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期看到 `hooks-security-seeds-datasources.json is 1573 tokens (ceiling 1573; headroom 0)` 与末行 `82 authored bundle file(s) within their ceilings`;`node -e "JSON.parse(require('fs').readFileSync('skills/objectstack-data/evals/hooks-security-seeds-datasources.json','utf8'))"` 无输出即解析通过。 - 需要你做的:批准本 PR(`skills/**` 受管面,Tier H;席位不 ready / 不 arm / 不入队)。 ## What changes **`skills/objectstack-data/evals/hooks-security-seeds-datasources.json` — case 7 joins the six.** Prompt: an app serving English and Chinese users; an `order` whose `amount` exceeds its account's `credit_limit` must not be saved; API clients branch on the code `CREDIT_LIMIT`; a zh-CN rep reads `超出账户信用额度`. Expected: a `beforeInsert` / `beforeUpdate` hook on `order` declaring `refusals: { CREDIT_LIMIT: { message: 'Over the account credit limit.' } }`, a sandboxed `body: { language: 'js', source }` that reads the account through `ctx.api` and refuses by code only — `throw Object.assign(new Error(), { code: 'CREDIT_LIMIT' })`, the body's own text never shown — and the zh-CN sentence under the bundle's nested `hooks: { HOOK: { refusals: { CREDIT_LIMIT: { message } } } }`, served whole in the caller's locale; a catalogue on a hook without a `js` body is refused at save. - `must_contain`: `refusals:`, `CREDIT_LIMIT: {`, `code: 'CREDIT_LIMIT'`, `language: 'js'`, `hooks: {`, `超出账户信用额度`. - `must_not_contain`: `new Error('` (a sentence on the throw — the old habit, `throw new Error('Email is required')` as `references/data-hooks.md` still spells its pre-catalogue examples — and also the half-habit `Object.assign(new Error('text'), { code })`, whose text is dead), `handler:` (an in-process handler cannot carry a catalogue; `refusals` without a `js` body is refused at save). Every spelling asserted is the spec's, read on `origin/main` `f708cf22`: `HookSchema.refusals` is `z.record(SCREAMING_SNAKE code, HookRefusalSchema { message, status? })` (`packages/spec/src/data/hook.zod.ts`), answered only on a `language: 'js'` body; the body's literal `code: 'X'` / `e.code = 'X'` throws are read at save by `hookRefusalCodesThrownIn` (`hook-refusal-codes.ts`); the translation key is `hookRefusalMessageKey(hook, code)` = `hooks.HOOK.refusals.CODE.message` (`system/i18n-resolver.ts`), and the bundle declares it as a nested `hooks` record (`system/translation.zod.ts`) — which is why the eval pins the nested `hooks: {` and not a dotted path a correct bundle would never contain (the stack's own `hooks:` is an array, so `hooks: {` cannot match it). The teaching in `references/data-hooks.md` agrees with the spec on every spelling; no disagreement to report. **Paid down first (the standing ruling, 「A:先削重复,余额按实测提高」).** Five sentences in cases 1–5 that restated their own `must_not_contain` assertions are gone — the shape the landed objectstack-formula precedent cut: case 1's "Never a dotted path (… `400 INVALID_FIELD`) and never a `formula` mirror (…)", case 2's "(an object-level `permissions` key is rejected)", case 3's "— there is no `field.columnName` on an external object", case 4's three "never" clauses (its one non-asserted clause, "Parents before children in the exported array", stays as its own sentence), case 5's "No validation rule and no `readonlyWhen`, …". Each cut spelling remains a mechanical `must_not_contain` on the same case; no assertion changed. 453 bytes = 114 units cut; case 7 adds 1129 bytes = 283 units. **`scripts/check-skills-token-ratchet.mjs`** — the one row for this file moves 1404 → 1573, pinned AT the measurement (zero headroom), with the arithmetic and the ruling citation beside it as its siblings carry: triage 6450886908 (room inside the package first, no raise) read together with the maintainer's standing ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」. Strictest first: the room was made inside the file; the raise is only the balance trimming could not cover. No other row moved (the open PR #26739 edits other rows; `origin/main` was merged before this PR opened and the row region is disjoint). `evals/README.md` untouched (142 / 143, headroom 1 — a mention of refusals does not fit, see Acceptance notes). **`.changeset/26834-hook-refusals-eval.md`** — `'@objectstack/skills': patch`, both declaration lines. Why a changeset and not `skip-changeset`: the two latest `skills/**`-only PRs on this eval path (bb60b86 #26416, 7c70f91 #26393) carried `skip-changeset`, but the three token-ceiling precedents this card names (b1b153a #26774, 40264be #26810, d7dd47a #26852) each added a `@objectstack/skills` patch changeset, and the changeset gate's own text in `.github/workflows/pr-automation.yml` says `skills/**` "ships in '@objectstack/skills', whose build copies it into the 'dist' that package's 'files' field publishes … never the label". Measured here: `packages/skills/scripts/sync-catalog.mjs` copied 93 files into `dist/skills/**` byte-identical; `dist/skills/objectstack-data/evals/hooks-security-seeds-datasources.json` carries the new `CREDIT_LIMIT` (3 hits; positive control `membershipKeys` 2 hits; absent-symbol control 0). The eval publishes, so the published-face rule owes the changeset. ### Two readings for a `skills/**` diff | reading | before (`origin/main` f708cf2) | after (this head) | |:--|--:|--:| | `evals/hooks-security-seeds-datasources.json` — bytes | 5616 | 6292 | | same file — tokens (gate convention, `ceil(bytes / 4)`) | 1404 / ceiling 1404 | 1573 / ceiling 1573 | | same file — lines | 65 | 75 | | `skills/objectstack-data/**` — sum of the 17 priced files the gate lists | 43289 | 43458 (+169) | | catalog-wide — sum of every priced `SKILL.md` | 56410 | 56410 (no `SKILL.md` touched) | `ceil($(wc -c < skills/objectstack-data/evals/hooks-security-seeds-datasources.json) / 4) == ceil(6292 / 4) == 1573`. ## Tests Head measured: `1a0fb94a`. <details><summary>Token ratchet — before, mid (the unpaid reading that proves the gate read the file), after</summary> ``` before (origin/main f708cf2): ✓ check-skills-token-ratchet: skills/objectstack-data/evals/hooks-security-seeds-datasources.json is 1404 tokens (ceiling 1404; headroom 0). ✓ check-skills-token-ratchet: 82 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted. exit 0 mid (case 7 applied, row not yet moved): ✗ check-skills-token-ratchet: skills/objectstack-data/evals/hooks-security-seeds-datasources.json is 1573 tokens; the ratchet ceiling is 1404 (over by 169). … exit 1 after (row 1404 -> 1573): ✓ check-skills-token-ratchet: skills/objectstack-data/evals/hooks-security-seeds-datasources.json is 1573 tokens (ceiling 1573; headroom 0). ✓ check-skills-token-ratchet: 82 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted. exit 0 node scripts/check-skills-token-ratchet.mjs --self-test → ✓ check-skills-token-ratchet self-test: 79 cases pass. exit 0 ``` </details> <details><summary>Published-face probe — `@objectstack/skills` build</summary> ``` node packages/skills/scripts/sync-catalog.mjs --self-test → ✓ sync-catalog self-test: 9 cases pass. exit 0 node packages/skills/scripts/sync-catalog.mjs → ✓ @objectstack/skills: 93 file(s) across 11 skill(s) copied from skills/** into dist/skills and read back byte-identical. exit 0 grep -c CREDIT_LIMIT dist/skills/objectstack-data/evals/hooks-security-seeds-datasources.json → 3 grep -c membershipKeys dist/…/hooks-security-seeds-datasources.json → 2 (positive control) grep -c NOT_A_SYMBOL_XYZ dist/…/hooks-security-seeds-datasources.json → 0 (absent control) cmp dist copy vs source → byte-identical ``` </details> <details><summary>Derived local gate families — `dispatch-gates --commands` (60 commands at this head), each exit captured before any pipe, `--ran` reconciled</summary> ``` node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 60 commands (derived from the tree at 1a0fb94; change set: 3 paths, 52 changed lines vs the 3000 human-merge threshold) runner: eval "$cmd" > log 2>&1; status=$?; printf '%s :: exit %d\n' → ran.list (57 × exit 0, 3 × exit 3) node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack ✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 57 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3). exit 0 NOT-MEASURED (exit 3 = EXIT_PREREQUISITE_NOT_MET, an unbuilt dist/; the diff touches no workspace package, so no build closure is owed — CI measures them on its fresh build): pnpm --filter @objectstack/lint run check:doc-formula-expressions (PREREQUISITE NOT MET — @objectstack/formula is not built) pnpm check:dual-build-cjs-loads (self-test 93 cases pass; gate body: some package has no dist/) pnpm check:lean-entry-closure (packages/objectql/dist absent) exit 0 (57): check:adr-0087-registration (+ --self-test), check:changeset-no-major, check:closing-keyword-parity (+ --self-test), check:comment-mask-corpus, check:declaration-mirrors (+ --self-test), check:doc-route-spelling --advisory (+ --self-test), check:dts-emitted --self-test, check:empty-changeset (+ --self-test), check:self-test-wired (+ --self-test), check:self-test-workflow-commands (+ --self-test), check:skills-token-ratchet (+ --self-test), check:whole-set-label-write (+ --self-test), ci/assert-turbo-remote-cache --self-test, ci/plan-test-core-shards --self-test, ci/prune-turbo-cache --self-test, pm/bare-root-worklist --self-test, pm/release-rehearsal-clone --self-test, release-pending-publish --self-test, check:agent-test-spelling, check:bash32-floor, check:changeset-gate-self-tests, check:ci-filter-parity, check:cli-command-ids, check:closed-ceremony-text, check:corpus-claim-drift, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:dts-closure, check:entry-guard, check:gitlink-declared, check:nul-bytes, check:parse-guard, check:pm-changeset-deadline-census, check:pm-dispatch-gates, check:pm-governed-merges, check:pnpm-filter-targets, check:pnpm-flag-spellings, check:published-files, check:ratchet-remedy-authority, check:refd-timer-probe, check:role-word, check:scripts-symbol-anchors, check:skill-compatibility, check:skill-frame-sync, check:skill-identifier-liveness, check:sourcemap-no-sources-content, check:watch-hint-literal ``` </details> Edited gate script's own suite (clause ⑤): `scripts/check-skills-token-ratchet.mjs` has no `*.test.*` naming it (`git grep -l check-skills-token-ratchet -- '*.test.*'` is empty); its suite is its `--self-test`, run above (79 cases pass). The local-only families `check:dev-prereqs` / `check:skill-frame-freshness` are not owed: no `packages/spec` build and no four-axis frame file touched. `test:repo` is CI's (`Test Core`); not run locally — the diff touches no vitest `repo` project test. Every family CI runs on the PR is cited from the PR's check-runs by the seat, not re-run here; at report time they are `in_progress`. No package build closure (①) is owed: the diff touches no workspace package source (`skills/**` at the repo root, one `scripts/` row, one changeset). No ablation: no tested runtime behaviour changed (eval data and a ratchet constant). No dogfood battery: no `!` in the title and no `(narrowing)` arm. ## Acceptance notes - **Observation, not filed — `skills/objectstack-i18n` names no `hooks` translation group.** `content/docs/protocol/kernel/i18n-standard.mdx` lists `hooks.{hook}.refusals.{code}` among the translation targets (PR #26831 added it), and `translation.zod.ts` declares the nested `hooks` record, but `skills/objectstack-i18n/SKILL.md`'s roster of top-level groups ("apps, messages, globalActions, picklists, dashboards, datasets, reports, pages, flows, positions, permissions, metadataForms, settingsCommon") does not name `hooks`, and nothing under that skill mentions it (`git grep -c hooks origin/main -- skills/objectstack-i18n/` is empty). An AI author writing the zh-CN bundle from that skill alone would not know where the refusal sentence goes. A coverage gap in a published skill, no runtime defect; the skills seat decides whether it rides a follow-up (a `SKILL.md` row at its own ceiling, same ratchet shape). Out of this card's file surface. - `evals/README.md` still describes this file as "search mirrors via hooks, permission-set scopes, external datasources, seeds (…)"; a mention of the refusal case does not fit its headroom of 1 token and the seat set README at +0. Noted, not changed. - `references/data-hooks.md` still spells its three pre-catalogue examples as `throw new Error('Email is required')` etc. (lines 163, 780, 785) beside the new `refusals` section — the file is out of this card's surface (the teaching landed with #26831) and the examples are legal where no catalogue is declared ("A hook that declares no `refusals` is untouched"); the eval now scores the habit where a user-visible, translated message is asked for. ## Verification - `node scripts/check-skills-token-ratchet.mjs` → `hooks-security-seeds-datasources.json is 1573 tokens (ceiling 1573; headroom 0)`, last line `82 authored bundle file(s) within their ceilings`. - `node -e "const f=require('./skills/objectstack-data/evals/hooks-security-seeds-datasources.json'); console.log(f.evals.length, f.evals[6].assertions)"` → `7` and the two assertion arrays above. - `ceil($(wc -c < skills/objectstack-data/evals/hooks-security-seeds-datasources.json) / 4)` → 1573. --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e5f909 commit 5fe0b96

3 files changed

Lines changed: 46 additions & 6 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): an objectstack-data eval asks for a hook's `refusals` catalogue instead of a sentence thrown from the body
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `references/data-hooks.md` teaches a hook's declared `refusals` catalogue (the body throws `{ code }`, the message is translated under `hooks.HOOK.refusals.CODE.message`), but no eval in `evals/hooks-security-seeds-datasources.json` exercised it: an agent asked for a refusal in a multi-language app could still write an English sentence into `throw new Error(...)`, which reaches zh-CN users untranslated, and nothing scored that wrong.
11+
- **Now.** Eval 7 asks for an `order` save refused over the account's credit limit in an app serving English and Chinese users, and expects `refusals: { CREDIT_LIMIT: { message } }` on the hook, a `language: 'js'` body that throws `{ code: 'CREDIT_LIMIT' }`, and the zh-CN sentence under the bundle's `hooks: { HOOK: { refusals: { CREDIT_LIMIT: { message } } } }`; it refuses a sentence on the throw (`new Error('`) and a `handler:` (a catalogue without a `js` body is refused at save). Five sentences in evals 1–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: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,25 @@ export const CEILINGS = new Map([
933933
// `applyOn: 'create'`. +309 tokens at headroom 0, ceiling +309; nothing
934934
// duplicated in the file to cut first. Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
935935
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
936-
['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 1404],
936+
// 1404 -> 1573 (card #26834, the eval owed by the spec PR that added a
937+
// hook's `refusals` catalogue): one case for a save refused over the
938+
// account's credit limit in an app serving English and Chinese users —
939+
// expected `refusals: { CREDIT_LIMIT: { message } }` on the hook, a
940+
// `language: 'js'` body throwing `{ code: 'CREDIT_LIMIT' }`, the zh-CN
941+
// sentence under the bundle's `hooks: { HOOK: { refusals } }`; NO sentence
942+
// on the throw (`new Error('`), NO `handler:`. Triage 6450886908 (room
943+
// inside the package first, no raise) read together with the standing
944+
// ruling, strictest first — maintainer ruling 2026-10-27 on the decision
945+
// card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, verbatim
946+
// and untranslated: 「A:先削重复,余额按实测提高」. PAID DOWN FIRST: five
947+
// sentences in cases 1–5 that restated their own `must_not_contain`
948+
// assertions (case 4 keeps its one non-asserted clause), 453 bytes = 114
949+
// units; the case adds 1129 bytes = 283 units. On `origin/main` the file
950+
// was 5616 bytes (1404 tokens, headroom 0); the LANDED file is 6292 bytes
951+
// -> 1573 tokens, ceiling +169, pinned AT the measurement, zero headroom.
952+
// The raising PR's body carries the same arithmetic.
953+
// ceil($(wc -c < skills/objectstack-data/evals/hooks-security-seeds-datasources.json) / 4) == ceil(6292 / 4)
954+
['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 1573],
937955
// 946 -> 1050 (#26297): the invoice-line case gains the owner opt-in
938956
// (`ownerMayEdit` on the controlled_by_parent line) and pins `added_by` as
939957
// `type: 'user'`, never a lookup to a `user` object. +104 tokens at

‎skills/objectstack-data/evals/hooks-security-seeds-datasources.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": "Support agents want to search tasks by the name of the project they belong to.",
7-
"expected_output": "Applies the one prescription: a stored `text` mirror field `project_name` on `task`, listed in `searchableFields: ['name', 'project_name']`, maintained by a `beforeInsert` / `beforeUpdate` hook on `task` (stamp the parent's name for the incoming `project_id`) and an `afterUpdate` hook on `project` (re-stamp on rename), plus a one-off backfill for rows written outside hooks. Never a dotted path (`project_id.name` is refused, `400 INVALID_FIELD`) and never a `formula` mirror (virtual, nothing to scan).",
7+
"expected_output": "Applies the one prescription: a stored `text` mirror field `project_name` on `task`, listed in `searchableFields: ['name', 'project_name']`, maintained by a `beforeInsert` / `beforeUpdate` hook on `task` (stamp the parent's name for the incoming `project_id`) and an `afterUpdate` hook on `project` (re-stamp on rename), plus a one-off backfill for rows written outside hooks.",
88
"files": [],
99
"assertions": {
1010
"must_contain": ["searchableFields", "project_name", "beforeInsert", "afterUpdate"],
@@ -14,7 +14,7 @@
1414
{
1515
"id": 2,
1616
"prompt": "Sales reps may only see their own leads, sales managers see their unit and everything below it, and every signed-in user can read the `product` catalog.",
17-
"expected_output": "Access is authored in permission sets, not on the object (an object-level `permissions` key is rejected). `lead` gets `sharingModel: 'private'`; the rep set's grant on `lead` carries `readScope: 'own'`, the manager set's `readScope: 'unit_and_below'` — an ADR-0057 scope that needs the enterprise `@objectstack/security-enterprise` plugin and `requires: ['hierarchy-security']`, failing closed to `own` without it. `product` is `sharingModel: 'public_read'`.",
17+
"expected_output": "Access is authored in permission sets, not on the object. `lead` gets `sharingModel: 'private'`; the rep set's grant on `lead` carries `readScope: 'own'`, the manager set's `readScope: 'unit_and_below'` — an ADR-0057 scope that needs the enterprise `@objectstack/security-enterprise` plugin and `requires: ['hierarchy-security']`, failing closed to `own` without it. `product` is `sharingModel: 'public_read'`.",
1818
"files": [],
1919
"assertions": {
2020
"must_contain": ["sharingModel: 'private'", "readScope", "unit_and_below", "public_read", "hierarchy-security"],
@@ -24,7 +24,7 @@
2424
{
2525
"id": 3,
2626
"prompt": "Expose the existing `customers` table from our Postgres reporting database as a read-only `legacy_customer` object.",
27-
"expected_output": "Declares a `defineDatasource` entry (credentials by `credentialsRef`, never inline) and points the object at it with `datasource: '<id>'`, mapping the physical table via `remoteName` and column names via `columnMap` — there is no `field.columnName` on an external object. Sets the datasource's `schemaMode` so the platform never alters the remote schema, and keeps the external write gate closed.",
27+
"expected_output": "Declares a `defineDatasource` entry (credentials by `credentialsRef`, never inline) and points the object at it with `datasource: '<id>'`, mapping the physical table via `remoteName` and column names via `columnMap`. Sets the datasource's `schemaMode` so the platform never alters the remote schema, and keeps the external write gate closed.",
2828
"files": [],
2929
"assertions": {
3030
"must_contain": ["defineDatasource", "datasource", "remoteName", "columnMap", "schemaMode"],
@@ -34,7 +34,7 @@
3434
{
3535
"id": 4,
3636
"prompt": "Seed the `country` reference table for every environment, and add demo `opportunity` rows (dev/test only) that close 45 days after install and are owned by whoever installs the package. A nightly job advances each opportunity's `stage`; replaying the seed must not reset it.",
37-
"expected_output": "Uses `defineSeed(Country, { externalId: 'code', mode: 'upsert', records })` for reference data and `defineSeed(Opportunity, { env: ['dev', 'test'], ... })` for demo rows, with lookups referenced by natural key and dynamic values as CEL: `close_date: cel`daysFromNow(45)``, `owner_id: cel`os.user.id``. Never `externalId: 'id'`, never `new Date()` / `Date.now()` (breaks build determinism), never `defineDataset` (the analytics layer), parents before children in the exported array. The job-owned column is declared `columns: { stage: { applyOn: 'create' } }`, so an `upsert` replay leaves it as the row holds it.",
37+
"expected_output": "Uses `defineSeed(Country, { externalId: 'code', mode: 'upsert', records })` for reference data and `defineSeed(Opportunity, { env: ['dev', 'test'], ... })` for demo rows, with lookups referenced by natural key and dynamic values as CEL: `close_date: cel`daysFromNow(45)``, `owner_id: cel`os.user.id``. Parents before children in the exported array. The job-owned column is declared `columns: { stage: { applyOn: 'create' } }`, so an `upsert` replay leaves it as the row holds it.",
3838
"files": [],
3939
"assertions": {
4040
"must_contain": ["defineSeed", "externalId", "upsert", "env:", "daysFromNow", "applyOn: 'create'"],
@@ -44,7 +44,7 @@
4444
{
4545
"id": 5,
4646
"prompt": "Finance may edit a contract's legal terms (`governing_law`, `liability_cap`) while it is a draft, but not once it is approved, signing or active. Legal still edits them.",
47-
"expected_output": "Field permission entries with a row condition on the finance set: `'clm_contract.governing_law': { readable: true, editable: false, when: \"status in ['approved', 'signing', 'active']\" }` and the same for `liability_cap`; the legal set keeps its edit grant. No validation rule and no `readonlyWhen`, which would lock the fields for every user.",
47+
"expected_output": "Field permission entries with a row condition on the finance set: `'clm_contract.governing_law': { readable: true, editable: false, when: \"status in ['approved', 'signing', 'active']\" }` and the same for `liability_cap`; the legal set keeps its edit grant.",
4848
"files": [],
4949
"assertions": {
5050
"must_contain": ["when:", "editable: false", "readable: true", "clm_contract.governing_law"],
@@ -60,6 +60,16 @@
6060
"must_contain": ["membershipKeys", "current_user.portal_account_ids", "userField", "valueField", "permissionSets", "external"],
6161
"must_not_contain": ["permissions: {"]
6262
}
63+
},
64+
{
65+
"id": 7,
66+
"prompt": "Our app serves English and Chinese users. An `order` whose `amount` exceeds its account's `credit_limit` must not be saved; API clients branch on the code `CREDIT_LIMIT`, and a zh-CN rep reads `超出账户信用额度`.",
67+
"expected_output": "A `beforeInsert` / `beforeUpdate` hook on `order` declares the refusal once, `refusals: { CREDIT_LIMIT: { message: 'Over the account credit limit.' } }`, and its sandboxed `body: { language: 'js', source }` reads the account through `ctx.api` and refuses by code only: `throw Object.assign(new Error(), { code: 'CREDIT_LIMIT' })` — the body's own text is never shown. The zh-CN bundle carries `hooks: { <hook>: { refusals: { CREDIT_LIMIT: { message: '超出账户信用额度' } } } }`, served whole in the caller's locale; a catalogue on a hook without a `js` body is refused at save.",
68+
"files": [],
69+
"assertions": {
70+
"must_contain": ["refusals:", "CREDIT_LIMIT: {", "code: 'CREDIT_LIMIT'", "language: 'js'", "hooks: {", "超出账户信用额度"],
71+
"must_not_contain": ["new Error('", "handler:"]
72+
}
6373
}
6474
]
6575
}

0 commit comments

Comments
 (0)