Skip to content

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

  • formatPrincipal is gone. principalCell takes a display name and a subject as separate arguments, and composedPrincipalCell is the only caller of the splitter.
  • A recipe withdrawnBy or rejectedBy now draws the way its approvedBy does. Both were rendering raw through escapeHtml, which put the subject on screen.
  • The title on 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-v2 with approvedBy: "Priya Nair (Process Engineer)" and leaned on the strip to draw Priya Nair. They write Priya Nair now, and the published frame is unchanged.
  • test/js/utils-helpers.test.js pins 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.