arrolio 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Rakefile +15 -0
  4. data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
  5. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
  6. data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
  7. data/TODO.complete/71-cover-layout-parity.md +45 -0
  8. data/TODO.complete/72-table-layout-parity.md +49 -0
  9. data/TODO.complete/73-toc-leaders-parity.md +55 -0
  10. data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
  11. data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
  12. data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
  13. data/TODO.complete/77-text-formatting-parity.md +55 -0
  14. data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
  15. data/TODO.complete/79-line-breaking-parity.md +80 -0
  16. data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
  17. data/TODO.complete/81-conditional-style-refinements.md +70 -0
  18. data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
  19. data/TODO.complete/83-nested-list-support.md +74 -0
  20. data/TODO.complete/84-figure-svg-rendering.md +70 -0
  21. data/TODO.complete/85-locality-reference-formatting.md +48 -0
  22. data/TODO.complete/86-list-item-text-overflow.md +80 -0
  23. data/TODO.complete/87-odd-even-page-templates.md +55 -0
  24. data/TODO.complete/88-hyphenation-support.md +52 -0
  25. data/TODO.complete/89-table-rowspan.md +49 -0
  26. data/TODO.complete/90-mathml-stacked-layout.md +65 -0
  27. data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
  28. data/TODO.complete/92-table-cell-height-parity.md +66 -0
  29. data/TODO.complete/93-figure-text-searchability.md +49 -0
  30. data/TODO.complete/94-fo-space-resolution.md +63 -0
  31. data/TODO.complete/95-title-header-placement.md +55 -0
  32. data/TODO.complete/96-pagination-drift-parity.md +517 -0
  33. data/TODO.complete/README.md +58 -38
  34. data/lib/arrolio/config_driven_pipeline.rb +45 -1
  35. data/lib/arrolio/content/document.rb +4 -3
  36. data/lib/arrolio/content/section.rb +3 -0
  37. data/lib/arrolio/content/table.rb +25 -14
  38. data/lib/arrolio/embedded_block_extractor.rb +54 -0
  39. data/lib/arrolio/engine/paged.rb +109 -28
  40. data/lib/arrolio/flavor/manifest.rb +0 -1
  41. data/lib/arrolio/flavor/registry.rb +1 -1
  42. data/lib/arrolio/flowable.rb +21 -0
  43. data/lib/arrolio/flowables/figure_flowable.rb +17 -0
  44. data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
  45. data/lib/arrolio/flowables/group_flowable.rb +57 -0
  46. data/lib/arrolio/flowables/list_flowable.rb +46 -18
  47. data/lib/arrolio/flowables/note_flowable.rb +84 -9
  48. data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
  49. data/lib/arrolio/flowables/positioned_block.rb +55 -0
  50. data/lib/arrolio/flowables/rotated_text.rb +51 -0
  51. data/lib/arrolio/flowables/table_flowable.rb +303 -71
  52. data/lib/arrolio/flowables/text_flowable.rb +67 -11
  53. data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
  54. data/lib/arrolio/flowables.rb +4 -0
  55. data/lib/arrolio/font/embedder.rb +22 -3
  56. data/lib/arrolio/font_scanner.rb +133 -0
  57. data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
  58. data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
  59. data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
  60. data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
  61. data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
  62. data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
  63. data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
  64. data/lib/arrolio/generic_adapter.rb +263 -317
  65. data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
  66. data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
  67. data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
  68. data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
  69. data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
  70. data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
  71. data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
  72. data/lib/arrolio/generic_flow_builder.rb +92 -187
  73. data/lib/arrolio/glyph_measurer.rb +16 -5
  74. data/lib/arrolio/inline_run.rb +2 -1
  75. data/lib/arrolio/layout_spec/page_template.rb +14 -8
  76. data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
  77. data/lib/arrolio/math_ml.rb +7 -0
  78. data/lib/arrolio/output/page.rb +5 -4
  79. data/lib/arrolio/output/placed_box.rb +5 -2
  80. data/lib/arrolio/renderer/pdf/assets.rb +94 -0
  81. data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
  82. data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
  83. data/lib/arrolio/renderer/pdf.rb +140 -215
  84. data/lib/arrolio/style/definition.rb +7 -3
  85. data/lib/arrolio/style/registry.rb +6 -1
  86. data/lib/arrolio/table/auto_layout.rb +19 -10
  87. data/lib/arrolio/table/grid.rb +105 -0
  88. data/lib/arrolio/table.rb +1 -0
  89. data/lib/arrolio/text_layout/greedy.rb +15 -1
  90. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
  91. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
  92. data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
  93. data/lib/arrolio/text_layout/line.rb +10 -2
  94. data/lib/arrolio/version.rb +1 -1
  95. data/lib/arrolio.rb +8 -4
  96. data/scripts/parity_check.rb +52 -0
  97. data/scripts/parity_diff.rb +89 -0
  98. data/scripts/xsl_profiles/standoc.yml +8 -2
  99. data/scripts/xsl_to_config.rb +28 -1
  100. metadata +91 -5
@@ -0,0 +1,55 @@
1
+ ---
2
+ priority: P2
3
+ impact: medium
4
+ depends_on: [76]
5
+ layer: render
6
+ status: pending
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The OIML reference PDF uses distinct page templates for odd vs even
13
+ pages:
14
+ - Odd pages: header right-aligned
15
+ - Even pages: header left-aligned (or centered when title_complementary
16
+ is present)
17
+
18
+ Our engine produces correct page-count parity but doesn't apply
19
+ distinct templates based on page number. The header alignment shifts
20
+ but the underlying template (margins, region extents) is the same.
21
+
22
+ ## Current state
23
+
24
+ - `header_align_for(page_number)` in `Engine::Paged` returns `:left`
25
+ for even, `:right` for odd — SHIPPED and now REACHABLE
26
+ (2026-08-25): a nil `header_align` flows through
27
+ PageSequenceStart/Output::Page as "no flavor opinion"; the builder
28
+ no longer defaults to :right, and the OIML flavor dropped its
29
+ explicit `header_align: right` pins. Explicit values still win.
30
+ - Page templates: single `body` template, no odd/even variants.
31
+
32
+ ## Approach
33
+
34
+ 1. **Add odd/even template variants to LayoutSpec.** Each
35
+ `page_template` entry can specify `odd` and `even` sub-templates
36
+ with different margins, headers, footers.
37
+
38
+ 2. **Wire PageSequenceMaster.** The XSL uses
39
+ `<xsl:conditional-page-master-reference>` with
40
+ `odd-or-even` condition. Map this to the engine's page-open logic.
41
+
42
+ 3. **Apply template per page.** `Engine::Paged#open_page` should
43
+ select the template based on `page_number.odd?` / `page_number.even?`.
44
+
45
+ ## Done-When
46
+
47
+ - [ ] Odd and even pages use different templates when configured
48
+ - [ ] Header alignment matches reference on all pages
49
+ - [ ] Blank pages inserted for even-page chapter starts
50
+ - [ ] Specs cover odd/even template selection
51
+
52
+ ## Measurement
53
+
54
+ Minor visual impact; mostly affects header position consistency.
55
+ Last measured: 2026-08-08.
@@ -0,0 +1,52 @@
1
+ ---
2
+ priority: P2
3
+ impact: medium
4
+ depends_on: [70, 79]
5
+ layer: text
6
+ status: pending
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Long words at line boundaries get pushed to the next line, leaving the
13
+ previous line underfull. FOP uses TeX hyphenation patterns to break
14
+ words at valid hyphenation points. Our KP breaker has no hyphenation.
15
+
16
+ ## Impact
17
+
18
+ Visible on paragraphs with long technical terms:
19
+ - "characteristics" → pushes to next line
20
+ - "classification" → underfull line before
21
+ - "metrological" → gap at line end
22
+
23
+ ## Approach
24
+
25
+ 1. **Choose a hyphenation gem.** Options:
26
+ - `text-hyphen` — pure Ruby, TeX patgen patterns
27
+ - `ruby-hyphen` — C extension, faster
28
+ - Inline TeX pattern data file (no dependency)
29
+
30
+ 2. **Integrate into ItemBuilder.** When a word exceeds remaining line
31
+ width, emit `Box + Penalty(flagged=true)` at each valid hyphenation
32
+ point. The KP breaker treats flagged penalties as valid break
33
+ opportunities with a demerit.
34
+
35
+ 3. **Language-aware patterns.** Use the document's `language` metadata
36
+ to select the correct pattern file (en, fr, de, etc.).
37
+
38
+ 4. **Minimum word length.** Don't hyphenate words shorter than 5 chars
39
+ (TeX default).
40
+
41
+ ## Done-When
42
+
43
+ - [ ] Long words break at valid hyphenation points
44
+ - [ ] Hyphen character (`-`) rendered at break point
45
+ - [ ] Language detection from document metadata
46
+ - [ ] No overfull lines from hyphenation
47
+ - [ ] Specs cover hyphenation edge cases
48
+
49
+ ## Measurement
50
+
51
+ Affects ~20-30 body paragraphs with long technical terms.
52
+ Last measured: 2026-08-08.
@@ -0,0 +1,49 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [72]
5
+ layer: flowable
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Table cells with `rowspan` (cells spanning multiple rows) were not
13
+ supported: covered cells shifted into the wrong columns (Table 5's
14
+ "Influence factor" column rendered under the p_LC column) and tall
15
+ table-cell text was crammed into one overfull line.
16
+
17
+ ## Solution (2026-08-17)
18
+
19
+ - `Table::Grid` — occupancy grid over a Content::Table honoring
20
+ colspan AND rowspan. Answers cell placement (which column each
21
+ cell really occupies) and which rows are welded together by a
22
+ vertical span. Shared by AutoLayout (natural widths), TableFlowable
23
+ (row heights, placement, splitting).
24
+ - `TableFlowable` places every cell at its grid column; a rowspan
25
+ cell is drawn once in its start row with the combined spanned-row
26
+ height, `valign: middle` centers its content (Table 5's "1.0").
27
+ - Row heights: single-row cells drive their row; a spanning cell that
28
+ does not fit its span distributes the deficit equally over the
29
+ spanned rows.
30
+ - `do_split` splits only at welded-group boundaries — a rowspan cell
31
+ is never cut by a page break. When the first group does not fit
32
+ the remainder, no head is returned and the whole table moves to
33
+ the next page (FOP semantics).
34
+ - `Engine::Paged#place` propagates the page state after a
35
+ whole-flowable move (previously the caller kept filling the old
36
+ page and content fell out of document order).
37
+
38
+ ## Done-When
39
+
40
+ - [x] `<td rowspan="2">` renders as a cell spanning 2 rows
41
+ - [x] Subsequent rows skip cells covered by rowspan
42
+ - [x] Cell content vertically centered in spanned area
43
+ - [x] Specs cover rowspan + colspan combinations
44
+ - [x] OIML Table 4 (rowspan header + colspan) and Table 5 (rowspan 9)
45
+ render correctly
46
+
47
+ ## Measurement
48
+
49
+ Table 5's page similarity 38% → 86% (2026-08-17).
@@ -0,0 +1,65 @@
1
+ ---
2
+ priority: P2
3
+ impact: medium
4
+ depends_on: [75]
5
+ layer: flowable
6
+ status: in progress
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ MathML stacked layout (mfrac, msqrt) renders flat.
13
+
14
+ ## Measured nuance (2026-08-19)
15
+
16
+ Do NOT expand line height for every line carrying sub/superscript
17
+ runs: the reference keeps ordinary symbol sub/sups compact (the
18
+ terminology region's E_min/D_max/v_min lines are single-height and
19
+ were aligned). Only the parenthesized display-style expressions
20
+ (e.g. 5.3.2's ": (0.3 <= pLC <= 0.8)," after a colon) lay out stacked
21
+ - whole expression ~10pt below the text baseline, parens at the
22
+ text level, box two lines tall. A global 1.9x on math lines was
23
+ tested and overshoots the document by 2 pages (29 vs 28). The
24
+ discriminator between compact sub/sup and stacked expressions is in
25
+ the base XSL's stem handling (not the OIML extension) - find it
26
+ before implementing.
27
+
28
+
29
+ MathML fractions (`<mfrac>`) render as "numerator denominator" on one
30
+ line instead of a stacked fraction. Other stacked elements (mroot,
31
+ msqrt) also need vertical layout.
32
+
33
+ ## Current state
34
+
35
+ - MathML integration: SHIPPED (plurimath/mml)
36
+ - `ELEMENT_HANDLERS` registry: mi, mn, mo, msub, msup, msubsup, mfrac
37
+ - 10 specs covering basic elements
38
+
39
+ ## Approach
40
+
41
+ 1. **Stacked fraction flowable.** Create a `MathFractionFlowable` that
42
+ renders numerator centered above denominator with a horizontal rule.
43
+
44
+ 2. **Vertical layout in renderer.** The renderer needs to handle
45
+ `Output::PlacedBox` with `data[:type] == :math_fraction`:
46
+ - Draw numerator at top
47
+ - Draw horizontal rule at middle
48
+ - Draw denominator at bottom
49
+
50
+ 3. **Mroot/msqrt.** Render radical sign with content.
51
+
52
+ 4. **Mtable.** Render simple matrices.
53
+
54
+ ## Done-When
55
+
56
+ - [ ] `<mfrac>` renders as stacked fraction with dividing line
57
+ - [ ] `<msqrt>` renders with radical sign
58
+ - [ ] `<mroot>` renders with index
59
+ - [ ] Math content vertically centered in line
60
+ - [ ] Specs cover fraction, sqrt, root
61
+
62
+ ## Measurement
63
+
64
+ Affects ~5 formula references in OIML body text.
65
+ Last measured: 2026-08-08.
@@ -0,0 +1,50 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [80]
5
+ layer: harness
6
+ status: in_progress
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The parity check reports overall similarity but doesn't show
13
+ per-element breakdowns. When a page has low similarity, we can't tell
14
+ if the issue is spacing, text content, image absence, or table layout.
15
+
16
+ ## Current state
17
+
18
+ - `rake parity:check` renders and diffs — SHIPPED
19
+ - Per-page similarity: SHIPPED
20
+ - Per-element diff: NOT implemented
21
+
22
+ ## Approach
23
+
24
+ 1. **Visual diff overlay.** Generate a side-by-side PNG comparison
25
+ of each page (reference vs ours) with differences highlighted.
26
+
27
+ 2. **Text diff per page.** Extract text from both PDFs per page and
28
+ show unified diff (added/removed lines).
29
+
30
+ 3. **Element classification.** Tag each difference:
31
+ - `spacing` — same text, different position
32
+ - `content` — different text
33
+ - `missing` — element in reference but not in ours
34
+ - `extra` — element in ours but not in reference
35
+
36
+ 4. **Parity leaderboard.** Track parity % per commit so we can see
37
+ which changes helped or hurt.
38
+
39
+ ## Done-When
40
+
41
+ - [ ] `rake parity:diff PAGE=5` shows detailed diff for page 5
42
+ - [ ] Visual overlay PNGs generated to `tmp/parity_overlays/`
43
+ - [ ] Text diff shows exact added/removed words
44
+ - [ ] Element classification helps identify root cause
45
+ - [ ] CI reports parity delta on each PR
46
+
47
+ ## Measurement
48
+
49
+ Critical for iterating on parity. Without this, fixes are guesswork.
50
+ Last measured: 2026-08-08.
@@ -0,0 +1,66 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [89]
5
+ layer: flowable
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Tables rendered with incorrect cell heights and padding (rows 16pt vs
13
+ the reference's 25.45pt pitch), table-cell footnotes inlined into the
14
+ header cell (inflating the header and the column widths), SVG figures
15
+ a third taller than the reference, and captions that could be
16
+ orphaned at a page bottom. Tables spanned the wrong pages and shifted
17
+ content on pages 19-27.
18
+
19
+ ## Root causes (measured against mn2pdf v2.55, 2026-08-17)
20
+
21
+ 1. The flavor XSL `table-row-style` sets `min-height: 8.3mm` — with
22
+ border/padding allowance the reference row pitch is 25.45pt. Our
23
+ rows had no floor (content + 4pt).
24
+ 2. `<fn>` inside a table cell is a TABLE FOOTNOTE in the FOP model:
25
+ only a superscript marker stays in the cell, the body renders
26
+ below the table. The adapter inlined the whole body into the cell.
27
+ 3. FOP sizes viewport-less SVGs (viewBox only) at CSS pixels → PDF
28
+ points (×72/96). We treated viewBox units as points.
29
+ 4. `do_split` ignored the caption and repeated header rows in the
30
+ head budget, over-filling the page.
31
+ 5. Table captions were separate flowables and could be orphaned.
32
+
33
+ ## Solution
34
+
35
+ - Flavor-configurable table geometry in `flow_rules.yml`:
36
+ `table: { min_row_height, cell_padding, footnote_font_size }` —
37
+ OIML calibrated to 25.45 / 3.3 / 9.0 from the XSL + reference.
38
+ - Adapter: table-cell `<fn>` elements contribute a superscript marker
39
+ run to the cell (selector `footnote_marker`) and their bodies to
40
+ `Content::Table#footnotes`; the inline run collector takes an
41
+ `exclude:` selector so footnote text never leaks into cells.
42
+ - `TableFlowable` renders table footnotes below the last row (marker
43
+ + body, 9pt, 10pt gap) and carries them across splits to the part
44
+ holding the final rows.
45
+ - Caption emitted inside `TableFlowable` (full caption on the head,
46
+ "Table N (continued)" on continuations) — never orphaned.
47
+ - SVG user-unit dimensions converted px → pt (×0.75) in both the
48
+ adapter (inline SVG viewBox) and the flow builder (external SVG).
49
+ - `Content::Table::Cell` gained `valign`; `align`/`valign` parsed
50
+ from the XML cell attributes.
51
+
52
+ ## Done-When
53
+
54
+ - [x] Row pitch matches the reference (25.45pt floor from config)
55
+ - [x] Table footnotes render below the table with an in-cell
56
+ superscript marker
57
+ - [x] Figures render at FOP's px→pt scale
58
+ - [x] Caption + header + first row group budgeted in do_split
59
+ - [x] Continuation caption appears when a table spans pages
60
+ - [x] Specs cover footnote extraction, geometry, viewBox scaling
61
+
62
+ ## Measurement
63
+
64
+ Overall similarity 62.7% → 64.07% (2026-08-17). Table region:
65
+ p19 34→61, p21 38→68, p22 42→61, p23 38→86.
66
+ Remaining drift documented in TODO 96.
@@ -0,0 +1,49 @@
1
+ ---
2
+ priority: P2
3
+ impact: medium
4
+ depends_on: [84]
5
+ layer: render
6
+ status: pending
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ SVG figures rendered via rsvg-convert are rasterized to PNG. Text
13
+ inside the figures (labels, annotations) becomes pixels and is not
14
+ searchable in the PDF. The reference mn2pdf/FOP renders SVG as vector
15
+ content, preserving text searchability.
16
+
17
+ This accounts for ~1,500 characters of the remaining text content gap
18
+ (53,472 vs 55,534 reference). Figure labels like "Peripheral devices",
19
+ "Keyboard to operate", "Maximum capacity Emax", etc. are visible in
20
+ the reference's pdftotext output but absent from ours.
21
+
22
+ ## Approach
23
+
24
+ 1. **Vector SVG embedding.** Instead of rasterizing via rsvg-convert,
25
+ embed the SVG as a PDF XObject (Form XObject). This preserves text
26
+ as actual PDF text operators.
27
+
28
+ 2. **Text overlay.** Alternatively, rasterize the SVG for visual
29
+ fidelity but add an invisible text layer with the SVG's text
30
+ content. The text layer is marked as invisible but searchable.
31
+
32
+ 3. **SVG text extraction.** Parse the SVG XML to extract all `<text>`
33
+ elements with their positions. Use these to create searchable text
34
+ in the PDF at the corresponding coordinates.
35
+
36
+ Option 3 is the most pragmatic: it doesn't require vector rendering
37
+ support but makes figure text searchable.
38
+
39
+ ## Done-When
40
+
41
+ - [ ] Figure text content appears in pdftotext output
42
+ - [ ] Text positions match visual figure positions
43
+ - [ ] No visual regression in figure rendering
44
+ - [ ] Text content gap reduced from ~2,000 to <500 chars
45
+
46
+ ## Measurement
47
+
48
+ `pdftotext` char count comparison. Currently 53,472 vs 55,534.
49
+ Last measured: 2026-08-09.
@@ -0,0 +1,63 @@
1
+ ---
2
+ priority: P0
3
+ impact: critical
4
+ depends_on: [81]
5
+ layer: engine
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Our engine ADDS consecutive flowable margins (space_after of previous
13
+ + space_before of current). FOP/FO uses space RESOLUTION: takes the
14
+ MAX of the two, not the sum. This causes every spacing change to
15
+ cascade incorrectly.
16
+
17
+ ## Evidence
18
+
19
+ The reference PDF has ~8pt paragraph spacing between consecutive body
20
+ paragraphs (measured from bbox: consecutive paragraph lines are 21.2pt
21
+ apart vs 13.2pt normal line height = 8pt extra).
22
+
23
+ Adding margin_bottom: 8 to the body style caused page explosion (27 →
24
+ 33 pages) because the margin cascaded to ALL child styles and ALL
25
+ paragraphs (including those inside notes, lists, terms).
26
+
27
+ ## Approach
28
+
29
+ 1. **Implement FO space resolution in Engine::Paged.** When placing
30
+ flowable B after flowable A, the gap should be:
31
+ ```
32
+ gap = max(A.space_after, B.space_before)
33
+ ```
34
+ Not `A.space_after + B.space_before`.
35
+
36
+ 2. **Track previous flowable's space_after.** The engine needs to know
37
+ the previous flowable's margin_bottom to resolve the space.
38
+
39
+ 3. **Apply to section-level flowables only.** Flowables inside
40
+ containers (NoteFlowable, ListFlowable) handle their own internal
41
+ spacing. Space resolution applies between sibling flowables in the
42
+ main flow.
43
+
44
+ ## Impact
45
+
46
+ With FO space resolution + body margin_bottom: 8:
47
+ - Standalone body paragraphs get 8pt gap (correct)
48
+ - Note body paragraphs still get 0 gap (correct — notes have their
49
+ own margin_bottom on the container)
50
+ - Heading-to-body gap: max(heading.space_after, body.space_before) =
51
+ max(12, 0) = 12pt (correct, no double-counting)
52
+
53
+ ## Done-When
54
+
55
+ - [ ] Engine resolves consecutive flowable spaces via max()
56
+ - [ ] body margin_bottom: 8 produces correct inter-paragraph spacing
57
+ - [ ] No cascade to note/list/term internal paragraphs
58
+ - [ ] Overall parity > 58%
59
+
60
+ ## Measurement
61
+
62
+ `bundle exec rake parity:check` — currently 53.99%.
63
+ Last measured: 2026-08-10.
@@ -0,0 +1,55 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [71]
5
+ layer: engine
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The document title block ("Part 1 - Metrological and technical
13
+ requirements") is rendered as BODY flow content, consuming ~58pt of
14
+ body region space on page 5. The reference (mn2pdf/FOP) renders it
15
+ in the HEADER area above the body region, getting it "for free".
16
+
17
+ ## Evidence
18
+
19
+ Reference page 5:
20
+ - Title at y=71-95 (ABOVE body region)
21
+ - "1 Introduction" at y=130 (body region start)
22
+
23
+ Our page 5:
24
+ - Title at y=126-144 (INSIDE body region)
25
+ - "1 Introduction" at y=183 (pushed down by title)
26
+
27
+ Net effect: our page 5 has ~58pt less content than reference. Over
28
+ the document, this accumulates to ~1 page of offset.
29
+
30
+ ## Approach
31
+
32
+ 1. **Static content per page sequence.** The PageSequenceStart
33
+ flowable should support a `title_template` that renders in the
34
+ header region on the first page of the sequence.
35
+
36
+ 2. **Renderer support.** The renderer draws the title at a fixed
37
+ position in the header area (y = margin_top to body_top) when the
38
+ page is the first of a sequence with a title template.
39
+
40
+ 3. **Flow builder change.** `append_title_block` should NOT emit a
41
+ body TextFlowable. Instead, pass the title to the
42
+ PageSequenceStart flowable.
43
+
44
+ ## Done-When
45
+
46
+ - [ ] Title renders above body region (y < 126 on A4 with 26.5mm/18mm)
47
+ - [ ] Body content starts at same y as reference (y=130 for heading)
48
+ - [ ] Page 5 contains Introduction + section 2 content (matching ref)
49
+ - [ ] Overall parity > 65%
50
+
51
+ ## Measurement
52
+
53
+ `bundle exec rake parity:check` — currently 62.7%. Page 5: 99.6%,
54
+ page 6: 66.7%, page 7: 50.0% (mostly empty due to figure push).
55
+ Last measured: 2026-08-16.