Skip to content

fix(scenarios): the seed loads a user-handle field with a user id, and both author loops run verify - #30903

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-30882-scenario-seed-owner
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-30882-scenario-seed-owner

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread
Fixes #30882
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:场景跑分器灌种子数据时,把"负责人"一类字段里的人员代号(如 u_sam)换成该角色真实用户的 id(字段本身和 owner_id 都换);没有对应角色的代号(u_tia)记在管理员名下,提示词照此写明。种子有被拒的行时,依赖这些行的问数和写操作记为「未测」而非失败,计分卡显示 ⚠ seed: N refused。构建与迭代两轮的提示词也让 AI 自己跑一遍跑分时用的同一条 verify 命令。
  • 为什么改:第一次真跑时 8 条种子只进了 1 条,后面的问数和写操作量的是跑分器自己的缺陷,不是被测应用,计分卡却把它们算成失败;AI 也从未被告知要过 verify,于是构建"降级"并不说明它修不好。
  • 风险与回滚:只动未发布的私有包 @objectstack/scenarios(QA 工具),不影响产品运行时;回滚即还原本 PR。其他卡片中指向记录或选项的范围字段照旧原样发送。
  • 先看:packages/qa/scenarios/src/runner/orchestrate.ts 的种子循环与 seedDependence;packages/qa/scenarios/src/runner/scorecard.ts 的未测计数;packages/qa/scenarios/src/runner/prompts.ts 的 userHandleFields 与 Row ownership 文案。
  • 两分钟验证:pnpm --filter @objectstack/scenarios exec vitest run test/runner,预期全部通过,其中"loads all 8 rows"断言种子 8/8 入库,"a refused seed row"两条断言 ⚠ seed 警告与未测标记。
  • 需要你做的:无,席位落地。

What changes

Defect 1 — the seed lost every owner-scoped row. The seed loop set owner_id for a row whose persona-scope field named a persona's handle, but sent the field itself as the handle text (owner: "u_sam"). A faithful build models that field as a user lookup and refuses the handle; a handle no persona holds (u_tia) had no user at all, and every contact under a refused account failed as a cascade.

  • prompts.ts userHandleFields(card): the direct scope fields whose seed values are user handles — a field a persona's direct scope rule names that the card declares as neither a lookup nor a select, and that is not id. Across the nine cards in docs/qa/scenarios/ that is exactly account.owner (crm), invoice.owner (invoice) and task.assignee (project). Every other direct scope field names a seeded row (asset.custodian = e06, department.id) or an option (ticket_comment.visibility = public) and is still sent verbatim — replacing those values would have broken the lookups on six other cards.
  • orchestrate.ts seed loop: a user-handle field now carries the persona's user id in the field itself and in owner_id. owner_id stays mapped: it is the platform's canonical, reassignable record owner (packages/objectql/src/registry.ts), the column owner-based sharing reads. A handle no persona holds is loaded as the admin user (field and owner_id), and the run record lists it under seed.handlesAsAdmin.
  • prompts.ts dataContract: the Row ownership paragraph now states the contract the seed keeps. The handle field and owner_id both get the user's id: the persona's user for a held handle, and the admin the runner signs in as for an unheld one. For crm it reads: "In democrm_account.owner the value is a handle that stands for a user, and the handle text is never stored: the runner loads that field, and the row's owner (owner_id), with that user's id (u_sam → the sdr user; u_tia, which no person below holds → the admin user the runner signs in as). Make that field accept a user id; a rule on it matches that user." This is the card's "state another contract in the build prompt" branch.

Defect 1, ask 3 — a refused seed row makes the Run half not measured, never a fail.

  • orchestrate.ts seedDependence(card, refused) takes out the refused rows and, through the card's declared lookups (expect.lookups), every seed row under one, transitively. A contact of a refused account is out whether or not the app refused it too.
  • A question is not measured when its object holds a row that is out, because its truth was computed over rows the app does not hold. An operation is not measured when its target, a lookup value it writes, or a post-state row (id, or a lookup value in its values) names a row that is out.
  • runHalf does not ask those turns. Each is recorded with notMeasured and the reason, for example seed: needs democrm_contact ct2, a child of democrm_account ac1, which the app refused. Everything else is asked and graded as before.
  • scorecard.ts adds a Seed row and an Iterate · Seed row, for example 7/8 ⚠ seed: 1 refused. The ask and operation tallies leave not-measured turns out of both passed and total, and their cells name the count, for example 4/4 · 5 not measured. A half with nothing measured stays not-run in the record, never fail.

