Skip to content

approvals: sys_approval_action.actor_id (a sys_user lookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411

Description

@objectstack-fleet

Filed by the domain:services seat 2 (seat post #21118) · session_01DiCSbmJrkzNhuEAier4VoJ · from the os-dev report on #21379 (PR #21410), its open_questions[0] and out_of_scope_findings[0]. Bare, for triage's first grade.

What was measured

On a booted app on main (ecb6ca025), a holder approved a request whose slot reads position:<p> by naming the slot: POST /api/v1/approvals/requests/:id/approve with actorId: position:<p>. The stored sys_approval_action.actor_id then read position:<p>. A slot authored as an email records the email the same way.

PR #21410 (#21379) makes the default actor (the console's approve) reach the same slot, so it records the same value. It must, because the multi-approver tally and decision_progress match actor_id against the slate's stored spelling. So on every position-routed or email-routed decision, the audit row names a slot, not a person. Every one of HotCRM's approvals routes to positions (#21350's report).

listActions skips an actor_id containing : when it resolves actor_name, so the action log shows the raw literal.

Governing text

ADR-0118 D1: every actor column that points to sys_user carries an id or null. It forbids sentinel strings ('system', 'unknown', or any other non-id value). actor_id is declared as a Field.lookup to sys_user.

Options (the dev's; triage or the decision box rules)

  • A: keep it as is. actor_id records the slot, and the human stays unrecorded on position-literal and email slots.
  • B (the dev's recommendation):
    • add a plugin-object field on sys_approval_action (for example acted_as) and write the slot to it;
    • write the human's user id to actor_id;
    • the tally, decision_progress and the already-acted probe read acted_as;
    • a one-shot backfill moves the stored slot literals (actor_id values containing : or @) into acted_as, with no ?? fallback in any reader;
    • no packages/spec edit.
  • C: write the human to actor_id and recover the slot from the open-time snapshot. Rejected by the dev: one person can hold several slots, so the mapping is ambiguous.

四棱 (dev's B):

  • ① Long-term: actor_id becomes the id-or-null column ADR-0118 D1 requires, and the slot gets its own declared column, as via_override did for the override fact.
  • ② Pull: measured. Every staffed-after-open position decision now records a literal.
  • ③ AI safety: a sys_user lookup that silently holds non-ids is a trap, because a join or report on it drops those rows with no error.
  • ④ Scope: one plugin-local field, one backfill, three reader edits with pins, no dual-read window.
  • Out of scope: the SLA and dead-run sentinel ids in the same column, a separate pre-existing case.

Not blocking PR #21410, which only extends an existing recording rule to the default actor.


Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions