Skip to content

test(printplay): #204 extract page-grid geometry to pure method + 13 tests - #512

Merged
jsboige merged 1 commit into
masterfrom
test/204-pnp-page-geometry-contract
Jun 17, 2026
Merged

test(printplay): #204 extract page-grid geometry to pure method + 13 tests#512
jsboige merged 1 commit into
masterfrom
test/204-pnp-page-geometry-contract

Conversation

@jsboige

@jsboige jsboige commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What

Dispatch #204 primaire (ai-01 → po-2024): extract ONE fragile uncovered contract in PrintAndPlayDocument to a pure method + tests, output-neutral + gate-safe, sibling to #500 (recto-verso reorder).

The Print & Play PDF (PrintAndPlayDocument) computed four derived layout quantities inline in Compose() with ZERO unit coverage:

  • nbColumns — configured value when > 0, else floor(contentWidth / cardWidth)
  • nbRows — floor(contentHeight / cardHeight)
  • nbCardsPerPage — rows × columns
  • nbPages — ceil(frontCount / cardsPerPage)

A regression in any of them (rounding instead of truncating columns, forgetting the header reserve, flooring instead of ceiling the page count) silently changes how many sheets print and how cards distribute across them — caught only by rendering and eyeballing the PDF. This is CLAUDE.md fragile area #2 (manual PDF layout math).

How (output-neutral extraction)

  • Extracted the arithmetic into a pure, deterministic ComputePageGeometry (static, no QuestPDF dependency, no I/O) + a PrintPlayPageGeometry readonly struct.
  • The Compose() call site reproduces the exact prior computation (total margin, header reserve pageHeight/10, floor-truncation, ceiling page count) — verified by reading the old inline block before extraction. Rendered output is byte-for-byte unchanged.
  • The latent divide-by-zero when cardsPerPage == 0 (card larger than the content area, or zero columns) is preserved as-is, NOT guarded, and flagged in the XML doc — guarding it is a behavior change that belongs in a separate PR.

13 contract tests (mirror #500 conventions)

Namespace Argumentum.AssetConverter.Tests.WebBasedGenerator, xUnit [Fact] + FluentAssertions, rich XML docs:

Area Tests
Columns configured honored >0; zero/negative fall back to floor division; floor-TRUNCATION not rounding (699/100→6, the fragile bit)
Rows floor division of content height
Cards/page rows × columns
Pages ceil remainder; exact multiple; partial last sheet still prints; zero cards → 0
Header reserves pageHeight/10; no-header contrast
Margin subtracted from both dimensions

Gate safety

  • ✅ Output-neutral extraction — no behavior change, no rendered-output change
  • ✅ Additive tests only — no production CSV, no config, no RowsetNb/rscount, no workflow
  • ✅ Divide-by-zero preserved-not-guarded (documented) — a guard would be a behavior change, out of scope
  • ✅ Builds clean; suite 342 passed / 0 failed / 5 skipped (baseline 329 + 13 new)

Contributes to #204. Refs #500 (sibling recto-verso reorder extraction).

🤖 Worker po-2024 — primaire of ai-01 dispatch msg-20260616T230915-1dn82f.

…tests

The Print & Play PDF (PrintAndPlayDocument) computed four derived layout
quantities inline in Compose() with ZERO unit coverage: nbColumns,
nbRows, nbCardsPerPage, nbPages. A regression in any of them (rounding
instead of truncating columns, forgetting the header reserve, flooring
instead of ceiling the page count) silently changes how many sheets
print and how cards distribute across them, caught only by rendering and
eyeballing the PDF.

Extracts the arithmetic output-neutral into a pure, deterministic
ComputePageGeometry (no QuestPDF dependency, no I/O) + a
PrintPlayPageGeometry readonly struct. The call site preserves the exact
computation (verified by reading the old inline block before extraction),
so rendered output is byte-for-byte unchanged.

13 contract tests pin the layout contract additively (mirrors #500's
recto-verso reorder test conventions):
- columns: configured honored when >0; zero/negative fall back to floor
  division; floor-TRUNCATION not rounding (the fragile bit)
- rows: floor division of content height
- cardsPerPage = rows x columns
- pages: ceil(count/perPage); partial last sheet still prints; exact
  multiple; zero cards -> zero pages
- header reserves pageHeight/10 (matches ComposePage header band)
- margin subtracted from both dimensions

Gate-safe: additive tests + output-neutral extraction. The latent
divide-by-zero when cardsPerPage==0 (card larger than content area) is
preserved as-is, NOT guarded, and flagged in the XML doc for a separate
behavior-change PR.

Suite: 342 passed / 0 failed / 5 skipped (baseline 329 + 13 new).

Contributes to #204. Refs #500 (sibling recto-verso extraction).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jsboige
jsboige merged commit 03cfe5f into master Jun 17, 2026
3 checks passed
@jsboige
jsboige deleted the test/204-pnp-page-geometry-contract branch June 17, 2026 05:11
jsboige added a commit that referenced this pull request Jun 17, 2026
… method + 9 tests (#521)

PdfManager.GenerateAlternateFaceAndBack (the #119 contract) assembles a single
PDF where each card's BACK is emitted immediately before its FRONT so that on a
recto-verso sheet each back lines up behind its matching front, and the ORIGINAL
CardSet order is preserved (so back-less Rules keep their place and appear first).
This ordering is a fragile contract with ZERO unit coverage — a regression
emitting front-then-back, dropping the back-less cards, or reordering by
back-presence silently misaligns every printed sheet, caught only by printing.

Extracts the path-sequencing output-neutral into a pure, deterministic
OrderImagesForAlternateFaceAndBack (static, no MagickImage, no I/O). The call
site emits the exact same path sequence (verified by reading the old inline
builder before extraction), so rendered output is byte-for-byte unchanged.

9 contract tests pin the contract additively (mirror #500 recto-verso reorder +
#512 page-grid geometry conventions):
- per-card: back-then-front when a back exists; front-only when no back;
  empty back == no back (the IsNullOrEmpty guard)
- ordering: original CardSet order preserved (mixed backs NOT grouped); a
  back-less head card stays first (the Rules-first #119 guarantee)
- counts: all-with-back doubles the slot count; all-without-back singles
- pairing: back immediately precedes its OWN front (indices 2i, 2i+1)
- degenerate: empty input emits nothing

Gate-safe: additive tests + output-neutral extraction. Suite 351/0/5
(baseline 342 + 9 new).

Contributes to #204. Refs #500/#512 (sibling recto-verso extractions), #119.

Co-authored-by: Your <your.email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant