Repository navigation
fix(plugin-approvals): the out-of-office notifications name the absent approver and the delegate by display name - #24146
Conversation
The battery's out-of-office request now asserts that the delegate's body names the absent approver, and the absent approver's body names the delegate, by display name and never by sys_user id. A control request routes an approver id no account answers to: its body keeps that id. Red against the unfixed source: both bodies of the `ooo` hop and the skipped body of the control still carry the raw ids. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
…t approver and the delegate by display name `openNodeRequest`'s out-of-office fan-out interpolated `sub.from` and `sub.to` as raw sys_user ids into the `approval.ooo_substituted` and `approval.ooo_skipped` bodies. Both bodies now name each person through `resolveUserNames`, the chain the approvals door names people with: one read for every person of every hop, and none when nothing was substituted. A person the chain answers no name for is named by the stored id, as every reader of that chain renders an unresolved address. The `ooo_substitute` audit comment keeps the ids by design: it is the request's audit trail of identities, served as written. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
…o-notification-names
📓 Docs Drift Check1 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 — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b703448205b79929e491bd8166ea3dc9324b526 && git checkout 9b703448205b79929e491bd8166ea3dc9324b526
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ffe8af094a4c230ec1354521f2efea89eba1f7a de3023f846d2dff1b8e37eebfaa623b41dcb1035 && git checkout -B drift-repro 9ffe8af094a4c230ec1354521f2efea89eba1f7a && git merge --no-ff de3023f846d2dff1b8e37eebfaa623b41dcb1035
node scripts/docs-audit/affected-docs.mjs --json 9ffe8af094a4c230ec1354521f2efea89eba1f7a |
席位核对 · ACCEPT ·
|
Fixes #24065
Clause-②: no
What changed
The out-of-office fan-out in
openNodeRequestnow names both people by display name in its two notification bodies. Path:packages/plugins/plugin-approvals/src/approval-service.ts, line numbers at this head.approval.ooo_substituted(:3640) goes to the delegate and names the absent approver.You are covering an approval on Deal D_ooo while u_ooo is out of office.now reads… while Olive Away is out of office.approval.ooo_skipped(:3652) goes to the absent approver and names the delegate.An approval on Deal D_ooo was routed to u_bob while you are out of office.now reads… was routed to Bob while you are out of office.Both names come from
resolveUserNames(:6680), the plugin's one person-label chain. The approvals door uses the same resolver forpending_approver_namesand for the names on the timeline.:3622, before the loop, with both people (sub.fromandsub.to) of every hop.sys_userread for three people.readUserNamesreturns before itsfind.No resolver was added.
resolveUserNamesitself, its fallback and its field-level-security note are unchanged. The record half (nameSubjectRecord, from #23978) is untouched.What the body shows when there is no name, and whether that can be an email
sub.fromandsub.toare the hop'sdelegator_idanddelegate_id(applyOooDelegation). Both aresys_userlookups, so the stored value is a user id, never an email. For each person, the body shows:nameOlive Away)namebut with anemailreadUserNameslabels such a row withu.name ?? u.email. This is the same label the approvals door already serves for that account, aspending_approver_namesand on the timeline.sys_user.nameis declaredrequired: true(packages/platform-objects/src/identity/sys-user.object.ts), so only a row written without a name reaches this case.sys_userreadApprovalActionRowstates for every reader of this chain: when no name is returned, "render the address".Each body goes to one party of the hop's delegation and names the other party.
A body is a stored copy of the name: messaging stores it and the inbox serves it. The
resolveUserNamesdocblock warns, under "STORED COPIES", that a later narrowing of names bysys_userfield-level security does not reach the copies stored in rows. It lists two columns, and these two bodies are a third copy. The comment at the call site says so. Adding the bodies to the docblock's list is outside this claim's file surface, so it is an open question in the report (see Acceptance notes).The audit comment keeps the ids, by design
The
ooo_substituterow'scomment(:3629, for exampleu_ooo → u_bob — leave) is unchanged. These read it:listActionsserves the comment as stored. It resolves names per read foractor_id,reassign_fromandreassign_to. This row has no person in any of those fields (actor_idis null).a.commentverbatim, in quotes:ApprovalsInboxPage.tsxandRecordApprovalsPanel.tsxat the console pinf8267df93a.sys_approval_action'srecentandall_actionslist views show the column, and an export of the object carries it.docs/qa/platform-checklist/areas/approvals.jsonverifies the row by its comment readingA → B, the two identities.This comment is the request's audit trail of identities. An id stays true when a user is renamed. A name written into this row would be one more copy that a later field-level-security narrowing cannot reach. Where the platform wanted a hand-off's parties to be readable, it chose structured fields with names resolved at read time:
reassign_from/reassign_toand their*_namecompanions replaced the"FROM → TO"text thatreassignused to write into its comment (specApprovalActionRow). A readable OOO timeline line is a job for that shape, not for this comment (see Acceptance notes).Premise check (
origin/main27d7c2aa1d)The premise holds.
:3624and:3636interpolatedsub.fromandsub.toas raw ids.:3613, andresolveUserNamesis at:6664.notify()passespayload.bodytomessaging.emitunchanged. The red run below shows the bodies as messaging received them.Pin
approval-notification-body.integration.test.tsis the battery that drives every call site that notifies. It runs the realApprovalServiceover a realObjectQLonSqlDriver(better-sqlite3, in memory), and records what reaches the messaging surface. This PR adds one request and two tests:ooo_goneis the control. Its approveru_gonehas nosys_userrow and an active delegation tou_alice. So in one fan-out, one person's name resolves and the other's cannot.Olive Awayinooo_substituted@ooo,Bobinooo_skipped@ooo, andAliceinooo_skipped@ooo_gone. It also asserts that the matching id is absent. It does not assert the rest of the sentence.ooo_substituted@ooo_gonebody namesu_gone, so the slot holds the id, notundefinedand not an empty string.The battery's coverage list gains the two
ooo_gonesites.Readings:
225281a720). One test failed and six passed. The three offenders were the bodies as sent:You are covering an approval on Deal D_ooo while u_ooo is out of office.,An approval on Deal D_ooo was routed to u_bob while you are out of office.andAn approval on Deal D_ooo_gone was routed to u_alice while you are out of office.The fallback pin was green before the fix, because the id was the whole text then.26ca62ac57: the battery passes 7/7.scripts/ablation-replace.mjs, WRAP mode):oooPersonreturns the bare id, which puts the old text back in both bodies. The anchor went from 1 hit to 0 and the blob from05d5b39c94e8to269142866144. The name pin went red with exactly the three offenders above, and the other six tests stayed green. The restore was proven: the blob is05d5b39c94e8, equal to HEAD, andgit diff HEADis empty.oooPersonreturns the chain's answer with no fallback. The blob went from05d5b39c94e8toc88096bc793b. The fallback pin went red withYou are covering an approval on Deal D_ooo_gone while undefined is out of office., and the other six tests stayed green. The restore was proven: the blob is05d5b39c94e8, equal to HEAD, andgit diff HEADis empty.src/, so neither ablation has adist/leg.Tests and gates (head
de3023f846, after mergingorigin/main462d02f00a)pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2passed 81 files / 1107 tests.pnpm --filter @objectstack/plugin-approvals typecheckexits 0.check:test-typecheckholds the ledger unchanged at 8 files / 324 errors.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands. I ran all 67 and recorded each exit before any pipe. 65 exited 0 on the first pass.PREREQUISITE NOT MET):pnpm check:dual-build-cjs-loads(some package had nodist/) andpnpm check:i18n(the CLI build closure was missing).check:i18nreports all 10 packages' bundles in sync.check:dual-build-cjs-loadsreports 111 require entry points across 69 packages load.dispatch-gates --ranover the final record: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Over the first-pass record, it reports 65 run and 2 NOT-MEASURED, derived from the two exit 3 codes.node scripts/check-changeset-no-major.mjs --base origin/main --eventwith this body exits 0 (patchunderClause-②: no).Acceptance notes
resolveUserNames"STORED COPIES" docblock listspending_approver_labelsandacted_as_labelas the stored copies that a field-level-security narrowing must follow. These two bodies are now a third. The call site records this, and the docblock does not. The claim excludedresolveUserNamesitself, so the one-line addition is left to the seat.ooo_substitute, so it shows the raw kindooo_substitute, followed by the comment"u_ooo → u_bob — leave". This is a reading of the source only, not measured in a running console. The structured-parties shape thatreassignuses is the fix pattern that preserves the audit comment. Carrier: none.Generated by Claude Code