Repository navigation
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
Description
Filing gate: ① finding, class b, with reach.
- reach: public door. approvals: ADR-0118 D1 family —
sys_useractor columns still hold sentinels and slot literals (system:sla/system:dead-run,reassign_from/reassign_to,sys_notification.actor_id) #21455's card body measuredPOST /api/v1/approvals/requests/:id/reassignon a booted showcase at53fd35e3e, and the storedreassign_from/reassign_towereposition:legalandmail@example.com. - The published contract's TSDoc says both are users.
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)
packages/spec/src/contracts/approval-service.ts,ApprovalActionRow.- Around lines 482 to 496, the TSDoc says
reassign_from/reassign_toare "the user whose pending-approver slot was moved, and the user who received it". - It says
reassign_from_name/reassign_to_nameare "sys_user.name". - After triage's approvals: ADR-0118 D1 family —
sys_useractor columns still hold sentinels and slot literals (system:sla/system:dead-run,reassign_from/reassign_to,sys_notification.actor_id) #21455 ruling (5960380029), carried out by 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, both fields hold a slot address in its stored spelling: a user id, an email orposition:<name>. On the object they are text fields, notsys_userlookups. - A
*_nameresolves only for a user id, or for an email an account carries. - The type (
string) does not change. This is a prose correction only.
- Around lines 482 to 496, the TSDoc says
docs/adr/0042-approval-sla-escalation.md§2, "Machine decisions → reserved actorsystem:sla" (lines 24, 76 to 85, 118 and 130).- It records that machine decisions carry
actor_id: 'system:sla'and that clients render it as "System (SLA)". - ADR-0118 D1 (accepted 2026-08-02, later) covers every
sys_useractor column and forbids a sentinel. 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 makes the SLA and dead-run sweeps recordactor_idnull. - ADR-0042 carries no superseded or amended status line for §2. That line is the record Prime Directive [WIP] Add Chinese version of the documentation #13 asks for when a recorded decision is reversed.
- Proposed line: "§2 superseded by ADR-0118 D1: machine actions record
actor_idnull; theescalaterow is the attribution."
- It records that machine decisions carry
docs/qa/platform-checklist/areas/approvals.json, itemapprovals.sla-escalation(lines 835 and 851). The verify text says theescalaterow's actor isSLA_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.
system:sla|system:dead-run|SLA_ACTOR_ID: 3 hits, all inside the approvals: ADR-0118 D1 family —sys_useractor columns still hold sentinels and slot literals (system:sla/system:dead-run,reassign_from/reassign_to,sys_notification.actor_id) #21455 family (approvals: ADR-0118 D1 family —sys_useractor columns still hold sentinels and slot literals (system:sla/system:dead-run,reassign_from/reassign_to,sys_notification.actor_id) #21455, PR fix(approvals): actor_id records the person who decided, acted_as the slot it was taken as; stored slot literals move out at boot #21493, 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).ADR-0042|0042-approval: 1 hit (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).reassign_from|reassign_to: 4 hits (approvals: ADR-0118 D1 family —sys_useractor columns still hold sentinels and slot literals (system:sla/system:dead-run,reassign_from/reassign_to,sys_notification.actor_id) #21455, PR fix(approvals): actor_id records the person who decided, acted_as the slot it was taken as; stored slot literals move out at boot #21493, 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, the seat post).- Positive control:
approval-service\.ts|ApprovalActionRow: 26 hits.
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
objectstack-fleet commentedon Oct 3, 2026
Triage: first grade —
documentation·priority:p3·domain:spec·area:workflow·pm:queue. One card, one PR, three text positionsTriage 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
mainnow. 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 texts:
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*_nameresolves only for a user id or an email an account carries.escalaterow is the attribution (Prime Directive 13).approvals.sla-escalation's verify text expects no actor on theescalaterow.Not here: the objectui timeline's "System (SLA)" rendering. It stays the PR #21514 Acceptance note it is.
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Claim: PM loop round 1
Session:
session_01YDt3PzwfrkuFzUBF89WPmMAccount:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21517-approval-actor-textsWorktree:
objectstack-issue-21517Domain:
domain:specSeat:
domain:spec#2File surface (at
origin/main24dc7c1134), per triage5964851617(one card, one PR, three text positions):packages/spec/src/contracts/approval-service.ts,ApprovalActionRow(about:482to:496): the reassign TSDoc saysreassign_from/reassign_tohold the stored slot address (a user id, an email, or a position address), and that a*_nameresolves 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 theescalaterow 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 theescalaterow.skills/**.Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceilingCONTRACT_REVIEW_TIER). Tier H (docs/adr/**): the at-tier review covers the ADR hunk; then ACCEPT, theneeds-user-decisionlabel 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.mdorplatform-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 isFixes #21517.Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
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" ] }objectstack-fleet commentedon Oct 3, 2026
Landed: PR #21556 →
f83d0669d7(Tier H, approved)domain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5966025001· 2026-10-03T07:45Zf83d0669d7, with one parent (10454b3afa), onos-zhuang's APPROVED review5399501462(an authorized approver).Fixes #21517closed this card ascompleted.bfc5e472: 4 of 4 files are blob-equal.5964851617), in one PR.ApprovalActionRow's reassign TSDoc:reassign_from/reassign_tohold a slot address in its stored spelling (a user id, an email, or a position address), and a*_nameresolves only for an account.actor_idnull, and theescalaterow is the attribution. The decision text is unchanged.approvals.sla-escalationchecklist item expects no actor on theescalaterow.@objectstack/specpatch. The review is5966526843, and the ACCEPT is5966645420.fromrather than a handed-over slot. It was read in code and not measured, so it is noted and not filed (ACCEPT5966645420).pm:dispatchedcomes off in this act.Generated by Claude Code
docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 sup…