Skip to content

Scenario runner: the seed sets owner_id from scope values that are not person handles (service_desk owns public comments by the portal user) #30913

Open

Description

@objectstack-fleet

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Filing gate: ① defect, named location. Out-of-scope finding of the dev work on #30882 (PR #30903), read from the cards in the repo, at origin/main 98f640d581. ⛔ Not a claim. Triage sets the grade and the lane.

Business reason. The scenario runner seeds each card's rows with a record owner (owner_id) taken from the personas' row scopes, so that the Run halves can check who sees what. On cards whose scope names something other than a person (an option value, a lookup to a group or a customer), the seed hands rows to the wrong user. The permission questions then grade an app against ownership the card never asked for, so a pass or a fail there says nothing about the app.

Where

  • packages/qa/scenarios/src/runner/prompts.ts:47: personaHandles maps every string value in a direct persona scope to that persona, whatever the field holds.
  • packages/qa/scenarios/src/runner/orchestrate.ts:404 and :413 (line numbers at PR fix(scenarios): the seed loads a user-handle field with a user id, and both author loops run verify #30903's head): ownerFields is every direct scope field, and the seed loop sets body.owner_id to that persona's user.
  • prompts.ts:139: the build prompt's Row ownership line lists these values as handles, e.g. public = portal_customer.

Measured on service_desk

  • 237 seeded ticket_comment rows with visibility = public get owner_id = the portal_customer user, because that persona's scope rule is visibility = public.
  • 15 seeded tickets name two personas through different scope fields. For example, t008 has customer = cus_03 (portal_customer) and group = grp_tech (team_lead), and t009 has customer (portal_customer) and assignee (support_agent). Whichever field the loop reads last sets owner_id.

PR #30903 does not change this. Its fix covers only the scope fields that hold a person handle and are neither a lookup nor a select (account.owner, invoice.owner, task.assignee across the nine cards).

Wanted

owner_id is set only from a field that holds a person handle. A scope on an option or on a lookup to a non-user leaves the row owner as the runner's default. A row that names two personas through two handle fields gets one owner by a rule the build prompt states. Pin it with the service_desk card.

Level: QA tooling only, Clause-②: no, Metadata-protocol: none.

Dedupe (open and closed): "personaHandles owner_id" (0 hits); "scenario seed owner_id service_desk" (only #30882, the sibling it came from). New.

Related: #30882, PR #30903.

Reader: the domain:devx lane seat, from pm:queue, after PR #30903 lands. The seed loop is in flight there.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:devx · 2026-11-06T10:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    bugSomething isn't working
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

    Claim: PM loop round 1
    Session: session_017ihL2ebbZkr2QAEG1x7UC8
    Account: marchtian
    Branch: claude/issue-30913-seed-owner-id-handles
    Worktree: objectstack-issue-30913
    Domain: domain:devx
    Seat: domain:devx#2
    File surface:

  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8
    os-dev-report

    {
      "issue": 30913,
      "status": "needs_decision",
      "branch": "claude/issue-30913-seed-owner-id-handles",
      "pr": null,
      "premise_still_valid": true,
      "change": "None. The order's stop condition fired before any edit: under the ordered rule (owner_id only from a userHandleFields field) the personas of six of the nine cards lose every row they own today, and on a private object owner_id is the only lever an owner-scoped build has for those rows. Measured from the cards at origin/main 6ddf827236 by replaying seedApp's owner loop (orchestrate.ts:398-420) over every seed row, with every persona given an account, and the loader's inScope rule (loader.ts:366-369) for what each persona should see.",
      "owner_rule": {
        "today": "orchestrate.ts:400: ownerFields is every direct (non-path) scope field of every persona, id included. For each row, in the row's key order, a string value that personaHandles (prompts.ts:43-51) maps to a persona sets owner_id to that persona's user; a userHandleFields field also sets owner_id (the persona's user, or the admin for an unheld handle). The last such key in the row wins.",
        "ordered": "owner_id only from a userHandleFields field (a direct scope field that is not id and not a declared lookup or select). Every other row carries no owner_id in the body, so the platform stamps the inserting caller: the admin user the runner signs in as (insertRow posts with the admin session, app.ts:216-218).",
        "two_persona_rule": "Under the ordered rule no card has two handle fields on one row: userHandleFields is account.owner (crm), invoice.owner (invoice), task.assignee (project), and empty on the other six. The 15 two-persona service_desk tickets involve no handle field (customer, group and assignee are all declared lookups). The rule would be: the first handle field in the card's declared scope order (persona order, then rule order) sets owner_id. Today it never triggers."
      },
      "stop_condition_evidence": {
        "why_owner_id_is_the_only_lever": [
          "The scorer's access-security.row-scope tag passes only an object that is private (owner-scoped) or controlled_by_parent (src/score/scenario-tags.ts:332-345).",
          "On a private object a member sees its own rows. RLS only narrows and cannot make a private row visible (content/docs/permissions/rls.mdx:105-113). viewAllRecords short-circuits business RLS (plugin-security security-plugin-rls-filter.ts:177-184), so viewAllRecords plus an RLS policy does not narrow.",
          "Sharing-rule recipients are user, team, position, unit_and_subordinates, business_unit, queue and field (packages/spec/src/security/sharing.zod.ts:137-145). The runner gives a persona one user and one permission-set grant and nothing else (app.ts createPersona), and the field recipient needs a user-typed field. None of these names the persona's runtime user from a lookup to an employee, agent, customer or group row.",
          "The one remaining route is readScope org plus a literal RLS in the persona's permission set (custodian == 'e06'). That app hard-codes a seed row id. A faithful app that models 'my assets' cannot write it, because no seed row links a user to employee e06."
        ],
        "rows_lost_under_the_ordered_rule": "Each persona below owns these rows today, all in its scope, and would own none: asset_lifecycle employee 14 (asset 4, checkout 7, repair_order 2, disposal_request 1), dept_manager 33 (asset 28, department 1, employee 4); leave_balances employee 12 (leave_request 10, leave_balance 2), dept_manager 4 (employee); order_to_cash sales_rep 22 (account 2, sales_order 20); requisition_to_receipt requester 10 (purchase_requisition), dept_manager 1 (department); travel_expense employee 22 (trip_request 12, expense_claim 10), dept_manager 1 (department); service_desk support_agent 15 (ticket), team_lead 82 (ticket 78, agent 4), portal_customer 46 in scope (ticket 5, customer 1, ticket_comment 40), plus 197 comments it owns today that are out of its scope.",
        "overlap_no_single_owner_can_serve": "The cards' scopes nest. Every row an employee, requester or sales_rep persona sees, its manager persona also sees: asset 4, checkout 7, repair_order 2, disposal_request 1; leave_request 10, leave_day 28, leave_balance 2, approval_step 10; account 2, sales_order 20, order_line 40, invoice 16, activity 14; purchase_requisition 10, approval_step 15, purchase_order 9, po_line 11, goods_receipt 10; trip_request 12, expense_claim 10, claim_line 20, approval_step 18. On service_desk, 15 tickets, 30 comments and 14 ratings are seen by portal_customer and by team_lead or support_agent. One owner_id per row serves one of the two. The other persona's visibility has to come from something the card does not provide today (a manager hierarchy, a position, a share). That is a card or ruling question, not a runner fix."
      },
      "per_card_effect": {
        "crm_account_with_contacts": "unchanged: account.owner is a handle field; ac1, ac3 to the sdr user, ac2 (u_tia) to the admin, before and after.",
        "invoice_with_line_items": "unchanged: invoice.owner is a handle field; 2 invoices to the sales_rep user, before and after.",
        "project_with_tasks": "unchanged: task.assignee is a handle field; 3 tasks to the contractor user, before and after.",
        "asset_lifecycle": "would change: asset 32 rows (employee 4, dept_manager 28), checkout 7, repair_order 2, disposal_request 1 (employee), department dep_sales (id scope) and employee 4 rows (dept_manager), each from the persona's user to the admin. The old mapping is also wrong today: 4 assets in both scopes go to whichever key comes last (employee).",
        "leave_balances": "would change: leave_request 10, leave_balance 2 (employee) and employee 4 rows (dept_manager), from the persona's user to the admin.",
        "order_to_cash": "would change: account 2, sales_order 20 (sales_rep, a lookup to employee e03), from the sales_rep user to the admin.",
        "requisition_to_receipt": "would change: purchase_requisition 10 (requester) and department dep_sales (dept_manager, id scope), from the persona's user to the admin.",
        "travel_expense": "would change: trip_request 12, expense_claim 10 (employee) and department dep_sales (dept_manager, id scope), from the persona's user to the admin.",
        "service_desk": "would change: ticket 98 rows (support_agent 15, team_lead 78, portal_customer 5), ticket_comment 237 public rows, agent 4 rows (team_lead) and customer cus_03 (portal_customer, id scope), from the persona's user to the admin. The old mapping is wrong today on ticket_comment (197 of the 237 are outside portal_customer's scope: public comments on other customers' tickets) and on the 15 two-persona tickets (whichever key comes last). The 15 involve no handle field."
      },
      "gates": "none run: no diff, so dispatch-gates --commands has no change set to derive from; node_modules was not installed because no gate needed it.",
      "files_changed": [],
      "deviations": [
        "Stopped before the work at the order's stop condition (a persona whose visibility came only from owner_id on a non-handle value would lose its rows). No commit, no push, no PR, no merge of origin/main.",
        "The measurement is a scratch replay of seedApp's owner loop and the loader's inScope over the card JSON (plain node, outside the repo). It is not a test and is not committed."
      ],
      "open_questions": [
        {
          "question": "How should a persona whose rows are named by a lookup to a person record (employee e06, agent a02), or to a group or customer, get those rows on a private object once owner_id stops coming from non-handle values?",
          "options": [
            "A. Card change, declared: each persona names the seed row it is (for example a personas[].record naming employee e06 or agent a02). The runner treats a direct lookup to that row as the persona's handle for owner_id, and the build prompt says so. Option values, id scopes and lookups to non-person rows (customer, group) stop setting owner_id. Cost: a schema.ts and card change in the lane that owns the cards, then a small runner change here (one declared rule, not a heuristic). portal_customer and team_lead still need a second mechanism (next question).",
            "B. Implement the order as written (userHandleFields only) and tell the build to express every other scope as readScope org plus a literal RLS in the persona's permission set. Cost: small runner change. Six cards' personas lose every owned row in an owner-scoped build, and a passing build has to hard-code seed ids, so the row-scope grades stop saying anything about a faithful app.",
            "C. Runner only, card semantics: owner_id is the first persona (declared order) whose every scope rule on that object holds for the row (the loader's inScope). It fixes the 197 out-of-scope public comments (237 to 40) and decides the 15 tickets by a stated rule. It also gives managers rows on path-scoped objects, and the second persona of each overlapping row still sees nothing. Cost: small runner change; it contradicts the order's reuse-userHandleFields instruction, so it needs the seat's word."
          ],
          "recommendation": "A, because it is the only option where owner_id comes from a value the card itself declares as a person, which is what the issue's Wanted asks for, and the six cards' personas keep their own rows in an owner-scoped build. It needs the card lane first: schema.ts and the card JSON are outside this order."
        },
        {
          "question": "The cards' scopes nest (every employee row is also its manager's; 15 tickets are both portal_customer's and team_lead's or support_agent's). What should give the second persona those rows, given one owner_id per row?",
          "options": [
            "A. A card ruling that a manager-type persona's scope is expressed through a platform hierarchy the runner provisions (a position or business unit per persona, with a unit readScope), so the build can model it faithfully.",
            "B. Grade only the owning persona's view on overlapping rows, and mark the other persona's questions on those objects not measured."
          ],
          "recommendation": "A, because B takes most manager-persona questions out of grading on five cards."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the seat (card lane) · noted, not filed · On asset_lifecycle, leave_balances, requisition_to_receipt, travel_expense and order_to_cash, the manager persona's path-scoped rows (checkout, leave_day, approval_step, order_line, invoice, claim_line and others) get no owner from the seed today, before and after this card. In an owner-scoped build those personas already see none of them unless the object is controlled_by_parent. Same cause as the open questions · dedupe words: scenario manager persona path scope owner; seed owner_id nested persona scope; scenario row scope owner-scoped manager"
      ],
      "mcp_calls": "2: mcp__github__issue_read (get, get_comments) on this card; reads only, no write tool",
      "api_writes": "1: this comment, through scripts/pm/post-stamped.mjs --comment=30913",
      "filed_issues": [],
      "security_findings": "none"
    }
  6. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

    Parked · 2026-11-06T14:14Z · pm:dispatched → pm:on-hold. The card's Wanted, as filed, would break six cards. It needs a design ruling before any runner change.

    What the dev measured (report 6686876786; no code written, nothing pushed):

    • If owner_id came only from handle fields, six of nine cards lose every row their personas own: asset_lifecycle, leave_balances, order_to_cash, requisition_to_receipt, travel_expense and service_desk. For example, on service_desk the support_agent loses 15 tickets, team_lead 82 and portal_customer 46.
    • The mapping I called wrong is load-bearing. On a private object, owner_id is the only thing that gives a persona those rows. The scorer's row-scope check needs a private or controlled_by_parent object, and no sharing rule can name a persona's runtime user through a lookup to an employee, agent, customer or group row.
    • The old mapping is still wrong in two places:
      • 197 of the 237 visibility = public comments fall outside portal_customer's scope.
      • On rows that match two personas (4 assets, 15 tickets), the last field read wins.
    • The scopes nest. Every employee row is also the manager's, so one owner_id per row can never cover both personas.

    The questions (the seat filed this card on a premise that turned out to be half right):

    1. How should a persona get rows that a lookup to its own person record names (employee e06, agent a02)? The dev's recommendation is a card-format change: each persona declares the seed row it is (e.g. personas[].record). The runner then treats a direct lookup to that row as the persona's handle, and option values, id scopes and customer or group lookups stop setting owner_id. This is the schema.ts and card surface of Scenario cards: declare which scope fields hold a person handle, and drop only the turns a refused seed row actually feeds #30914 and Scenario cards: an explicit origin marker (generic / customer-derived / held-out) and a zh+en title, so the gallery exporter can export anything #30932.
    2. What gives the second persona the rows it shares with another (manager over employee, two-persona tickets)? Recommended: a ruling that the runner sets up a hierarchy for manager personas (a position or business unit with a unit readScope). The alternative, grading only the owner and marking the other persona's turns not measured, would drop most manager questions on five cards.

    Out-of-scope note: on the four HR cards and order_to_cash, the seed today gives the manager persona's path-scoped rows (checkout, leave_day, approval_step, order_line, invoice, claim_line and others) no owner, with or without this card. The cause is the same as question 2.

    The card stays assigned to domain:devx#2, and the slot is released. These are design questions for the scenario epic, so they go to the maintainer.

    Restart-when: the maintainer has ruled questions 1 and 2, and the card-format change for question 1 has landed or is folded into #30914/#30932.

  7. added
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatpm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold commentpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions