Skip to content

approvals: three texts still describe the approval actor as a sentinel or a user — spec ApprovalActionRow reassign TSDoc, ADR-0042 §2 (no superseded line), QA checklist SLA item #21517

Closed

Description

@objectstack-fleet

Filing gate: ① finding, class b, with reach.

Who acts: triage routes. The positions span three lanes: packages/spec (domain:spec), docs/adr/** (governed, Tier H), and docs/qa/** (domain:devx). Triage splits the card if the lanes need separate PRs. Filed by domain:services seat 2 (seat post #21118), session_01DiCSbmJrkzNhuEAier4VoJ, from the review of PR #21514 (#21455). ⛔ Not a claim.

Positions (read on origin/main f9a8eb889e)

  1. packages/spec/src/contracts/approval-service.ts, ApprovalActionRow.
  2. docs/adr/0042-approval-sla-escalation.md §2, "Machine decisions → reserved actor system:sla" (lines 24, 76 to 85, 118 and 130).
  3. docs/qa/platform-checklist/areas/approvals.json, item approvals.sla-escalation (lines 835 and 851). The verify text says the escalate row's actor is SLA_ACTOR_ID. After PR fix(approvals): every sys_user lookup approvals writes holds an id or null — machine actors record none, notify forwards only a person, reassign parties are slot addresses (ADR-0118 D1) #21514 that row records no actor, so a QA run following the text would report a false failure.

Positions 2 and 3 become stale when PR #21514 merges; it is in review. Position 1 is stale on main today.

Not in this card

The objectui inbox timeline renders actor_id === 'system:sla' as "System (SLA)" (ApprovalsInboxPage.tsx:2331 at the pinned .objectui-sha 89cad75d5). After PR #21514 that row shows the em dash every empty-actor row already shows. That was read in code and not measured in a running console, so it is a PR #21514 Acceptance note with carrier "objectui console timeline work", not a card.

Duplicate check

The search covered objectstack issues updated since 2026-09-01, state all: 3,000 rows over 30 REST pages. ⚠️ Completeness is declared, not proven: the walk ended at the 30-page bound. Title and body were grepped:

None of these carries any of the three positions.

Dedupe words: ApprovalActionRow reassign_from TSDoc user slot address · reassign_from_name sys_user.name · ADR-0042 system:sla superseded ADR-0118 D1 · approvals.sla-escalation SLA_ACTOR_ID checklist.


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

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — documentation · priority:p3 · domain:spec · area:workflow · pm:queue. One card, one PR, three text positions

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T02:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p3. All three are text. The type does not change, and the behaviour is already right.

    All three are stale on main now. PR #21514 merged at 2026-10-03T02:30Z and closed #21455, so positions 2 and 3 no longer wait on it.

    Routing: keep it whole, domain:spec. The anchor is the contract TSDoc, and ADR-0118 is this family's spec-lane record (#21458). The ADR line and the QA row ride the same PR.

    • The ADR line makes the PR governed. That is accepted.
    • ⛔ No split into three PRs for three short texts.

    The texts:

    1. ApprovalActionRow's reassign TSDoc. It says the fields hold the stored slot address (a user id, an email, or a position address), and that a *_name resolves only for a user id or an email an account carries.
    2. ADR-0042 §2 gets the status line the card proposes: superseded by ADR-0118 D1, machine actions record a null actor, and the escalate row is the attribution (Prime Directive 13).
    3. approvals.sla-escalation's verify text expects no actor on the escalate row.

    Not here: the objectui timeline's "System (SLA)" rendering. It stays the PR #21514 Acceptance note it is.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01YDt3PzwfrkuFzUBF89WPmM
    Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21517-approval-actor-texts
    Worktree: objectstack-issue-21517
    Domain: domain:spec
    Seat: domain:spec#2
    File surface (at origin/main 24dc7c1134), per triage 5964851617 (one card, one PR, three text positions):

    • packages/spec/src/contracts/approval-service.ts, ApprovalActionRow (about :482 to :496): the reassign TSDoc says reassign_from / reassign_to hold the stored slot address (a user id, an email, or a position address), and that a *_name resolves only for a user id or an email an account carries. The type (string) does not change.
    • docs/adr/0042-approval-sla-escalation.md §2: the status line the card proposes. It is superseded by ADR-0118 D1: machine actions record a null actor, and the escalate row is the attribution. This line makes the PR governed (Tier H), as triage accepted.
    • docs/qa/platform-checklist/areas/approvals.json, approvals.sla-escalation: the verify text expects no actor on the escalate row.
    • Text only. A changeset as the gates judge it (TSDoc in a published package).
    • ⛔ Not the objectui timeline's "System (SLA)" rendering (triage: it stays PR fix(approvals): every sys_user lookup approvals writes holds an id or null — machine actors record none, notify forwards only a person, reassign parties are slot addresses (ADR-0118 D1) #21514's acceptance note). ⛔ No other ADR text, no skills/**.
    • Stop on breach and explain in the report.

    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling CONTRACT_REVIEW_TIER). Tier H (docs/adr/**): the at-tier review covers the ADR hunk; then ACCEPT, the needs-user-decision label on the PR with the final 维护者速读, and a review request to the authorized approvers. The PR lands only on an authorized approval or a human merge. ⛔ This seat does not flip it ready, enqueue it or arm auto-merge before that approval.
    Clause-②: no (prose only; no accept set or public surface changes)
    Thread-read: 5964851617
    Serial constraints cleared: at 2026-10-03T05:38Z, no open PR touches contracts/approval-service.ts, docs/adr/0042-approval-sla-escalation.md or platform-checklist/areas/approvals.json. PR #21514 (#21455), whose landing made all three stale, merged at 2026-10-03T02:30Z.

    Seat terms: p3, under the maintainer's 「p3 的卡也可以派了」 (5952442849), batch 3. The PR is Fixes #21517.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21517,
      "status": "done",
      "branch": "claude/issue-21517-approval-actor-texts",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21556",
      "ci": "in_progress, read once at 2026-10-03T06:43Z on head bfc5e472c2: 32 check runs, 12 success, 3 skipped, 17 in_progress, 0 failure. Not awaited; CI convergence is the PM's.",
      "session": "session_01YDt3PzwfrkuFzUBF89WPmM (subagent of the domain:spec seat 2 PM session; the relay derived the same id from the container)",
      "premise_still_valid": true,
      "summary": "All three positions were still stale at base 24dc7c1134 and are corrected in one draft PR, #21556 (Fixes #21517, Clause-②: no, Tier H by docs/adr/**, kept draft). (1) The ApprovalActionRow reassign TSDoc now says reassign_from / reassign_to hold a slot address in its stored spelling (a user id, an email, or a position address), are not sys_user references and make no claim about who made the move (that is actor_id). It says a *_name resolves only for a user id or an email an account carries. The type does not change. (2) ADR-0042 gets one status continuation line in the ADR-0046 house form: Superseded in part (2026-08-02, ADR-0118 D1), covering §2's reserved actor system:sla wherever the record names it. Machine actions record actor_id null, and the escalate row is the attribution. The decision text is untouched. (3) The approvals.sla-escalation checklist item is at revision 2: step 5 and the single-shot clause expect no actor on the escalate row (actor_id absent on the read, stored null). A @objectstack/spec patch changeset is included. Cleanup (node_modules, then git worktree remove) runs after this comment is read back.",
      "A1_reading_per_position": {
        "1_spec_tsdoc": "STALE at 24dc7c1134: approval-service.ts:482-496 said 'the user whose pending-approver slot was moved, and the user who received it' and '*_name (sys_user.name), when resolvable'. Runtime at the same sha: reassign() records the slot addresses from/to as the slate spells them; sys-approval-action.object.ts declares both Field.text ('in the slot's stored spelling: a user id, an email, or a position address'); listActions() resolves names only for addresses without ':' via resolveUserNames (by sys_user.id, then by sys_user.email for unresolved '@' addresses).",
        "2_adr_0042": "STALE at 24dc7c1134: no superseded or amended status line. §2 (lines 76-89), the TL;DR (line 24), Mechanics (line 118) and Consequences (line 130) still record actor_id 'system:sla'. Runtime: escalateRequest() inserts actor_id: null and the auto-decision runs under SYSTEM_CTX, which records nobody (recordedActor = actingUserId).",
        "3_checklist": "STALE at 24dc7c1134: step 5 and acceptance[1].verify said 'actor SLA_ACTOR_ID'. Runtime: escalate row actor_id null; rowFromAction maps null to undefined; REST GET /approvals/requests/:id/actions answers res.json({ data: rows }), so the key is absent on the read."
      },
      "before_after": {
        "1_spec_tsdoc": {
          "before": "Structured hand-off parties on a `reassign` action (#4365): the user whose pending-approver slot was moved, and the user who received it. Previously the pair existed only inside a default free-text `comment` (`\"FROM_ID → TO_ID\"`), which clients could neither parse nor render readably. `comment` is now pure user input; consumers render the hand-off from these fields (via the resolved `*_name` companions below). | reassign_from_name: Display name of `reassign_from` (`sys_user.name`), when resolvable. | reassign_to_name: Display name of `reassign_to` (`sys_user.name`), when resolvable.",
          "after": "Structured hand-off parties on a `reassign` action: the pending-approver slot that was handed over, and the address it was handed to. A reassignment moves a slot, not necessarily a person, so both hold a slot address in its stored spelling — a user id, an email, or a position address (`position:NAME`; any other `type:value` literal a slate kept is stored the same way). Like `acted_as`, neither is a `sys_user` reference, and neither makes a claim about who made the move: that person is `actor_id`. // Previously the pair existed only inside a default free-text `comment` (`\"FROM → TO\"`), which clients could neither parse nor render readably. `comment` is now pure user input; consumers render the hand-off from these fields — through the `*_name` companions below where one resolved, and as the address itself where none did. | reassign_from_name: Display name of `reassign_from` (`sys_user.name`). It resolves only when the address names an account: a user id, or an email an account carries. A position address never resolves, so absent means \"render the address\". | reassign_to_name: Display name of `reassign_to`; resolves as {@link ApprovalActionRow.reassign_from_name} does.",
          "note": "In this comment, NAME / FROM_ID / TO_ID / FROM / TO stand for the angle-bracket placeholders the source spells."
        },
        "2_adr_0042": {
          "before": "**Status**: Accepted — implemented (proposed 2026-06-12 · calibrated 2026-06-12)",
          "after": "**Status**: Accepted — implemented (proposed 2026-06-12 · calibrated 2026-06-12) + new line: · **Superseded in part (2026-08-02, [ADR-0118](./0118-non-user-actor-contract.md) D1)** — §2's reserved actor `system:sla`, wherever this record names it: machine actions record `actor_id` null, and the `escalate` row is the attribution."
        },
        "3_checklist": {
          "before": "step 5: GET /:id/actions — assert exactly one action='escalate' row (the audit-first idempotency marker, actor SLA_ACTOR_ID) whose comment names the action | acceptance[1].verify: ... exactly one action='escalate' row (actor SLA_ACTOR_ID); a second sweep adds no further escalate row ...",
          "after": "step 5: GET /:id/actions — assert exactly one action='escalate' row (the audit-first idempotency marker) whose comment names the action and which carries no actor: actor_id is absent on the read (stored null), because a machine action records no actor and the escalate row is the attribution (ADR-0118 D1) | acceptance[1].verify: ... exactly one action='escalate' row with no actor (actor_id absent on the read, stored null — ADR-0118 D1: a machine action records no actor, and the escalate row is the attribution); a second sweep adds no further escalate row ... | revision 1 → 2, history entry ref #21517"
        }
      },
      "files_changed": [
        ".changeset/21517-approval-reassign-slot-address-tsdoc.md (+9)",
        "docs/adr/0042-approval-sla-escalation.md (+1)",
        "docs/qa/platform-checklist/areas/approvals.json (+5/-4)",
        "packages/spec/src/contracts/approval-service.ts (+19/-7)"
      ],
      "line_budget": "45 changed lines (+34/-11) over 4 files, under the 5000-line human-merge threshold. No skills/** touched, so no skill line budget applies. The ADR edit is the one-line minimum.",
      "governed_reading": "node scripts/pm/check-governed-merges.mjs --branch claude/issue-21517-approval-actor-texts (exit 3): '⛔ GOVERNED — ... landing tier: H(人合)', docs/adr/** ×1, docs/adr/0042-approval-sla-escalation.md; 'size: 45 changed line(s) (+34 / -11) ≤ 5000'. --test on the 4-path three-dot list gives the same verdict. PR #21556 is open as a draft and was not readied, queued or armed.",
      "semver_decision": "patch changeset for @objectstack/spec, because the TSDoc ships in the published .d.ts (verified: the new sentence is in packages/spec/dist/contracts/index.d.ts and index.d.mts after the build). Clause-②: no. Gate lines, with the PR body as the --event payload at merge base fd5a1cd597: check-changeset-no-major exit 0: '✓ This diff introduces no `major` bump.' and '✓ LEVEL AXIS: this PR declares clause-② `no`, so no package here is declared to have grown a published surface.' check-empty-changeset exit 0: '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' check-adr-0087-registration exit 0: '✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).' check-changeset-fixed exit 0.",
      "gates": [
        "pnpm --filter @objectstack/spec build :: exit 0 :: 'check-dts-emitted: @objectstack/spec - 38/38 declared declaration file(s) present.' (os-verify-lock VERDICT command-exit 0)",
        "pnpm --filter @objectstack/spec check:generated :: exit 0 :: '✓ All 15 generated artifacts are up to date'",
        "pnpm --filter @objectstack/spec typecheck :: exit 0 :: 'check:test-typecheck: OK — @objectstack/spec's test layer compiles'",
        "pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 :: exit 0 :: 'Test Files 604 passed (604) / Tests 17861 passed | 1 todo (17862)'",
        "pnpm check:doc-authoring :: exit 0 :: '✓ doc authoring guard: 17314 customer-facing string(s) across 1249 spec sources clean — no internal issue-id references'",
        "pnpm check:platform-checklist :: exit 0 :: 'check-platform-checklist: OK — 15 areas, 269 items (265 active, 2 planned)'",
        "pnpm check:nul-bytes :: exit 0 :: 'check-nul-bytes: OK (scanned 9898 text file(s) ... no raw ASCII control bytes).'",
        "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list (86 derived commands, all run on bfc5e472c2 with exit codes recorded first) :: exit 0 :: '✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 85 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).'",
        "NOT MEASURED: pnpm check:dual-build-cjs-loads :: exit 3 :: 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' (83 packages). Reason: it needs a full-workspace pnpm build, the diff is one comment block in packages/spec, and CI Build Core runs it.",
        "pnpm --filter @objectstack/lint run check:doc-formula-expressions: exit 3 (prerequisite) on the first pass; after turbo build of formula+lint under the lock, exit 0 on both passes",
        "pnpm check:lean-entry-closure: exit 3 (prerequisite) on the first pass; after turbo build of objectql under the lock, exit 0 ('✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.'), and exit 0 again on bfc5e472c2",
        "roster gates whose roster sits in a touched directory: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity :: all exit 0 (check:meta-url-spelling and check:spec-changes passed inside check:generated)"
      ],
      "tests": "Spec build, check:generated, typecheck and the local vitest project ran under os-verify-lock on the spec source that HEAD bfc5e472c2 carries. That spec source is byte-identical to a1c75e6813: the merge of origin/main fd5a1cd597 touched only driver-turso and a changeset. Results: build exit 0, check:generated exit 0 ('✓ All 15 generated artifacts are up to date'), typecheck exit 0, vitest 604/604 files and 17861 tests passed. The derived 86-command union was re-run on HEAD bfc5e472c2 after the merge: 85 exit 0 and 1 NOT MEASURED (dual-build-cjs-loads, exit 3, prerequisite). The type does not change, so there was no reverse verification or ablation to run (no assertion was added). To show the build picked up the change, the new TSDoc sentence was found in packages/spec/dist/contracts/index.d.ts and .d.mts, and the old sentence 'pending-approver slot was moved' was in no dist file.",
      "A6_hot_files": "origin/main was merged before pr_create (fd5a1cd597, driver-turso only, no overlap). Open-PR file sweep over REST: 11 open PRs, 422 file rows, all pages read (the Version Packages PR #21352 has 293 files, read in 3 pages). 0 PRs touch the three files. Control: 3 rows under packages/spec/ were matched, so the matcher reads rows.",
      "deviations": [
        "The dispatch named no labels, so label-write added none and only assigned os-tesla. The labelers added documentation, size/s and tooling, which the read-back reports as preserved.",
        "The commit trailer is the model-free pair Co-authored-by: Claude + Claude-Session, per AGENTS.md. The harness reminder's model-named trailer was not used.",
        "The checklist history entry cites ref #21517 (the card), following the file's own ref convention. The TSDoc and the changeset cite no tracker number."
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called (no read and no write).",
      "api_writes": "3 — each is POST /repos/objectstack-ai/objectstack/dispatches through the fleet-write relay. (1) pr_create, which created PR #21556 as a draft; the read-back of the 9222-byte body was identical. (2) label-write --issue 21556 --assign os-tesla, which ran POST /issues/21556/assignees; the read-back matched. (3) post-stamped --comment=21517, this report. git push ran 3 times and is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted in PR #21556 Acceptance notes, not filed · read in code, not measured: in reassign() on the admin-rescue branch (the caller holds no slot and `from` is not on the slate), the whole slate is replaced with `to`, and reassign_from records the caller-named from, or the admin's own user id when none is given, instead of a slot that was handed over · dedupe words: reassign admin rescue reassign_from actorId override slate replaced"
      ]
    }
  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21556 → f83d0669d7 (Tier H, approved)

    domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5966025001 · 2026-10-03T07:45Z

    • Landed: PR docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor #21556 went through the merge queue as f83d0669d7, with one parent (10454b3afa), on os-zhuang's APPROVED review 5399501462 (an authorized approver). Fixes #21517 closed this card as completed.
    • Blob check against the ACCEPTed head bfc5e472: 4 of 4 files are blob-equal.
    • Delivered: the three texts triage's ruling of record named (5964851617), in one PR.
      • ApprovalActionRow's reassign TSDoc: reassign_from / reassign_to hold a slot address in its stored spelling (a user id, an email, or a position address), and a *_name resolves only for an account.
      • ADR-0042 §2: one status line, "superseded in part by ADR-0118 D1". Machine actions record actor_id null, and the escalate row is the attribution. The decision text is unchanged.
      • The approvals.sla-escalation checklist item expects no actor on the escalate row.
      • Release: @objectstack/spec patch. The review is 5966526843, and the ACCEPT is 5966645420.
    • Carried: the admin-rescue reassign branch records a caller-named or admin from rather than a handed-over slot. It was read in code and not measured, so it is noted and not filed (ACCEPT 5966645420).
    • pm:dispatched comes off in this act.

    Generated by Claude Code

  5. added a commit that references this issue on Oct 7, 2026
    f83d066
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 itdocumentationImprovements or additions to documentationdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions