# Technical Debt Log Append-only log of known technical debt. Maintained by `.claude/agents/qa-tech-debt.md`. Do not delete paid-down entries — mark them resolved instead. | Date | Item | Type (unintentional/unavoidable/deliberate) | Impact ("interest rate") | Status | Resolution | |---|---|---|---|---|---| | 2026-09-08 | `EnvelopeRenderer.Cli` exits `64` ("render not implemented") for any argument-valid run, since the Debenu render engine isn't wired in yet. Self-documenting and expected to disappear once "Render text-only PDFs through Debenu Quick PDF" (Sprint 1 Batch 2) lands and exit `0` becomes reachable. | Deliberate | Low | Paid Down | `RenderNotImplemented` branch removed from `Program.cs`; exit `0` is now reachable and exit `1` covers all template/CSV/render failures (see `code/CLI_CONTRACT.md`). | | 2026-09-08 | `DebenuPdfRenderer` always embeds TrueType fonts fully (`AddTrueTypeFont(..., Embed: 1)`), cached once per unique font name per document. Fine at today's scale (one sample render: ~1.3 MB for 392 pages, one font) but full embedding could add up if a template ever uses many distinct fonts/styles at very high page counts, working against the sub-2GB PDF constraint. | Deliberate | Low | Open | Revisit if the "Time-box the first high-volume benchmark" story (not in this sprint) shows file size becoming an issue; Debenu also exposes `AddSubsettedFont` as a smaller-footprint alternative if needed. Sprint 3's batching mitigation made this concern concrete: each batch now re-embeds the font from scratch (see the 2026-09-21 entry below). | | 2026-09-08 | `EnvelopeRenderer.Desktop`'s Render button re-enables as soon as the CLI process is confirmed *started* (`CliProcessLauncher.LaunchAsync` returning), not once it finishes rendering — this story (Sprint 1 Batch 4) intentionally does not track render completion. An operator can click Render again (e.g. against the same output path) while a prior render is still in progress, since nothing yet observes the child process's lifetime or exit code. | Unavoidable (scope boundary of this story) | Low | Resolved | Sprint 1 Batch 5 ("Show render progress and completion summary") replaced `CliProcessLauncher.LaunchAsync`/`Launch` with `RunAsync`/`Run`, which stream the CLI's redirected stdout/stderr, wait for the process to actually exit, and return exit code + final progress event. `MainForm.OnRenderClick` now only re-enables the Render button in the `finally` block after that awaited call completes, not when the process starts — verified with a real launch against the sample CSV (button stayed disabled for the full render, both success and forced-failure runs). | | 2026-09-14 | The render path's actual high-volume throughput badly misses the product's stated "100,000 records at 300 DPI in under 10 minutes" target: a real (not simulated) 100k-record benchmark run showed throughput degrading monotonically from ~399 rec/s to ~15 rec/s (and still falling) by record 4,827 of 100,352, isolated with reasonable confidence to Debenu Quick PDF Library 10.13's in-memory document model rather than this repo's own O(1)-per-record merge/CSV code. Projected full-run time is on the order of 45-90+ minutes — 5x-10x+ over target. Full methodology, raw data, and root-cause investigation in `code/BENCHMARK.md`. | Unavoidable (external vendor library characteristic, not yet confirmed fixable) | High (directly threatens a hard product constraint — `project_config.md`'s "render 100,000 records at 300 DPI in under 10 minutes" — and would be worse at the stated 1,000,000-record ceiling) | Resolved (for the 100,000-record target) | Sprint 3 story "Investigate and address high-volume render throughput degradation" confirmed the Debenu-internal-document-model hypothesis (a scaled-down probe showed the per-page cost reliably resets after a save+release/reopen cycle) and implemented a batching mitigation in `DebenuPdfRenderer` (save+release/reopen every 300 pages, merged via Debenu's `MergeFileListFast`). The full 100k-record benchmark was re-run to completion (not time-boxed): 315 seconds, ~47.5% under the 10-minute budget, valid 100,352-page PDF, 397 MB. Full results in `code/BENCHMARK.md`'s "Sprint 3 follow-up" section. This closes the 100,000-record risk but opens a new, narrower one at the 1,000,000-record ceiling -- see the next entry. | | 2026-09-21 | The Sprint 3 batching mitigation for the above throughput issue (save+release/reopen every 300 pages, merged via `MergeFileListFast`) fixes the 100,000-record/10-minute target with margin, but introduces a new file-size risk at the product's stated 1,000,000-record ceiling: each batch re-embeds the template's TrueType font from scratch (~1.05 MB per extra batch, measured directly), so linear extrapolation of the 100k run's real numbers (397 MB, ~350 MB of which is batching overhead) to 1,000,000 records suggests a combined output size in the neighborhood of 4.5-5 GB -- over the product's sub-2GB final PDF constraint (`project_config.md`). Not yet observed directly (no 1,000,000-record run has been performed); this is a qualitative, evidence-informed extrapolation, not a confirmed failure. Full analysis in `code/BENCHMARK.md`'s "Re-assessment of the 1,000,000-record ceiling" section. | Unintentional (side effect of the chosen mitigation, not present in this form before it) | Medium (only threatens the stated ceiling's extreme end, not the tested and confirmed 100,000-record target; no story currently commits to rendering 1,000,000 records) | Open | Not fixed as part of this story (out of scope: the story's AC2 asks for one mitigation sized against the 100k target). Two candidate directions identified, not yet attempted: a batch size that scales with total record count, or switching to Debenu's `AddTrueTypeSubsettedFont` so each batch only re-embeds the glyphs actually used instead of the full font. Should be revisited before any story commits to rendering at or near the 1,000,000-record ceiling. | | 2026-10-09 | A rotated text element bound to a CSV column (or, since Sprint 5, containing any field run within mixed content) renders at a **different position on every record's page** instead of a consistent one. Root cause confirmed by code inspection: `DebenuPdfRenderer.AddPage` measures each record's own resolved text (`_pdf.GetTextWidth(draw.Text)`) to compute the rotation pivot (`RotatedTextAnchorCalculator.ComputeAnchor`'s `centerLocalX = width / 2.0`), so the bounding-box center — and therefore the anchor point passed to `DrawRotatedText` — shifts whenever a record's resolved text length differs from another's. Static (fixed-text) rotated elements are unaffected, since their width never varies by record, which is why this wasn't caught during Sprint 4's live verification (only a static "ROTATED" label was tested rotated). Compounding finding: this also breaks the "Set a rotation angle..." story's own already-shipped AC4 (canvas and rendered PDF must share the same pivot) for any dynamic/mixed content — `TemplateCanvasControl.MeasureElement` measures `element.DisplayText`, a fixed design-time placeholder, so the canvas always shows one static position that cannot match the actual per-record rendered position except by coincidence. User-reported (2026-10-09): observed as rendered records appearing to "jump around" position-to-position when rotation is applied to real per-record data. | Unintentional (an unanticipated interaction between Sprint 4's "rotate around bounding-box center" design and any per-record-varying resolved text — never exercised together until real dynamic/mixed content was rotated) | High (breaks correct print positioning for any real workflow combining rotation with dynamic or mixed-content fields — a realistic, likely-common combination — and breaks an already-shipped, verified acceptance criterion) | Resolved | Sprint 6 Batch 1 fixed this by choosing the stable fixed-anchor pivot for any rotated text element containing at least one field run, while leaving rotated static text on the Sprint 4 bounding-box-center path. `RenderEngine` now marks dynamic/mixed `TextDraw`s with `UsesFixedRotationPivot`; `DebenuPdfRenderer` uses `(X, Y)` directly for those rotated draws instead of measuring record-specific text width; `CanvasElementEditor`/`TemplateCanvasControl` use the same pivot rule for drawing, hit-testing, and rotate handles. Verified by 336/336 passing tests, including a real-DLL regression proving two different text widths use the same authored rotated transform anchor, plus a real CLI render of `sample-envelope-template2.xml` against the 392-record sample CSV and a bitmap smoke of the actual WinForms canvas drawing the rotated dynamic field. | | 2026-10-19 | Address Control lines rendered on the designer canvas (`TemplateCanvasControl.DrawAddressControl`) and, since Sprint 7, on the new record-accurate preview panel (`TemplatePreviewControl`) visually overlap when a control uses a small font size with the default 1.25 line-spacing multiplier (observed with `sample-envelope-template3.xml`'s 8pt lines against the real sample CSV): each line's GDI+ `MeasureString` height is taller than the `FontSize * LineSpacingMultiplier` row height the baseline math advances by, so consecutive lines' bounding boxes visually overlap on screen even though their baselines are correctly spaced per `TEMPLATE_FORMAT.md`. Confirmed via live built-`.exe` screenshots of both surfaces during Sprint 7 batch 1 verification — present identically on the pre-existing (Sprint 6) design canvas, so this is not a Sprint 7 regression, just newly re-observed because a second surface now shares the same measurement approach. | Unintentional (side effect of using GDI+ `MeasureString`'s full line height as a stand-in for the real Debenu-rendered glyph height, which the code's own remarks already flag as "a design-time visual approximation... not a guarantee of pixel-for-point parity with the final PDF") | Low (cosmetic, canvas/preview-only — the actual PDF render path, `DebenuPdfRenderer`/`RenderEngine`, does not use this measurement at all and is unaffected; does not block reading resolved text, just makes tightly-spaced small-font address blocks visually crowded in the designer) | Open | Not fixed as part of Sprint 7 (out of scope for both the preview-panel and snap-to-grid stories). Candidate direction: derive each line's row height from the same font's ascent/descent (or a smaller line-height fraction of `MeasureString`) instead of the raw measured string height, or accept a documented minimum recommended `LineSpacingMultiplier` for small fonts. Revisit if an operator reports this as more than cosmetic. **Product-owner review confirmation (Sprint 7 Review, 2026-10-19):** Impact/status agreed as logged — Low, Open, non-blocking. Independently confirmed the root cause is confined to `TemplateCanvasControl`/`TemplatePreviewControl`'s GDI+ measurement path and that neither `DebenuPdfRenderer` nor `RenderEngine` (the real PDF render path) references this measurement at all, so no print-output correctness is at risk. No pushback on leaving this open; agree it should not block Sprint 8 planning. | | 2026-09-04 | `EnvelopeRenderer.Cli` only read `DEBENU_LICENSE_KEY` from the process environment. That's fine for `dotnet run --project ... --` (the CLI inherits the invoking shell's env directly), but `EnvelopeRenderer.Desktop` launches the CLI as a child process, which only inherits whatever environment variables were already present in whatever launched the desktop app itself (a double-clicked `.exe` or Start Menu shortcut typically has none) — so every desktop-launched render failed with Debenu error 999 regardless of a valid key existing on disk. Real-user-reported: the operator correctly guessed a `key.txt` dropped next to the exe should work (matching how the CLI's own test helper already resolved keys), but production code had no such fallback. This is a real Definition-of-Done verification gap from Sprint 1 Batches 4-5: the "real success run" verification used an in-process test harness with the env var set directly in that process, never the actual built `.exe` launched the way an operator would, so the gap wasn't caught before Sprint Review. | Unintentional | High (silently broke the desktop app's core success path for any non-`dotnet run` launch) | Resolved | Added `DebenuLicenseKeyResolver` (`code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs`) to the shipped CLI: env var first, then a `key.txt` walked up from the executable's own directory — the same rule the test-only helper already used, now shared via delegation instead of duplicated. Documented in `CLI_CONTRACT.md`'s "Debenu license key" section and `code/README.md`'s desktop-app instructions. Verified by running the actual built `EnvelopeRenderer.Cli.exe` from the Desktop app's own output folder with no environment variable set at all, `key.txt` sitting next to it: exit `0`, valid 1.3 MB `%PDF-1.4` output, all 392 records. 5 new unit tests added (`DebenuLicenseKeyResolverTests.cs`); full suite 111/111 passing. | | 2026-10-19 | Post-Sprint-7-review, the plain editing canvas (`TemplateCanvasControl`/`CanvasElementEditor`) now deliberately rotates every element — static and dynamic/mixed alike — around its own bounding-box center (`RotationPivotCalculator.ComputeForCanvasEditing`), instead of the Sprint 6 fixed-anchor rule (`RotationPivotCalculator.Compute(isDynamic, ...)`) it previously shared uniformly with the real render and the new Sprint 7 preview panel. The editing canvas only ever draws an element's literal authored `{ColumnName}` bracket-token text (`TextElementLayout.DisplayText`), never a per-record resolved value, so the record-to-record text-width drift the fixed-anchor rule exists to prevent cannot occur there — applying it anyway just made a dynamic/mixed element's rotate-handle drag swing around a corner instead of spinning in place, a jarring interactive inconsistency the human product owner asked to have fixed after using the Sprint 7 increment. The real render (`RotatedTextAnchorCalculator`/`DebenuPdfRenderer`) and the new preview panel (`TemplatePreviewControl`/`TemplatePreviewBuilder`) are intentionally untouched and still call `RotationPivotCalculator.Compute` with the real `isDynamic` value, so the original 2026-10-09 record-drift defect fix is fully preserved where it actually matters. See the dated note added to the "Keep rotated dynamic and mixed-content fields positioned consistently across records" story in `backlog/epics/02_template_designer_gui_foundation.md` for the full reasoning and human product-owner approval record. | Deliberate (a considered, approved trade-off decoupling a purely cosmetic/interactive editing-canvas behavior from a record-accuracy rule that surface was never actually subject to — not a regression or an oversight) | Low (the editing canvas was never a record-accuracy surface for rotation to begin with, since it never draws resolved per-record text; the real render and record-accurate preview panel, where the original defect actually mattered, are unaffected and keep the exact same fixed-anchor behavior) | Logged (deliberate design decision, not a defect to resolve) | Not applicable — this row documents a considered, already-implemented, and approved design decision for future reference (e.g. if a future story ever gives the editing canvas its own resolved-per-record preview capability, this decoupling would need to be revisited). | | 2026-10-26 | Sprint 8 ("Rotate the whole Address Control...") extracted its *new* rigid-group-rotation and drag-handle math into framework-free, unit-tested `EnvelopeRenderer.Desktop.Core.Design` classes (`AddressControlLayout.BoxCenter`, `PointRotation`, `AddressControlRotateHandle`), but the *pre-existing* (Sprint 6) Address Control interaction logic it builds on top of — `TemplateCanvasControl.HitTestAddressControl`, `HitTestAddressResizeHandle`, and the whole-control move/resize mouse handlers — still lives directly in the WinForms `Views` project rather than a `CanvasElementEditor`-equivalent for Address Controls, so it remains untestable by `EnvelopeRenderer.Desktop.Tests` (verified only via a live built-`.exe` reflection-driven smoke, not a unit test) unlike the standalone-element equivalents (`CanvasElementEditor.HitTest`/`HandlePosition`/`RotateDragTo`, which are unit tested). This is a proportional, in-scope decision for this sprint (retroactively refactoring Sprint 6's already-shipped, already-smoke-verified move/resize/hit-test code was not part of either committed story), not an oversight, but it leaves a real, growing architectural inconsistency between the two element kinds' testability. | Unintentional (a natural side effect of adding new, better-architected code alongside older code that predates the pattern, not a deliberate call to leave the old code as-is) | Low (no known behavioral defect — the smoke-tested code paths work correctly per this sprint's live verification; purely a testability/maintainability gap, and the two newly-added rotation classes themselves are fully unit tested) | Open | Candidate direction: extract `TemplateCanvasControl`'s Address-Control-specific hit-test/drag/resize state and math into a new `AddressControlEditor` (Desktop.Core), mirroring `CanvasElementEditor`'s existing role for standalone elements, so all four interaction kinds (select, move, resize, rotate) are unit tested consistently. Not required by any currently-committed story; revisit if Address Control interaction logic grows further (e.g. multi-select epic 6 work) or a live-verified defect is found in it. **2026-10-27 update:** this row's own trigger occurred — Sprint 10 Batch 2 ("Select multiple elements at once on the canvas") added `AddressControlsInRect`/`ApplyMultiDragToAddressControls`/`ToggleAddressControlMultiSelect` directly to `TemplateCanvasControl`, following the same untested-WinForms-only pattern rather than doing the extraction, a deliberate choice (the standalone-element half of the exact same feature *is* fully unit tested in `CanvasElementEditor`, so multi-select's core logic has real test coverage even though the Address Control half doesn't) rather than scope-creeping an unrelated refactor into an already-large story. Still Open; the case for the extraction is now stronger with two features built on the untested side. Sprint 10 Batch 3 ("Align and distribute multiple elements") added a third: `GetAddressControlWorldBounds`, in the same file, following the same pattern (its standalone-element counterpart, `CanvasElementEditor.GetWorldBounds`, is unit tested; this one isn't). | | 2026-10-27 | A standalone canvas element's selection highlight border (`TemplateCanvasControl.DrawElement`'s `isSelected` rectangle) visibly wraps only part of a multi-word `DisplayText` (e.g. the default "Static text" highlights only "Static") rather than the full rendered string, for every plain (non-boxed) element regardless of selection mode — discovered via a live built-form screenshot while verifying Sprint 10 Batch 2's multi-select highlighting, and confirmed pre-existing (present identically for the ordinary single-selection case, `MeasureElement`/`DrawElement`'s non-box branch is unmodified by Batch 2). Root cause not yet confirmed by code inspection beyond the immediate suspect: `MeasureElement`'s non-boxed path calls `Graphics.MeasureString(text, font)` (the no-layout-rectangle overload), a GDI+ API with a long-documented history of measuring narrower than what `Graphics.DrawString` actually renders for certain text/hint combinations — plausible but not yet isolated with a minimal repro. | Unintentional (a GDI+ measurement/render mismatch, not anything introduced by this sprint's own drawing changes) | Low (cosmetic, canvas-only design-time approximation — does not affect hit-testing correctness, which was unaffected in this session's own multi-select verification, nor the real PDF render path, which never calls this method) | Open | Not investigated further or fixed as part of Sprint 10 Batch 2 (out of scope for the multi-select story; affects the pre-existing single-selection highlight equally). Candidate direction: try `Graphics.MeasureString` with a generously large layout rectangle (rather than the no-rectangle overload) or `TextRenderer.MeasureText`, and compare against `DrawString`'s actual rendered extent on a real screenshot. Revisit if an operator reports the selection highlight looking wrong, or before any story that depends on this measurement being visually accurate (e.g. further canvas polish). |