[orchestrator] Raised by product-architect reviewing PR #2028 (#1912). Filed rather than folded in, because the right fix is one small generic rather than N hand-written Record maps — see below.
Problem
#1912 item 1 replaced a template-literal i18n key interpolation in getAttachmentNote with an exhaustive ATTACHMENT_TYPE_KEYS: Record<AttachmentType, string> map, so a fourth AttachmentType member becomes a compile error rather than a raw i18n key printed onto a bank-facing PDF.
That fixed one of five such sites in client/src/lib/reportContent/buildReportContent.ts. The other four all interpolate a union into a reportT() key, and all four render into the same bank-facing PDF:
| Line |
Key |
Union |
Members |
| 143 |
sourceReports.table.title.${useCase} |
SourceReportType |
3 |
| 146 |
sourceReports.sourceType.${report.source.sourceType} |
BudgetSourceType |
5 |
| 204 |
sources.lines.invoiceStatus.${status} |
InvoiceStatus |
4 |
| 274 |
sourceReports.coverLetter.subject.${useCase} |
SourceReportType |
3 |
Line 204 is the sharpest: it launders its value through const status = invoice.status as InvoiceStatus at line 198 — the exact widening-plus-cast shape #1912 just removed from getAttachmentNote two functions above.
Outside that file, the same AttachmentType union is interpolated at client/src/components/documents/LinkedDocumentsSection.tsx:276, plus roughly a dozen status-union sites app-wide (screen-only, lower stakes).
Why this is latent rather than live
Every one of those key sets was verified complete against both en/budget.json and de/budget.json. Nothing is broken today. The defect arrives when someone adds a union member — which is precisely when nobody is thinking about i18n keys.
A residual gap the Record pattern does not close
Record<AttachmentType, string> enforces union ↔ map parity. It does not enforce map ↔ locale-JSON parity. A fourth member with a map entry but no budget.json key still prints a raw key onto the PDF. The map is also module-private, so no test can iterate it. Whatever generic comes out of this should address that — either by exporting the mapping for a parity test, or by deriving keys in a way the existing i18n.parity.test.ts can see.
Acceptance Criteria
Notes
[orchestrator] Raised by
product-architectreviewing PR #2028 (#1912). Filed rather than folded in, because the right fix is one small generic rather than N hand-writtenRecordmaps — see below.Problem
#1912 item 1 replaced a template-literal i18n key interpolation in
getAttachmentNotewith an exhaustiveATTACHMENT_TYPE_KEYS: Record<AttachmentType, string>map, so a fourthAttachmentTypemember becomes a compile error rather than a raw i18n key printed onto a bank-facing PDF.That fixed one of five such sites in
client/src/lib/reportContent/buildReportContent.ts. The other four all interpolate a union into areportT()key, and all four render into the same bank-facing PDF:sourceReports.table.title.${useCase}SourceReportTypesourceReports.sourceType.${report.source.sourceType}BudgetSourceTypesources.lines.invoiceStatus.${status}InvoiceStatussourceReports.coverLetter.subject.${useCase}SourceReportTypeLine 204 is the sharpest: it launders its value through
const status = invoice.status as InvoiceStatusat line 198 — the exact widening-plus-cast shape #1912 just removed fromgetAttachmentNotetwo functions above.Outside that file, the same
AttachmentTypeunion is interpolated atclient/src/components/documents/LinkedDocumentsSection.tsx:276, plus roughly a dozen status-union sites app-wide (screen-only, lower stakes).Why this is latent rather than live
Every one of those key sets was verified complete against both
en/budget.jsonandde/budget.json. Nothing is broken today. The defect arrives when someone adds a union member — which is precisely when nobody is thinking about i18n keys.A residual gap the
Recordpattern does not closeRecord<AttachmentType, string>enforces union ↔ map parity. It does not enforce map ↔ locale-JSON parity. A fourth member with a map entry but nobudget.jsonkey still prints a raw key onto the PDF. The map is also module-private, so no test can iterate it. Whatever generic comes out of this should address that — either by exporting the mapping for a parity test, or by deriving keys in a way the existingi18n.parity.test.tscan see.Acceptance Criteria
Recordmaps — makes "union member added without a corresponding i18n key" a compile-time or test-time failure.reportT()sites inbuildReportContent.ts(priority: these reach the bank-facing PDF).LinkedDocumentsSection.tsx:276.as InvoiceStatuscast atbuildReportContent.ts:198is removed, not preserved around the new mechanism — the cast is the same defect as the key interpolation, one layer down.Notes
frontend-developer, withproduct-architecton the mechanism design.