Skip to content

docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor - #21556

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-21517-approval-actor-texts
Oct 3, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-21517-approval-actor-texts

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21517
Clause-②: no

Three texts still described the approval actor as a sentinel or as a user after the actor-column change landed (88fb5e85a0). This PR corrects all three, as triage ruled on the card (comment 5964851617): one card, one PR, three text positions. Text only: no schema, export, type or runtime change.

What changes

  1. ApprovalActionRow reassign TSDoc (packages/spec/src/contracts/approval-service.ts).

    • Before: reassign_from / reassign_to were "the user whose pending-approver slot was moved, and the user who received it", and each *_name was "sys_user.name, when resolvable".
    • After: 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. A *_name resolves only when the address names an account: a user id, or an email an account carries. A position address never resolves, so an absent name means "render the address".
    • The type (string) does not change. The rewritten block cites no tracker number; the old block's tracker citation went with the rewrite.
  2. ADR-0042 status line (docs/adr/0042-approval-sla-escalation.md). One continuation line under Status, in the form ADR-0046 uses for a decision superseded in part:

    · Superseded in part (2026-08-02, ADR-0118 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.

    ADR-0118 is linked in the file. The decision text is not rewritten: the record keeps what was decided on 2026-06-12, and the status line points to what supersedes it (Prime Directive 13). "Wherever this record names it" covers the TL;DR, Mechanics and Consequences mentions as well as §2 itself.

  3. Platform checklist approvals.sla-escalation (docs/qa/platform-checklist/areas/approvals.json), revision 1 to 2 with a history entry.

    • Before: step 5 and the single-shot clause's verify text expected the escalate row's actor to be SLA_ACTOR_ID.
    • After: both expect no actor. actor_id is absent on the GET /:id/actions read and stored null, because a machine action records no actor and the escalate row is the attribution (ADR-0118 D1). A QA run that followed the old text would report a false failure.
  4. Changeset: @objectstack/spec patch, because the TSDoc ships in the published .d.ts.

The texts were checked against the runtime (read at 24dc7c1134)

  • Reassign write. reassign() in packages/plugins/plugin-approvals/src/approval-service.ts records reassign_from: from, reassign_to: to, the two slot addresses as the slate spells them. sys-approval-action.object.ts declares both as Field.text, described as "in the slot's stored spelling: a user id, an email, or a position address".
  • Names. listActions() sends only addresses without a : to resolveUserNames. That function looks each one up by sys_user.id, then looks up the ones still unresolved that contain an @ by sys_user.email.
  • Escalate row. escalateRequest() inserts actor_id: null. rowFromAction maps null to undefined, and the REST route answers res.json({ data: rows }), so the key is absent on the read.
  • New TSDoc in the build. After pnpm --filter @objectstack/spec build, the new sentence is in packages/spec/dist/contracts/index.d.ts and index.d.mts, and the old "pending-approver slot was moved" sentence is in no dist file.

Gates

The union was re-run on HEAD bfc5e472c2, the final commit. That commit is a merge of origin/main fd5a1cd597, which touched only driver-turso and a changeset, nothing on this PR's paths.

  • pnpm --filter @objectstack/spec build: exit 0. check-dts-emitted: @objectstack/spec - 38/38 declared declaration file(s) present.
  • 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
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: exit 0. Test Files 604 passed (604), Tests 17861 passed | 1 todo
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 86 commands. All 86 were run on bfc5e472c2 with their exit codes recorded, then reconciled: ✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 85 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). Among them:
    • pnpm check:doc-authoring: ✓ doc authoring guard: 17314 customer-facing string(s) across 1249 spec sources clean — no internal issue-id references
    • pnpm check:platform-checklist: check-platform-checklist: OK — 15 areas, 269 items (265 active, 2 planned)
    • pnpm check:nul-bytes: check-nul-bytes: OK (scanned 9898 text file(s) ... no raw ASCII control bytes).
    • check-empty-changeset: ✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
    • check-adr-0087-registration: ✓ ... this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
    • check-changeset-no-major: ✓ This diff introduces no `major` bump. The level axis was also run offline with this body as the --event payload (see the report on the card).
    • NOT MEASURED: pnpm check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET. The gate reads the built output of all 83 workspace packages, which takes a full pnpm build. This PR edits one comment block in packages/spec, and CI's Build Core runs the gate.
  • Roster gates whose roster sits in a directory this diff touches were run too, all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. check:meta-url-spelling and check:spec-changes ran inside check:generated.
  • node scripts/pm/check-governed-merges.mjs --branch claude/issue-21517-approval-actor-texts: ⛔ GOVERNED, Tier H, by docs/adr/0042-approval-sla-escalation.md. Size is 45 changed lines.

Acceptance notes

  • Governed, Tier H. The ADR line makes this PR governed, and triage accepted that. It stays a draft. It lands only on an authorized approval or a human merge.
  • Only the status line, no section note. ADR-0046 also puts a note at the superseded section. The ruling names the status line, so only that line is added. A reader who lands on §2 directly sees no marker there. Adding one is a follow-up for the ADR's owner if wanted.
  • Reassign edge, read in code and not measured. In reassign(), from is input.from ?? slot ?? actorId. On the admin-rescue branch (the caller holds no slot, and from is not on the slate), the whole slate is replaced with the one address to. The audit row then records reassign_from as the caller-named from or the admin's own user id, not a slot that was handed over. The new TSDoc states the contract ("the pending-approver slot that was handed over"), which matches the object's field description. This branch was not driven through a public door, so it is noted here and not filed.
  • Not here: the objectui timeline's "System (SLA)" rendering. Triage ruled 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.
  • Release: a patch changeset (Clause-②: no). The TSDoc ships in the published .d.ts, and every changeset gate run passed.

维护者速读(草稿)

改了什么:三处文字,全部是说明性文本,不改任何类型、接口或运行时行为。① 审批动作记录的 TSDoc 现在写明:「转办来源 / 转办目标」两个字段存的是审批槽位的地址(用户 id、邮箱或岗位地址),不一定是某个人,显示名只在地址对应到一个账号时才解析得出。② ADR-0042 在状态行加一句:第 2 节「SLA 机器决策记为保留身份 system:sla」已被 ADR-0118 D1 部分取代,机器动作的行为人记为空,由 escalate 那一行本身说明是 SLA 触发的。③ QA 测试清单里的 SLA 升级用例改为期望 escalate 行没有行为人。

为什么改:运行时早已按 ADR-0118 实现(转办字段存槽位地址,SLA 扫描记空行为人),但这三处文字还是旧说法。AI 或开发者按旧文字写代码、跑 QA,会把岗位地址当成用户去关联,或把正确行为误报成失败。ADR 被推翻却没有状态行,也违反「已接受的 ADR 在被新 ADR 取代前一直有效」的原则。

风险与代价(含回滚):风险很低:只改文字,发布的 .d.ts 只有注释变化,附 patch 级 changeset。因为碰了 docs/adr/**,本 PR 属于受管面(Tier H),需要您批准才能合入。回滚就是 revert 这个提交,没有数据或迁移影响。

席位意见:

你要做的:看一眼 ADR-0042 新加的那一行状态说明,措辞没问题就批准;如果还希望在第 2 节标题下也加一条指向说明,请在 PR 上说一声。


Generated by Claude Code

claude added 2 commits October 3, 2026 05:44
…erseded 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
@github-actions github-actions Bot added the size/s label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6cf1154a65ffcae3fdfe607a5d221572157ee353 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 22deb8f42377a842a3b6ca4b5f56248741178458 — the merge of head bfc5e472c2717552b7d1226dd7fb78af6cebc348 into base 6cf1154a65ffcae3fdfe607a5d221572157ee353, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 22deb8f42377a842a3b6ca4b5f56248741178458 && git checkout 22deb8f42377a842a3b6ca4b5f56248741178458
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6cf1154a65ffcae3fdfe607a5d221572157ee353 bfc5e472c2717552b7d1226dd7fb78af6cebc348 && git checkout -B drift-repro 6cf1154a65ffcae3fdfe607a5d221572157ee353 && git merge --no-ff bfc5e472c2717552b7d1226dd7fb78af6cebc348

node scripts/docs-audit/affected-docs.mjs --json 6cf1154a65ffcae3fdfe607a5d221572157ee353

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bfc5e472c2717552b7d1226dd7fb78af6cebc348
Local-runs: none

Inputs read, and nothing else: card #21517 (body; comments 5964851617 triage grade, the ruling of record; 5966025001 claim; 5966458246 dev report), PR #21556 (body, the 4-file list, the net diff against main at the head, and its one comment, the docs-drift bot), the head's check-runs, and the sources the three texts must agree with, at refs: docs/adr/0118-non-user-actor-contract.md at origin/main 6cf1154a65, PR #21514 (landed 88fb5e85a0, card #21455), and the runtime that writes the reassign and escalate rows at origin/main (packages/plugins/plugin-approvals/src/approval-service.ts, sys-approval-action.object.ts, packages/rest/src/rest-server.ts). Nothing was built, run or re-run.

① Derived judgments

The diff is 4 files, +34/−11: one new changeset, one ADR status line, one checklist item revised, one TSDoc block. No schema, export, type, runtime or test line changes, so the diff implies no accept-set change and one public-surface text change: the TSDoc that ships in @objectstack/spec's declaration files. Each position, judged against the runtime at origin/main:

  1. ApprovalActionRow reassign TSDoc — right. reassign() writes reassign_from: from, reassign_to: to, where from = input.from ?? slot ?? actorId and to is the caller's address, both as the slate spells them; the person is actor_id: recordedActor(context). sys-approval-action.object.ts declares both columns as Field.text({ maxLength: 255 }), "in the slot's stored spelling: a user id, an email, or a position address". listActions() resolves a *_name only for an address without a colon, through resolveUserNames (by sys_user.id, then by sys_user.email for an unresolved address carrying an at-sign); every type:value literal is skipped, so a position never resolves. A slate keeps position: and the deprecated role: spelling (POSITION_ADDRESS_PREFIXES), so "any other type:value literal a slate kept is stored the same way" is the runtime's own comment at the insert, restated. "Like acted_as, neither is a sys_user reference" matches the object. The optional string types are unchanged, and the tracker citation the old block carried is gone, as the house rule asks. One reading recorded, not a fault: resolveUserNames falls back to the account's email when it has no name, so "display name (sys_user.name)" is the same approximation the old text made.

  2. ADR-0042 hunk (governed, Tier H) — right on all four counts.

    • Status line only. The hunk is one added line, line 4, between **Status** and **Deciders**. The TL;DR, §1 to §3, Mechanics, Consequences and References are byte-identical to main; no decision text is rewritten.
    • House form. ADR-0046 line 4 reads · **Superseded in part (2026-06-13, ADR-0048)** — the bare-name overwrite premise; note at §3.2. The new line has the same shape, · **Superseded in part (2026-08-02, ADR-0118 D1)** — what is superseded, with the ADR id as a relative link, the form ADR-0005, ADR-0017 and ADR-0055 use in their **Amended** lines. It carries no "note at §2" pointer because no section note is added, which is honest.
    • True to ADR-0118's text. ADR-0118's status is Accepted on 2026-08-02, so the date is right. D1 says every actor column pointing at sys_user stores null for a system-initiated write, forbids every sentinel string, and makes "System" a render rule rather than data; "machine actions record actor_id null" states that decision correctly for system:sla. "The escalate row is the attribution" is the wording the ruling of record prescribes (triage 5964851617, the card's proposed line); it rests on ADR-0042 §1's own marker and on ADR-0118 D5 (which automation acted is answered by an association, never by the actor column), and nothing in it contradicts ADR-0118.
    • Runtime agrees. escalateRequest() inserts the escalate row with actor_id: null; the auto-decision calls decide() under SYSTEM_CTX naming SLA_ACTOR_ID as its acting identity, and recordedActor(SYSTEM_CTX) is null, so nothing records the sentinel (SLA_ACTOR_ID's docblock: never recorded). "Wherever this record names it" is needed and true: the TL;DR, §2, Mechanics and Consequences still say system:sla, and the line covers them without rewriting them.
    • The PR is a draft (draft: true, auto_merge: null); landing needs an authorized approval, which this record does not give.
  3. Checklist approvals.sla-escalation — right. Step 5 and the single-shot clause now expect no actor on the escalate row, absent on the read and stored null. Stored: actor_id: null, above. Read: rowFromAction maps row.actor_id ?? undefined, and the REST handler for GET /approvals/requests/:id/actions answers res.json({ data: rows }), so the key is absent in the JSON. Revision 1 to 2 with a history entry in the file's own ref convention; the only SLA_ACTOR_ID left in the file is that entry's description of the old text. A run following the old text would have reported a false failure, as the card said.

  4. Changeset — judged in ②.

Check-runs on the head, read at 2026-10-03T06:52Z: 24 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 6 in progress (Lint & Repo Gates, Test Core 1, 2, 4 and 5 of 6, Type Check · workspace), 0 failed. Among the successes: Governed Surface Queue Guard, Check Changeset, Check Documentation Links, Spec property liveness, Build Core (which hosts check:dual-build-cjs-loads), Type Check · source gates, and the three card-and-branch claim gates. An in-progress gate is read as in progress, not as a pass; the landing waits on them whatever this record says.

② Semver level

@objectstack/spec patch — right. The diff publishes exactly one thing, the rewritten TSDoc, which ships in the package's declaration files, so skip-changeset (for a diff that publishes nothing from any released package) would be wrong, and patch is the floor. Nothing is added to or removed from the exported shape: reassign_from, reassign_to and both *_name members stay optional string, so no minor is owed and no clause-② arm applies. The PR body's Clause-②: no line is right for the same reason, and the changeset's own Clause-②: no agrees with it. The changeset body says what the text said before and what it says now, names no tracker, and Check Changeset is green.

③ Boundary flags

Dev flags, from report 5966458246 and the PR's Acceptance notes, each answered:

  • open_questions: [] — nothing to answer.
  • Labels (deviation 1): dispatch named none; documentation, size/s and tooling are the auto-labeler's. No action.
  • Commit trailer (deviation 2): a1c75e6813 ends with the model-free pair AGENTS.md names; the merge commit carries none. Compliant.
  • Checklist ref: "#21517" (deviation 3): the item's history entries already cite cards (#7331, #7530), and check:doc-authoring covers spec runtime strings, not docs/qa/**. Compliant.
  • Status line only, no §2 note: the ruling of record names the status line, and only that line is added; a marker under §2 is the maintainer's option at approval, which the PR's 维护者速读 already asks about. Within the ruling.
  • Reassign admin-rescue branch (out-of-scope finding, carrier: acceptance note): verified in code. When the caller holds no slot and from is not on the slate, next = [to] and the row records reassign_from as the caller-named from, or the admin's own id when none was given. The stored value is still a slot address in its stored spelling (a user id), so the TSDoc's shape claim holds; its "slot that was handed over" is loose on that one branch. It was read in code and not measured through a public door, so the acceptance-note carrier is the same disposition the card gave the objectui rendering. Answered; a measured finding would be a new card on the approvals runtime, not this PR's.
  • objectui "System (SLA)" rendering: triage ruled 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. Not this card's.
  • NOT MEASURED check:dual-build-cjs-loads: it needs the full workspace build; Build Core runs it on the head and is green. Accepted.
  • Tier H: the PR touches docs/adr/**; it is a draft, not readied, queued or armed (Governed Surface Queue Guard green). It lands only on an authorized approval or a human merge. This record approves nothing and merges nothing.

Implemented-by: claude/issue-21517-approval-actor-texts
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21556 @ bfc5e472 (GOVERNED, Tier H: awaiting an authorized approval)

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

  • Shape (read on GitHub): a draft against main. The first line is Fixes #21517, and Clause-②: no sits at a line start. 4 files, +34 / −11: three text positions, as triage's ruling of record directed (5964851617, one card, one PR), and one changeset.
  • Contract review: at-tier PASS 5966526843 on this exact head. It judged each position against the runtime at origin/main:
    • ApprovalActionRow's reassign TSDoc matches reassign() and the object: both fields hold a slot address in its stored spelling (a user id, an email, or a position address). A *_name resolves only for an address without a colon, by account id, then by email. The type is unchanged, and the tracker citation is gone.
    • The ADR-0042 hunk (the governed one): one added status line, in ADR-0046's "Superseded in part" house form. The decision text is byte-identical, the date is ADR-0118's acceptance date, and "machine actions record actor_id null" is true to ADR-0118 D1. The runtime agrees: escalateRequest() inserts actor_id: null.
    • The approvals.sla-escalation checklist item expects no actor on the escalate row: stored null, and absent on the REST read.
    • Release: @objectstack/spec patch, because the TSDoc ships in the published .d.ts. Clause-②: no.
  • CI on bfc5e472: 32 success and 3 skipped by design (Build Docs, Console Pin Gate, packed-tarball smoke).
  • Governed surface: check-governed-merges reads GOVERNED, Tier H, from docs/adr/0042-approval-sla-escalation.md. 45 changed lines.
  • Serial: at this read, no other open PR touches the three files.

Carried, decided here:

  • The admin-rescue reassign branch (the dev's acceptance note, verified in code by the review). When the caller holds no slot and from is not on the slate, the row records the caller-named from, or the admin's own user id, rather than a slot that was handed over. The stored value is still a slot address, so the TSDoc's shape claim holds. Its "slot that was handed over" is loose on that one branch. → noted, not filed: read in code and not measured at a door, so it is not a card under the filing gate.

The landing, as the governed rule sets it:

  1. needs-user-decision goes on PR docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor #21556 with this ACCEPT, and the final 维护者速读 is posted on the PR.
  2. A review is requested from the authorized approvers.
  3. ⛔ This seat does not flip it ready, enqueue it or arm auto-merge until an authorized approval is on it. Then it clears the label with a paired comment, runs the pre-landing checks and lands it.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #21556

domain:spec 坐席 2(session_01YDt3PzwfrkuFzUBF89WPmM)· 2026-10-03T07:11Z · 对照达档复核 5966526843 校正

改了什么:三处与审批「谁做的」有关的过时文字,一张卡、一个 PR(分诊裁定,5964851617):

  • ApprovalActionRow 的转办字段注释:reassign_from / reassign_to 存的是「槽位地址」原样(用户 id、邮箱或 position:名称),不是 sys_user 引用;显示名只在地址对应到账号时才解析得出。字段类型不变。
  • ADR-0042 只加一行状态说明:「部分被 ADR-0118 D1 取代」——机器动作记录 actor_id 为空,escalate 行本身就是归属。ADR 原有决策正文一字未改。
  • QA 清单 approvals.sla-escalation:期望 escalate 行不带操作人(库里为 null,REST 读取时没有这个键)。

为什么改:PR #21514(#21455)按 ADR-0118 D1 把审批里的「系统操作人」改成了空值、把转办字段改成了槽位地址;这三处文字还停在旧说法上。

风险与代价(含回滚):

  • 只改文字,零运行时、零契约变化。@objectstack/spec patch(注释会进发布的 .d.ts)。
  • 碰到 docs/adr/**,所以是 Tier H(人合)——这是分诊明确接受的,未拆分。
  • 回滚:发布前 revert 即可。
  • 达档复核 PASS;CI 全绿后才可落地。

席位意见:建议批准。改动最小,三处都已对照运行时代码核实。ADR 只加状态行、不改历史决策,符合 ADR 的记录方式。若你希望在 §2 正文旁再加一条注记,可以在批准时说明。

你要做的:在 PR #21556 上点 APPROVE(批准后由本坐席清标签并落地)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 3, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Approval received on PR #21556: this seat lands it

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


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

3 participants