Repository navigation
fix(plugin-approvals): approval.requested names the record by label and title; the empty-slot warning states the slot, not the request's fate - #23592
Conversation
…nd title; the empty-slot warning states the slot, not the request's fate The approval.requested body named the record as object/id. It now names it the way the approvals inbox row does: the object's label and the record's title, read through the same live resolution enrichRows uses (factored out as resolveSubjectDisplays). With no title, label and id; the unredacted submit snapshot is never consulted for a title. The per-slot "expanded to nobody" warning no longer says the request cannot advance: on the default admin_rescue path the request opens on the organization's administrators, which the node-level line states. Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
… narrowed by any reader's field-level security Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 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 9f078a86166e2f72e86fc2932dc431d6a932729f && git checkout 9f078a86166e2f72e86fc2932dc431d6a932729f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1a9d488136826f71d990b8364106e23d6fd6a0ce 7c8b072027989481271a78f9e2ad84217bae9e14 && git checkout -B drift-repro 1a9d488136826f71d990b8364106e23d6fd6a0ce && git merge --no-ff 7c8b072027989481271a78f9e2ad84217bae9e14
node scripts/docs-audit/affected-docs.mjs --json 1a9d488136826f71d990b8364106e23d6fd6a0ce
|
…label and id only; the title is withheld until the subject-title read is narrowed per reader The body no longer embeds the record title from any source: the inbox's subject-title read runs under the system context with no reader's field-level security, and notification text is stored and may be e-mailed. nameSubjectRecord now reads only the object label, through objectLabelOf, which resolveSubjectDisplays shares (enrichRows' behaviour unchanged). The empty-slot warning is unchanged. Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
席位核对
Verdict: ACCEPT at head
Generated by Claude Code |
Part of #23556
Clause-②: no
Both texts the card names are server-authored, and both now say what is true. Scope is the card's two "expected" lines, per triage.
1.
approval.requestednames the record by its object label and idBefore:
A decision on crm_opportunity/JCqLLy5dSug1MldB is waiting on you.After:A decision on Opportunity JCqLLy5dSug1MldB is waiting on you.The object's API name appears only when its schema declares no label, as in the inbox.The body carries no title, from any source: not the live record's and not the submit snapshot's. It stays that way until #23594 narrows the inbox's subject-title read (
resolveSubjectDisplays, the one fix point) by each reader's field-level security, because notification text is stored and may be e-mailed.nameSubjectRecordreads only the label, throughobjectLabelOf, whichresolveSubjectDisplaysalso uses;enrichRowsbehaves exactly as before.2. The per-slot warning states the slot, not the request's fate
Before:
approver 'position:sales_manager' expanded to nobody — the slot routes to no one and the request cannot advance until someone is added or the approver is re-pointed, followed at once by the node-level line that opens the request on the organization's administrators.After:
approver 'position:sales_manager' expanded to nobody — the slot names no one, so no one is asked through it. Staff it or re-point the approver; a slate left naming no one is decided by the node's onEmptyApprovers.This is the card's second shape: the consequence is left to the node-level line. The resolver sees one approver entry, not the slate. Whether the request opens on the slate's other people, or the node's
onEmptyApproversdecides an empty slate, is known only toopenNodeRequest, which already logs that decision on every branch:admin_rescuewith and without administrators,fallback,auto_approve, andfail, which throws. So the routing is stated once, by that line. The old ending was false on the default path. It was also false on a mixedfirst_responseslate, where the request opens on the other people.Premise check, measured on
origin/mainatfa6a6085c0approval-service.ts:3598and:2216-:2219). Each was pinned by the test the card names.record_titlethrough a lookup that honours the target's declared exposure. It does not. The exposure check from896a4342e6(servesPayloadDisplayTarget) applies only to lookup targets named inside the payload snapshot (payload_display). The subject record's own title is read under the system context, with no exposure check and no field-level narrowing. The body matches that resolution exactly, so a recipient is shown nothing their own inbox row does not already show them. The gap in the list itself is in Acceptance notes.Pins (moved, not added beside)
approval-requested-notification.integration.test.ts(realObjectQLoverSqlDriver):A decision on Deal "ladder" is waiting on you.;listRequestsrow'sobject_labelandrecord_title;Deal D_untitledand the snapshot title is absent.approval-service.test.ts(the moved WARN pin, four graph types): the line names the slot andonEmptyApprovers, and does not matchcannot advance.admin-rescue-named-slate.integration.test.ts: the card's acceptance line at its real scene, anadmin_rescuenode whose organization has two administrators. The per-slot line and the routing line each fire once, and no line matchescannot advance. This file is outside the claim's declared file surface. It is the only fixture with an organization administrator, and building one elsewhere would have duplicated it.Ablations (one-time, run at
452d891d16, restore proven byscripts/ablation-replace.mjs: blob equal to HEAD,git diff HEADempty)cannot advanceassertion, with both scene counts greenenrichRowsover the unredacted row (the tempting shortcut)Deal "Snapshot-only title"Tests and gates, final head
8e5919cb46pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2: 74 files, 1054 tests passed.pnpm --filter @objectstack/plugin-approvals typecheck: exit 0, test layer included (check:test-typecheck: OK, debt held).dispatch-gates --commandsnamed 67 for this change set; all 67 ran with exit 0.--ranreconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. On the first pass,check:dual-build-cjs-loadsandcheck:i18nanswered 3 (PREREQUISITE NOT MET, no dist); after a workspace build both answered 0.origin/mainwas merged once before opening, atc980d114d3. It brought no change underplugin-approvals. The approvals closure was rebuilt,spec check:generatedpassed, and the suite and gates above were rerun after it.Changeset
.changeset/23556-approval-request-names-record.md,patchon@objectstack/plugin-approvals. Both are server-authored texts in a released package. No schema, translation key, export or notification field moves.Acceptance notes
record_title, and with it the 920bc12 toasts, the one-tap action page, and now this body, can carry a field the reader's field-level security hides. Probe: an approver whose readable and queryable set isid, amountgets the payload served withouttitle. The same approver'sgetRequestandlistRequestsrows still carryrecord_title: "Synthetic secret title", because the subject read runs under the system context. The remedy belongs inresolveSubjectDisplays, the one function both readers now share; the notification would then pass each recipient. The stake for this PR is that notification text is stored, so titles already sent cannot be withdrawn.object/id:approval.ooo_substituted,approval.ooo_skipped, the reassignment, both reminder bodies, the escalation pair, the escalation-action line and the revision-limit rejection. Each is now a one-line change throughnameSubjectRecord.#22824is the routing this text now defers to. It is not changed here.Seat re-scope note (seat's append, patch round 2, head
7c8b072027)Appended by
domain:servicesseat 1 (session_017WLmd5in9RXu7AjyHR8fKQ); the dev writes this body once and never PATCHes it, per the dev contract. Seat decision6183426689on #23556: the notification carries no title until #23594 lands, so this PR isPart of #23556, and #23556 keeps the title half (Blocked-by: #23594). Section 1 above is replaced with the dev's round-2 text (report6183760676). Where the sections below still describe round 1, round 2 supersedes them:A decision on Deal D_ladder is waiting on you., and the parity check againstrecord_titleis dropped. The negative case is "[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] a record whose title resolves is still named by label and id". There the live title and the snapshot title differ, and both are asserted absent; the inbox resolving the live title is the scene control.7c8b072027. Letting the title through gives 2 failed / 8 passed, and the restore is proven.7c8b072027, with the same counts: 74 files / 1054 tests, and 67 of 67 gates.Generated by Claude Code