Skip to content

Commit a1c75e6

Browse files
committed
docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded line; SLA checklist expects no actor
- spec ApprovalActionRow: reassign_from / reassign_to hold the slot address in its stored spelling (a user id, an email, or a position address); the *_name companions resolve only for a user id or an email an account carries. - ADR-0042: status line records §2's reserved actor `system:sla` as superseded in part by ADR-0118 D1 (machine actions record actor_id null; the escalate row is the attribution). - platform checklist approvals.sla-escalation (revision 2): the escalate row carries no actor. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
1 parent 24dc7c1 commit a1c75e6

4 files changed

Lines changed: 34 additions & 11 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The `ApprovalActionRow` documentation now says what `reassign_from` and `reassign_to` hold. It said both were users. They hold a slot address in its stored spelling: a user id, an email, or a position address such as `position:legal`. A reassignment moves a slot, not necessarily a person, and the person who made the move is `actor_id`. The `reassign_from_name` and `reassign_to_name` documentation now says when a name resolves: only for a user id, or for an email an account carries. A position address never resolves, so a consumer renders the address when the name is absent.
6+
7+
Clause-②: no
8+
9+
Documentation only. No schema, export or type changes.

‎docs/adr/0042-approval-sla-escalation.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ADR-0042: Approval SLA escalation — a jobs-backed scanner with audit-row idempotency
22

33
**Status**: Accepted — implemented (proposed 2026-06-12 · calibrated 2026-06-12)
4+
· **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.
45
**Deciders**: ObjectStack Protocol Architects
56
**Builds on**: [ADR-0019](./0019-approval-as-flow-node.md) (approval as flow node), [ADR-0041](./0041-flow-trigger-family.md) (triggers vs jobs vs hooks — this is the canonical "jobs, not trigger" case), thread interactions (#1740)
67
**Closes**: [#1742](https://github.com/objectstack-ai/objectstack/issues/1742)

‎docs/qa/platform-checklist/areas/approvals.json‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@
811811
"title": "A node's SLA escalation fires once past its timeout — the declared action runs and an escalate timeline row lands",
812812
"since": "v16",
813813
"status": "active",
814-
"revision": 1,
814+
"revision": 2,
815815
"priority": "P2",
816816
"surface": "api",
817817
"personas": ["dev admin"],
@@ -832,7 +832,7 @@
832832
"in a writable package author + register a flow whose approval node config.escalation = {enabled:true, timeoutHours:1, action:'reassign', escalateTo:'<position machine name or user id>', notifySubmitter:true}",
833833
"trigger the flow; GET /api/v1/approvals/requests/:id — status=pending and the request carries sla_due_at = created_at + timeoutHours (the SLA is materialized on open)",
834834
"[needs clock control] advance the clock past sla_due_at (inject a clock / drive ApprovalService.runEscalations() with a clock whose now() is beyond the deadline) and run one escalation sweep",
835-
"GET /:id/actions — assert exactly one action='escalate' row (the audit-first idempotency marker, actor SLA_ACTOR_ID) whose comment names the action",
835+
"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)",
836836
"assert the declared action's effect: reassign → pending_approvers swapped to the escalatees + an approval.escalated notification to them; auto_approve/auto_reject → request finalized approved/rejected and the owning run resumes; notify → an approval.sla_breached notification to the pending approvers",
837837
"with notifySubmitter!==false, read the submitter's inbox — an approval.sla_breached notification addressed to them",
838838
"idempotency: run the sweep a SECOND time — GET /:id/actions shows NO second escalate row (single-shot, marker-guarded)",
@@ -848,7 +848,7 @@
848848
{
849849
"clause": "past the deadline the sweep escalates exactly ONCE: one action='escalate' timeline row, and a re-run adds none",
850850
"oracle": "api",
851-
"verify": "after advancing past sla_due_at and sweeping, GET /:id/actions has exactly one action='escalate' row (actor SLA_ACTOR_ID); a second sweep adds no further escalate row (the audit row is the idempotency marker, written before any mutation)",
851+
"verify": "after advancing past sla_due_at and sweeping, GET /:id/actions has 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 (the audit row is the idempotency marker, written before any mutation)",
852852
"evidence": "actions reads after the first and second sweeps"
853853
},
854854
{
@@ -882,7 +882,8 @@
882882
"packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (sla_due_at surfaced on the request)"
883883
],
884884
"history": [
885-
{ "revision": 1, "date": "2026-08-08", "change": "initial — pins the ADR-0042 SLA escalation (declared action fires once past timeout + escalate timeline row); blocked on a clock-control timing harness (hour granularity), with the sla_due_at materialization and the strict-schema build clause runnable today", "ref": "claude/platform-test-checklist-ocwugl" }
885+
{ "revision": 1, "date": "2026-08-08", "change": "initial — pins the ADR-0042 SLA escalation (declared action fires once past timeout + escalate timeline row); blocked on a clock-control timing harness (hour granularity), with the sla_due_at materialization and the strict-schema build clause runnable today", "ref": "claude/platform-test-checklist-ocwugl" },
886+
{ "revision": 2, "date": "2026-10-03", "change": "the escalate row's expected actor corrected: step 5 and the single-shot clause asserted actor SLA_ACTOR_ID, but under ADR-0118 D1 the sweep records no actor (actor_id null, absent on the GET /:id/actions read) and the escalate row is the attribution, so a run following the old text would report a false failure. Nothing else in the item changes", "ref": "#21517" }
886887
]
887888
},
888889
{

‎packages/spec/src/contracts/approval-service.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -480,19 +480,31 @@ export interface ApprovalActionRow {
480480
/** Display name of the actor (`sys_user.name`), when resolvable. */
481481
actor_name?: string;
482482
/**
483-
* Structured hand-off parties on a `reassign` action (#4365): the user whose
484-
* pending-approver slot was moved, and the user who received it. Previously
485-
* the pair existed only inside a default free-text `comment`
486-
* (`"<from_id> → <to_id>"`), which clients could neither parse nor render
483+
* Structured hand-off parties on a `reassign` action: the pending-approver
484+
* slot that was handed over, and the address it was handed to. A
485+
* reassignment moves a slot, not necessarily a person, so both hold a slot
486+
* address in its stored spelling — a user id, an email, or a position
487+
* address (`position:<name>`; any other `type:value` literal a slate kept
488+
* is stored the same way). Like `acted_as`, neither is a `sys_user`
489+
* reference, and neither makes a claim about who made the move: that
490+
* person is `actor_id`.
491+
*
492+
* Previously the pair existed only inside a default free-text `comment`
493+
* (`"<from> → <to>"`), which clients could neither parse nor render
487494
* readably. `comment` is now pure user input; consumers render the hand-off
488-
* from these fields (via the resolved `*_name` companions below).
495+
* from these fields — through the `*_name` companions below where one
496+
* resolved, and as the address itself where none did.
489497
*/
490498
reassign_from?: string;
491499
/** See {@link ApprovalActionRow.reassign_from}. */
492500
reassign_to?: string;
493-
/** Display name of `reassign_from` (`sys_user.name`), when resolvable. */
501+
/**
502+
* Display name of `reassign_from` (`sys_user.name`). It resolves only when
503+
* the address names an account: a user id, or an email an account carries.
504+
* A position address never resolves, so absent means "render the address".
505+
*/
494506
reassign_from_name?: string;
495-
/** Display name of `reassign_to` (`sys_user.name`), when resolvable. */
507+
/** Display name of `reassign_to`; resolves as {@link ApprovalActionRow.reassign_from_name} does. */
496508
reassign_to_name?: string;
497509
/**
498510
* Whether the actor was admitted to this action ONLY by the privileged

0 commit comments

Comments
 (0)