Skip to content

Commit c0f88b9

Browse files
feat(scenarios)!: cards declare user-handle fields and persona records; seed dependence is decided per turn (#31275)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X4Jt9kqe7MNvcC7UcRXr6hv)_ Fixes #30914 Clause-②: yes (narrowing) — the published card schema (`ScenarioCardSchema`, `.` entry) gains `run.userHandleFields` and `personas[].record`; the loader refuses cards it loaded before (a scope rule ending on an undeclared field, a scoped persona naming no person); `./runner`'s `seedDependence` takes the loaded scenario and asks per turn. Metadata-protocol: none Skills-docs: none — no skill or docs page teaches the scenario card format; its own documentation, `docs/qa/scenarios/README.md`, is updated here. ## 维护者速读 - 改了什么:场景卡片格式新增两项声明——`run.userHandleFields`(哪些种子字段的值是"用户句柄",如 `account.owner = u_sam`)与 `personas[].record`(有行范围的角色对应哪一条种子行,如员工 `e06`);加载器拒收"范围规则落在既非句柄、也非查找/选项/`id` 的字段上"以及"有范围却没指明是谁"的卡;跑测记录新增 `seed.rewritten`;种子行被应用拒收时,只把"预期答案因此改变"的那几轮问答记为未测,其余照常评分。 - 为什么改:句柄字段原来靠推断,新卡可能被悄悄写错种子;一行被拒就整对象作废,遮掉了本可评分的结果(#30882 契约复审的 S1、S2);`record` 是维护者 2026-11-07 13:34 对 #30913 裁决的卡片格式那一半(S3)。 - 风险与回滚:只动 `@objectstack/scenarios`(QA 工具包)与九张卡;九张卡全部照常加载,种子改写的字段集合与改前完全相同(`account.owner`、`invoice.owner`、`task.assignee`);私有覆盖目录里的卡若没有这两项声明会被拒收,补声明即可。回滚即 revert 本 PR。 - 先看:`packages/qa/scenarios/src/loader.ts`(新拒收与 `truthsOver`)、`packages/qa/scenarios/src/runner/orchestrate.ts`(`seedDependence` 按轮判断)、`docs/qa/scenarios/service_desk.json`(三个角色的 `record`)。 - 两分钟验证:`pnpm --filter @objectstack/scenarios exec vitest run test/refusals.test.ts test/runner/units.test.ts`,预期全绿;其中 ct4 用例显示 `contacts_per_account` 以 `sdr` 身份仍被评分。 - 需要你做的:无,席位落地。 ## What changes **S2 — user-handle fields are declared.** - `run.userHandleFields: [{ object, field }]` (optional, at least one entry) lists the seed fields whose text stands for a user. `userHandleFields` in `src/runner/prompts.ts` now reads this list and nothing else; it no longer infers a handle from "a direct scope field that is no lookup or select". - The loader refuses a persona `scope` rule whose last field (the last hop of a relation path) is not `id`, a declared lookup, a declared select or a declared handle field. It also refuses a handle declaration on the row id, a declared lookup, a declared select, an object the card does not seed, a field no seed row carries, and a duplicate. - The run record's `seed.rewritten` lists each `object.field` the seed loaded with a user id in place of the handle text (`RunHalf.seed`, `src/runner/orchestrate.ts`). - The three control cards declare their handle field. Before and after, measured over the nine cards: before (`origin/main`, inferred) `crm_account_with_contacts:account.owner`, `invoice_with_line_items:invoice.owner`, `project_with_tasks:task.assignee`; after (declared) the same three. **S1 — seed dependence per turn.** - `seedDependence(scenario, refused)` keeps its row walk (the refused rows plus their children through the declared lookups), then computes every truth again over the seed less those rows with `truthsOver` (new in `src/loader.ts`, the loader's own resolver, row-scope test and withheld rule, factored out as `seedReader` and `withholds`). `question(q, persona)` is not measured only when that turn's expected answer changes, or is no longer defined (`seed: its expected answer is not defined without …`). - `runHalf` asks it per turn (inside the persona loop). Operations are unchanged. - The ct4 case: refusing contact `ct4` on `crm_account_with_contacts` used to leave 5 turns not measured (`contacts_per_account/sdr`, `unattached_contacts`, `best_covered_account`, `tech_contacts`, `contacts_by_industry`); it now leaves 2 (`unattached_contacts`, `contacts_by_industry`, the two whose answer ct4 changes). `contacts_per_account` as `sdr` stays measured. **S3 — `personas[].record = { object, id }`.** - Required on a persona with a `scope`, unless that persona is named by a direct scope rule on a declared handle field; optional on a global persona. The loader refuses a record naming no seeded row (including a row of an object the card does not seed). `PERSON_USER_FIELD = 'user'` fixes the person object's user field by name. - Filled on the six cards the ruling names: `asset_lifecycle` (employee e06, dept_manager e05), `leave_balances` (e07, e05), `order_to_cash` (e03, e01), `requisition_to_receipt` (e02, e13), `travel_expense` (e02, e01), `service_desk` (agent a02, agent a04, customer cus_03). Each id is the one the persona's `role` already names. - Format and loader only: the runner does not read `record` (#30913's half). **Namespacing and docs.** `namespacedScenario` (`src/runner/namespace.ts`) prefixes the object of each handle declaration and each `record`. `docs/qa/scenarios/README.md` documents both declarations and the new refusals; the package README documents per-turn dependence and `seed.rewritten`. One `patch` changeset. ## Tests - `test/refusals.test.ts`: 12 new refusal pins (one refusal each, exact path) and two controls: a scoped persona naming its record (and a global persona naming one) loads clean; deleting `run.userHandleFields` from the crm card refuses both of `sdr`'s scope rules and its missing person. The gallery-floor fixture now scopes its second persona on a declared select and names its record. - `test/runner/units.test.ts`: `userHandleFields` reads the declaration (a card declaring none has none); across the nine cards the handle set is the three fields and every record is pinned; `seedDependence` per turn on the ac1 case, the ct4 case and a truth the rest of the seed no longer defines. - `test/runner/orchestrate.test.ts`: `seed.rewritten` in the record; the ct4 run (7/7 measured, 2 not measured; the driver is asked `contacts_per_account` as `sdr`); the all-accounts-refused run keeps the three turns no account feeds, and a half with every turn not measured is still `not-run`. Ablations, each through `scripts/ablation-replace.mjs` on committed HEAD `33a20989`: mutation landed (anchor 1 → 0, replacement 0 → 1, blob changed), suite red, restored to the HEAD blob; `git diff HEAD` empty after all legs. The tests import `src/` directly (no `dist/` in the path), so no rebuild leg. | Leg | Mutation | Red | |:--|:--|:--| | A1 | scope rule ending on an undeclared field not refused | 2 failed (the refusal pin; the no-declaration test) | | A2 | handle declared on a lookup, select or id not refused | 3 failed (the three pins) | | A3 | handle on an unseeded object not refused | 1 failed | | A4 | handle field no seed row carries not refused (`checkField` call deleted) | 1 failed | | A5 | duplicate handle declaration not refused | 1 failed | | A6 | record naming no seeded row not refused | 2 failed (row missing; object unseeded) | | A7 | scoped persona with no person not refused | 2 failed (the pin; the no-declaration test) | | A8 | per-turn comparison removed (every turn on an out row not measured) | 4 failed (both runCard dependence tests, both units dependence tests) | | A9 | `seed.rewritten` not recorded | 2 failed (both seed-record pins) | ## Acceptance notes - **Reading of "required for a persona with a scope".** The S3 ruling says required for a scoped persona; the #30913 ruling (Q1 = A′, item 5) keeps the user-handle route for crm, invoice and project, whose scoped personas are users with no person row. Both hold here: `record` is required unless the persona is named by a direct scope rule on a declared handle field. On the nine cards that is exactly the three controls. - **Global personas carry no record.** It is optional for them and the ruling names only the scoped ones; whether #30913's owner rule wants one on `asset_admin`, `hr_officer` and the like is that card's call. - **The terminal-field rule reads the last hop of a path.** `contact.account.owner` ends on `account.owner`, a declared handle; `order_line.order.owner.team` ends on `employee.team`, a declared select. The card's wording is "a scope on a field"; reading only direct rules would leave a path ending on an undeclared field to inference, which is what S2 removes. - **File surface.** `src/runner/namespace.ts` is outside the claim's list: without it the namespaced card the runner reads keeps bare object names in the two new keys and `userHandleFields` answers `account.owner` where the seed loop looks for `democrm_account.owner`. `src/runner/seed.ts` needed no change. `orchestrate.ts` changes `seedDependence`, its caller in `runHalf`, the `RunHalf.seed` type and one line of the seed loop (`rewritten`). - **Serial with #31159.** No change to `runner/driver.ts` or `runner/cli.ts`. `origin/main` moved by two commits since the branch point, neither in `packages/qa/scenarios`. ## Verification - Heads: code and tests at `33a20989`; `4b4df50a` adds one sentence to the changeset only. The derived loop started on `33a20989`; the changeset-reading gates were run again on `4b4df50a` (below). Base `bdefd49f`; `origin/main` has moved two commits since, neither in `packages/qa/scenarios`. - `pnpm --filter @objectstack/scenarios exec vitest run`: Test Files 28/28, Tests 571/571 (after `pnpm --filter '@objectstack/scenarios^...' build`, VERDICT command-exit 0, and the package's own build, which `test/build/*` read). - `pnpm --filter @objectstack/scenarios typecheck` (`tsc --noEmit`, its program includes `test/**`): exit 0. - `node --stack-size=4000 node_modules/eslint/bin/eslint.js packages/qa/scenarios --no-inline-config` (the root `pnpm lint`, scoped; the package has no lint script): exit 0. - On `4b4df50a`: `check-changeset-no-major.mjs --base origin/main` with this body as the event: exit 0 ("`yes (narrowing)` and metadata-protocol `none` … a non-protocol change at `patch`"); `check-adr-0087-registration.mjs --base origin/main`: exit 0 (the `no-migration-prescription` disposition read); `check-empty-changeset.mjs --base origin/main`: exit 0; `check-partof-closing-keyword.mjs` with `PR_BODY` set to this body: exit 0. - **Not measured:** a live model run (paused by the maintainer) and a recorded-driver replay (no recording in this container); the per-turn rule is pinned on the real crm card through the fake-app `runCard` tests instead. <details><summary>Derived gates (dispatch-gates --commands, 62 commands)</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/ci/snapshot-next.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vouch-for-reference-callers :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` - `dispatch-gates --ran`: "62 derived famil(ies) accounted for — 61 run, 1 NOT-MEASURED". - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, exit 3 PREREQUISITE NOT MET (it loads every package's `dist/`; only the scenarios closure was built). CI's lint job runs it on a full build. - `check-plugin-teardown-shape.mjs --self-test` answered exit 3 on the shallow clone until `git fetch --depth=1 origin 621a487…`; rerun exit 0 (recorded above as the rerun). - `check:type-check-debt` re-measured under the lock (414 s): "0 raw tsc error(s) total, none above its recorded number". </details> <details><summary>Gates outside the derived set: the artifact-roster and declared-wide families (47 commands)</summary> ```text node scripts/check-closing-target-claim.mjs :: exit 2 node scripts/check-partof-closing-keyword.mjs :: exit 2 node scripts/check-platform-checklist-watchdog.mjs :: exit 0 node scripts/check-published-list-mirrors.mjs :: exit 0 node scripts/check-single-claim-paths.mjs :: exit 2 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/pm/check-governed-queue-guard.mjs --toolchain-needed :: exit 0 node scripts/pm/check-governed-queue-guard.mjs :: exit 1 pnpm --filter @objectstack/spec run check:error-code-provenance :: exit 0 pnpm --filter @objectstack/spec run check:meta-url-spelling :: exit 0 pnpm --filter @objectstack/spec run check:react-blocks :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm check:auth-mount-ledger :: exit 0 pnpm check:authz-resolver :: exit 0 pnpm check:changeset-fixed :: exit 0 pnpm check:cli-examples-parity :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:error-code-casing :: exit 0 pnpm check:error-status-conformance :: exit 0 pnpm check:facade-delegation-only :: exit 0 pnpm check:filter-alias-parity :: exit 0 pnpm check:future-spec-major :: exit 0 pnpm check:i18n-stale-fill :: exit 0 pnpm check:init-service-contract :: exit 0 pnpm check:live-db-isolation :: exit 0 pnpm check:meta-type-normalized :: exit 0 pnpm check:object-def-param-keys :: exit 0 pnpm check:optional-error-sink :: exit 0 pnpm check:overlay-whitelist-table :: exit 0 pnpm check:pm-governed-prose :: exit 0 pnpm check:pm-label-desc-cap :: exit 0 pnpm check:pm-settings-deny-roster :: exit 0 pnpm check:published-readme-exports :: exit 0 pnpm check:resume-authority-declared :: exit 0 pnpm check:route-envelope :: exit 0 pnpm check:route-ledger-census :: exit 0 pnpm check:runner-env-posture :: exit 0 pnpm check:scaffold-emission-policy :: exit 0 pnpm check:select-gate-families :: exit 0 pnpm check:select-shard-packages :: exit 0 pnpm check:settings-bind-window :: exit 0 pnpm check:skill-top-level-keys :: exit 0 pnpm check:stack-collection-maps :: exit 0 pnpm check:startup-registry-verdict :: exit 0 pnpm check:tenant-chokepoint :: exit 0 pnpm check:verify-stand-in :: exit 0 pnpm check:wildcard-fallthrough :: exit 0 ``` - The four non-zero rows judge a pull request, not the tree, and refuse without its context (exit 2 "handed no pull request", exit 1 for the event file): NOT MEASURED locally, read from this PR's check-runs. `check-partof-closing-keyword.mjs` with `PR_BODY` answered exit 0 (above). - `pnpm --filter @objectstack/spec run check:error-code-provenance` is among them, exit 0: the diff adds no error-code constant. </details> Every CI family is read from this PR's check-runs, not re-run here. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nVm4yfBmbggVVSu4R)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b3bd75d commit c0f88b9

20 files changed

Lines changed: 425 additions & 89 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/scenarios': patch
3+
---
4+
5+
feat(scenarios): a scenario card declares which seed fields hold a user handle and which seed row each scoped persona is, and a refused seed row takes out only the turns whose answer it changes
6+
7+
Clause-②: yes (narrowing) — the published card schema (`ScenarioCardSchema` in the `.` entry) gains `run.userHandleFields` and `personas[].record`, and the loader (`loadScenarios`, `loadScenarioFile`) refuses a card it loaded before: a persona `scope` rule ending on a field that is neither `id`, a declared lookup or select, nor a declared user-handle field, and a scoped persona that names no person. `truthsOver` is a new export of the same entry. In `./runner`, `seedDependence` now takes the loaded scenario (it was handed the card) and its `question` takes the persona the turn is asked as, and `RunHalf.seed` gains `rewritten`.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) The card format is QA tooling in a devx package, not the metadata protocol: no key of any platform schema is added, removed, renamed or re-shaped, and nothing an app author writes is refused. What moves is which scenario cards the loader accepts; a card author adds the two declarations the refusal names, and there is nothing for a metadata migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing at the scenario card loader (`@objectstack/scenarios`): a card that leaves the runner to infer what a scope value names is refused until it declares it. All nine cards in `docs/qa/scenarios/` carry the declarations and load as before.
13+
14+
- **User-handle fields, declared.** `run.userHandleFields: [{ object, field }]` lists the seed fields whose text stands for a user (`account.owner` = `u_sam`). The runner loads each such cell with the user's id, and `userHandleFields` (runner `prompts.ts`) reads this list instead of inferring it from what the card does not declare. The loader refuses a scope rule whose last field is none of `id`, a declared lookup, a declared select or a declared handle field, and a handle declaration on the row id, a lookup, a select or a field no seed row carries. The run record's `seed.rewritten` lists the fields the seed rewrote. Across the nine cards the rewritten set is unchanged: `account.owner`, `invoice.owner`, `task.assignee`.
15+
- **The person a persona is.** `personas[].record: { object, id }` names the seed row that is this person (`{ object: "employee", id: "e06" }`). It is required for a persona with a `scope`, unless the persona is named by a direct scope rule on a declared handle field (the three control cards), and optional for a global persona. The loader refuses a record that names no seeded row. The person object's user field is `user` by name. The runner does not read `record` yet.
16+
- **Seed dependence per turn.** When the app refuses a seed row, the runner computes each question's truth again over the seed less the refused rows and their children (`truthsOver`, the loader's own rules), and marks not measured only the turns whose expected answer changes. Before, every question on an object holding a refused row was not measured. On `crm_account_with_contacts`, refusing contact `ct4` now leaves `contacts_per_account` as `sdr` (whose scope never reads it) measured.
17+
18+
**Who moves.** An author of a scenario card in a private overlay: add `run.userHandleFields` for each field holding a user handle, and `record` on each scoped persona that is a seed row. A caller of `seedDependence` from `@objectstack/scenarios/runner` passes the `LoadedScenario` and asks `question(q, persona)` per turn (`null` for a turn over all rows).

