Skip to content

fix(plugin-approvals): store the admin_rescue timeline note as a code and render it in the reader's language - #25627

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25576-admin-rescue-comment-i18n
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25576-admin-rescue-comment-i18n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25576

Clause-②: no
Metadata-protocol: none

What changed

The system note an admin_rescue request writes on its own timeline ("Routed to this organization's administrators because the approver slate named no one: position 'X' has no holder.") was composed server-side as one English sentence and stored as the comment of a sys_approval_action row, and listActions served it as stored, so a zh-CN reader read English.

Now the note is stored as a code plus its parameters, and the approvals door renders it in the reader's language:

  • Stored shape. openNodeRequest records the note on the request's node-config snapshot as __adminRescueNote: { action_id, abandoned, fallback? }: the id of the timeline row it explains, the abandoned approver slots (type:value), and, when a declared onEmptyApprovers: 'fallback' did not help, why (not_declared / resolved_to_no_one). The snapshot is where a request already keeps what it resolved at open time (__flowLabel, __round, __resolvedFrom; the open path's own comment: "Stamped on the snapshot (precedent: __flowLabel), so no schema migration"), so no column, object schema or spec contract moves. The row's comment is still the English sentence, byte-identical to what it stored before.
  • Catalog. The sentence and its clauses are new entries in the plugin's existing messages face (translations/messages.ts, approvalAdminRescue*), in en, zh-CN, ja-JP and es-ES. The stored English is those en entries interpolated, never a second spelling (admin-rescue-note.ts). The admin_rescue:reason audit string and the log lines are built from the same clauses (the module-private emptySlateReason is replaced by adminRescueReason).
  • Read path. listActions (GET /api/v1/approvals/requests/:id/actions) finds the row the snapshot names and overlays its comment, in place, with the note rendered at the caller's ExecutionContext.locale. A read, never a write. The served ApprovalActionRow shape is unchanged.

Precedent reused (no second mechanism)

Who renders it, and the objectui half

The server half is complete on the approvals door: any reader of GET …/approvals/requests/:id/actions with a locale gets the localized note. Not verified from this session: which door the console's record Approvals tab reads its timeline through (objectui is not attached here). If RecordApprovalsPanel reads sys_approval_action through the generic data door rather than the approvals actions route, it is served the stored English, and the objectui half is to read the timeline through the approvals door.

Rows already stored

A request opened before this change has no __adminRescueNote on its snapshot, so its note is served as stored, in English. No migration: the stored text stays correct English, it is display-only audit text, and its clauses survive only inside that prose. Parsing prose back into a code is the defect this replaces, so no backfill reconstructs them. Pinned by the CONTROL case below.

Serial constraint

PR #25600 (SLA reassign) landed on main while this was in flight, so this branch merged origin/main (b9c264fd8, clean, no conflict). The diff stays out of the SLA-reassign bodies.

Verification (head b9c264fd8)

  • pnpm --filter @objectstack/plugin-approvals typecheck: exit 0. pnpm --filter @objectstack/plugin-approvals test: Test Files 96 passed (96), Tests 1259 passed (1259).
  • New pins: admin-rescue-note.test.ts (the English is the sentence the row always stored; zh-CN / ja-JP render every clause and the separator; a clause the pack lacks falls back to English; a missing note sentence renders nothing; the snapshot reader accepts only the shape it writes). admin-rescue-named-slate.integration.test.ts adds five cases on a real ObjectQL + SqlDriver with a real in-memory i18n service loaded with the plugin's own ApprovalsTranslations. They pin: the snapshot stores the code and names the row; a zh-CN reader of listActions reads 审批人名单未指向任何人,因此已转交本组织管理员处理:岗位「sales_manager」无人任职。; the fallback case renders both clauses joined the zh way; en and no-locale readers get the stored English; and a CONTROL where a pre-change request and a neighbouring system comment stay as stored.
  • Ablation (deleting the localizeAdminRescueNote call in listActions, through scripts/ablation-replace.mjs): 3 failed / 13 passed in the integration file, with the zh-CN, fallback and CONTROL cases red. Restore verified byte-identical: blob after restore 3527886551f8 = HEAD, git diff HEAD empty.
  • approver-address-readers.test.ts: the new actor_id presence read is classified in PERSON_SITES (role display, count 2), as that pin requires of every read of the column.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands): all 72 run on b9c264fd8, every one exit 0. --ran reconciliation: "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN". check:i18n: "all bundles in sync, no undeclared authoring keys". eslint --no-inline-config on the 8 touched .ts files: exit 0.

Acceptance notes

  • The step_name / step_label half of the original card is not in this PR: triage dropped it as objectui#13248 (closed as completed).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01QeMwrPsf2zjo2pQLBzdWFV

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

2 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.listActions (sdk, the route ledger binds it to GET /api/v1/approvals/requests/:id/actions, selected by route anchor /approvals/requests/:id/actions), listActions (sdk, the bare tail of client method approvals.listActions, bound to GET /api/v1/approvals/requests/:id/actions))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class))

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

  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))
  • content/docs/releases/v18/18-1.mdx (via ApprovalService (symbol, a top-level class))

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
  • 6 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 481ac6211641ccb7292508ed423f02e432322e4a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 481ac6211641ccb7292508ed423f02e432322e4a

⚠️ 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 481ac6211641ccb7292508ed423f02e432322e4a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T11:47Z · head b9c264fd8f · claim 6382518123 · report 6383420920

Checklist conclusions (read on GitHub, not from the report):

  • Shape: draft, base main, author objectstack-fleet[bot] (opened through the relay), assignee sunhaolin. Fixes #25576 is right: the card's remaining half after triage's narrowing (audit 6371325002) is delivered whole. Clause-②: no and Metadata-protocol: none are line-initial in the body and the changeset.
  • Scope: 9 files, all in packages/plugins/plugin-approvals/src/ plus one changeset (+426/-45). Not governed (check-governed-merges --pr 25627: 0 of 9). PR fix(plugin-approvals): an SLA reassign to a target that resolves to no one keeps the slate #25600's SLA-reassign region landed mid-run and was merged cleanly.
  • Changeset, read sentence by sentence: patch on @objectstack/plugin-approvals. The note is stored as a code and parameters on the request's node-config snapshot (__adminRescueNote); listActions renders the row's comment through the plugin's messages catalog at the caller's locale; the sentence ships in en, zh-CN, ja-JP and es-ES; the stored comment stays the English sentence; older requests keep their stored English, with no migration. Each matches the diff.
  • Behaviour, read in the diff: emptySlateReason moved to admin-rescue-note.ts as adminRescueReason; the open path records { action_id, abandoned, fallback? } on the snapshot, and the English stays one source (the en catalog). The zh-CN render pinned by the tests reads 「审批人名单未指向任何人,因此已转交本组织管理员处理:岗位「sales_manager」无人任职。」. The stored-shape choice (snapshot, no column) follows the snapshot precedent (__flowLabel, __round, __resolvedFrom) and adds no contract surface.
  • The objectui half (the dev's open question 2), measured by the seat: objectui packages/app-shell/src/hooks/useRecordApprovals.ts:360 on origin/main reads the timeline through GET /approvals/requests/:id/actions, the door this PR localizes. Nothing to file for objectui.
  • CI on head b9c264fd8f: 34 success, 7 skipped, 0 red; every skip is on main's expected-skip roster.
  • Writes: mcp_calls 3, all GitHub MCP reads (issue_read); no write tool. Every write went through the relay as the fleet.

Deviations accepted: the worktree built on the platform-created branch; one PERSON_SITES classification in approver-address-readers.test.ts (the pin requires every read of the column to be classified; no gate weakened); commit trailers in the AGENTS.md form.

Landing: pr_ready + automerge_enable through the relay in this act. Fixes #25576 closes the card on merge; the seat strips pm:dispatched then and archives the cloud session session_01QeMwrPsf2zjo2pQLBzdWFV.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 11:49
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 11:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit ca80f05 Oct 24, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25576-admin-rescue-comment-i18n branch October 24, 2026 12:11
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-approvals: the admin_rescue system comment is written into the approval timeline as English prose, so a zh-CN viewer reads it untranslated

2 participants