refactor(reports): remove TFunction from reportPdf/* (ADR-034 enforcement) - #2007
Conversation
…dd new label fields and AC7 regression blocks
- coverLetterPdf.test.ts: removed TFunction import/mock; added coverLetterReferenceLabel/
coverLetterSubjectLabel/skipReasonLabels to fixture; updated 15 buildCoverLetterContent()
call sites (t param removed); updated reference/subject assertions to use label values
('Reference: REF-42', 'Subject: My Subject'); added AC7 sentinel describe block
- overviewPdf.test.ts: removed TFunction import/mock; added 3 new label fields to makeLabels()
with key-echo values (preserves all existing footnote assertions unchanged); updated ~55
buildOverviewContent() call sites; added AC7 sentinel describe block for skipReasonLabels
- merge.test.ts: removed TFunction import/mock; added 3 new label fields to makeContent();
removed t from ~18 generateReportPdf() call sites; updated mockBuildCoverLetterContent/
mockBuildOverviewContent assertions; updated toHaveLength(3)→(2) for 2-arg shape; updated
test description and doc-comment signatures
All 137 tests pass (17 + 99 + 21). Fixes #2001
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…ts (#2001) The production file (buildReportContent.ts) was updated to populate labels.coverLetterReferenceLabel, labels.coverLetterSubjectLabel, and labels.skipReasonLabels.{footnoteFetchFailed,footnoteInvalidPdf} via reportT() — but the test file had no assertions for these new fields. Add describe('buildReportContent — labels: 3 new fields (#2001)') with 4 tests using the existing identity-TFunction pattern (t = key => key) to verify each field resolves the correct i18n key. 68 tests pass (was 64). Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
`realRender.test.ts`: removed stale `t`/`tEn`/`tDe` last arg from all `buildOverviewContent`, `buildCoverLetterContent`, and `generateReportPdf` call sites; kept `renderOverviewPdfContent` (test-local helper) args intact; added 3 new `ReportContentLabels` fields to inline labels fixture. `ReportContentEditor.test.tsx`, `applyOverrides.test.ts`, `applyAiContent.test.ts`: added 3 new required `ReportContentLabels` fields (`coverLetterReferenceLabel`, `coverLetterSubjectLabel`, `skipReasonLabels`) to each file's fixture/`makeLabels()` helper. All 218 tests across the 4 files pass. Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…call One remaining stale argument in `realRender.test.ts`: the call `buildOverviewContent(contentDe, new Map(), reportTDe)` in the production i18n singleton describe block. `buildOverviewContent` now takes 2 arguments. Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
|
[ux-designer] Design Review — PR #2007 (refactor: remove TFunction from reportPdf/*)Verdict: APPROVED — no design system concerns Scope confirmedThis PR is a pure backend/library refactor with no visual surface area changes:
Design system checklist
Visual deltaZero. The PDF output reads the same translated strings it always did — the refactor only moves where they are resolved (from deep inside |
|
[product-architect] Architecture review of PR #2007 (issue #2001) — CHANGES REQUESTED The refactor itself is the right shape and the primary deliverable lands cleanly. Two blocking findings, both narrow. Verified
H1 (blocking) — the type assertion at
|
|
[product-owner] Requirements Review — PR #2007 (#2001: remove
|
| AC | Result | Evidence |
|---|---|---|
1. Three label fields added, populated via reportT in the budget namespace |
PASS | types.ts:67-72 adds coverLetterReferenceLabel, coverLetterSubjectLabel, skipReasonLabels{footnoteFetchFailed,footnoteInvalidPdf}; buildReportContent.ts:300-305 populates all four via reportT, which is getFixedT(reportLanguage, 'budget') (ReportWizardPage.tsx:254) — namespace matches |
2. Dynamic skip-footnote key resolved where reportT is available; every producible reason resolves in en + de |
PASS | SkippedDocument['reason'] (reportPdf/types.ts:17) is a closed union of exactly footnoteFetchFailed / footnoteInvalidPdf; both producers (merge.ts:62,82,91,182) emit only those two; both are keys of skipReasonLabels; both resolve in en (Document could not be retrieved / Document is not a valid PDF) and de (Dokument konnte nicht abgerufen werden / Dokument ist keine gültige PDF-Datei). No key echo possible today |
3. buildCoverLetterContent / buildOverviewContent drop the TFunction param |
PASS | coverLetterPdf.ts:8, overviewPdf.ts:491-493 |
4. generateReportPdf drops the TFunction param; ReportWizardPage updated |
PASS | merge.ts:31-38; call site ReportWizardPage.tsx:313 plus reportT removed from the useCallback dep array |
5. grep TFunction client/src/lib/reportPdf/ --include=*.ts | grep -v test → no matches |
PASS | Enumerated all 9 production .ts files under reportPdf/ at df7d2c7: zero matches. Went one step further — none of the 9 references i18n, useTranslation, toLocale*, or Intl. either, so the module boundary is now genuinely locale-free, not just TFunction-free |
6. Every key exists in en/budget.json and de/budget.json |
PASS | All four keys pre-existed in both locales; no i18n files touched by this PR (correct — nothing new to translate) |
7. Regression test fails if a string is resolved through TFunction, verified to discriminate |
PASS — verified empirically, see below | |
8. npm run typecheck passes (sweep all ReportContentLabels construction sites) |
PASS | Static Analysis green. The #2000 trap the AC warned about did fire and was caught: commit 3b26a82c fixed the four missed construction sites (realRender.test.ts, ReportContentEditor.test.tsx, applyOverrides.test.ts, applyAiContent.test.ts) |
9. Quality Gates green |
PASS | Green, plus Static Analysis, all 6 Jest shards, and all 16 E2E shards green (mergeStateStatus: CLEAN) |
AC7 discrimination — verified by mutation, not by reading
I did not take "the tests pass" as evidence. I checked out df7d2c7 in a throwaway worktree, mutated each production line, and confirmed the sentinels go red — then reverted:
coverLetterPdf.ts:45,53— replaced${reportContent.labels.coverLetterReferenceLabel}/...SubjectLabelwith literals → both AC7 sentinels fail, plus 2 pre-existing prefix tests.overviewPdf.ts:840— replaced${reasonLabel}with${reason}(i.e. label lookup bypassed) → bothskipReasonLabelssentinels fail, plus 3 pre-existing footnote tests.buildReportContent.ts:300,303— swapped in the wrong i18n key (sourceReports.table.referencefor the reference label,footnoteInvalidPdfforfootnoteFetchFailed) → the 2 corresponding#2001label tests fail while the other 66 stay green, proving the AC1 tests pin the specific key and not merely "some string".
9 tests red under mutation, 116 green after revert. The sentinel fixture values (LABEL-REF-SENTINEL, FETCH-SENTINEL, …) are deliberately distinct from what an identity-t would echo, so none of these can pass vacuously.
Behavioural regression check (not an AC, but the one way this refactor could have broken something)
Moving these three strings from generate time (reportT passed into generateReportPdf) to build time (reportContent.labels) would produce stale labels if the report language could change without reportContent being rebuilt. It can't: baselineContent's useMemo lists reportT in its dependency array (ReportWizardPage.tsx:282-292) and reportT is memoized on reportLanguage, so switching the report language rebuilds every label. applyAiContent and applyOverrides both shallow-spread ...content, so labels survives the override pipeline untouched. No regression.
Finding — M1 (Medium, non-blocking): the new lookup re-opens the same hazard class in a new form
overviewPdf.ts:835-838:
const reasonLabel =
reportContent.labels.skipReasonLabels[
reason as 'footnoteFetchFailed' | 'footnoteInvalidPdf'
] ?? reason;The as assertion plus the ?? reason fallback is exactly the "enforced by the convention that everyone remembers" shape the story was written to eliminate — just moved from TFunction to the label lookup. If a third value is ever added to SkippedDocument['reason'] (reportPdf/types.ts:17), the compiler stays silent, skipReasonLabels silently lacks the field, and the raw identifier (footnoteWhateverNew) gets printed into a document handed to a bank. That is the same "found by the next reviewer" failure mode as #1938 and #1993, and AC2's "no key echoed into the PDF" would then be false.
I'm capping this at Medium and not blocking, because AC2 as written is about the values the code can produce today, and it holds — the union is closed and both members are mapped. But the story's purpose is to close the class, so it is worth closing properly.
Concrete suggestion (roughly two lines, both inside code this PR already touches): type the parameter as the union instead of string, and drop both the cast and the fallback —
overviewPdf.ts:493:skippedDocuments: Map<string, SkippedDocument['reason'][]>merge.ts:101:new Map<string, SkippedDocument['reason'][]>()(skip.reasonalready has that type, so nothing else changes)
Adding a reason then fails the build in types.ts/buildReportContent.ts until a label is added — which is what AC5 asks for in spirit. If you'd rather not widen the diff, say so and I'll file it as a follow-up issue instead.
Informational
- I1 — Scope is tight and appropriate: 3 production files, 1 page, 7 test files, 2 agent-memory files (rides along per CLAUDE.md). No i18n changes, no behavioural changes. Two incidental Prettier reformats got swept in (
ReportContentEditor.test.tsx:1611, and an italics normalisation inqa-integration-tester/MEMORY.md) — harmless, and since CI has noformat:check, formatting-on-touch is the right habit. - I2 — No UAT scenario for this story, per its own Notes: there is no user-visible defect to observe, and AC5 is statically verifiable. Every AC here is machine-checkable, so refactor(reports): remove TFunction from reportPdf/* so ADR-034 locale decoupling is compiler-enforced #2001 goes straight to Done on merge — it does not need to sit in UAT.
- I3 — All 16 E2E shards are green on this head commit, including shard 8/16. E2E shard 8/16 red across four beta PRs: dashboard "New Invoice" shortcut opens no modal (Scenario 13, #1735) #2005 (the dashboard "New Invoice" failure that was blocking the next
beta→mainpromotion) is now closed, and this run confirms the fix has landed on the branch's base. The promotion blocker is clear from this PR's perspective.
Records the PR #2007 review findings in product-architect memory: the widen-then-`as`-narrow union anti-pattern, the four predictable places a hardening PR falsifies its own ADR, and why key-echo fixtures are non-discriminating. Co-Authored-By: Claude product-architect <noreply@anthropic.com>
- Export `ReportSkipReason` as a named type from reportContent/types.ts; re-export from the barrel - Use `Record<ReportSkipReason, string>` for `skipReasonLabels` (was inline object type) - Wire `ReportSkipReason` through `SkippedDocument.reason`, `buildOverviewContent` param, and `merge.ts` internal map — single authoritative definition, no inline duplication - Drop the stale `as`-cast + `?? reason` fallback in overviewPdf.ts; direct index now types cleanly as `string` - Update test type annotations: `Map<string, ReportSkipReason[]>` in overviewPdf.test.ts and realRender.test.ts Fixes #2001 Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com> Co-Authored-By: Claude frontend-developer <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
|
[product-architect] Re-review of PR #2007 (round 2) — APPROVED Both blocking findings verified resolved against the pushed head H1 — closed, and closed properly
The producer→transport→label-table chain now shares one type, so adding a skip reason is a compile error at the single population site in
H2 — closedADR-034 updated in wiki commit Re-verified on the pushed head
Non-blocking follow-ups (do not hold the merge)
Good refactor — it closes a hazard class that had been fixed one call site at a time across three separate rounds (#1938, #1993, #2001), and it closes it by removing the channel rather than the symptom. Approved. |
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
|
🎉 This PR is included in version 2.14.0-beta.12 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 2.14.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
TFunctionparameter frombuildCoverLetterContent,buildOverviewContent, andgenerateReportPdf— these three functions inreportPdf/*now read all locale-resolved strings exclusively fromReportContentLabelscoverLetterReferenceLabel,coverLetterSubjectLabel, andskipReasonLabelsfields toReportContentLabels; populated inbuildReportContent.tsviareportTat the report-content layer where the locale is already resolvedcoverLetterPdf.test.ts,overviewPdf.test.ts, andbuildReportContent.test.tsdiscriminate: they fail if any string is sourced fromTFunctioninstead oflabels.*Fixes #2001
Test plan
grep -rn "TFunction" client/src/lib/reportPdf/ --include="*.ts" | grep -v testreturns no outputCo-Authored-By: Claude dev-team-lead noreply@anthropic.com
Co-Authored-By: Claude frontend-developer noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester noreply@anthropic.com
🤖 Generated with Claude Code