Skip to content

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

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21455-adr0118-actor-family
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21455-adr0118-actor-family

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21455
Clause-②: no

What changes

Under ADR-0118 D1 a lookup to sys_user holds a user id or null, never a placeholder. Four writers in the approvals plugin broke that, and a lookup holding a non-id drops the row from every join and report on it, silently. This applies triage's position-by-position ruling (5960380029). It builds on #21411 (PR #21493, 6f17d1d36): the same recordedActor, the same slot-address column kind (acted_as), and the same boot-time repair.

  1. SLA and dead-run sweeps: actor_id is null.

    • escalateRequest writes the escalate row with actor_id: null. Before, it wrote system:sla.
    • The auto_approve / auto_reject decision runs under SYSTEM_CTX. That context vouches for nobody, so recordedActor records null. The special case that kept system:sla is removed.
    • abandonForDeadRun writes its recall row with actor_id: null. Before, it wrote system:dead-run.
    • The fact that a sweep acted is the row's own kind. The escalate row's comment names the configured action, and the dead-run recall names the run and its status. DEAD_RUN_ACTOR_ID is deleted.
    • SLA_ACTOR_ID stays, as the sweep's acting identity only: decide() under a system context requires an explicit actor. Its docblock says it is never recorded.
  2. reassign_from / reassign_to are slot-address fields. They are now Field.text({ maxLength: 255 }), the declared form of acted_as, instead of Field.lookup('sys_user'). The descriptions say they hold the slot's address in its stored spelling: a user id, an email or a position address. Translations are regenerated with node scripts/check-i18n-bundles.mjs --write, and the zh-CN, ja-JP and es-ES help texts are written by hand. The zh-CN labels also become neutral (from party / to party instead of from person / to person).

  3. notify forwards a person or nothing. notify no longer takes an actor string. A caller hands in actorContext (the action's ExecutionContext), and notify forwards recordedActor(actorContext) as actorId, or omits it. So no string path exists for a slot literal or a sentinel.

    • Callers that pass the context: sendBack (both notifications), reassign, remind (both), requestInfo and comment.
    • Callers that pass nothing: the three SLA notifications, and the two out-of-office notifications, which already passed nothing.
    • service-messaging is unchanged. It writes sys_notification.actor_id from EmitInput.actorId as given, so the producer is where this is fixed.
  4. Stored sentinels. backfillActionSlots pass 1 already scanned actor_id for values that contain : or @. The $nin exclusion of the two sentinels is gone, and each row takes one of two arms with the same single update call:

    • a slot literal moves to acted_as, as before;
    • a sentinel is nulled, and gets no acted_as, because a sweep took no slot.

    The result gains sentinelsCleared, and the plugin's boot log reports it. The pass stays idempotent: a second run writes {0, 0, 0}. The sentinels are now spelled only in LEGACY_MACHINE_SENTINELS (renamed from RESERVED_MACHINE_ACTORS). The update call-site count is unchanged, so the tenant-audit census needs no regeneration.

  5. Docs. In content/docs/automation/approvals.mdx, the dead-run sentence that documented system:dead-run now says the row records no actor and names the run. Two short sentences say the escalation rows and notifications record no actor, and that reassign_from / reassign_to hold slot addresses.

Readers of the sentinels (measured by grep at e72a7d2176)

  • This repo: no runtime reader keys on system:sla or system:dead-run.
    • The SLA re-run guard keys on action: 'escalate' (the ADR-0042 §1 marker), not the actor.
    • The dead-run sweep re-scans only status: 'pending' requests, so it does not key on the actor either.
    • listActions resolves names for ids and skips : values.
    • No REST approvals route and no analytics code reads either value.
    • The remaining hits: tests (updated here), release-owned CHANGELOG.md files, ADR-0042, the QA checklist JSON, and the backfill's legacy list.
  • objectui at the pinned .objectui-sha (89cad75d5): apps/console/src/pages/system/ApprovalsInboxPage.tsx:2331 renders actor_id === 'system:sla' as "System (SLA)". Other actors go through formatIdentity, which shows an em dash for an empty actor. See the acceptance notes.

What the field-type change does to a stored column (measured)

The old declaration created the table and stored three reassign rows (a position, an email, a user id). The new declaration then booted over the same database. A positive control confirmed the DDL capture: it saw the create table on the first boot.

SQLite (better-sqlite3) PostgreSQL 16.14
column before varchar(255) character varying(255)
DDL on the upgrade boot none none
column after varchar(255) character varying(255)
stored values all 3 unchanged all 3 unchanged
foreign keys / indexes on the columns 0 / 0, before and after 0 / 0, before and after
detectManagedDrift for reassign_from / reassign_to none none
a fresh column (as acted_as today) TEXT text

The lookup arm never emitted a foreign key (sql-driver.ts, the #11567 note), so nothing has to be dropped. Neither column is in a list view, highlightFields or an index. In listActions, reassign_from_name / reassign_to_name resolve where an address names an account: a user id, or an email an account carries (the existing resolveUserNames). A position has no name. The family pin asserts all three cases.

Pins

  • actor-lookups-hold-ids.integration.test.ts (new). This is the family pin, on a real ObjectQL over SqlDriver (better-sqlite3 in memory, real DDL).

    • It reads the sys_user lookups from the plugin's own manifest objects, not from a re-listed set.
    • It drives every write path through the real ApprovalService: position-slot and email-slot actions, comment, request-info, remind, three reassign shapes, a vote, the action link, send-back within budget and past it, recall, an out-of-office substitution, all four SLA actions, and the dead-run sweep.
    • It then asserts:
      • every stored value of every declared lookup is a sys_user id or null;
      • every declared lookup was written at least once, so a new lookup stays red until the battery writes it;
      • every notify actor is a person, and each person-driven topic forwards the holder, never the address;
      • each machine actor writes null, and the machine topics name no actor;
      • a position and an email round-trip in the slot fields, with the names resolving as described above.
  • action-slot-backfill.integration.test.ts now plants three sentinel rows, one of them an approve at step 0 on a pending request (the shape pass 2 stamps). It asserts { literalsMoved: 3, sentinelsCleared: 3, votesStamped: 1 }, then 0 sentinels left in the lookup, user-id rows untouched, and a second run writing {0, 0, 0}.

  • action-slot-backfill-wiring.test.ts: a run that only cleared sentinels still logs.

  • The existing pins that asserted the sentinels now assert null:

    • approval-service.test.ts: SLA notify, auto_approve, the system-context case, and dead-run;
    • approval-actor-impersonation.test.ts;
    • run-status-liveness.test.ts;
    • approval-service-sla-calendar-clock.test.ts.

    The approver-address-readers.test.ts ledger drops the $nin site.

Ablations (each through scripts/ablation-replace.mjs, wrapped, direction predicted first; every restore proven: blob equals HEAD and git diff HEAD empty)

leg mutation predicted observed
A the escalate row writes SLA_ACTOR_ID again red: id-or-null and machine pins, the SLA tests, the SLA-clock test; round-trip green 5 failed / 335 passed, those five exactly
B notify forwards SLA_ACTOR_ID when no context vouches red: notify pin, machine pin, SLA notify test; stored-row pin green 3 failed / 337 passed, those three exactly
C reassign_from back to Field.lookup('sys_user') red: declared-kind and id-or-null pins; round-trip and coverage green 2 failed / 4 passed; the offenders are named: position:legal and the email
D the backfill's sentinel arm removed (sentinel = false) red: the backfill pin 1 failed / 4 passed (sentinelsCleared 0, not 3)
E a new sys_user lookup that nothing writes red: the coverage pin only 1 failed / 5 passed; it names the new field

The first attempt at leg E was a no-op: the tool refused it because the replacement contained the anchor (count 1 to 1), and it restored the file. I re-ran it with a semantically identical re-spelling.

Gates, at e72a7d2176 (after merging origin/main 85e29b885)

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 95 commands, and all 95 exit 0.
    • Two of them first exited 3 (PREREQUISITE NOT MET: check:skill-examples, check:dual-build-cjs-loads). After turbo run build --filter=!@objectstack/docs, both exit 0.
    • --ran with exit codes: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.
    • The 4 in-path roster gates also exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity.
  • pnpm --filter @objectstack/plugin-approvals typecheck: exit 0 (check:test-typecheck OK).
  • pnpm --filter @objectstack/plugin-approvals test: 59 files, 875 tests passed.
  • pnpm --filter @objectstack/service-messaging test: 46 files, 507 tests passed.
  • packages/qa/dogfood/test/position-address-readers.dogfood.test.ts: 1 of 1 passed, against a rebuilt plugin-approvals dist.
  • eslint is narrowed to the 16 changed .ts files: --no-inline-config --format json reports 16 files, 0 errors and 0 warnings.
    • The population comes from eslint.config.mjs's packages/**/*.{ts,…} globs.
    • The config never enables type-aware linting (no parserOptions.project), so this diff cannot move the verdict on an untouched file.

Changeset

@objectstack/plugin-approvals patch, Clause-②: no. It says it supersedes the "The SLA sweep keeps its reserved system:sla actor for now" sentence of the unreleased .changeset/21411-approval-actor-person.md, which this PR does not edit. Both ship in one release.

Acceptance notes

  • Console rendering (objectui). At the pinned sha, the inbox timeline labels system:sla "System (SLA)" and otherwise shows an empty actor as an em dash. After this PR, the SLA and dead-run rows have no actor, so they show the em dash, as out-of-office, cancel and backfilled rows already do. ADR-0118 D1 makes "System" a render rule for an empty actor, and D5 answers "which automation" from association fields: for an SLA auto-decision that is the escalate row right before it, whose comment names the action. The renderer change is objectui work, the same carrier as approvals: sys_approval_action.actor_id (a sys_user lookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411's acted_as rendering. No reader needs a column the row kind does not carry, so none is added.
  • ADR-0042 §2 ("Machine decisions → reserved actor system:sla", and clients rendering it) is the decision ADR-0118 D1 supersedes for this column. ADR-0042 has no amended status line, and docs/adr/** is outside this PR's surface (Tier H). This needs a seat or maintainer decision.
  • ApprovalActionRow.reassign_from / reassign_to TSDoc in packages/spec/src/contracts/approval-service.ts still says "the user whose pending-approver slot was moved", and *_name says sys_user.name. The type (string) is unchanged. The text needs a spec-lane correction, and this PR makes no packages/spec edit.
  • docs/qa/platform-checklist/areas/approvals.json (the SLA single-shot item) says the escalate row's actor is SLA_ACTOR_ID. The row now records no actor. The item needs a one-word change, outside this PR's surface.
  • Behaviour change for integrations: code that tested actor_id for system:sla / system:dead-run reads the row kind instead, and code that expanded reassign_from / reassign_to as sys_user reads them as addresses. The changeset says both.

Generated by Claude Code

claude added 4 commits October 3, 2026 00:57
… slot addresses (wip)

The SLA and dead-run sweeps write actor_id null, notify forwards only the
person a context vouches for, reassign_from / reassign_to become slot-address
text columns, and the boot-time backfill nulls stored machine sentinels.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
… slot addresses; translations and changeset

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals, touching 26 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via approvals.getRequest (sdk, the route ledger binds it to GET /api/v1/approvals/requests/:id, selected by route anchor /approvals/requests/:id), approvals.listActions (sdk, the route ledger binds it to GET /api/v1/approvals/requests/:id/actions, selected by route anchor /approvals/requests/:id/actions), getRequest (sdk, the bare tail of client method approvals.getRequest, bound to GET /api/v1/approvals/requests/:id), listActions (sdk, the bare tail of client method approvals.listActions, bound to GET /api/v1/approvals/requests/:id/actions))
  • content/docs/api/plugin-endpoints.mdx (via /approvals/requests/:id (route, bridged from symbol requestInfo — its route source's handler names it; bridged from symbol sendBack — its route source's handler names it))
  • content/docs/automation/approvals.mdx (via sys_approval_action (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_approval_request (literal, a string literal in notify), getRequest (sdk, the bare tail of client method approvals.getRequest, bound to GET /api/v1/approvals/requests/:id), /approvals/requests/:id (route, bridged from symbol requestInfo — its route source's handler names it; bridged from symbol sendBack — its route source's handler names it))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), sys_approval_action (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_approval_request (literal, a string literal in notify), /approvals/requests/:id (route, bridged from symbol requestInfo — its route source's handler names it; bridged from symbol sendBack — its route source's handler names it))
  • content/docs/automation/workflows.mdx (via sys_approval_action (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_approval_request (literal, a string literal in notify))
  • content/docs/kernel/services-checklist.mdx (via /approvals/requests/:id (route, bridged from symbol requestInfo — its route source's handler names it; bridged from symbol sendBack — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via requestInfo (symbol, a method of class ApprovalService), sendBack (symbol, a method of class ApprovalService))
  • content/docs/plugins/packages.mdx (via sys_approval_action (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_approval_request (literal, a string literal in notify))
  • content/docs/ui/translations.mdx (via sys_approval_request (literal, a string literal in notify))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via sys_approval_action (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_approval_request (literal, a string literal in notify), getRequest (sdk, the bare tail of client method approvals.getRequest, bound to GET /api/v1/approvals/requests/:id))
  • content/docs/releases/v17/17-0.mdx (via getRequest (sdk, the bare tail of client method approvals.getRequest, bound to GET /api/v1/approvals/requests/:id))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class), sys_approval_request (literal, a string literal in notify))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 37 pages)
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 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 f9a8eb889ead684cb3f35f4ee013e634513ad1af → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 84c31c23c48cdaf1067e177e4c6177245362ab7f — the merge of head e72a7d21767e3f154947ee1503aaa6fe0b72218d into base f9a8eb889ead684cb3f35f4ee013e634513ad1af, 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 84c31c23c48cdaf1067e177e4c6177245362ab7f && git checkout 84c31c23c48cdaf1067e177e4c6177245362ab7f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f9a8eb889ead684cb3f35f4ee013e634513ad1af e72a7d21767e3f154947ee1503aaa6fe0b72218d && git checkout -B drift-repro f9a8eb889ead684cb3f35f4ee013e634513ad1af && git merge --no-ff e72a7d21767e3f154947ee1503aaa6fe0b72218d

node scripts/docs-audit/affected-docs.mjs --json f9a8eb889ead684cb3f35f4ee013e634513ad1af

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f9a8eb889ead684cb3f35f4ee013e634513ad1af → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 02:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 02:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 88fb5e8 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21455-adr0118-actor-family branch October 3, 2026 02:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…erseded in part by ADR-0118 D1; SLA checklist expects no actor (objectstack-ai#21556)

Fixes objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants