# Sprint Backlog
**Sprint:** 7 **Dates:** 2026-10-19 - 2026-10-23
**Sprint Goal:** Give operators an accurate, record-specific layout preview with record navigation — built on one shared text-resolution routine across CLI render, canvas, and preview, closing the canvas/render-drift risk class that caused the 2026-10-09 rotation defect — and round out capacity with a self-contained grid/guide snapping tool for faster manual layout work.
## Committed Items
| Story | Size | Status | Tasks |
|---|---|---|---|
| Render an accurate, record-specific preview of the current template | 8 points | Done | - [x] Extract `RenderEngine.ResolveText`'s per-run resolution logic (or an equivalent) into a routine callable from `EnvelopeRenderer.Desktop.Core`, so CLI render, canvas collapse/mapping state, and the new preview panel all resolve text through the same code, not three independent implementations
- [x] Add a new, dedicated preview panel to `TemplateDesignerForm` (separate from the editable design canvas, which keeps showing bracket tokens)
- [x] Wire the preview panel to draw every standalone text element and every Address Control line using one selected CSV record's real resolved values via the shared resolution routine
- [x] Reproduce current shipped rendering rules exactly in the preview: address-line collapse (`AddressLineCollapser`), per-run mixed static/field content, and both rotation pivot rules (fixed authored anchor for dynamic/mixed elements, bounding-box center for static elements)
- [x] Auto-refresh the preview after a layout edit, a field remapping, or a different record being selected
- [x] Show a clear, specific message in the panel (not a blank/stale preview) when the selected record can't be resolved, e.g. a bound column missing from the loaded CSV
- [x] Unit tests: shared resolution routine correctness, preview panel resolution/refresh behavior, rotation-pivot and collapse parity with existing render/canvas tests
- [x] Live actual built-form smoke (per Sprint 6 retrospective action) showing the real preview panel rendering a real record's resolved text in the built `.exe` |
| Jump to a specific record number | 5 points | Done | - [x] Add a UI control for the operator to enter a record number and navigate to it
- [x] Integrate a full-file forward-only streaming reader (mirroring the CLI's proven `CsvRecordSource` pattern) via skip/take to fetch the requested record, replacing the bounded 20-row `CsvPreviewLoader` for this feature — no new indexed/random-access reader
- [x] Wire the fetched record into the preview panel from the prior story so navigation updates what's shown
- [x] Handle invalid or out-of-range record numbers with a clear operator-facing message
- [x] Unit tests, including navigation against a real CSV larger than 20 rows to prove the full-file reader (not the sample loader) is actually used
- [x] Live actual built-form smoke of entering a record number and seeing the preview panel update in the built `.exe` |
| Snap elements to grid and guides | 5 points | Done | - [x] Add a toggleable snap-to-grid state and grid increment to the canvas editing path
- [x] Update `CanvasElementEditor`'s drag (and resize) math to round position/edges to the nearest grid increment continuously during the gesture, not only on release
- [x] Paint grid lines/guides in `TemplateCanvasControl` while snap is enabled so alignment is visible
- [x] Confirm snapped positions persist and reopen as ordinary `X`/`Y` values with no template format change
- [x] Unit tests for the snap math on drag and resize
- [x] Canvas-only bitmap smoke against the built app (sufficient per this story's own scope note — no form-layout/properties-panel/toolbar surface is touched, so the heavier full-form smoke is not required) |
## Notes
- Capacity signal: completed totals across Sprints 1-6 are **20, 19, 18, 18, 15, 18** — six data points now, a stable 18-20 point range. Sprint 5's 15 was a deliberate under-commit around one large, high-structural-risk story (confirmed at that sprint's planning and not a capacity miss), and Sprint 6 landed exactly on an 18-point commitment with zero scope change. This plan commits **18 points**, the same proven low end used for Sprints 3, 4, and 6.
- Planning rationale — why the full epic 4 pair plus one epic 6 stretch item, not just the pair alone: the epic 4 pair (13 points) is clearly under the 18-20 range on its own, and capacity would go unused without a second pull. "Snap elements to grid and guides" (epic 6, 5 points) is deliberately chosen as that second pull over any other Ready epic 6 story: it has zero dependencies, is contained to the existing `CanvasElementEditor`/`TemplateCanvasControl` pair with no CLI/XML surface, and per its own story notes needs only a canvas-only smoke rather than the heavier full-form smoke — the lowest-risk way to reach 18 points without threatening "don't start what you can't finish." This matches product-owner's own stretch recommendation from the 2026-10-16 refinement outcome.
- Why not more from epic 6 instead: "Select multiple elements at once on the canvas" (5 pts) and "Align and distribute multiple elements" (5 pts, hard-depends on the former) only deliver real operator value together — splitting them across sprints repeats the exact anti-pattern epic 8 deliberately avoided in Sprints 5-6. Pulling both alongside the epic 4 pair would total 23 points, over the proven range. "Undo and redo layout changes" (13 pts) is explicitly flagged by dev-team as the backlog's highest-uncertainty estimate with no prior undo/redo-shaped story to size against, and its own sizing note recommends against pairing it with another large/novel story in the same sprint (the epic 4 preview story is itself non-trivial, touching a genuinely new drawing surface and a cross-cutting resolution-routine extraction) — deferring it whole to a future sprint, ideally with its own short feasibility check on document-cloning first, rather than starting it under time pressure this sprint.
- Product-risk-first ordering (Sprint 6 retrospective carry-in #2, reaffirmed here): the epic 4 pair is pulled first and the epic 6 item second. The preview story exists specifically to close a known-shape defect class (divergent per-record text-resolution paths) that already caused one shipped-output defect (the 2026-10-09 rotation-positioning bug) — this outweighs epic 6's plain backlog-order position below epic 4.
- Sprint 6 retrospective carry-in #1 (built-form smoke for GUI-facing stories) is written directly into each committed story's tasks above: both epic 4 stories touch `TemplateDesignerForm` layout and require a full built-form smoke; "Snap elements to grid and guides" touches canvas interaction only, so its own story notes call for a canvas-only smoke instead — this distinction is intentional, not a shortcut.
- Sprint 6 retrospective carry-in #3 (size composite/multi-part element stories from real cross-layer code inspection): verified as already held for this sprint's candidates rather than assumed. Epic 4's preview story was sized "via code inspection of `TemplateCanvasControl`, `AddressBlockPreviewCalculator`, `RenderEngine`, `TemplateDesignerForm`" (2026-10-16); "Snap elements to grid and guides" was sized "via inspection of `CanvasElementEditor.DragTo`/`BeginDrag`/`EndDrag` and `TemplateCanvasControl`'s paint routine" (2026-10-16) — both cite specific real code paths, not visible-UI guesses, so no re-sizing was needed at planning time.
- Impediments: template asset path strategy (absolute vs. relative) and UNC timeout/retry behavior remain open in `logs/impediment_log.md`. Neither blocks this sprint's committed items — none touch image assets or network paths.
- Carried over from previous sprint: none (Sprint 6 completed both committed stories, 18/18 points).
- Not committed: "Select multiple elements at once on the canvas" + "Align and distribute multiple elements" (10 pts combined, epic 6 — natural pull for Sprint 8, kept as a pair per the reasoning above); "Undo and redo layout changes" (13 pts, epic 6 — recommend a short document-cloning feasibility check before it's committed to any sprint, given its flagged uncertainty); "Warn on text overflow before render" (epic 4) remains Not Ready, blocked on an open product question for the human product owner about what "overflow" means with no element having an enforced bounding width today — not blocking this sprint.
## Execution Order
Sequenced by product risk first, then feature dependency.
| Batch | Story | Why it's gated here |
|---|---|---|
| 1 | Render an accurate, record-specific preview of the current template | Highest product risk in this sprint — closes the divergent text-resolution-path defect class that already caused a shipped-output bug (2026-10-09 rotation defect). No dependency on any other committed story, and the story below depends on it. |
| 2 | Jump to a specific record number | Depends on Batch 1's preview panel to have somewhere to show the navigated-to record. |
| 3 | Snap elements to grid and guides | No dependency on Batches 1-2 or vice versa. Pulled last as the self-contained, lowest-risk stretch item that rounds capacity out to the proven 18-point low end; if the sprint runs short on time, this is the item to flag as at-risk first, not either epic 4 story. |
## Daily Scrum Log
| Day | Date | Completed | Planned | Blocked/At risk |
|---|---|---|---|---|
| 1 | 2026-10-19 | **Batch 1** ("Render an accurate, record-specific preview of the current template", 8 points) done, all 6 ACs met. Extracted the shared per-run resolution routine into `EnvelopeRenderer.Desktop.Core.Design.TextResolver` (`TryResolve`/`ComputeUnmappedFlags`) and refactored `AddressBlockPreviewCalculator` and `TemplateCanvasControl.ResolveAddressLinePreviewText` to call it instead of each keeping its own near-duplicate — one resolution implementation inside Desktop.Core instead of two, mirrored by test-data parity against `RenderEngineTests`' CLI scenarios (the CLI keeps its own independent `RenderEngine.ResolveText`, per this project's deliberate desktop/CLI project split documented in `EnvelopeRenderer.Desktop.csproj`). Also extracted the previously-duplicated rotation-pivot rule (static bounding-box-center vs. dynamic/mixed fixed-anchor) out of both `CanvasElementEditor` and `TemplateCanvasControl` into one shared `RotationPivotCalculator`. Built `TemplatePreviewBuilder` (framework-free, Desktop.Core) to produce ordered, already-resolved `PreviewTextDraw`s reproducing address-line collapse, mixed-content concatenation, both rotation pivot rules, and Address Control line layout/spacing — the desktop-model mirror of `RenderEngine.Render`/`BuildDraws`. Added a new read-only `TemplatePreviewControl` (WinForms) wired into `TemplateDesignerForm` beside (not replacing) the existing editable design canvas, laid out via a deterministic two-column `TableLayoutPanel` (preview area + properties panel) rather than ambiguous multi-Dock-Right ordering, specifically to avoid repeating the Sprint 6 dock-order bug class. The preview auto-refreshes via `Invalidate()` on every `TemplateCanvasControl.ElementsChanged` (layout edits/remaps) since it re-resolves against the live shared document on every repaint; an unresolvable-column or no-selected-record case shows a specific red message instead of a blank/stale preview. Tests: full suite **383/383** passing (`dotnet test code/EnvelopeRenderer.slnx`: 101 CLI, 282 desktop — up from 349/349), with new coverage in `TextResolverTests`, `RotationPivotCalculatorTests`, and `TemplatePreviewBuilderTests` (the latter deliberately mirrors `RenderEngineTests`' scenario names/data: mixed-content concatenation, collapsible-blank-line shift, rotated-dynamic fixed-anchor stability across different resolved text lengths, Address Control expansion/collapse/line-spacing, render/z-order, unknown-column failure). Live verification: built the desktop `.exe` (0 warnings/errors) and drove the real built `TemplateDesignerForm`/`TemplateCanvasControl`/`TemplatePreviewControl` via a throwaway reflection-driven harness project referencing the built assemblies (calling the same internal `LoadCsvFromPath`/`NavigateToRecord` entry points and public `TemplateLayoutXmlSerializer`/`AddStaticTextElement` APIs the real UI uses) — full-form screenshot confirmed no dock/clipping regression (properties panel, record-navigation toolbar, and preview panel all fully visible); preview screenshots against the real 392-record sample CSV and `sample-envelope-template3.xml` (Address Control) showed correctly resolved names/addresses with the blank `Address 3` line correctly collapsed; preview screenshots against `sample-envelope-template2.xml` (rotated dynamic `{Full Name}`) showed the rotated text's anchor point staying fixed in the same screen position across two records with very different name lengths ("WILLIAM EDWARD ZIMMERMAN JR" vs. "MURIEL LYN ZIMMERMAN") while the adjacent, still-token-showing design canvas visibly differed from the resolved preview — direct visual proof of this story's core fix; a synthetic bad-column template produced the exact expected "Cannot preview — the template references CSV column 'NoSuchColumn'..." message instead of a blank panel. One new, pre-existing (not a regression) cosmetic issue noticed during this verification — Address Control lines visually overlapping in both the canvas and the new preview at small font sizes — logged to `logs/technical_debt_log.md` (Low impact, canvas/preview-only, does not affect the real PDF render path). | Begin Batch 2 ("Jump to a specific record number"), which builds directly on this batch's preview panel and CSV-loaded-headers state. | None. |
| 1 | 2026-10-19 | **Batch 2** ("Jump to a specific record number", 5 points) done, all 4 ACs met. Added `EnvelopeRenderer.Desktop.Core.Csv.CsvRecordNavigator.TryReadRecord(path, recordNumber)` — a forward-only streaming reader mirroring the CLI's proven `CsvRecordSource` pattern/CsvHelper configuration (not the bounded 20-row `CsvPreviewLoader`, and not a new indexed/random-access reader, per the story's own scope note); returns a clear operator-facing error for an out-of-range record, a non-positive record number, a missing file, or a malformed CSV, without ever throwing. Wired a "Record #" `NumericUpDown` + "Go" button + status label into the new preview area (`TemplateDesignerForm.BuildPreviewArea`/`NavigateToRecord`); loading a CSV now automatically navigates to record 1 through this same full-file path (replacing the prior hardcoded "first bounded-sample row" used only for the canvas's own separate collapse/mapping preview, which is unchanged). An out-of-range or unreadable navigation leaves the previously-shown preview untouched and surfaces the specific error in the status label, rather than blanking a valid preview. Tests: `CsvRecordNavigatorTests` covers first-record, case-insensitive headers, out-of-range, non-positive input, missing file, no-path, and — the key proof this uses the full-file reader, not the bounded sample — a record beyond row 20. Full suite still **383/383** (this story's coverage is folded into the same run as Batch 1, both landed together). Live verification (same harness/session as Batch 1): navigated to record 2, to record 392 (the last record — proving full-file reads far past the 20-row bound), and to an out-of-range record 999999 (status correctly read "Record 999999 is out of range — the CSV has 392 data row(s)." while the preview panel correctly kept showing record 392's resolved text rather than going blank), then back to record 1 via the same `NumericUpDown`/"Go" path an operator would use. | Begin Batch 3 ("Snap elements to grid and guides") — no dependency on Batches 1-2. | None. |
| 1 | 2026-10-19 | **Batch 3** ("Snap elements to grid and guides", 5 points) done, all 4 ACs met, bringing Sprint 7 to 18/18 points Done. Added `EnvelopeRenderer.Desktop.Core.Design.GridSnapper.Snap` (pure rounding-to-nearest-increment helper) and `CanvasElementEditor.SnapToGridEnabled`/`GridSizePoints` (defaulting to off/10pt, so no existing drag behavior changes unless an operator opts in); `DragTo` now applies snapping on every call, i.e. continuously through the drag gesture rather than only at `EndDrag`. `TemplateCanvasControl` mirrors the same toggle/grid size for Address Control move and resize (handled directly in that control's mouse handlers, not through `CanvasElementEditor`) and paints light dotted grid lines across the page whenever snap is enabled, so the alignment grid is visible, not just felt. Snapped values are still plain `X`/`Y`/`Width` doubles — no template/XML format change, confirmed by inspection of `TemplateLayoutXmlSerializer` (untouched). Added a "Snap to grid" checkbox + grid-size (pt) `NumericUpDown` to the designer's canvas-settings toolbar. Tests: `GridSnapperTests` (rounding including banker's-rounding-at-exact-midpoint, non-positive grid size treated as no-op, fractional grid sizes) and new `CanvasElementEditorTests` cases (snap-disabled exact positioning, snap-enabled rounding, continuous-during-gesture snapping asserted mid-drag before release, custom grid size, and the off-by-default guarantee). Full suite **400/400** passing (`dotnet test code/EnvelopeRenderer.slnx`: 101 CLI, 299 desktop). Live verification: per this story's own scope note (canvas-interaction-only, no form/panel/toolbar layout touched), a canvas-only bitmap smoke was sufficient rather than the heavier full-form smoke — built the desktop `.exe` and drove the real `TemplateCanvasControl` via the same reflection-driven harness, invoking its actual (overridden) `OnMouseDown`/`OnMouseMove`/`OnMouseUp` handlers with real `MouseEventArgs` to simulate a genuine drag: with snap off, a drag landed at the raw pointer position (~121.6, ~559.6 for a ~123.4, ~561.7 target — pixel-rounding-only difference); with snap on at a 20pt grid, an intermediate move mid-gesture (before mouse-up) already landed exactly on-grid at (140, 480), and the final released position landed exactly on-grid at (220, 380) — both exact multiples of 20, confirming continuous snapping, not just an on-release correction. Screenshots confirmed the dotted grid lines are visible and the element visibly moved between grid intersections. | Sprint 7 complete — move to product-owner Sprint Review. | None. |
## Post-review fixes (2026-10-19)
Sprint 7 was already reviewed and retro'd before the human product owner, using the shipped increment, reported two same-increment issues. Both are fixed here as post-review corrections to already-Done Sprint 7 stories, not new sprint work — no phase transition, no new story/points.
- **"Jump to a specific record number" — auto-refresh instead of a "Go" button.** The operator wants the preview to update the instant the record number changes, with no explicit click. Fixed: `_recordNumberInput.ValueChanged` now calls `NavigateToRecord((int)_recordNumberInput.Value)` directly in `TemplateDesignerForm`, guarded by the existing `_suppressEvents` flag so the programmatic reset to record 1 on CSV load (which already calls `NavigateToRecord(1)` explicitly right after) doesn't also fire a redundant navigation through the event handler. `_goToRecordButton` (the `&Go` button) has been removed entirely, per the human product owner's explicit preference. This is a UX correction to how the story's AC1 is satisfied ("The UI allows the operator to enter a record number and navigate to it") — automatic instead of button-triggered — not a change to any AC's substance; ACs 2-4 (preview updates, clear message on invalid input, full-file reader) are unaffected and unchanged. See the dated note on the "Jump to a specific record number" story in `backlog/epics/04_live_preview_and_record_navigation.md`.
- **Rotation feel on the editing canvas — dynamic/mixed elements now rotate like static text.** The Sprint 6 fixed-anchor rotation-pivot rule (dynamic/mixed content pivots around its authored `(X, Y)` anchor instead of its bounding-box center, to keep per-record resolved-text-width drift from moving the pivot) was being applied uniformly to the plain editing canvas as well as the real render and the new Sprint 7 preview panel — but the editing canvas only ever draws an element's literal `{ColumnName}` token text, never a per-record resolved value, so that drift-prevention rule never actually applied there; it just made a dynamic/mixed element's rotate-handle drag swing around a corner instead of spinning in place like static text, a jarring interactive inconsistency. Fixed, with the human product owner's explicit approval of this exact trade-off: added `RotationPivotCalculator.ComputeForCanvasEditing` (always takes the center-pivot path, regardless of `IsDynamic`) and switched `TemplateCanvasControl.RotationPivot` and `CanvasElementEditor.RotationPivot` (used for drawing, hit-testing, the drag handle's position, and live rotate-drag math) to call it. The real render (`RotatedTextAnchorCalculator`/`DebenuPdfRenderer`) and the new preview panel (`TemplatePreviewControl`/`TemplatePreviewBuilder`) are untouched — both still call `RotationPivotCalculator.Compute` directly with the real `isDynamic` value, so the original 2026-10-09 record-drift defect fix is fully preserved where it actually matters. Documented with a dated note on the "Keep rotated dynamic and mixed-content fields positioned consistently across records" story in `backlog/epics/02_template_designer_gui_foundation.md` (reopening language in an already-accepted AC, per this project's convention for that kind of change) and a new Deliberate-debt row in `logs/technical_debt_log.md`.
- **Tests updated:** `RotationPivotCalculatorTests` gained `ComputeForCanvasEditing_MatchesStaticBoundingBoxCenter` and `ComputeForCanvasEditing_DifferentMeasuredWidths_MovesWithTheBoundingBox`. Three `CanvasElementEditorTests` cases that previously asserted the fixed-anchor pivot for dynamic elements on the canvas (`HitTest_RotatedDynamicElement_UsesFixedAnchorPivot`, `HandlePosition_RotatedDynamicElement_OrbitsAroundFixedAnchor`, `RotateDragTo_DynamicElement_UsesFixedAnchorZeroDirection`) were rewritten to assert the new center-pivot behavior instead (renamed to `..._UsesCenterPivot_SameAsStatic`/`..._OrbitsAroundCenter_SameAsStatic`), reusing the same math as their static-element counterparts. `RotatedTextAnchorCalculator`/CLI tests and `TemplatePreviewBuilderTests` were not touched and still pass unchanged, confirming the render/preview paths are unaffected. Full suite: **400/400 before -> 402/402 after** (Desktop 299 -> 301, CLI 101 unchanged; net +2: 2 new `RotationPivotCalculatorTests` cases, plus 3 `CanvasElementEditorTests` cases rewritten in place — same count, new assertions — to replace their prior fixed-anchor-for-dynamic versions).
- **Live smoke:** built the desktop `.exe` and confirmed both fixes against the real app: dragging the rotate handle on a static element and on a dynamic/mixed element now both visibly spin in place around their own center identically (no more corner-swing for the dynamic element); typing a new record number into the "Record #" field updates the preview panel immediately with no click and no "Go" button present at all, including typing an out-of-range record number (status label correctly reported the specific out-of-range message while the preview kept showing the last valid record, matching the story's existing AC3 behavior).