Defect 2 — the author loop never ran verify. verifyArgs(card) is ['verify', ...verifyFlags(card)]. buildHalf runs exactly that, and the build prompt and the iterate prompt both name it next to validate and build: for the crm card, npx objectstack validate, npx objectstack build and npx objectstack verify --reference-shapes, "until all three pass". The template-mode iterate prompt is unchanged: that mode edits the installed copy through the metadata API and runs no verify.

Tests

  • test/runner/orchestrate.test.ts, new top-level describe runCard — a refused seed row makes what reads it not measured, never a fail, with RefusingApp, a double that refuses the seed rows it is told to:

    • shows ⚠ seed: 1 refused, leaves the questions on that object unasked and unscored, and grades the rest as usual. It refuses ct4: the scorecard reads | Seed | 7/8 ⚠ seed: 1 refused |; the five contact question turns are not measured and were not asked; the four account turns are graded (4/4); the five operations are graded (5/5); and the record's run-ask is pass with notes 4/4 · 5 not measured.
    • an operation whose target hangs off a refused row is not run; a half with nothing measured is not-run, not fail. It refuses all three accounts: only contact_bad_email runs, all 9 question turns are out, run-ask is not-run and run-operations is pass.
    • The clean run now also asserts | Seed | 8/8 | — |, with no warning.
  • test/runner/units.test.ts a refused seed row takes out its children and what the card names on them, and nothing else pins the rule on ac1: its target, a child, and the account an operation writes are out; add_contact and contact_bad_email are not.

  • test/runner/orchestrate.test.ts, new top-level describe runCard — the seed against a build whose owner field is a user lookup. It uses UserLookupApp, an app double that refuses an owner/owner_id naming no user and a contact whose account names no loaded account, as the first live run's app did.

    • the double refuses the handle the old seed sent, so it can fail.
    • loads all 8 rows: the owner field carries the persona user id, and a handle no persona holds is the admin: seed equals { inserted: 8, refused: [], handlesAsAdmin: ['u_tia'] }; ac1 and ac3 carry the sdr user in owner and owner_id, ac2 the admin; all five contacts load; the five operations pass.
    • The existing ownership test now expects ac2's owner_id to be the admin (it was undefined).
  • test/runner/units.test.ts:

    • both authoring loops name the verify invocation the runner scores with, next to validate and build.
    • a user-handle field is a scope field the card declares as neither lookup nor select; the contract says it is loaded with a user id, which also pins that service_desk has none.
  • The regression test is built from the card in the repo (crm_account_with_contacts), not from the first run's recording, which the epic seat holds. No live model was run.

Acceptance notes

  • Non-persona handles become the admin, not a provisioned user. The seat accepted this as the card's "state another contract in the build prompt" branch. The admin is a user the runner already holds, it grants no new permission, and it keeps all 8 rows loadable with no new HTTP path. The cost: two unheld handles on one card would both map to the admin, so a truth grouped by owner could not tell them apart. No card today has one.
  • The not-measured rule is object-level for questions. A question is out when any row of its own object is out, even if its filter would not have read that row. That is the reading the card states ("depends on a refused row"), and it never scores a truth the app could not have answered. The cost is that one refused contact takes out every contact question.
  • Not-measured turns are not asked. That saves model turns, and a grade against rows the app lacks would mean nothing.
  • The package README is not updated. It does not describe the Seed row or the not-measured marks, because fix(scenarios): the loader refuses an operation its persona cannot reach, and two cards stop writing hidden fields #30846 has the README in flight.
  • Out of scope, seen while reading other cards. personaHandles treats every direct scope string value as a handle, so the seed loop's owner_id mapping (unchanged here) also fires on fields that name rows or options. On service_desk, 237 public ticket_comment rows are owned by the portal_customer user (visibility = public), and 15 tickets name two personas, so the last field read wins owner_id. This is reported to the seat, not fixed here.

Verification