‎docs/qa/scenarios/README.md‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ question's computed truth; a card that does not load is red in CI.
3333
| 1 | `prompt` | `{ short, detailed }`, each `{ zh?, en? }` with at least one language. |
3434
| 2 | `expect` | The structural expectation the deterministic scorer reads, field for field: `objects`, `minFieldsPerObject?`, `selectFields?` (`{ object, field, minOptions }`), `lookups?` (`{ object, field, to }`), `dashboards?`, `appNavTargets?`. The runner's build prompt states every row by name ([Build checks trace to the prompt](#build-checks-trace-to-the-prompt)). |
3535
| 3 | `capabilities`, `businessFunctions` | Tags `<area>.<name>`, where `<area>` is a closed set: the basenames of `docs/qa/platform-checklist/areas/*.json`, read from the tree. `businessFunctions` is the marketplace taxonomy's business-function facet, read from `@objectstack/spec/marketplace` (`MarketplaceBusinessFunctionsSchema`, defined once in `packages/spec/src/marketplace/taxonomy.zod.ts`): 1-3 distinct values, the primary one first — the card is classified the way the template it graduates into will be listed. |
36-
| 4 | `run.seed`, `run.questions`, `run.personas` | Seed rows per object; at least five read-only questions, each with a **declarative** truth spec; at least two personas, each a role, the objects or fields it must NOT see, and optionally a row `scope`. |
36+
| 4 | `run.seed`, `run.userHandleFields?`, `run.questions`, `run.personas` | Seed rows per object, and which seed fields hold a user handle; at least five read-only questions, each with a **declarative** truth spec; at least two personas, each a role, the objects or fields it must NOT see, and optionally a row `scope` and the person `record` it is. |
3737
| 5 | `run.operations`, `run.guardrails`, `run.agentActions?` | At least two writes, each acting as a persona and declaring its `outcome`; the five guardrail checks; optionally the agent-callable business actions. |
3838
| 6 | `iterate` | A one-sentence change request plus the `expectDelta` it should add to `expect`. |
3939

@@ -159,7 +159,23 @@ one of three shapes: `{ kind: "scalar", value }`, `{ kind: "groups", groups: [{
159159
## Personas, operations and guardrails
160160

161161
A persona names at least one object or field it must not see; its references must resolve
162-
against `expect.objects` and the seed. An operation is a `create` (with `values`), an
162+
against `expect.objects` and the seed.
163+
164+
**What a scope value names is declared, never inferred.** The field a `scope` rule ends on
165+
(the last hop of a path) is `id` or a declared lookup (the value names a row), a declared
166+
select (an option), or a **user-handle field**: a seed field listed in
167+
`run.userHandleFields` (`[{ "object": "account", "field": "owner" }]`), whose text
168+
(`u_sam`) stands for a user. The runner loads a handle field's cells with that user's id and
169+
never the text, and the run record lists the fields it rewrote (`seed.rewritten`). The loader
170+
refuses a scope rule ending on any other field, and a handle declaration on the row id, a
171+
declared lookup or select, or a field no seed row carries.
172+
173+
**A scoped persona names the person it is.** `record: { "object": "employee", "id": "e06" }`
174+
is the seed row that is this person (an employee, an agent, a customer); the loader refuses
175+
one naming no seeded row. It is required on a persona with a `scope`, unless that persona is
176+
named through a handle instead (a direct scope rule on a declared user-handle field,
177+
`owner: u_sam`, as on the three controls), and optional on a global one. The person object's
178+
user field is `user`, fixed by name: no card chooses it. An operation is a `create` (with `values`), an
163179
`update` (with `target` and `values`) or a `delete` (with `target`); a target is a seeded row
164180
id, and every written field is one the seed carries. Every operation names the persona it
165181
acts for (`actingAs`) and declares its `outcome`, so grading reads it with code only:

‎docs/qa/scenarios/asset_lifecycle.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,7 @@
767767
{
768768
"id": "employee",
769769
"role": "Employee (asset holder), Sales - e06",
770+
"record": {"object": "employee", "id": "e06"},
770771
"hidden": {
771772
"objects": ["depreciation_entry"],
772773
"fields": [
@@ -787,6 +788,7 @@
787788
{
788789
"id": "dept_manager",
789790
"role": "Department manager, Sales - e05",
791+
"record": {"object": "employee", "id": "e05"},
790792
"hidden": {"fields": [{"object": "employee", "field": "home_address"}]},
791793
"scope": [
792794
{"object": "department", "field": "id", "value": "dep_sales"},

‎docs/qa/scenarios/crm_account_with_contacts.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
{"id": "ct5", "full_name": "Eli Moss", "email": "eli@bitworks.test", "account": "ac3"}
4040
]
4141
},
42+
"userHandleFields": [{"object": "account", "field": "owner"}],
4243
"questions": [
4344
{
4445
"id": "tech_accounts",

‎docs/qa/scenarios/invoice_with_line_items.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
{"id": "l5", "invoice": "i3", "product": "Gadget", "quantity": 3, "unit_price": 25, "amount": 75}
4747
]
4848
},
49+
"userHandleFields": [{"object": "invoice", "field": "owner"}],
4950
"questions": [
5051
{
5152
"id": "invoices_not_draft",

‎docs/qa/scenarios/leave_balances.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@
817817
{
818818
"id": "employee",
819819
"role": "Employee, Engineering, two years of service — e07",
820+
"record": {"object": "employee", "id": "e07"},
820821
"hidden": {"fields": [{"object": "employee", "field": "personal_phone"}]},
821822
"scope": [
822823
{"object": "leave_request", "field": "employee", "value": "e07"},
@@ -828,6 +829,7 @@
828829
{
829830
"id": "dept_manager",
830831
"role": "Department manager, Engineering — e05",
832+
"record": {"object": "employee", "id": "e05"},
831833
"hidden": {
832834
"fields": [
833835
{"object": "employee", "field": "personal_phone"},

‎docs/qa/scenarios/order_to_cash.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,7 @@
838838
{
839839
"id": "sales_rep",
840840
"role": "Sales rep, team east, owner of Cedar Logistics and Bluebird Outfitters — e03",
841+
"record": {"object": "employee", "id": "e03"},
841842
"hidden": {"objects": ["payment"], "fields": [{"object": "product", "field": "unit_cost"}]},
842843
"scope": [
843844
{"object": "account", "field": "owner", "value": "e03"},
@@ -850,6 +851,7 @@
850851
{
851852
"id": "sales_manager",
852853
"role": "Sales manager, team east — e01",
854+
"record": {"object": "employee", "id": "e01"},
853855
"hidden": {"fields": [{"object": "payment", "field": "bank_reference"}]},
854856
"scope": [
855857
{"object": "account", "field": "owner.team", "value": "east"},

‎docs/qa/scenarios/project_with_tasks.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
}
4949
]
5050
},
51+
"userHandleFields": [{"object": "task", "field": "assignee"}],
5152
"questions": [
5253
{
5354
"id": "open_tasks",

‎docs/qa/scenarios/requisition_to_receipt.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@
848848
{
849849
"id": "requester",
850850
"role": "Requester (employee), Sales — e02",
851+
"record": {"object": "employee", "id": "e02"},
851852
"hidden": {
852853
"fields": [{"object": "department", "field": "budget"}, {"object": "supplier", "field": "bank_account"}]
853854
},
@@ -862,6 +863,7 @@
862863
{
863864
"id": "dept_manager",
864865
"role": "Department manager, Sales — e13",
866+
"record": {"object": "employee", "id": "e13"},
865867
"hidden": {"fields": [{"object": "supplier", "field": "bank_account"}]},
866868
"scope": [
867869
{"object": "department", "field": "id", "value": "dep_sales"},

‎docs/qa/scenarios/service_desk.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@
691691
{
692692
"id": "support_agent",
693693
"role": "Support agent, Billing group — a02",
694+
"record": {"object": "agent", "id": "a02"},
694695
"hidden": {
695696
"fields": [{"object": "customer", "field": "contract_value"}, {"object": "agent", "field": "hourly_cost"}]
696697
},
@@ -703,6 +704,7 @@
703704
{
704705
"id": "team_lead",
705706
"role": "Team lead, Technical group — a04",
707+
"record": {"object": "agent", "id": "a04"},
706708
"hidden": {"fields": [{"object": "agent", "field": "hourly_cost"}]},
707709
"scope": [
708710
{"object": "agent", "field": "group", "value": "grp_tech"},
@@ -714,6 +716,7 @@
714716
{
715717
"id": "portal_customer",
716718
"role": "Customer portal user, Harbor & Pine Foods — cus_03",
719+
"record": {"object": "customer", "id": "cus_03"},
717720
"hidden": {
718721
"objects": ["escalation_rule"],
719722
"fields": [

0 commit comments

Comments
 (0)