ADR 0089: A display name is rendered verbatim¶
Status: Accepted Date: 2026-09-08 Issue: #2090 Related: #2087 (the re-shoot that put two of the renderings in one frame), #2081 (the top bar's collapse ladder), #966 (the identity badge), #2093 (the same question asked of the actor's identifier, one layer down)
Context¶
A display name can carry a trailing parenthetical. The gateway did something different with it in each of the places it drew an identity.
| Surface | What it did with Priya Nair (Process Engineer) |
|---|---|
| ChangeRequest author and signature rows | Stripped it, and drew Priya Nair |
Recipe approvedBy and releasedBy |
Stripped it |
Recipe withdrawnBy and rejectedBy |
Kept it whole, subject and all |
| Top bar, collapsed to initials | Stripped it |
| Top bar, full | Kept it, behind a 260px cap that then ellipsised it |
The #2087 re-shoot of system-changerequest-diff-fermentation put two of those
in one frame. The top bar read Samuel Okafor (Production Supervisor) · Sup…
while the e-signature two blocks below it read Priya Nair against a stored
displayName of Priya Nair (Process Engineer). The only surface that kept a
parenthetical was the one surface with a hard width cap in front of it, so the
string that survived was also the string that got cut.
The stripper was formatPrincipal in internal/gateway/static/js/utils.js. Its
comment named the case it was written for, an IdP display name carrying its own
subject in the form Alice Smith (google-oauth2|1234). For that input, dropping
the parenthetical is plainly right. The regex was never that narrow. It took any
trailing parenthetical, so Sam Okafor (Contractor) lost its qualifier too.
This product's own capture directory shows what that costs.
hack/docs-shots/openldap.yaml ships two SCADA integration accounts,
FUXA SCADA (control) and FUXA SCADA (read-only). They are a pair on purpose,
so that one clip can show both outcomes of the same write. The parenthetical is
the whole of what distinguishes them, and the stripper collapsed both to
FUXA SCADA.
That matters most where the drawn name is a 21 CFR Part 11 signature
manifestation. Section 11.50(a)(1) wants the printed name of the signer. A
heuristic cannot tell an opaque subject from a job title, so a name it chooses is
a name the record does not hold. The raw value did survive, in a title
attribute. A screenshot loses it, a print loses it, and an exported batch
production record loses it.
Decision¶
A display name is drawn verbatim. The only identity string the UI takes apart is one this product composed itself.
The gateway carries an identity as two fields nearly everywhere it carries one at
all, a display name and a subject. ChangeRequestSigDTO has signerDisplayName
beside signer. An ElectronicSignature has SignerName beside SignerID. An
AuditRecord actor has userName beside userID. Where both are present there
is nothing for a heuristic to recover, so principalCell(displayName, subject)
draws the name and puts the subject in the title beside it. The audit trail, the
record print and the historian's personnel block already rendered that way. The
ChangeRequest surfaces do now too.
The exception is real, and it is one composition.
internal/gateway/recipe_approval.go writes
fmt.Sprintf("%s (%s)", user.DisplayName(), user.Subject) into four
MasterRecipe status fields, approvedBy, releasedBy, rejectedBy and
withdrawnBy, because those have always been single strings on a CRD. The last
parenthetical in one of those values is a subject this product put there, so
splitComposedPrincipal takes it back off. That is the inverse of a known
Sprintf. It guesses at nothing. A display name carrying a parenthetical of its
own survives it, because the match is anchored at the end.
userInitials in auth.js goes on dropping every parenthetical, and it is not
performing the same act. It builds initials, because "PNPE" names nobody, and
what it builds is never offered as the identity. The full label sits on the
badge's title at every width, and the name drawn beside the initials is verbatim.
Alternatives Considered¶
Strip everywhere, including the top bar. This was the other credible answer, and it is the one that makes every surface agree most cheaply. It fails on the FUXA pair. Two accounts the deployment created as distinct identities would render as one name on every surface that shows a name. It also asks the gateway to decide that text a person put in a directory record is noise, on the surfaces where the printed name is the regulated artifact.
Split server-side into new DTO fields. The gateway is the composer, so it
could undo its own Sprintf once and serve approvedByDisplayName beside
approvedBySubject. That is the same operation one layer down. The stored value
is what an inspector reads, so the split has to stay reversible wherever it
happens. Four fields are affected and all four are already served, so a new pair
of DTO fields per field buys nothing the named helper does not.
Give the top bar a rendering that fits. topbar-fit.js already collapses the
identity name to initials as its first step, but only when the whole row
overflows. A label longer than the 260px cap ellipsises while the row still fits,
so a long name is cut without the ladder ever running. Whether an ellipsis or
initials is the better answer there is a UI question with 281 committed captures
behind it, and this ADR does not settle it. What is settled here is which string
the top bar draws. How wide it is allowed to be stays open.
Consequences¶
formatPrincipalis gone.principalCelltakes a display name and a subject as separate arguments, andcomposedPrincipalCellis the only caller of the splitter.- A recipe
withdrawnByorrejectedBynow draws the way itsapprovedBydoes. Both were rendering raw throughescapeHtml, which put the subject on screen. - The
titleon a ChangeRequest author or signer cell now carries the name and the subject together, which is what the audit trail has always shown. - A seed that wants a bare name on a capture writes a bare name. The docs-shots
and clip-recording seeds promoted
bds-v2withapprovedBy: "Priya Nair (Process Engineer)"and leaned on the strip to drawPriya Nair. They writePriya Nairnow, and the published frame is unchanged. test/js/utils-helpers.test.jspins both halves, including the case the splitter must leave alone. A display name with its own parenthetical, composed with a subject, keeps the parenthetical and loses the subject.