Package tests and typecheck (head b1f0628)
  • pnpm --filter @objectstack/scenarios test :: exit 0 — Test Files 18 passed (18), Tests 245 passed (245)
  • pnpm --filter @objectstack/scenarios typecheck :: exit 0 (the tsconfig includes test/**/*)
Ablations (each restored, blob equal to HEAD, git diff HEAD empty)
  • Deleting body[k] = userId; in the seed loop: the 8-row pin goes red. inserted is not 8 and refused is not empty: the double refuses the handle.
  • Replacing the verify-naming checks line with the old validate-and-build text: the verify-invocation pin goes red.
  • Feeding seedDependence an empty refused list in runHalf: both refused-seed run tests go red.
  • Deleting the ⚠ seed: N refused suffix in seedCell: both refused-seed run tests go red. The first attempt used an empty --replacement, which the tool refused before running anything; it was redone with --delete.
Derived gates on the final diff: 48 of 48 run, every one exit 0 (head b1f0628, merge base 98f640d, origin/main not moved since round 1)

🤖 Generated with Claude Code

…d both author loops run verify

The seed loop left a persona-scoped owner field holding the handle text
(`owner: "u_sam"`), which a build that models the field as a user lookup
refuses, and sent a handle no persona holds (`u_tia`) as-is. A user-handle
field (a direct scope field the card declares as neither lookup nor select,
and not `id`) now carries the persona's user id, or the admin's for an
unheld handle, in the field and in `owner_id`; the record lists the unheld
handles. Scope fields naming a row or an option are sent verbatim, as
before.

The build and iterate prompts now name the same `objectstack verify`
invocation `buildHalf` scores with (`verifyArgs`), next to validate and
build, and the data contract says the handle field is loaded with a user id.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

24 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 98f640d581339769d54cf27c8f7b02cbc46849f4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 29b4cb91e9d31281ca4e67ed1197737467515a3a — the merge of head b1f06284dc614f299377ceb493c2cb7a6bb4ed0e into base 98f640d581339769d54cf27c8f7b02cbc46849f4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 29b4cb91e9d31281ca4e67ed1197737467515a3a && git checkout 29b4cb91e9d31281ca4e67ed1197737467515a3a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 98f640d581339769d54cf27c8f7b02cbc46849f4 b1f06284dc614f299377ceb493c2cb7a6bb4ed0e && git checkout -B drift-repro 98f640d581339769d54cf27c8f7b02cbc46849f4 && git merge --no-ff b1f06284dc614f299377ceb493c2cb7a6bb4ed0e

node scripts/docs-audit/affected-docs.mjs --json 98f640d581339769d54cf27c8f7b02cbc46849f4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…and the scorecard says so

A refused seed row and, through the card's declared lookups, every seed row
under it are out. A question on an object holding such a row, and an
operation whose target, lookup values or post-state name one, are not asked:
each is recorded with the reason, and counts in neither passed nor total. The
scorecard gains a Seed row (`7/8 ⚠ seed: 1 refused`), the ask and operation
cells name how many were not measured, and a half with nothing measured stays
`not-run` in the record.

