From a1c75e68137fdc8229203526a1440b8ae2c7e0f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 05:44:43 +0000 Subject: [PATCH] =?UTF-8?q?docs(approvals):=20reassign=20TSDoc=20holds=20s?= =?UTF-8?q?lot=20addresses;=20ADR-0042=20=C2=A72=20superseded=20line;=20SL?= =?UTF-8?q?A=20checklist=20expects=20no=20actor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM --- ...17-approval-reassign-slot-address-tsdoc.md | 9 +++++++ docs/adr/0042-approval-sla-escalation.md | 1 + .../platform-checklist/areas/approvals.json | 9 ++++--- .../spec/src/contracts/approval-service.ts | 26 ++++++++++++++----- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .changeset/21517-approval-reassign-slot-address-tsdoc.md diff --git a/.changeset/21517-approval-reassign-slot-address-tsdoc.md b/.changeset/21517-approval-reassign-slot-address-tsdoc.md new file mode 100644 index 00000000000..2e2d24bafd1 --- /dev/null +++ b/.changeset/21517-approval-reassign-slot-address-tsdoc.md @@ -0,0 +1,9 @@ +--- +'@objectstack/spec': patch +--- + +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. + +Clause-②: no + +Documentation only. No schema, export or type changes. diff --git a/docs/adr/0042-approval-sla-escalation.md b/docs/adr/0042-approval-sla-escalation.md index cfb4bc1a563..1afa238c0e1 100644 --- a/docs/adr/0042-approval-sla-escalation.md +++ b/docs/adr/0042-approval-sla-escalation.md @@ -1,6 +1,7 @@ # ADR-0042: Approval SLA escalation — a jobs-backed scanner with audit-row idempotency **Status**: Accepted — implemented (proposed 2026-06-12 · calibrated 2026-06-12) +· **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. **Deciders**: ObjectStack Protocol Architects **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) **Closes**: [#1742](https://github.com/objectstack-ai/objectstack/issues/1742) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index e3a1c775537..8c113832cb1 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -811,7 +811,7 @@ "title": "A node's SLA escalation fires once past its timeout — the declared action runs and an escalate timeline row lands", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": ["dev admin"], @@ -832,7 +832,7 @@ "in a writable package author + register a flow whose approval node config.escalation = {enabled:true, timeoutHours:1, action:'reassign', escalateTo:'', notifySubmitter:true}", "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)", "[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", - "GET /:id/actions — assert exactly one action='escalate' row (the audit-first idempotency marker, actor SLA_ACTOR_ID) whose comment names the action", + "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)", "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", "with notifySubmitter!==false, read the submitter's inbox — an approval.sla_breached notification addressed to them", "idempotency: run the sweep a SECOND time — GET /:id/actions shows NO second escalate row (single-shot, marker-guarded)", @@ -848,7 +848,7 @@ { "clause": "past the deadline the sweep escalates exactly ONCE: one action='escalate' timeline row, and a re-run adds none", "oracle": "api", - "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)", + "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)", "evidence": "actions reads after the first and second sweeps" }, { @@ -882,7 +882,8 @@ "packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (sla_due_at surfaced on the request)" ], "history": [ - { "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" } + { "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" }, + { "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" } ] }, { diff --git a/packages/spec/src/contracts/approval-service.ts b/packages/spec/src/contracts/approval-service.ts index 6ed13aee9a8..4b458e36cc5 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -480,19 +480,31 @@ export interface ApprovalActionRow { /** Display name of the actor (`sys_user.name`), when resolvable. */ actor_name?: string; /** - * 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` - * (`" → "`), which clients could neither parse nor render + * 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:`; 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` + * (`" → "`), 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). + * from these fields — through the `*_name` companions below where one + * resolved, and as the address itself where none did. */ reassign_from?: string; /** See {@link ApprovalActionRow.reassign_from}. */ reassign_to?: string; - /** Display name of `reassign_from` (`sys_user.name`), when resolvable. */ + /** + * 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_from_name?: string; - /** Display name of `reassign_to` (`sys_user.name`), when resolvable. */ + /** Display name of `reassign_to`; resolves as {@link ApprovalActionRow.reassign_from_name} does. */ reassign_to_name?: string; /** * Whether the actor was admitted to this action ONLY by the privileged