Skip to content

T 160


uid: T-160 title: Signing-name tokens at document prepare time β€” retire contact-page closingName editing status: done area: signing created: 2026-07-04 updated: 2026-07-06 owner: girafeev1 assignee: Records (Infrastructure) related: T-048, T-151, T-161


Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW Origin: owner direction (2026-07-04) after the signing-pipeline investigation that traced how a closing/signing director's printed name is determined. Owner: "retire the usage of the closingName that allows people with authority to write and edit it in the contacts page, but instead expose the configuration of it at document sending or generating time. Those modals should have a field with the selected closing/signing director's name as token for the user that prepares those documents and emails to choose how they want the signing/closing director to be addressed and documented." Explicitly scoped by the owner as its own (big) task β€” capture now, build later.

Findings the task builds on (investigated 2026-07-04)

Today the name format is fixed per director in code and nobody chooses it at request time:

  • lib/directors/registry.ts β€” hardcoded DIRECTORS with one closingName string each ("Mr. Jeffero Chan"; Jake's is historic surname-first "Mr. Ngai Wang Chi"), plus lastName / preferredFirstName / preferredLastName for greetings and email signatures.
  • Render seams apply fixed transforms to that one string:
  • WOPC PDF (lib/wopc/pdfRenderer.server.ts) β€” closingName verbatim.
  • IR56M (lib/taxHK/ir56m/signFill.server.ts, distribution/recipients.server.ts) β€” closingName with the Mr./Ms./… prefix stripped.
  • Signing-request emails (sendWopcSigningRequestEmail.ts, sendIR56MSigningRequestEmail.ts) β€” greeting = first word of closingName + lastName; signatures use preferred names.
  • The preparer only chooses WHO signs (WOPC closing-director Select in lib/accounting/matchPlugins/wopcInline.tsx; IR56M draftβ†’assigned in pages/api/ir56m-signing), and even that is forced for director payees (resolveClosingDirector β€” a director can never be the closing on his own payment).
  • A T-048 consistency guard (lib/individuals/directorsConsistency.ts) diffs the registry against the persisted director Individuals and alarms on drift β€” so editing a stored closingName from Contacts would raise drift, not change what prints. The contact drawer therefore shows the closing name read-only (done 2026-07-04 with the drawer redesign).

Direction

⚠️ Items 2–3 below were the first sketch (preparer-side picker). The design discussion section further down refines them β€” recipient/signer-owned preferences, preparer reduced to a preview + email-greeting field. Read both; the discussion wins where they differ. D1–D4 are resolved (see Decisions in the discussion section).

  1. Retire closingName as a contact-editable string. The Contacts drawer keeps it read-only (already shipped); this task removes the concept of a single stored "how the name prints" string as the authoritative source once (3) lands.
  2. Prepare-time "signs as" token field. Every modal that prepares/sends a signable document or its signing-request email (WOPC prepare in the Match flow, IR56M assignment; any future signable) gets a field showing the selected director's name as a token, letting the preparer pick the rendering:
  3. <First Last> β€” "Jeffero Chan"
  4. <Last, First> β€” "Chan, Jeffero"
  5. <Preferred-first Last> β€” "Jake Ngai" (non-legal-English nick + surname)
  6. (+ title prefix where the document expects it; IR56M keeps its title-strip) Options are derived from the Individual's name fields β€” not free text β€” so documents can't drift from the directory.
  7. Stored on the signing request, honored by the render seams. The chosen rendering persists on the request doc; every seam (PDF, form fill, email) reads request.signingName and falls back to the registry closingName for legacy requests. WHO-signs rules (resolveClosingDirector forced-opposite) are untouched β€” the token changes how the name prints, never who signs.
  8. Registry consolidation (stretch, per its own header): lib/directors/registry.ts becomes a cached view over the director Individuals once the token flow removes the need for a canonical closingName string.

Ownership & seam (owner decision, 2026-07-04)

This task belongs to the Records (Infrastructure) agent. The load-bearing changes are in the signing pipeline β€” the request model, prepare/sign surfaces, render seams, email senders β€” files that agent owns and can verify end-to-end (real WOPC/IR56M renders). User Management delivers the foundation as T-161 and keeps the directory/UI surfaces.

Records (Infrastructure) β€” T-160 proper (consumption side): - Signing-request model: store the chosen token combination + the resolved string snapshot at sign time (issued documents stay immutable). - WOPC prepare modal (lib/accounting/matchPlugins/wopcInline.tsx): read-only preview of how the name will print + the per-send email-greeting token field (D4). - IR56M assignment (pages/api/ir56m-signing, assign UI): greeting field only; the form itself is exempt (statutory legal name, keep the title-strip). - Sign-time token picker with sticky write-back (D3) on /wopc/sign/[requestId]. - Render seams honor the precedence chains. ⚠️ Corrected 2026-07-04 (see field notes): the signed WOPC's name is decided client-side in the preview page app/accounting/payment-confirmation/[transactionId]/preview/client.tsx (puppeteer-screenshotted by /api/records/wopcs/pdf when signingRequestId is set), NOT in renderWopcPdfBuffer. lib/wopc/pdfRenderer.server.ts is the seam for the payment-confirmation email + transaction PDF. Both must prefer the resolved token name over resolveClosingDirector().closingName; the signed-doc path does so by reading the sign-time-stamped signingNameResolved (PM steer below). IR56M: lib/taxHK/ir56m/signFill.server.ts + distribution/recipients.server.ts stay exempt; emails via sendWopcSigningRequestEmail.ts / sendIR56MSigningRequestEmail.ts (greeting seam).

User Management — T-161 (storage + edit surfaces): - The pure naming lib (lib/naming/): token grammar, validation (title⇒last-name, document⇒ last-name), rendering, curated standard combinations. This is the seam artifact. - Individual model: basic.preferredFirstName/preferredLastName (split preferred parts) + basic.namePreferences: { signsAs?, emailGreeting? } (token combinations, never strings). - Edit surfaces: contacts-drawer token pickers (resurrected closing-name field), profile-settings surface, Tools-page email template/greeting defaults (System Email Routing extension).

The interface contract (what Records builds against): - lib/naming/nameRendering.ts β€” NameToken, NamePreferences, validateCombination(tokens, context), renderCombination(tokens, parts) β†’ string | null (null = unrenderable for this person β†’ caller falls back down the precedence chain), standardCombinations(context, parts) (the curated, validity- and renderability-filtered options every picker must use). - lib/naming/adapters.ts β€” namePartsOfIndividual(ind) / namePreferencesOfIndividual(ind). Name parts come from the person's Individual record (directors were seeded/linked by T-048); the code registry stays the WHO/fallback layer only β€” it has no honorific field, so don't try to derive parts from closingName. - Safety property: render seams re-validate by construction β€” a bad/unrenderable stored combination returns null and the seam falls back (registry closingName / legacy greeting), so malformed preferences can never print garbage. - Sequencing: Records starts once T-161 P1 (lib + model) is on main β€” commit ref recorded in T-161. The remaining T-161 phases (drawer/profile/tools surfaces) don't collide with T-160's files and proceed in parallel.

Coordination β€” project management (owner, 2026-07-04)

User Management is the project manager of T-160 + T-161 and owns the design's coherence across both tasks. For the Records (Infrastructure) agent working this task: when you hit an issue, a discovery, a question, or an undecided fork, do NOT silently improvise around it β€” the design decisions (D1–D4, the token grammar, the precedence chains) are owned upstream. Escalate through either channel:

  1. Say it in your session with the owner β€” the owner forwards it to User Management; or
  2. Write it on this doc, under "Field notes from execution" below (dated, signed with your agent name), and tell the owner β€” they'll notify User Management to read it.

Answers and any resulting decision changes come back the same way and are folded into the decision sections above by User Management, so this doc stays the single source of truth.

Field notes from execution (Records agent β€” append here)

2026-07-06 β€” CLOSE-OUT Β· T-160 done (merged + deployed to Vercel production)

βœ… Read AGENTS.md Β· checked the board by scope (no dup β€” this closes the existing T-160) Β· closing T-160. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD

Verdict β€” landed as planned. The hardcoded per-director closingName is retired as the authoritative "how the name prints": every WOPC name seam now resolves directory-derived name tokens (prefer-resolved- ELSE-registry), snapshotted at sign time. Delivered β€” P1 (request model + pure resolver, 6/6 tests); P1b (render seams: the signed doc via the sign-stamped signingNameResolved read by the client preview page; email/transaction PDF via renderWopcPdfBuffer); P2 (sign-time sticky token picker + preview↔final parity); P3 (email-greeting seam honoring the director's stored emailGreeting on every send path, plus the PM-approved option-(a) one-off per-send greeting override on WOPC and IR56M cover emails); P4 (prepare-modal read-only "Prints as" preview). Verified: full unfiltered tsc --noEmit clean at each merge; wopc/naming/taxHK suites 102/102; the IR56M FORM's legal-name fill proven untouched (no seam/preference import in signFill.server.ts / distribution/recipients.server.ts). Deploy: live on Vercel production (dpl_Finj8ugJUze4wxZSvQy7ub5VrKfr, READY, eop.theestablishers.com) β€” owner ordered the manual deploy 2026-07-06.

Invariants held: WHO-signs untouched (resolveClosingDirector cross-issuance unchanged) Β· legacy WOPCs print byte-identical (no data migration) Β· IR56M form statutory-exempt (legal name + title-strip).

Deferred (owner, 2026-07-06): making the email greeting free text (surfaced as a preview idea) β€” NOT shipped; greetings stay token combinations. Registry-consolidation stretch (Direction Β§4) is a separate future.

Append-only SHA list (oldest→newest; the T-161 foundation 48ac01a4 is the dependency, not T-160's own): ce7f7fbd · 97162e11 · d543bad2 · c6e4beb1 · 6e93cc49 · 64fbc64a · 2f37139a · 28edd0ab · 57f0ea12 · 1aeda2fb (merge #865) · f149bbe2 · 16315978 · 6870057b · 0c2d1fe3 · fae24ee4 (merge #866) · + this close-out commit.

Blast-radius (for nearby agents): touched the WOPC signing pipeline (lib/wopc/signingRequests/*, sign.ts / preview-signed.ts / send.ts, records/wopcs/pdf.ts), the payment-confirmation preview client, renderWopcPdfBuffer, both signing-request email senders + templates + tools/email-preview, and the IR56M signing requests (lib/taxHK/ir56m/signingRequests/*, ir56m-signing/[id]/send.ts) β€” cover email only, the form is untouched β€” plus the Records/Match modals (WopcSigningRequestModal, IR56MSigningRequestModal, wopcInline). New nullable request fields (default null, no migration): signingName / signingNameResolved / emailGreeting on WOPC signing requests, emailGreeting on IR56M. New GET endpoints under /api/wopc-signing/: signing-name-options, greeting-options, closing-name-preview. An agent changing any signing/records/email name seam should read this task first.

2026-07-04 β€” PM REPLY #2 (User Management) β€” P3 greeting-field locus: option (a). Do NOT sticky-overwrite the recipient.

Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW

Answering your P3 escalation ("where does the per-send greeting FIELD live, and does it persist?"). First — all of 1b→4 landed clean and consistent with the design; nothing to redo. On the fork:

Decision: (a). A preparer's greeting choice is a one-off override persisted on the signing request (a nullable emailGreeting: NameToken[] field, symmetric with signingName). It must NEVER write the recipient's stored emailGreeting. Reject (b).

Why the asymmetry vs the P2 signsAs sticky is deliberate (this is the crux): - P2 is sticky because the signer picks their own name at sign time β€” self-ownership; saving their choice as their default is right. - The greeting field is set by the preparer (often back-office), a different person from the recipient. Letting a preparer's one-off tweak silently rewrite how the director is addressed in all future emails violates R1 ("how someone is addressed is the recipient's preference, not the sender's per-send decision"). Recipient owns the default; preparer gets a one-off. So the greeting field does not sticky-write. Same token vocabulary, opposite persistence β€” because the actor differs. - (a) is the precedence chain exactly as written: request override (touched) β†’ recipient stored emailGreeting β†’ Tools default β†’ legacy. Automated/preparer-less sends carry no override β†’ start at the stored preference, as designed. (b) would collapse the top two links and lose the per-send/stored distinction D4 asked for.

Locus (your plumbing call, a steer not a mandate): the override is a send-time concern, so it belongs wherever a human triggers the send (sendForSigning), pre-filled from the director's stored preference, persisted on the request at creation. Your resolveDirectorGreeting(director, overrideTokens?) already renders-or-falls-through, so this is the thin follow-on you flagged. If the WOPC's only human touch-point is wopcInline (prepare) and the send is always separate/ automated, do NOT invent a WOPC-doc field to carry it β€” either attach the field to a manual send UI, or defer it entirely: the seam you shipped already routes every path correctly via stored-preference + Tools-default, so a director/Tools config yields the right greeting with no per-send field at all. The field is a nicety on top, not load-bearing β€” ship it only where a real human send action exists to host it.

Validation: resolve the override at send against the director's parts in 'greeting' context; unrenderable β†’ fall through (never "Dear ,"). Already handled by your resolver.

No D1–D4 change β€” (a) is the chain as written. IR56M: cover-email greeting only, same rule; the form stays exempt.

(Owner: 4 ⚠️ RUNTIME-VERIFY items are now stacked below across P1b/P2/P3/P4 β€” all exercise the signed-document path, unverifiable from either agent's sandbox. They need a deploy or local run; flagging for the deploy decision.)

2026-07-04 β€” Phase 4 (shipped): WOPC prepare-modal read-only "prints as" preview

Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD - Intent delivered: the preparer drops out of the document-name decision β€” the WOPC prepare takeover (wopcInline.tsx) now shows a READ-ONLY preview of how the closing name will actually print (the director's stored signsAs β†’ registry closingName), instead of only the registry default. The signer still makes the authoritative choice at sign time (Phase 2). - Shipped: - New GET /api/wopc-signing/closing-name-preview?payeeAbbreviation=&closingDirectorId= β†’ { resolvedName } via the same resolveWopcClosingNameServer (no request at prepare time β†’ resolves WHO + the director's stored default). Session-gated. - wopcInline.tsx: fetches the resolved name when the preview is open / the closing director changes; renders a "Prints as: " line under the closing-director picker (falls back to the registry label until the resolve returns), and feeds the resolved name to the live PaymentConfirmationComponent preview so it matches the closing block. - Note: wopcInline is the prepare/SAVE flow β€” it does NOT send the signing email (that's the later Records-tab send β†’ sendForSigning). This is why the P3 per-send greeting field needs a persistence home (see the P3 escalation), and why P4 here is name-preview only. - Full unfiltered tsc clean; wopc + naming suites 50/50. - ⚠️ OWNER RUNTIME-VERIFY: set a director's signsAs, open a WOPC prepare preview with that director as closing β†’ confirm "Prints as" and the mini-preview show the token rendering.

2026-07-04 β€” Phase 3 (shipped: greeting seam): email greeting honors the stored preference

Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD - Shipped (load-bearing seam): both signing-request email senders now resolve the greeting through the T-160 precedence instead of the hardcoded closingName-salutation split. - New lib/email/directorGreeting.server.ts β€” resolveDirectorGreeting(director, overrideTokens?): override tokens β†’ the director's stored emailGreeting (rendered against their Individual's parts, 'greeting' context) β†’ legacy "<salutation from closingName> <lastName>". ALWAYS non-empty (never "Dear ,"); byte-identical to today for any director with no stored greeting. - WopcSigningRequestEmail + IR56MSigningRequestEmail templates now take a single resolved greeting string (was directorSalutation+directorLastName); sendWopc…/sendIR56M… resolve it and pass it. Updated the two email-preview samples + the IR56M send-demo. - Both senders gained an optional greetingTokens arg (the preparer override), wired to the resolver β€” ready for the modal field once its locus is settled (below). - This fixes every send path at once β€” including the automated, preparer-less ones (wopcPipeline, create-pending, transactions/[id]) which the design says start at the stored-preference link. IR56M: only the cover-email greeting; the FORM stays statutory-exempt. - Full unfiltered tsc clean. - ⚠️ ESCALATION β†’ PM (User Management): where does the per-send greeting FIELD live, and does it persist? The precedence lists "prepare-modal token field if touched" as a link ABOVE the stored preference, i.e. a one-off override β€” but the WOPC send is triggered from ~5 call sites (modal + automated), and wopcInline.tsx prepares the request while sendForSigning (a later, sometimes automated step) sends it. So a per-send override needs a persistence home (on the signing request?) to reach send time. Two readings: (a) persist an override on the request (separate top link, as written); (b) the field edits the recipient's stored emailGreeting (sticky, like the P2 signsAs write β€” collapses the top two links). I did NOT improvise this (coordination rule). The seam is shipped and correct regardless of the answer; only the FIELD's wiring depends on it. Please pick (a) or (b). Meanwhile I've left greetingTokens plumbed so either is a thin follow-on.

2026-07-04 β€” Phase 2 (shipped): sign-time name picker with sticky write-back

Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD - Intent delivered: the assigned director chooses HOW their name prints, at sign time, from token combinations over their STORED name parts (never free text). Default = their saved signsAs. The choice is persisted on the request AND sticky-saved as their new default. - Shipped: - repo.server.ts β€” setSigningName(requestId, tokens) persists the chosen combination on the request (mirrors stampSigningNameResolved). - signingName.server.ts β€” applySignTimeSigningName({request, tokens, by, persistDefault}): validates the tokens against the director's parts (validateCombination('document') + renderCombination, THROWS on invalid β€” never a silent fall-back to a different name), persists on the request, and sticky-saves namePreferences.signsAs on the director's Individual via updateIndividualServer (preserving any emailGreeting), mirroring pages/api/profile/name-preferences.ts. persistDefault:false for admin upload-on-behalf so a back-office actor sets only THIS document's name, not the director's personal default. - sign.ts β€” accepts body.signingName (image paths only; an uploaded signed PDF bakes its own name). Applies it β†’ 400 on an invalid payload β†’ reflects it on the in-memory request so the Phase-1b resolve+stamp picks it up. - Preview/final parity (closed a Phase-1b gap): preview-signed.ts now runs the SAME resolver and forwards &signingName= on both render paths β€” so a director with a stored signsAs no longer sees the registry name in preview but their chosen name on the signed PDF. Preview resolves (never persists/stamps); the picker override drives a live re-render. - New GET /api/wopc-signing/[requestId]/signing-name-options β†’ standardCombinations + the currently-selected tokens + the registry default label (auth = assigned director / admin). - preview/client.tsx β€” a "Signing as" Select (image sigs, >1 rendering only). Untouched β†’ sends nothing β†’ server keeps the stored default (no sticky rewrite). Touched β†’ re-renders the preview with the chosen name and submits the same tokens, so preview == committed PDF. - Full unfiltered tsc clean; Phase-1 pure-resolver test still 6/6. - Invariants held: WHO-signs untouched; legacy/no-Individual directors see no picker and print byte-identical; snapshot still frozen at sign; IR56M exempt. - ⚠️ OWNER RUNTIME-VERIFY: on /wopc/sign/[id]/preview, pick a non-default rendering β†’ confirm the preview re-renders with it AND the submitted PDF prints it; re-sign and confirm the picker pre-selects the now-sticky choice.

2026-07-04 β€” Phase 1b (shipped): render seams wired, per PM steer

  • βœ… Read AGENTS.md. Built under User Management's approved architecture (one resolver, prefer-resolved-ELSE-registry, stamp at sign time). Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Shipped:
  • lib/wopc/signingName.server.ts β€” resolveWopcClosingNameServer(...): WHO (assigned director / cross-issuance) β†’ getIndividualServer(director.id) parts + signsAs β†’ the pure resolveWopcClosingName. Null only when there's no director β†’ caller keeps its default.
  • repo.server.ts β€” stampSigningNameResolved(requestId, name) (mirrors the pending- signature stash write path).
  • Signed doc: sign.ts resolves + stamps signingNameResolved BEFORE the render and forwards the resolved STRING as &signingName= on BOTH render paths (overlay + template); /api/records/wopcs/pdf passes it to the preview page; client.tsx prefers it over resolveClosingDirector().closingName β€” no token math / name-parts in the browser (per PM).
  • Email + transaction PDF: renderWopcPdfBuffer calls the same resolver inline + prefers it.
  • Full unfiltered tsc clean; the Phase-1 pure-resolver test still 6/6.
  • Invariants held: prefer-resolved-ELSE-registry (legacy WOPCs print byte-identical, no migration); snapshot frozen at sign; WHO-signs untouched; IR56M exempt.
  • ⚠️ OWNER RUNTIME-VERIFY (PM's ask β€” I can't from the sandbox): set a non-registry signsAs on a director, sign a WOPC, confirm the token name prints on the signed PDF. The pre-sign live in-browser preview intentionally still shows the registry closingName.

2026-07-04 β€” Phase 1 (foundation): request model + WOPC document-name resolver

  • βœ… Read AGENTS.md. Picked up T-160 (assigned to me); verified T-161's lib/naming/ foundation is on main (48ac01a4) and read its real API (renderCombination β†’ string|null, validateCombination, standardCombinations, adapters).
  • Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Shipped this phase (branch claude/t160-signing-name-tokens, own PR β€” GCP-fix PR #854 kept separate per the owner):
  • WOPCSigningRequest gains signingName: NameToken[] | null (the chosen combination)
    • signingNameResolved: string | null (the sign-time snapshot). Wired the repo deserializer + the insert defaults (both null at create).
  • New pure resolver lib/wopc/signingName.ts β€” resolveWopcClosingName(...) implementing the owner precedence: snapshot β†’ request tokens β†’ stored signsAs β†’ registry closingName. Never blanks (unrenderable combo falls through). 6/6 unit tests (__tests__/lib/wopc/signingName.test.ts); full tsc --noEmit clean.
  • signingName/signingNameResolved govern how the name prints, never who signs (cross-issuance resolveClosingDirector untouched β€” guardrail).
  • Phase plan (remaining): 1b wire lib/wopc/pdfRenderer.server.ts callers to the resolver (caller fetches the director's Individual by director.id, resolves parts + signsAs, passes the resolved name); 2 sign-time sticky token picker on /wopc/sign/[requestId]; 3 email-greeting precedence + prepare-modal greeting field (wopcInline.tsx, IR56M assign); 4 WOPC prepare read-only name preview. IR56M form stays statutory-exempt throughout.
  • To confirm at 1b (for the PM / User Management): that a director's registry.id is the doc id for getIndividualServer(id) (T-048 seed link) β€” CONFIRMED: seedDirectors.server.ts seeds Individual.id = d.id, so getIndividualServer(director.id) β†’ the director's Individual (parts + namePreferences).

2026-07-04 β€” DISCOVERY (escalation): the signed-WOPC name seam is the CLIENT preview page, not pdfRenderer.server.ts

Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD

Tracing the render path before wiring Phase 1b, I found the task's named seam is incomplete for the load-bearing case: - The SIGNED WOPC is produced by pages/api/wopc-signing/[requestId]/sign.ts β†’ fetches /api/records/wopcs/pdf?ref=…&signingRequestId=…, which puppeteer-screenshots the CLIENT preview page app/accounting/payment-confirmation/[transactionId]/preview/client.tsx. That page sets directorName from resolveClosingDirector(payee, closingDirectorId).closingName (~L391–396). So the name that prints on the signed legal document is decided client-side, NOT in renderWopcPdfBuffer. - renderWopcPdfBuffer (lib/wopc/pdfRenderer.server.ts) β€” the seam T-160 Phase 1b named β€” is the render path for the payment-confirmation email (wopc/send-payment-email.ts), the transaction PDF, and a preview-PDF. It also sources the name from resolveClosingDirector().closingName. - So there are two name seams, both currently keyed off the registry closingName; the authoritative one for the signed document is the client page, which can't do the async token resolution (fetch Individual + signing request) mid-render.

Recommended architecture (single server-side resolution point β€” for PM sign-off): resolve the T-160 token closing name server-side at the WOPC/confirmation data source (or in /api/records/wopcs/pdf when signingRequestId is present) and pass it INTO the render β€” the client page and renderWopcPdfBuffer both simply prefer that resolved string over their resolveClosingDirector().closingName override. This keeps one resolution site (using the pure resolveWopcClosingName from Phase 1 + getIndividualServer), avoids a client-side token API, and keeps the signed PDF and the email consistent. signingNameResolved (Phase-1 model field) is stamped at sign time so re-renders of a signed WOPC use the snapshot. IR56M unaffected.

Why escalate (not improvise): it's a signed legal document's signatory name across two seams, un-runtime-verifiable from this sandbox (auth-gated), and the task named a single seam that turns out not to be the signed-document path β€” a design coherence call the PM (User Management) owns. Holding the render-seam wiring for a one-line steer on the resolution point; the Phase-1 model + pure resolver + tests are already landed and safe.

2026-07-04 β€” PM REPLY (User Management) β€” resolution point APPROVED, Phase 1b green-lit

Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW

Independently verified your discovery β€” correct on all counts. Traced it live on main: sign.ts:239–249 fetches the pdf endpoint with signingRequestId; pdf.ts:47–63,184 puppeteer- navigates to the preview page; client.tsx:391–396 sets directorName from resolveClosingDirector().closingName. Also confirmed your load-bearing link: seedDirectors.server.ts:61 seeds Individual.id = d.id, so getIndividualServer(director.id) is sound. Good catch, and right to escalate β€” the signed doc is a different (client) seam than the contract named, and that's a coherence call, not an improvisation.

Decision β€” resolve server-side, deliver as a pre-resolved STRING, client never does token math. No client-side token/Individual API. Concretely:

  1. Single resolver = your resolveWopcClosingName (Phase 1). Same precedence everywhere: snapshot β†’ request tokens β†’ stored signsAs β†’ registry closingName. Two seams is fine β€” they don't need one physical call site, they need one resolver + the same prefer/fallback rule at each.

  2. Signed document (authoritative) β€” wire via a stamped string, not client resolution:

  3. In sign.ts, before the pdf fetch, resolve the closing name (resolveWopcClosingName + getIndividualServer(assignedDirectorId)) and stamp signingNameResolved on the request (field already added; use the same request-doc write path as the pending-signature stash).
  4. The preview page, when signingRequestId is present, reads signingNameResolved (piggyback the /api/wopc-signing/{id}/signature fetch it already does, or a sibling field) and prefers it over resolveClosingDirector().closingName. That's the only change to client.tsx β€” a preference, zero token math, no name-parts exposed to the client.
  5. Make the Phase-2 sign-time picker default to the director's stored signsAs and ALWAYS stamp on sign β€” so even an untouched picker bakes the correct name and the preview page only ever needs to read one field.

  6. renderWopcPdfBuffer (email + transaction PDF) is already server-side β†’ call the same resolver inline (fetch Individual, resolve), prefer resolved else registry.

  7. Pre-sign live in-browser preview (non-puppeteer, no snapshot): fine to keep showing resolveClosingDirector().closingName today β€” the sign-time picker is the authoritative choice moment. Do NOT block 1b on this. (Optional later: have the data endpoint feeding the preview resolve the stored signsAs so the pre-sign preview matches the picker default.)

Invariants (non-negotiable): - Prefer-resolved-ELSE-registry at every seam β†’ every legacy/unconfigured WOPC prints byte-identical to today. Never blank. This is the safety property that lets 1b ship without a data migration. - signingNameResolved frozen at sign; re-renders of a signed WOPC read the snapshot (immutability guardrail). The snapshot is also what removes the two-seam drift risk β€” once stamped, every seam reads the same string. - Who-signs untouched (resolveClosingDirector forced-opposite stays server-enforced); tokens change only how the name prints. - IR56M statutory-exempt throughout.

No owner decision needed β€” this is an implementation-locus clarification, fully inside D1–D4 (the resolved string still honors the document precedence chain). I've corrected the "Records (Infrastructure) β€” T-160 proper" seam bullet above so the naming is right for later readers. Green light: proceed with Phase 1b under this architecture; Phases 2–4 as you planned. One ask back: at 1b, please runtime-verify (you can, I can't from here) that the signed-render preview actually reads the stamped field β€” screenshot a signed WOPC with a non-registry signsAs set and confirm the token name prints. Ping via this section or your owner session if anything here fights the code.

Guardrails

  • Issued documents are immutable β€” no re-render of already-signed PDFs.
  • The IR56M title-strip and the greeting/lastName derivations must keep working for both token and fallback paths.
  • Director-payee cross-issuance forcing stays server-enforced.

Design discussion β€” who chooses the rendering? (owner ↔ User Management, 2026-07-04)

Recorded for the agent taking this task over. The owner is handing T-160 to a different agent now that the contacts page holds no power over how documents/emails render a director's name; this section preserves the rationale behind the decisions so far.

The owner's two questions:

  1. Is it a good idea to have a field β€” or an actual prompt window β€” making the user who prepares the signing-request email choose how the director is addressed in its salutation (manually inputted, or composing their own order of salute / first name / last name / preferred name)?
  2. Should the signing director be able to choose how he/she is addressed on the document they are going to sign?

Recommendation R1 β€” email greeting: a pre-filled token field, never a blocking prompt, never free text.

  • A forced modal on every send is friction disproportionate to a courtesy string (two directors, routine sends), and forcing a fresh choice each time produces inconsistency ("Dear Jake" one week, "Dear Mr. Ngai" the next) rather than preventing it.
  • Free text invites typos into a colleague's inbox and breaks this task's founding principle: renderings are derived from directory name fields, never typed.
  • A free-order composer (arrange salute/first/last/preferred arbitrarily) is over-general β€” the real greeting space is tiny. Enumerate it as token chips (Mr. <Last> β†’ "Mr. Chan", <Preferred First> β†’ "Jake", Mr. <First> <Last> β†’ "Mr. Jeffero Chan"). A builder can output "Chan Mr."; a chip list cannot.
  • Deeper principle: how someone is addressed is the recipient's preference, not the sender's per-send decision. Default from a stored per-director greeting preference used by ALL outgoing email to that director; the prepare modal shows it as a pre-filled token field the preparer may adjust β€” a field, not a gate.

Recommendation R2 β€” document rendering: signer-owned, durable, with a statutory carve-out.

  • The name on a signed document belongs to the signer. Precedent already in the data: Jake's surname-first "Mr. Ngai Wang Chi" IS a personal rendering preference β€” currently frozen in code where nobody can own it. A director-owned stored preference is its proper home.
  • IR56M is exempt. IRD filing β†’ the signatory name is a compliance fact: legal name, existing title-strip stays. Preferences apply to WOPC (and correspondence), never statutory forms.
  • Durable, not per-signing. Per-document variance makes the issued series inconsistent ("0601 says 'Jake Ngai', 0602 'Mr. Ngai Wang Chi'"). Mechanic: the director's stored "signs as" preference is the default; at sign time (they are already looking at the render) they may adjust among the tokens, and the adjustment saves back as their new default (sticky). The signature itself approves the rendering β€” no extra confirmation loop.
  • The preparer drops out of the document-name decision. The prepare modal shows a read-only preview of how the name will print; the email-greeting field is the only thing the preparer can touch. This supersedes the first-sketch preparer-side picker and eliminates any preparer-vs-director precedence conflict.

Token grammar & validation (owner rule, 2026-07-04):

  • A rendering is an ordered combination of stored name parts β€” title? plus firstName / lastName / preferredFirstName / preferredLastName (incl. the "Last, First" comma form) β€” resolved at render time from the CURRENT directory fields. The stored thing is always the combination, never a literal string.
  • Hard rule: a title requires the last name. "Mr. Chan" βœ“ Β· "Mr. Jeffero Chan" βœ“ Β· "Mr. Jeffero" βœ— β€” owner: "That would be highly inappropriate." Enforce at configuration time (invalid combinations simply not offered/selectable), not as a render-time surprise. Applies to greetings and closing blocks alike; IR56M is unaffected (never carries a title).
  • Sufficiency check: Jake's historic surname-first "Mr. Ngai Wang Chi" = title + last + first β€” representable in the grammar, so migrating off the literal closingName string loses nothing.

Resulting precedence chains:

  • Document (WOPC): sign-time token choice (persisted as new preference) β†’ director's stored "signs as" combination β†’ registry closingName (legacy fallback).
  • Email greeting: prepare-modal token field if touched β†’ recipient's stored greeting preference β†’ Tools-page template default (D4) β†’ today's derivation from the registry (legacy fallback). Automated, preparer-less emails start at the second link.
  • IR56M: legal name with title-strip, always. No preference consulted.

Where preferences live: on the person's Individual record β€” fits the registry-consolidation stretch goal. Edit surfaces per D2: the person self-serves via a profile/settings surface (directors additionally at sign time, D3), and authorized editors may configure the combination from the contact drawer's resurrected closing-name field. In every surface the stored value is the token combination β€” free-text name editing stays retired. (This supersedes the earlier "never through the contacts page" phrasing: the ban was on the literal string, not on token-based configuration.) The signing request snapshots the resolved string at sign time (issued documents stay immutable). The T-048 drift guard compares identity fields only, so preference fields don't trip it.

Decisions (owner, 2026-07-04):

  • D1 β€” RESOLVED: agreed. Email greeting is a pre-filled token field β€” no blocking modal, no free-text escape hatch, no free-order composer. Tokens only.
  • D2 β€” RESOLVED: profile settings surface, + token-based resurrection of the contacts field. People manage their own preferences on a profile/settings surface. The owner also doesn't mind bringing the closing-name field back to the contact drawer β€” storing the configuration of the combination of stored first/last/preferred names, never the actual string. See the grammar above.
  • D3 β€” RESOLVED: sticky, token-based sign-time adjustment. Confirmed meaning: at sign time the director switches among token renderings only (never types), and the choice saves back as their new stored default so the issued series stays consistent. The titleβ‡’last-name rule constrains every offered option.
  • D4 β€” RESOLVED: no preparer override on document names; preparers DO choose email greetings. The per-send greeting token field stays on prepared emails (signing requests and other notification/request-type emails). In addition, users with enough clearance configure email addressing defaults on the Tools page β€” extending the System Email Routing tool (system/emailPolicy, components/tools/SystemEmailRoutingTool.tsx) toward recipient + email-template/greeting configuration. That surface also covers automated emails that have no preparer (data-integrity alerts, invoice-overdue notices, …).
  • Implementation note (User Management as PM, 2026-07-04, shipped in T-161 P4): for automated emails the Tools-page setting is the on-switch β€” their legacy behaviour is an impersonal batch send with no greeting, so a recipient's stored preference alone does not make a category start greeting; the category must have a greeting default configured first. Once on, the send becomes per-recipient, each recipient's own preference outranks the default, and an unrenderable name degrades to no greeting line. Prepared emails (Records' half) always greet, so they start at the recipient-preference link as chained above.

Log

  • 2026-07-09 owner granted a one-off production deploy (executed by User Management via the deploy hook) β€” the four stacked ⚠️ RUNTIME-VERIFY items in the field notes are now checkable against production.

  • 2026-07-04 created from the owner's direction after the signing-pipeline report; no code yet. The contact drawer's closing-name field shipped read-only the same day (see T-048 drawer redesign commit) so nothing new gets written to the retired path in the meantime.

  • 2026-07-04 owner handed future execution to another agent; design discussion (email-salutation field vs prompt; director-chosen document rendering) recorded above with recommendations R1/R2 and open decisions D1–D4.
  • 2026-07-04 owner resolved D1–D4 (token field, no free text Β· profile settings surface + token-combination resurrection of the contacts closing-name field Β· sticky token-only sign-time adjustment Β· preparer greeting choice on prepared emails + clearance-gated Tools-page recipient/template configuration), and set the titleβ‡’last-name validation rule. Design is settled; ready for the successor agent to plan and execute against.
  • 2026-07-04 Records (Infrastructure) landed T-160 Phase 1 (request-model fields + pure resolveWopcClosingName + tests, on branch claude/t160-signing-name-tokens) and escalated a render-seam discovery: the signed WOPC's name is decided in the client preview page, not renderWopcPdfBuffer. User Management (PM) independently verified the finding on main, approved the single-server-side-resolution-point architecture (stamp signingNameResolved at sign time; every seam prefers it, else registry), corrected the seam naming in the contract, and green-lit Phase 1b. Full exchange in "Field notes from execution". No D1–D4 change.