The build prompt's ownership paragraph now states the seed contract exactly:
the handle field and `owner_id` both carry the user's id, the persona's user
for a held handle and the admin the runner signs in as for an unheld one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b1f06284dc614f299377ceb493c2cb7a6bb4ed0e
Local-runs: none
Scope: packages/qa/scenarios/src/runner/orchestrate.ts, packages/qa/scenarios/src/runner/prompts.ts, packages/qa/scenarios/src/runner/scorecard.ts, packages/qa/scenarios/test/runner/orchestrate.test.ts, packages/qa/scenarios/test/runner/units.test.ts + packages/qa/scenarios/src/runner/app.ts, packages/qa/scenarios/src/runner/project.ts, packages/qa/scenarios/src/runner/observe.ts, packages/qa/scenarios/src/schema.ts, packages/qa/scenarios/src/loader.ts, packages/qa/scenarios/src/score/scenario-run-grade.ts, packages/qa/scenarios/src/score/scenario-operation-grade.ts, packages/qa/scenarios/README.md, docs/qa/scenarios/README.md, docs/qa/scenarios/*.json (all nine cards)

Inputs: card #30882 (body and all three comments, read as claims), PR #30903 body, labels (size/m, tests, skip-changeset) and file list, git diff origin/main...b1f06284 (merge base 98f640d581, 5 files, +380/-36, three commits), the files above at origin/main and at the head, and the head's check-runs. The dev reports and the seat claim were checked against the tree, not adopted.

① Derived judgments

Defect 1, asks 1 and 2 — a user-handle field carries a user id in the row and in owner_id. The seed loop (orchestrate.ts runHalf) now, for every direct scope field holding a string, maps the handle to a persona account and sets owner_id as before; when the field is in userHandleFields(card) it also writes body[field] = userId and body.owner_id = userId, with userId the persona's user or app.adminUserId() for a handle no persona holds, and records the unheld handle in seed.handlesAsAdmin. Read against the crm card: ac1 and ac3 (u_sam) carry the sdr user in owner and owner_id, ac2 (u_tia) carries the admin in both; the five contacts reference loaded accounts. Against a build that models owner as a user lookup, 8/8 load. Verified by reading, and the UserLookupApp pin asserts it.

userHandleFields across the nine cards. The rule: a direct (non-path) persona scope field, not id, not in expect.lookups, not in expect.selectFields. I evaluated it by hand against every card at origin/main:

  • asset_lifecycle: custodian / employee / reported_by / requested_by / department are all declared lookups, department.id is id. None. Values such as e06 stay verbatim.
  • crm_account_with_contacts: account.owner (values u_sam, u_tia). Selected.
  • invoice_with_line_items: invoice.owner (values u_ana x2, u_ben x1; u_ben unheld, loads as admin). Selected.
  • leave_balances: employee / department fields are lookups. None.
  • order_to_cash: account.owner and sales_order.owner are declared lookups to employee (e03 is a seeded row). None, correctly.
  • project_with_tasks: task.assignee (values u_kim x3, u_lee x3; u_lee unheld). Selected.
  • requisition_to_receipt: requester is a lookup, department.id is id. None.
  • service_desk: assignee / group / customer are lookups, customer.id is id, ticket_comment.visibility is a select (public). None; the units test pins this card at the empty set.
  • travel_expense: traveler / claimant are lookups, department.id is id. None.
    So the selection is exactly account.owner, invoice.owner, task.assignee, as the PR body says; it catches every field whose seed values are person handles and leaves every row id and option value alone. No card has a truth grouped or filtered on one of those three fields, no card hides one of them (so hiddenValues is unaffected), and the only operations that name a handle (project_with_tasks add_task / task_without_title, values.assignee: u_kim) do so in op.values, which neither operationPrompt nor the operation grader reads (the grader reads post-state where / values only, and those do not name the assignee). The rule is correct for the library as it stands; its nature as an undeclared heuristic is noted under ④.

Unheld handle to admin. The card's ask 2 offers two branches: provision a non-login user, or state another contract in the build prompt. The PR takes the second and states it: the Row ownership paragraph now reads, for crm, that in democrm_account.owner the handle text is never stored, the runner loads the field and owner_id with the user's id, u_sam to the sdr user, and u_tia, which no person below holds, to the admin user the runner signs in as, and asks the build to make the field accept a user id with a rule on it matching that user. I compared this sentence with the seed loop line by line: field and owner_id both get the id; held handle gives the persona's user; unheld gives the admin. It matches. The cost the PR names (two unheld handles collapse to one user) is real but bites no card today: invoice has one unheld (u_ben), project one (u_lee), crm one (u_tia), and no truth groups by those fields. Honest and stated.

Defect 1, ask 3 — a refused seed row makes the Run half not measured. seedDependence(card, refused) builds the out set: the refused rows plus, to a fixed point, every seed row whose declared lookup names an out row. A question is not measured when any seed row of its object is out; an operation when its target, a lookup value in its values, or an applied post-state row (id, or a lookup value in its values) is out. runHalf pushes such turns with notMeasured and does not call the driver. askTally / opsTally count only measured turns in passed and total and carry notMeasured; seedTally renders N/M ⚠ seed: K refused; verdictOf gives not-run on total === 0, so a half with nothing measured never reads fail, and the run record's notes carries the · N not measured suffix. The CLI prints renderScorecard, so the Seed rows reach the operator.

  • Can a turn that depends on a refused row still be graded? I looked for a path: truths and persona scopes traverse only lookups declared in expect.lookups (the loader's checkPath), and the closure walks exactly those, so a question whose truth reads a refused row through a path has an out row on its own object and is dropped. An operation whose post-state where (not values) names a lookup value is the one shape not inspected; in every card the same lookup is also in op.values, which is inspected. No false grade found.
  • Can a turn that does not depend on one be wrongly dropped? Yes, by design: the question rule is object-level. On crm, a refused ct4 (account null) drops contacts_per_account/sdr, whose scope account.owner = u_sam never reads ct4. The PR body states this. It never produces a false verdict, and the cell shows the count, so it is a should-fix (④), not a must-fix.
  • Not asking the dropped turns: the card says "mark ... as not measured"; a turn with no grade and no model call is the strictest reading of that and costs nothing the card asks for. Acceptable.

Defect 2 — both author loops name the scored verify invocation. verifyArgs(card) = ['verify', ...verifyFlags(card)]; buildHalf runs ops.os(dir, verifyArgs(scenario.card)) and project.ts os() spawns npx objectstack ...args. authoringChecks(card) renders npx objectstack validate, npx objectstack build and npx objectstack ${verifyArgs(card).join(' ')}, and both buildPreamble and iteratePrompt embed it. For the iterate pass buildHalf receives iteratedScenario, which changes only expect, so verifyFlags (read from capabilities) is the same list the prompt named. Exact match. templateIteratePrompt is unchanged and names no verify; template mode runs none, so there is nothing to name.

Tests. Reverting each fix reds its pin: deleting body[k] = userId makes UserLookupApp refuse u_sam (the 8-row pin reads refused: []); restoring the old preamble reds the verbatim verify pin; passing an empty refused list to seedDependence leaves nine turns asked and graded (the ScriptedDriver answers from the loader's truth, so they pass, and total: 4 / notMeasured: 5 fail); dropping the ⚠ seed suffix reds both scorecard string pins. The RefusingApp second case (all three accounts refused) pins the transitive closure without the double cascading, not-run on run-ask and pass on the one measured operation. The units test pins seedDependence per operation and question, including move_eli (target ct5 under ac3, out through the account it writes). Doubles: UserLookupApp refuses an owner or owner_id naming no known user on democrm_account and a contact.account naming no loaded account, with the first run's VALIDATION_FAILED wording; it learns persona user ids from createPersona. That is at least as strict as the live door for owner and faithful to the cascade the card recorded. The dataContract sentence is pinned verbatim, as the card's "must match" requires.

The landing. @objectstack/scenarios is private: true with no build step, so skip-changeset is right and nothing publishes. Clause-②: no and Metadata-protocol: none are correct: no spec, no authorable key, no governed path. Fixes #30882: ask 1 (field carries the id), ask 2 (contract stated and the prompt matches the code), ask 3 (not measured, ⚠ seed: N refused, dependents marked) and Defect 2 (verify named in both loops) are each met in the tree, so Fixes is honest for the asks; the card's Verification step (a live re-run of crm) is a model run the epic seat holds the recording for and is not performed here, which the PR says plainly. The PR body matches the tree at every claim I could check against the diff (the field selection, the three sentences, the test names and assertions, the acceptance notes). Files: five, all under packages/qa/scenarios/src/runner/ and test/runner/; loader.ts, the nine card JSONs, packages/qa/scenarios/README.md and refusals.test.ts are untouched, as the #30846 constraint requires. Draft: yes (the seat lands).

Check-runs on the head (all completed). Required contexts: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (planner produced six shards, 1/8 to 6/8 all success), Build Core success, Governed Surface Queue Guard success, Test UI success, Live E2E UI success; Lint UI, Type Check UI, Build & E2E UI skipped by path filter (no ui/ change). Also green: Dogfood Regression Gate, PR-body guards, Check PR Size. Nothing in progress.

② Semver level

None. The package is private and unpublished; skip-changeset is correct. No published package moves.

③ Boundary flags

④ Design judgment

  • S1 (should-fix). The object-level question rule in seedDependence drops turns that do not depend on the refused row (crm: ct4 refused takes out contacts_per_account/sdr, whose scope never reads ct4). The loader's truth evaluator can decide dependence exactly: a question depends on the out set when its truth, or any persona truth, differs when evaluated over the seed minus the out rows. That keeps the "never a false fail" property and measures more. Follow-up, not this PR; the cell shows the count so the loss is visible.
  • S2 (should-fix). userHandleFields is a heuristic the card format does not declare: a future card with a direct scope on an undeclared text or option field would have that field sent as a user id and refused, with nothing at authoring to say why. Contract-first (the card is the producer) says the card should declare its user-handle fields and the loader should refuse a direct scope value on an undeclared non-lookup field that is not a handle. loader.ts is out of bounds for this PR; file it behind fix(scenarios): the loader refuses an operation its persona cannot reach, and two cards stop writing hidden fields #30846. Meanwhile, recording the selected fields in the run record (beside handlesAsAdmin) would make a misfire readable in the record rather than only in the refusals.
  • S3 (should-fix). File the personaHandles over-matching finding as a devx card (PD chore: version packages #10), naming service_desk ticket_comment.visibility = public and the two-persona rows as the repro; do not leave it in the report comment.
  • S4 (should-fix). The card's Verification (re-run --only crm_account_with_contacts --modes scratch, seed 8/8) remains owed to the epic seat; the seat should say so on the card at landing so a closed card does not read as a measured re-run.
  • S5 (nit). buildHalf's comment says "the invocation every authoring prompt names"; the template iterate prompt names none. The package README's persona-contract line now understates the seed (it says the row is owned by the persona's user, not that the field carries the id); it is held by fix(scenarios): the loader refuses an operation its persona cannot reach, and two cards stop writing hidden fields #30846 and should be corrected in its wake.

No must-fix. The tree does what the card's four asks say, the prompt matches the code word for word, the tests pin each ask and go red when its fix is reverted, and the half with nothing measured is not-run from verdictOf through the scorecard and the run record.

Implemented-by: claude/issue-30882-scenario-seed-owner
Reviewed-by: session_017ihL2ebbZkr2QAEG1x7UC8

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

席位核对 — ACCEPT · domain:devx#2 · Head-sha b1f06284dc614f299377ceb493c2cb7a6bb4ed0e · 2026-11-06T10:49Z

  • Shape: 5 files under packages/qa/scenarios, +380/−36, read from the diff and not the report: src/runner/orchestrate.ts, src/runner/prompts.ts, src/runner/scorecard.ts, test/runner/orchestrate.test.ts, test/runner/units.test.ts.
  • The card's asks, each met in the tree:
    • Defect 1, ask 1: a persona-scoped handle field (account.owner, invoice.owner, task.assignee across the nine cards) now carries the persona's user id in the row and in owner_id.
    • Defect 1, ask 2: a handle no persona holds (u_tia) loads as the admin user the runner signs in as, and the run record lists it. This is the card's "state another contract in the build prompt" branch, and the build prompt's Row ownership text states it word for word.
    • Defect 1, ask 3: a refused seed row makes the Run half not measured, never a fail. The scorecard shows ⚠ seed: N refused. Turns that depend on a refused row are recorded as not measured with their reason, and a half with nothing measured reads not-run.
    • Defect 2: both the build and iterate prompts name npx objectstack verify --reference-shapes, the same verifyArgs(card) that buildHalf scores with.
  • Review: independent contract review PASS on this head (6683045112), with no must-fix. The seat's reading of its should-fixes:
  • Gates:
    • Head checks: 33 success and 17 skipped, none red, mergeable clean.
    • check-expected-skips: OK, all 17 skips are in main's roster (exit 0).
    • check-governed-merges: NOT governed (exit 0). 416 changed lines, which is ≤ 3000.
    • Dev's dispatch-gates --commands: 48 of 48 exit 0 on this head, and --ran reconciled. Package tests 245/245.
  • Out of scope: Skills-docs: n/a, Clause-②: no, Metadata-protocol: none, skip-changeset (the package is private: true).
  • Landing: non-governed. Ready, then auto-merge through the merge queue. Fixes #30882. The live re-run (run-scenarios --only crm_account_with_contacts --modes scratch, seed 8/8) needs a model and the maintainer's go-ahead, so it is not done here. It goes back to the epic seat, which holds the first run's recording.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 10:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 10:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit d8b2295 Nov 6, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30882-scenario-seed-owner branch November 6, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

1 participant