sdc-sdapp-w3-pdf-parity-print-options
W3 — PDF Parity Diff + Print Options
Section titled “W3 — PDF Parity Diff + Print Options”Roadmap: apps/sd-app/ROADMAP.md → W3 · ADRs: docs/adr/0003-pdf-package-carve-out.md, docs/adr/0004-print-options.md
Parent planning thread: sdc-sdapp-full-levpro-port · Related: sdc-sdapp-pdf-reports
Background
Section titled “Background”packages/sd-api/reports.py generates PDFs for all three strategies in EN + FR and passes 18 tests —
but those tests check the code against its own expectations, never against LevPro’s real output.
Sample Printout.pdf (a genuine 2004 advisor-facing Interest-Only summary) has been sitting in the
archive unused.
Separately, LevPro’s print options aren’t exposed at all. frmPrintOptions.frm offered four
checkboxes plus a “Prepared for” field; sd-app’s output shape is fixed, and prepared_for exists in
reports.py wired to no UI field, so it can only ever render as an em-dash.
Scope — two steps, in order
Section titled “Scope — two steps, in order”Step 1 — Parity diff + Print Options
Section titled “Step 1 — Parity diff + Print Options”- Parity target:
D:\FSS\Software\Archive\Leverage Pro\Sample Printout.pdf— extracts cleanly viamarkitdown(verified 2026-08-31). Contains the assumptions list, a 5-row return table (0/3/5/7/10% → No Leverage / Leverage / $ Increase / % Increase), the notes block, the NOT-GUARANTEED disclaimer, and a client/spouse/advisor/manager signature grid. - Layout specs:
Developer Documentation\Summary Page Layout.doc,Summary Page Layout, Interest Only.doc,Projection Page Layout.doc,Summary Page Handout with Signatures.doc. These are Word 97 binaries —markitdownfails on them. Extract with a latin-1 printable-run scrape:re.findall(r'[\x20-\x7e]{6,}', open(f,'rb').read().decode('latin-1'))— verified working. - Acceptance gate (domain facts, per
AGENTS.md— not “it renders”): generate the equivalent report from sd-api, extract both PDFs withpdfplumber, compare field by field — every assumption line, every number in the 5-row table, every note, and the signature block present, exact count. A render with no errors is not proof of correctness. - Decision D4 (Talbot): replicate LevPro’s print options verbatim — all four checkboxes
(One-Page Summary, Projection Table, signature space, legal disclaimer) plus “Prepared for”, all
visible. F.A.S.T. simplification is deferred to
UPGRADES.md, not dropped. See ADR-0004. - Each of the four flags must demonstrably change the generated PDF’s extracted content.
Step 2 — Carve out packages/pdf-reports/
Section titled “Step 2 — Carve out packages/pdf-reports/”Only after Step 1 is green (decision D3, Talbot: “after, simpler”). Mechanical move of
reports.py + tests into a standalone package; sd-api imports it. No behaviour change during the
move — anything needed belongs in Step 1. See ADR-0003.
Constraints
Section titled “Constraints”- Do not write
packages/i18n/messages/*.jsondirectly while parallel workstreams run — return EN + FR key/value pairs for the orchestrator to merge. - Charts embedded in PDFs are a later upgrade (LevPro’s Print dialog had no chart option at all). Out of scope here; the new package is its natural home.
- Extract
Sample Printout.pdfand the four layout.docs; write the expected-fields list. - Field-by-field pdfplumber diff of generated vs. reference; fix what it surfaces.
- Print Options UI mirroring
frmPrintOptions.frmone-to-one; wireprepared_for. - Test per flag proving it changes extracted PDF content.
- Step 2 carve-out once green.
- Full green bar.
Outcome — 2026-08-31 (Step 1 done; Step 2 open)
Section titled “Outcome — 2026-08-31 (Step 1 done; Step 2 open)”Shipped (monorepo 6dc9549, aadfa9a). The parity diff was the point, and it paid off: all five table rows matched Sample Printout.pdf exactly, and it found four real defects the existing 18 tests could not — those tests check the code against its own expectations:
- Breakeven row rendered
$ Increase/% Increaseas an em-dash, suppressing legitimate0/0%values LevPro prints. - The entire 7-bullet Assumptions block was missing.
- Three Notes lines missing.
- No signature block.
All fixed, with tests asserting the reference PDF’s own figures. Print-options endpoint exposes all four frmPrintOptions.frm flags plus prepared_for.
Discrepancy for Talbot: frmPrintOptions.frm implements signatures / legal disclaimer / conservative-leverage-checklist as a mutually-exclusive radio group, not the four independent checkboxes ADR-0004 assumed. Built per Talbot’s explicit decision (independent, all visible), which also matches Sample Printout.pdf — it shows the disclaimer and signature grid together, which a strict radio group cannot produce. So the decision looks right and its stated premise was wrong.
Remaining: the Print Options Svelte panel (backend ready; w3_* i18n keys in place, currently unused) and Step 2 — the packages/pdf-reports/ carve-out, now unblocked since its gate is green.
Outcome — 2026-09-01 (Step 2 + panel; workstream complete)
Section titled “Outcome — 2026-09-01 (Step 2 + panel; workstream complete)”Shipped (monorepo ceac92b).
Print Options panel — PrintOptionsPanel.svelte, all four frmPrintOptions.frm checkboxes visible per ADR-0004 (not simplified), plus the “Prepared for” field that had never had a UI. Wired into int-only and term-loan, placed inside the projection-results block only since the endpoint rejects analysis_type: 'historical'. Company name deliberately absent — it resolves server-side from the Account setting (ADR-0005), so no competing per-report input. w3_company_name_label left unused.
packages/pdf-reports/ carved out per ADR-0003, now that the parity gate is green. reports.py moved byte-for-byte (verified against the git blob, and git recorded it as a rename); sd-api imports it; no call sites touched.
Judgment call worth recording: tests/test_reports.py was not a pure unit-test file — 15 of 21 tests drove main.py’s endpoints via TestClient. Moving it wholesale would have made the library’s own suite import its consumer. Split instead: the 5 tests calling render_* directly moved to the new package; the 16 testing main.py/report_routes.py wiring stayed. 110 + 5 = 115 — zero net loss, verified independently.
Caught at reconciliation, not by the agent: the pre-commit hook failed on 3 no-undef eslint errors (Blob, URL). Root cause was the hand-maintained Svelte globals list in eslint.config.mjs, not the code — both are legitimate browser globals needed by any component that hands the user a generated file. Added, with a comment so the next occurrence is diagnosed rather than worked around. This is why the orchestrator runs the commit: the agent’s green bar (pytest/check/build) did not include lint.
Green: 260 sd-math, 110 sd-api, 5 pdf-reports, 41 vitest, pnpm check 0 errors, build clean, lint clean, i18n 231/231.