Repository navigation
fix(plugin-approvals): an approval notification names the record by its title, as the recipient may read it - #26389
Conversation
…ts title, as the recipient may read it Each approval notification is delivered to one named recipient, so its body now names the record by its display title (ADR-0079 nameField) as that recipient may read it, narrowed by their field-level security, and by the record id when the title is empty or unreadable to them. notify() resolves the name per recipient and splits a notice whose recipients read different names. The token-bearing action-link pages keep naming the record by its id. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kfr6fhXWwYppMd27Z6fPM8
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
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 e48c997c8ac3efbf1c279b1248b0ab321e3e68e4 && git checkout e48c997c8ac3efbf1c279b1248b0ab321e3e68e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abb9046057b6713b78712dfe53a329deefa534ba 6cc01b7264b7ae8d03d948301360eb7672fa25d0 && git checkout -B drift-repro abb9046057b6713b78712dfe53a329deefa534ba && git merge --no-ff 6cc01b7264b7ae8d03d948301360eb7672fa25d0
node scripts/docs-audit/affected-docs.mjs --json abb9046057b6713b78712dfe53a329deefa534ba
|
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26181, patch changeset for @objectstack/plugin-approvals. Not governed (0 of 11 paths), 770 changed lines. |
| Fix | Ruling 6429362190 letter A′. Every approval notification that names a record now goes through notify({ about, text }), and subjectNamesFor resolves the name per recipient. It reads the record under the system context, as the inbox does (#7497 ruling C), then picks the title as served to that recipient (servedFields over an envelope built by resolveUserAuthzGrants and vetOrganizationClaim in the request's organization). An empty title, an unreadable or masked title field, an unknown e-mail, or any failure gives the id. Recipients who read the same name share one emit, so no recipient gets another's title. The action-link pages keep label and id, with the dated #26181 note beside the #23594 comment. The templates keep {{object}} {{record}}. A sent-back note in the approver's own words still names no record. |
| Evidence | 38 check runs succeeded and 10 were expected skips on this head, 0 failed; mergeable clean. Tests (a)–(d) in notification-subject-title-per-recipient.integration.test.ts cover the ruling's four cases: a readable title, an empty title, an FLS-hidden or masked title, and two recipients with different FLS on one request. They are red on main for the title cases, and the FLS ablation is red. Package suite 1284/1284, derived gates 98 of 98 exit 0. |
| Carried | Claim surface, recorded here: content/docs/automation/approvals.mdx gains the per-recipient naming paragraph, because its Deal D_123 example was made false by this change. Overlap: #26337 merge-tree clean; #26180 (same plugin) has no PR yet and re-measures against this one. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26181
Clause-②: no
Metadata-protocol: none
维护者速读
approval-service.ts的notify()和subjectNamesFor();新测试notification-subject-title-per-recipient.integration.test.ts;action-link-pages.ts新加的说明。pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 src/notification-subject-title-per-recipient.integration.test.ts,预期 4 个用例全绿。What changes
notify()names the record per recipient. A notice about a record now handsnotify()the record (about) and a builder for its sentence (text), instead of a text built once.notify()askssubjectNamesFor()for the name each recipient reads, groups the recipients by name, and emits once per name, so a recipient never receives a text built around another recipient's title. A dedup key on a split notice is suffixed with each part's first recipient so messaging does not fold one part into another (no current call site sends a multi-recipient notice with a dedup key that reaches messaging; the reminder to slot literals is dropped before emit).subjectNamesFor()is the inbox's own title rule (resolveSubjectDisplays) asked as the recipient: the record is read once under the system context, and the title (ADR-0079nameFieldviaresolveDisplayField) is picked from it as served to the recipient,titleAsServed()over theirservedFields()answer. The id stands when the record has no title, when the recipient may not read the title field or is served it masked, and when any read fails (atwarn).titleAsServed()is extracted fromtitlesAsServed()so the inbox and the notifications share one spelling.recipientContext()builds the recipient's envelope the way the@mentionrecipient gate inplugin-auditdoes:resolveUserAuthzGrantsin the request's organization, the organization kept whilevetOrganizationClaimbacks it under the posture in force. An email address is looked up to its account; one no account carries gets the id.{{object}} {{record}}(no title-plus-id suffix); the literal-body notices (comment, request for information, send-back note) name no record; the token-bearing action-link pages keep the id, with a dated note beside the security(approvals): the inbox's subject record_title is resolved under the system context and served to every approver without their field-level security, against payload-redaction's serve-time contract and #7497's ruling C #23594 comment inaction-link-pages.ts.content/docs/automation/approvals.mdxsaid the outcome notification names the record asDeal D_123; it now says title as the recipient may read it, id as the fallback, id on the action pages.Tests
notification-subject-title-per-recipient.integration.test.ts(realApprovalServiceonObjectQL+SqlDriver, a field-visibility double keyed by user): (a) a reader of the title field reads it in the literal body and the keyedrecordparam, onapproval.requestedandapproval.approved; (b) an empty title gives the id; (c) a recipient who may not read the title field, or is served it masked, gets the id and no trace of the title; (d) two recipients with different field-level security on one request get different bodies, both on per-approver emits (the opening) and on one emit naming both (the SLA breach, split into two emits).main: withapproval-service.tsandapproval-notification-text.tschecked out at the base6390fe52, (a) and (d) fail (Received: "A decision on Contract C1 is waiting on you."); (b) and (c) pass there, as the fallback controls they are.titleAsServed(record, displayField, readable)mutated to ignorereadable(viascripts/ablation-replace.mjs, mutation proven on disk, restored to the HEAD blob) turns (c) and (d) red.approval-requested-notification(the titled-record case now pins the live title, never the snapshot's),approval-decision-outcome-notification,approval-notification-body(the record name per call site),approval-notification-keyed-text(localized bodies and the end-to-end messaging render).Acceptance notes
plugin-audit'scontextForMentionedUserand approvals'recipientContext. Lifting one into@objectstack/coreis outside this card's file surface. Noted, not filed.filterApproversWhoCanRead), so the organization claimrecipientContextvets holds for every approver that can be notified; one that does not hold resolves with no active organization and fails closed to the id.Verification
All at head
6cc01b7264, base6390fe52.Package suite and typecheck (under the verify lock)
Red on main, green on the fix
Ablation: the field-level security narrowing
Derived gates and lint
Four exited 3 (prerequisite not met) on the first pass and were re-run green after the prerequisite:
check-plugin-teardown-shape --self-testafter fetching its pinned positive control621a4876;check:i18nafter the build closure it names (check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys));check:skill-examples(264 prose examples type-check);check:dual-build-cjs-loads.pnpm exec eslint --no-inline-config packages/plugins/plugin-approvals/srcexit 0. The repo-widepnpm lintandtest:repoare CI's.Overlap with open PRs
Open PRs listed and each one's files read: only #26337 touches the same files (
approval-service.ts,approvals.mdx).git merge-tree --write-tree HEAD e4513376exits 0 (no conflict). #26180 has no open PR yet.Generated by Claude Code