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,517 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [89, 92]
5
+ layer: layout
6
+ status: in progress
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ With table geometry correct, the remaining parity gap is accumulated
13
+ pagination drift: our body text consumes slightly different vertical
14
+ space than the reference, page by page, so headings and tables land
15
+ on different pages. Whole-table moves (correct FOP semantics) turn
16
+ small drift into large whitespace gaps.
17
+
18
+ ## Progress (2026-08-18, 59.12% after geometry corrections)
19
+
20
+ Fixed (all verified against the reference):
21
+ - **Body region geometry**: the body was shrunk by the before/after
22
+ region extents — XSL-FO keeps the body at the full content
23
+ rectangle; extents only size the header/footer strips inside the
24
+ margins. The old geometry wasted 102pt per page and "matched" the
25
+ reference only by accident.
26
+ - **Section levels** derive from the autonumber's dotted depth
27
+ ("5.1.1" → 3); the fmt-title depth attribute is absent for most
28
+ sections, so nearly every heading resolved to level 1 and its
29
+ level-specific margins never applied.
30
+ - **Term entry spacing**: +12pt before each entry's number, +6pt
31
+ after the preferred term (reference pitch 109.5pt vs our 87.5pt).
32
+ Terms region now within ±0.2pg.
33
+ - **Heading margins**: heading_2 margin-bottom 20pt, heading_3 8pt
34
+ (reference level-2/3 gaps measured 25pt vs our 17pt).
35
+ - **Header/footer offsets** calibrated to the reference (10.34mm /
36
+ 13.6mm from the body margin).
37
+
38
+ The metric moved 64.07% → 59.12% because the corrected geometry
39
+ re-flowed every page; the old alignment was built on a wrong body
40
+ position. Remaining deficits are now clean and quantified below.
41
+
42
+ ## Drift map (heading absolute-position delta, 2026-08-18, 59.12%)
43
+
44
+ Measured as (our page×770 + y) − (ref page×770 + y) per heading;
45
+ page breaks reset drift to ~0, so each region is independent:
46
+
47
+ | Region | Drift | Notes |
48
+ |--------|-------|-------|
49
+ | 2.1–3.1 (intro/foreword) | +0.7..1.0pg | we consume more space (open) |
50
+ | 3.2–3.9 (terms) | ±0.2pg | ALIGNED after term spacing fix |
51
+ | 5.1–5.1.5 | ±0.05pg | aligned |
52
+ | 5.1.5→5.1.6 | −103pt | Figure 4 block shorter than reference |
53
+ | 5.3.2→5.4 | −212pt | Two FEATURE gaps, not spacing (2026-08-18): (1) the footnote in 5.3.2's second paragraph renders INLINE in our output; the reference pins its body ("1) Associated with apportionment...") to the PAGE BOTTOM via the FOP footnote area, leaving 115pt of body space free on p19. (2) the "0.3 ≤ pLC ≤ 0.8" formula renders as a centered DISPLAY line in the reference; we inline it. See TODO 60 (page-bottom footnotes) and TODO 90 (display formulas) |
54
+ | 5.5.1→5.5.2 | −125pt | Example blocks fixed (2026-08-18): "Example:" block label + 35.4pt body indent; remainder is creep/math content |
55
+ | 5.6.1→5.6.2 | −127pt | open |
56
+ | 5.7.x | −70pt | open |
57
+ | 6.x–Annex | −0.8pg | follows from above |
58
+
59
+ Beware false anchors when measuring: cross-references in body text
60
+ ("see 5.1.6 and 5.1.7") match the heading regex — validate monotonic
61
+ page order before trusting a span.
62
+
63
+ ## Key discovery (2026-08-19): the forced breaks are compensating
64
+
65
+ The reference flows CONTINUOUSLY - no forced section breaks in the
66
+ XSL; its fresh-page section starts (3 Terminology, 5 Metrological,
67
+ Bibliography) are natural landings. Our content under continuous
68
+ flow is only 23-25 pages vs the reference's 28: a genuine ~3-5 page
69
+ DENSITY debt that the per-section forced breaks (insert_page_break_
70
+ before/after_section: true) mask by resyncing every region.
71
+ Continuous flow scores 33%, selective breaks (3/5/Bibliography)
72
+ score 42%, forced breaks score 64.53%.
73
+
74
+ Density-debt suspects, in priority order:
75
+ 1. mfrac/msqrt flattened to "/" text (TODO 90 open piece): the 3.7
76
+ measurement/error terms contain fraction formulas the reference
77
+ renders as tall stacked blocks (2-3 lines each); ours render one
78
+ line. Likely 1-2 pages.
79
+ 2. Line pitch: ours 13.0 vs reference 13.4 (~0.4pt/line = ~0.7pg).
80
+ 3. Figures 1-4 sizing and the foreword/intro region's +0.7pg.
81
+
82
+ The builder now supports `section.page_break_before_numbers` for
83
+ landings-matched breaks once the density debt closes.
84
+
85
+ ## Shipped (2026-08-25): measure overflows FIXED (three coupled bugs)
86
+
87
+ 1. Per-run font measurement: the KP ItemBuilder and InlineRun#width
88
+ measured EVERY run with the paragraph's font — bold runs priced
89
+ at regular widths, so following runs' x_offsets under-allocated
90
+ and words OVERLAPPED ("(OIML , R)" corruption). GlyphMeasurer
91
+ gained an optional font_name on width_of_run/string; both call
92
+ sites pass run.style.font_name.
93
+ 2. Justify SHRINK was never rendered: `extra_offset` accumulated
94
+ only when justify was positive, so compressed lines (ratio
95
+ > -1, legitimate TeX shrink — e.g. nat 456.5 vs measure 450.7)
96
+ drew at natural width, past the measure. Both gates fixed
97
+ (positive-only and the <= 0 early return in text_chunks).
98
+ 3. List marker geometry now flavor config (list.geometry:
99
+ marker_indent 18.4, marker_width 19.8, item_spacing 5.6) and
100
+ unordered lists render the configured bullet, not the fmt-name
101
+ autonum placeholder ("—" -> "■", x=90.7, hang 110.5/113.2).
102
+ After: ZERO words past the measure document-wide (was 20+ on p4-8).
103
+ Metric 52.52 -> 46.72 on the knife edge (correct bold widths
104
+ re-wrap every bold-bearing paragraph and reshuffle mid-document
105
+ boundaries; p16 is a pure off-by-one shift, no content loss).
106
+ Recovery rides on the density debt below.
107
+
108
+ ## Open: foreword lines overflow the measure (2026-08-24)
109
+
110
+ Rendered foreword lines reach x=545 vs the body right edge 523
111
+ (+22pt into the margin; one line +94pt of text beyond the layout
112
+ measure). The LAYOUT is verified correct: TextFlowable#emit
113
+ receives width=450.7 and the standalone KP layout wraps the same
114
+ runs within it (lines <=456). Suspect a renderer-vs-layout metric
115
+ mismatch on these paragraphs (justified word positioning computed
116
+ from different measurements than the layout used). Start here:
117
+ instrument render_line_runs' cursor vs the line's placed-run
118
+ x_offsets for a foreword line.
119
+
120
+ ## Shipped (2026-08-24): forced line breaks honored
121
+
122
+ Two coupled bugs fixed: Ruby's split drops trailing empty segments,
123
+ so a lone "\n" run emitted NO forced-break penalty; and the KP
124
+ dynamic program let paths skip forced breaks entirely (the parfill
125
+ glue makes the spanning single line feasible), so even correct
126
+ penalties were ignored. Result: <br>-separated spans rendered GLUED
127
+ ("class A;15 degC for class B") - text corruption. All 57 breaks in
128
+ the document now produce their lines (5.6.1.2's per-class pairs
129
+ match the reference structure). The metric drops 65.29 -> 52.93 on
130
+ the page-boundary knife edge (the ~18 in-paragraph breaks add ~250pt
131
+ mid-document and reshuffle every later page); the recovery is the
132
+ remaining density debt plus the foreword's indented measure (ref
133
+ wraps foreword text narrower - an open style item).
134
+
135
+ ## New finding (2026-08-24): 5.6.1.2 unitsml list inlined
136
+
137
+ The reference renders 'for load cells of class A; / 5 degC' as
138
+ stacked line PAIRS (one per class); we inline the whole set as one
139
+ flowing line ('5 degC for load cells of class A; 15 degC for...').
140
+ The unitsml values now reserve stacked height (the +digit trigger),
141
+ but the LIST STRUCTURE itself is missing - the XML's list-ish
142
+ markup around these items needs adapter investigation. Same shape
143
+ likely in 5.6.1.3 ('2 degC for load cells of class A...') and the
144
+ 6.x marking lists.
145
+
146
+ ## Ruled out (2026-08-24)
147
+
148
+ - Note leading: styling note bodies :note applies the style's 8pt
149
+ paragraph margin between every body line (same trap as the
150
+ example fix) and any line_spacing lift (1.3-1.6 swept) drops the
151
+ metric 2-3pp. Notes are correctly spaced in aggregate.
152
+ - Figure sizing: reference figure 1's vector text spans 332pt wide
153
+ vs our placed 337pt - figures are at the right scale (the 0.75
154
+ sweep stands). The -324pt 2.3-to-3 continuous span is page-break
155
+ interaction around figure 1, not missing figure height.
156
+
157
+ ## Remaining 3.1 ledger (2026-08-19, fifth pass)
158
+
159
+ After the note-lists restore, 3.1's entry gaps over 15pt:
160
+ after 3.1.3.1 -25, after 3.1.3.2 -40, after 3.1.8 -69, after
161
+ 3.1.10 -19. Content verified COMPLETE (the apparent missing words
162
+ in 3.1.9 were a debug-display truncation artifact - always dump
163
+ full lines before diagnosing); the deficits are small spacing
164
+ deltas (note-to-source gaps ~6pt, bullet pitch 14 vs 17).
165
+
166
+ ## Fixed (2026-08-19, fourth pass)
167
+
168
+ - Nested-term definition text no longer glues into the parent: the
169
+ raw <concept> element (refterm + renderterm + xref triple) is
170
+ skipped in favor of <fmt-concept> (the rendered form) - term
171
+ references now render once, with their number ("analogue-passive
172
+ load cell (3.1.3.1)"). 'term' joined the block-level skips and
173
+ definition paragraphs scope to the entry's own term.
174
+ - The verified drift map (labels now correct) shows: section 1
175
+ occupies a full page for us vs 414pt in the reference (the forced
176
+ break), 3.1.3.2/.3 spans were short from the (now fixed) missing
177
+ content, and after-section breaks were no-ops. Section-2 region
178
+ runs +44/+24pt per subsection (open).
179
+
180
+ ## Correction (2026-08-19, third pass)
181
+
182
+ Section 3.7's per-entry pitch difference is 0pt TOTAL over all 21
183
+ entries - perfectly aligned. The earlier per-subsection span table
184
+ is UNRELIABLE: each row's span belongs to the PREVIOUS section
185
+ (off-by-one labeling), and it was measured on a scratch render.
186
+ Before fixing any more spacing, re-derive the drift map on the
187
+ current render with verified labeling (print heading + position for
188
+ both PDFs side by side). Also tested and REVERTED: a blanket
189
+ SCRIPT_LINE_FACTOR 1.4 on sub/sup-bearing lines (+2 pages overshoot
190
+ - script lines are far too numerous for blanket factors).
191
+
192
+ ## Diagnostic notes (2026-08-19, second pass)
193
+
194
+ - Pages 18-20 (18-32%) are one page behind the reference from p17:
195
+ section 3's remaining ~470pt debt (3.7 -104, 3.8 -190, 3.9 -57,
196
+ 3.5/3.6 -70 combined) pushes section 4 to a forced break a page
197
+ late. Section 4's own span is aligned (229 vs 237pt).
198
+ - 3.8's term entry pitch is ALIGNED (88 vs 89pt) and the "[1]"
199
+ source marker renders inline correctly (a superscript y-offset
200
+ makes pdftotext split it into a separate row - false alarm); the
201
+ -190pt accumulates later in the region (check its tables/notes).
202
+ - Terminology pitch overall: ours 89.4 vs ref 92.4 (~174pt total) -
203
+ a ~3pt/entry source-line or entry-gap remainder.
204
+
205
+ ## Approach
206
+
207
+ 1. Instrument per-region spacing: compare y-gaps of consecutive
208
+ headings/paragraphs ours vs reference (re-derive with
209
+ `pdftotext -bbox` + heading regex).
210
+ 2. Terms (3.x): measured (2026-08-17) our entry pitch 87.5pt vs
211
+ reference 109.5pt. Per entry the reference has +6pt after the
212
+ preferred term and +12pt before the next entry's number
213
+ (number/preferred/definition lines otherwise identical). CAUTION:
214
+ closing this deficit is a knife edge — +6pt/entry already pushes
215
+ section 4/5 one page later and the overall metric drops ~15pp
216
+ (5.x/6.x misalign). The spacing must land so the region END
217
+ matches the reference exactly; apply the full +18-22pt/entry only
218
+ together with matching the region's non-term content (notes,
219
+ figures 1-3 spacing).
220
+ 3. Intro/foreword (2.x): we run 0.8pg longer — compare note/paragraph
221
+ spacing on pages 5-6.
222
+ 4. Figure caption spacing in 5.1.6-5.1.7 (~70pt excess).
223
+ 5. Each closed gap re-aligns a table and compounds: fix regions in
224
+ document order, re-running parity after each.
225
+
226
+ ## Done-When
227
+
228
+ - [ ] Every region in the drift map within ±0.1pg
229
+ - [ ] No table whole-jump leaves >40pt whitespace where the reference
230
+ splits or starts cleanly
231
+ - [ ] Overall parity ≥ 70%
232
+
233
+ ## Shipped (2026-08-25): header parity + centered back-matter headings
234
+
235
+ - The TODO 87 `header_align_for` parity logic was unreachable: the
236
+ builder defaulted `header_align: right` and the OIML flavor YAML
237
+ pinned it. `nil` now means "no flavor opinion" through
238
+ PageSequenceStart/Output::Page, and the flavor drops the pin —
239
+ even pages get left headers (ref p4/p28 x=72.3 confirmed).
240
+ - header_offset 10.34 -> 11.68mm: header yMin 39.3 -> 35.55 (ref
241
+ 35.54). The offset ADDS to the baseline height (page_h - top +
242
+ offset), so the earlier 10.34 was 3.8pt low.
243
+ - Preface level-1 + Bibliography headings are CENTERED (XSL
244
+ refine_title-style: preface ancestor + level 1 -> text-align
245
+ center) via the new flavor `title_styles: {preface_1: ...,
246
+ bibliography_1: ...}` map — context-driven, zero core flavor
247
+ knowledge. Foreword gap 33.4pt vs ref 33.6 ✓.
248
+ - The TODO 60 document-end marker bridge removed: markers now emit
249
+ ONLY at paragraph reference sites (FOP semantics; unreferenced
250
+ footnote bodies don't render). The bridge double-registered the
251
+ footnote on the last page.
252
+ - Continuous flow re-measured: 25pp vs ref 28. Fresh drift deltas:
253
+ 3.1 -189, 3.7 -204 (tail entries), 5.3.2 -150, 5.5.1 -95,
254
+ 5.6.1.3 -134, 5.7.2.4 -134, 6.2 -119, 6.2.3 -173; 3.2/5.1.2
255
+ run +100/+157 (we're longer there — table/figure regions to
256
+ re-check). Note: the parity harness is TEXT-based (position
257
+ insensitive) — geometry fixes only move it via page boundaries.
258
+
259
+ ## Shipped (2026-08-25): term entry sibling spacing
260
+
261
+ FOP applies space-before BETWEEN sibling blocks: each term entry's
262
+ second+ definition paragraphs ("(For notes...)" line) and the
263
+ SOURCE line carry ~7.6pt space-before in the reference (gaps 21pt
264
+ vs our 14pt = ~10pt/entry deficit, x63 entries ~ 630pt). New
265
+ flavor config `term: {definition_paragraph_space_before: 7.6,
266
+ source_space_before: 7.5}`; the builder applies it after the first
267
+ definition paragraph and on the SOURCE flowable. Entry spans now
268
+ 101pt vs ref 98 (3.1.4/.5 measured). Continuous flow 25 -> 26pp
269
+ (ref 28); forced-break render grows to 30pp (knife edge — metric
270
+ 40.92% and IGNORED until debt closes; text-based harness punishes
271
+ boundary reshuffles only).
272
+
273
+ ## Fresh drift map (2026-08-25, continuous, post term-spacing)
274
+
275
+ Drift is now nearly UNIFORM: -1165pt at 3.1 to -1507 at 5.7.2 —
276
+ i.e. ~1.5 pages consumed by the foreword/intro region, growing
277
+ slowly. Remaining per-region deltas:
278
+ - 5.1.2 span +419pt (WE RUN LONG): our Table 1 + Figure 4 + Table
279
+ 2 blocks consume ~598pt where the ref packs ~180pt (heading-to-
280
+ heading, p15-16 vs p17). Needs table/figure height methodology
281
+ (row-pitch? whole-table keep_together page moves? figure scale).
282
+ Ref Table 1 rows pitch ~25-26pt (419/445/470) - same floor as
283
+ ours; suspect Figure 4 image height + page-break waste.
284
+ - 3.7 span -182pt (tail entries 3.7.16-.21 still unsampled).
285
+ - 5.3.2 span -218pt (the display formula + footnote region;
286
+ TODO 90 display-formula piece).
287
+ - 5.6.1.3 -69, 3.1 -72, 5.1.7 -116: smaller residues.
288
+ - 5.5.1 fixed (was -95, now -6); 5.6.1.3 improved (-134 -> -69).
289
+
290
+ ## Shipped (2026-08-25, PR #97): table captions carry inline runs
291
+
292
+ extract_table_caption flattened via text_of — embedded stems
293
+ serialized to raw asciimath ('n_("LC")') and rendered across ~3
294
+ stacked pseudo-lines: Table 1's caption block was ~190pt vs the
295
+ reference's ~20pt (the bulk of the 5.1.2 +419pt deficit). Captions
296
+ now collect inline runs (math subscripts survive: "(nLC)" one
297
+ line). Pages 30 -> 29; parity 40.92 -> 41.82.
298
+
299
+ ## Shipped (2026-08-26, PRs #99-#100): inline headers, atomic figures, keep-with-next
300
+
301
+ - Title-less numbered clauses are inline-header clauses (standoc
302
+ inline-header): the number prefixes the first paragraph
303
+ ('2.1 This Recommendation ...' inline) instead of consuming a
304
+ heading line. Content::Section#heading? is now title-based.
305
+ - Figures are atomic: Flowables::FigureFlowable composes image +
306
+ caption (keep-together) so a caption can never orphan onto the
307
+ next page. caption_gap 28pt / block_gap 24pt from the reference.
308
+ - XSL-FO keep-with-next implemented in the engine: a heading that
309
+ fits but whose next flowable's min_keep_height would not moves
310
+ with it (orphaned '3.6' heading above moved Figure 3 was the
311
+ observed defect). Heading styles set keep_with_next: true.
312
+ - The 5.3.2 'display formula' is actually an INLINE stem
313
+ (block=false) that JEuclid gives stacked vertical extent - the
314
+ MATH_LINE_FACTOR model already covers it. The remaining 5.3.2
315
+ delta is footnote-zone + whole-table page-move interaction, not
316
+ a missing display-math feature.
317
+ - Terminology preamble (VIM/VIML/D9/D11/B18 list) matches the
318
+ reference's flowing-text shape already.
319
+ - REFERENCE NOTE: the ref breaks to a fresh page before '3
320
+ Terminology' (p6 ends at Figure 2's caption with ~260pt
321
+ whitespace) - a landings-matched break to apply via
322
+ page_break_before_numbers once debt closes.
323
+
324
+ Drift after: 3.1 -1111 (was -1345), 3.6 span +242 is page-move
325
+ whitespace (atomic figure), 3.7 -93, 5.3.2 -117, 5.6.1.3 -69.
326
+ Forced-break parity 42.23 -> 44.82; continuous 26pp vs ref 28.
327
+
328
+ ## LANDINGS REACHED (2026-08-26, PRs #102-#105): 28/28 pages, 65.79%
329
+
330
+ - Landings-matched breaks shipped: '3 Terminology' and '5
331
+ Metrological' start fresh pages (the reference's only
332
+ top-of-page section landings); each preface clause gets its own
333
+ page (ToC p3, Foreword p4). insert_page_break_before: false.
334
+ - Example bodies: line_spacing 1.5 + 3pt item spacing (ref pitch
335
+ 19-20pt; was 13.6/no gap) - ~130pt recovered in 5.5.1.
336
+ - Lead-in paragraphs keep with following TABLES and LISTS
337
+ (mn2pdf keep-with-next): TableFlowable#min_keep_height = caption
338
+ + repeated header + first welded group; ListFlowable's = first
339
+ item. Figures EXCLUDED - the ref breaks freely between text and
340
+ figures. This reproduced the ref's page-end whitespace before
341
+ table sections and matched the page count.
342
+ - Per-kind list geometry: ordered markers at the margin (x=72,
343
+ hang 92), fixing 6.1/6.2 lists and nested depth.
344
+ - Part-title block space reserved (title.block_space 55pt): first
345
+ body page matches the ref exactly (title y70, heading y130).
346
+ - Trajectory today: 44.82 -> 57.61 -> 61.34 -> 65.22 -> 65.79.
347
+
348
+ Remaining offsets (page map): section 3 (p8-16) runs ~1-3 entries
349
+ ahead per page (terms region ~15-25pt/page denser than ref);
350
+ 5.1-5.7 and 6.x run ~0.3 page ahead. Both realign at the landings.
351
+ Next: per-entry comparison in 3.2-3.5 (notes/lists/tables inside
352
+ terms) and the 5.1.x table-split row pitch.
353
+
354
+ ## Shipped (2026-08-26, PRs #107-#108): KP line geometry exact
355
+
356
+ - Continuation lines no longer carry a phantom space indent:
357
+ build_placed_runs priced the first placed run from the
358
+ break-following item (the leading glue), shifting every wrapped
359
+ line +2.75pt (+5.5 after double spaces). Offsets are now relative
360
+ to the line's first box. Continuation x=72.28 = ref exact.
361
+ - Line#justify_stretch clamped at zero for overfull lines, so the
362
+ renderer's justify-SHRINK path (PR #95) could never fire:
363
+ compressed lines drew at natural width, up to 10pt past the
364
+ measure, 2.75pt spaces vs ref 2.15. Verified: gaps 2.13-2.18 vs
365
+ ref 2.15, line end 522.96 vs ref 522.99, zero overflows.
366
+ - Remaining wrap deltas (ours 5 lines where ref fits 4) trace to
367
+ inter-word natural width vs FOP's glue model - next calibration
368
+ if pursued.
369
+
370
+ ## Shipped (2026-08-26, PR #110): FOP-faithful shrink pricing - 66.6%
371
+
372
+ FOP justifies every line to the measure after breaking: inter-word
373
+ shrink is not penalized. TeX's cubic badness made our DP prefer an
374
+ extra loose line where the reference compresses (5.6's paragraph:
375
+ ours 5 lines, ref 4, identical words/measure). Demerits now charge
376
+ zero for negative ratios; free shrink alone let the DP pack whole
377
+ paragraphs onto one line through the un-rejectable forced final
378
+ break, so overfull forced lines carry OVERFULL_FORCED_PENALTY as a
379
+ last resort. The 5.6 paragraph now wraps identically to the
380
+ reference.
381
+
382
+ ## Residuals (post-#110 census, same-page entry spans)
383
+
384
+ Mostly +/-9-14pt (heading-gap + line-box differences):
385
+ - level-2 heading blocks: ours 33pt vs ref 25 (heading_2 line box
386
+ 15.4 + mb 20 - collapsed term mt 12). Tested mb 20->12: OVERSHOT
387
+ (27pp, 51.66%) - the heading gap is NOT simply margin; likely
388
+ FOP's heading line-height (normal ~1.17 vs our inherited 1.24).
389
+ Next attempt: heading_2 line_spacing, not margin.
390
+ - 3.1.3.1 -19, 5.1 +35, 5.1.6 -19.
391
+ - p16's ~5-line lead accumulates across 3.5-3.8's cross-page
392
+ spans (page-end whitespace differences, not entry deficits).
393
+
394
+ ## Negative results (2026-08-26 keep-rules investigation)
395
+
396
+ - WIDOWS/ORPHANS are NOT enforced by the reference: it ends p16
397
+ with the lone word 'conversion.' (a 1-line widow) and carries
398
+ single SOURCE lines to page tops. An orphans/widows guard in
399
+ TextFlowable#do_split was implemented, measured (29pp, 57.2),
400
+ and REVERTED. Do not re-add without new evidence.
401
+ - TERM ENTRIES are NOT atomic: the reference splits them
402
+ mid-entry on full pages (p9 ends mid-definition, p16 starts with
403
+ a carried SOURCE line). Blanket entry grouping measured 30pp and
404
+ was reverted. The narrower rule reproducing the ref's gaps at
405
+ p8/p11/p12/p13 (50-90pt page-end whitespace, next entry at page
406
+ top) is still unidentified - number+preferred+whole-first-def-
407
+ paragraph was the closest hypothesis (needs number/preferred
408
+ keep_with_next + widows=2 min_keep, which over-moved elsewhere).
409
+ - heading_2 correction (ours 33 vs ref 25 per heading, ~25 spots):
410
+ full fix (mb 12, spacing 1.13) lands at 27pp - it must ship
411
+ together with reproducing the ref's terms-region gaps (+~260pt)
412
+ or the bibliography-page whitespace; partial bisection is
413
+ bistable 27/29pp. Coupled landing documented, do not bisect.
414
+ - Keep-with-next is now CHAIN-aware (PR #113).
415
+
416
+ ## Shipped (2026-08-27, PRs #115-#116): widows model + termnote attachment - 66.95%
417
+
418
+ - Style widows/orphans defaults 2 -> 1 (the reference does not
419
+ enforce them; evidence in the negative results below).
420
+ TextFlowable#min_keep_height honors style widows (first line at
421
+ the new default).
422
+ - <termnote> now belongs to its entry's definition (was a sibling
423
+ block), rendered between definition and SOURCE in document
424
+ order; term-embedded notes carry +7pt before/after
425
+ (term.note_spacing; measured 3.1.3.1: def->Note 21 vs our 14,
426
+ Note->list 20 vs 13). Wrapper group deliberately NOT
427
+ keep-together (29pp when it was).
428
+ - NOTE margin model finding: the engine's margin collapse is
429
+ min(prev_after, curr_before) and ListFlowable#height ignores
430
+ style margins entirely - note spacing only lands via explicit
431
+ Spacers. A margin-model cleanup is a candidate TODO.
432
+ - Remaining term-entry internals: Note->its-list still 13 vs 20
433
+ (the note's internal list spacing); list-in-note indent ours
434
+ 118/138 vs ref 143/163 (~25pt deeper in the reference); entry
435
+ tail after a list 17 vs 28.
436
+
437
+ ## Shipped (2026-08-27, PR #118): note-body list geometry - term internals exact
438
+
439
+ 3.1.3.1's note block now matches the reference line-for-line:
440
+ text -> list 20pt (note.body_spacing 7), bullet marker o at
441
+ +43.7pt from the note column (x=144 vs ref 143.1) with 17pt pitch
442
+ (note.list overrides, note-nested lists only), note trailing 3pt
443
+ after (list -> next entry 28pt exact; leading stays 7pt for
444
+ def -> Note 21).
445
+
446
+ ## Entry-head keeps: near-miss retry (2026-08-27, not shipped)
447
+
448
+ With the note fixes landed, retried term definition_widows 2 +
449
+ number/preferred keep_with_next TOGETHER with the heading_2
450
+ correction (mb 12, spacing 1.13): p8's gap REPRODUCED (ours ws 69
451
+ vs ref 76) and pages held 28/28 - but p17 collapsed (ws 670: the
452
+ section-4 tail interacts with the tightened headings) and p11/p12
453
+ still filled. 62.29 vs committed 66.59. NOT shipped; the missing
454
+ pieces are p11/p12's gaps and the section-4 tail interaction. The
455
+ recipe is recorded here - flip term definition_widows: 2, add
456
+ keep_with_next to the term number/preferred styles, and heading_2
457
+ mb 12 / spacing 1.13 - all three together.
458
+
459
+ ## Recipe probe round 2 (2026-08-27): heading_1 gap found, p17 persists
460
+
461
+ Applied the full recipe PLUS heading_1 margin_bottom 9pt (new
462
+ finding: mid-flow level-1 headings run 17pt to first text vs the
463
+ reference's 26 - section 4 'Description of load cells' measured
464
+ directly). Result: section-3 same-page spans went CLEAN (only
465
+ 3.5.2 +12 / 3.5.11 +15 left), p8's gap reproduced - but the
466
+ page-fill boundaries at p9-p15 still mismatch (ours fills where
467
+ ref leaves 23-73pt), p17 still collapses (ws 670: section 4's
468
+ last paragraph splits 1/2 lines onto a near-empty page before the
469
+ '5' landing), and 6.2.3 runs -56. Net 61.69 vs committed 66.59.
470
+
471
+ Remaining to unlock the landing (all measured):
472
+ - the ~23-73pt/page fill mismatch through p9-p15 despite clean
473
+ entry spans (the gaps sit at DIFFERENT entries than the ref's -
474
+ cumulative position offset upstream of each page)
475
+ - section 4's tail: needs ~17pt more than heading_1's +9 gave
476
+ - 6.2.3 -56pt
477
+
478
+ ## Shipped (2026-08-27, PR #121): dead-zone line breaking - 66.8%
479
+
480
+ CONTENT BUG (not spacing): paragraphs with math rendered as ONE
481
+ overfull line clipped at the page edge - 6.2.3's items lost
482
+ 'vmin, Y, where Y = (Emax-Emin)/vmin (see 3.5.15)'. Root causes:
483
+ (1) normalize_text stripped newline-boundary whitespace (glued
484
+ 'whereY='); (2) with real fontist metrics the break candidates
485
+ sat in a DEAD ZONE (all over-shrunk or too-loose; the emergency
486
+ pass's fixed 20pt stretch cannot bridge 70-160pt-short
487
+ candidates) - it now accepts any stretch ratio; (3) the overfull
488
+ single line was CHEAPER than the loose fix (cubic badness 5.5e7
489
+ vs the 1e6 penalty) - OVERFULL_FORCED_PENALTY now 1e9.
490
+ Regression spec added (dead-zone paragraph >1 line).
491
+
492
+ DEBUGGING NOTE: standalone layout scripts that register
493
+ layout_spec.font_paths (empty for this flavor) silently use
494
+ ESTIMATE metrics - the pipeline resolves fonts via fontist
495
+ (FontScanner). Always register the fontist way in debug scripts
496
+ or the layout differs from the pipeline.
497
+
498
+ ## Shipped (2026-08-28, PR #123): dl geometry + heading_1 space-before - 68.92%
499
+
500
+ - 3.9's abbreviation <dl>: 19pt/item vs ref 16pt (57pt on p16).
501
+ Definition lists carry their own kind (:definition) and geometry
502
+ (list.geometry.definition: marker at margin, 28pt column, 2.6pt
503
+ item spacing).
504
+ - heading_1 margin_top 16pt: mid-page level-1 headings sit 33pt
505
+ below the preceding block in the ref (ours 14); ref landing
506
+ pages apply it at page tops too ('3 Terminology' y90 not y75).
507
+ - CONTENT census clean: the remaining word-count deltas vs the ref
508
+ are all extraction artifacts (figure vector text = TODO 93, and
509
+ JEuclid's literal 'unitsml(degC)' alt text) - no dropped content
510
+ remains after the dead-zone fix.
511
+ - Recipe (heading_2 + term keeps) retested on this base: entry
512
+ gaps reproduce (p8 69 vs 76) but nets -6pp; still blocked by the
513
+ p9-p15 fill mismatch.
514
+
515
+ ## Measurement
516
+
517
+ `bundle exec rake parity:check` — 64.07% (2026-08-17).
@@ -1,48 +1,68 @@
1
1
  # TODO.complete — Path to 100% mn2pdf parity
2
2
 
3
- This directory tracks every piece of work needed to make Arrolio's
4
- OIML output byte-equivalent (visually + textually) to mn2pdf's
5
- output for the same input XML.
6
-
7
- The reference rendering is `mn2pdf v2.55` (Java), invoked via
8
- Metanorma. Reference PDF for r060/1 lives at
9
- `~/src/mn/mn-samples-oiml/_site/documents/r060/1/document.pdf`
10
- (28 pages, 744 KB, fonts: FuturaPT-Book/Demi/Light,
11
- TimesNewRomanPSMT family, Cambria Math).
12
-
13
- The authoritative style reference is
14
- `~/src/mn/metanorma-taste/data/oiml/oiml.xsl` (767 lines).
15
-
16
- ## Front matter shape (every file)
17
-
18
- ```yaml
19
- ---
20
- priority: P0 # P0 = blocks parity; P1 = important; P2 = polish; P3 = future
21
- impact: high # high = >5% similarity swing; med = 1-5%; low = <1%
22
- depends_on: [] # TODO numbers
23
- layer: foundation | metrics | text | flowable | engine | output | render | adapter | harness
24
- status: pending # pending | in_progress | done
25
- est: 1d # 1d = single focused session; 5d = multi-week module
26
- ---
27
- ```
3
+ ## Current baseline (2026-08-09)
28
4
 
29
- ## Critical path to 90%+ similarity
5
+ `bundle exec rake parity:check` reports **62.7%** overall similarity
6
+ on the OIML r060/1 fixture, **28 pages vs 28 reference** (all pages PARTIAL+).
30
7
 
31
- ```
32
- 01 → 02 → 04 → 05 → 06 → 07 → 09 → 11 → 14 → 18 → 22
33
- ```
8
+ 376 specs pass, 0 failures, 0 rubocop offenses across 174 files.
9
+
10
+ ## Status snapshot
34
11
 
35
- After 22 lands, every remaining TODO is a polish item.
12
+ | TODO | Status | Layer | Impact | What shipped |
13
+ |------|--------|-------|--------|-------------|
14
+ | 60 | done | adapter | med | Per-page footnote model: marker, zone reservation, page-bottom bodies |
15
+ | 61 | done | engine | high | Typed errors with metadata, strict mode |
16
+ | 62 | done | font | low | FontScanner + FontMetrics::Registry auto-resolution |
17
+ | 63 | done | adapter | med | Zero flavor artifacts in lib/ (grep-enforced) |
18
+ | 64 | done | adapter | med | Selector-driven adapter specs |
19
+ | 66 | done | content | high | Content::Note, Example, TermEntry, FigureGroup, BibliographyItem |
20
+ | 70 | done | metrics | critical | TTF metrics via Fontisan, font embedding, subsetting |
21
+ | 71 | in progress | flowable | high | PositionedBlock, RotatedText, Style#text_transform shipped |
22
+ | 72 | in progress | flowable | high | colspan, bold headers, continuation caption, row min_height |
23
+ | 73 | done | flowable | med | Right-aligned page numbers, bold level-1, sub-indent |
24
+ | 74 | done | render | high | SVG rendering via rsvg-convert (covered by TODO 84) |
25
+ | 75 | done | adapter | high | MathML extraction from fmt-stem (recursive math element search) |
26
+ | 76 | in progress | engine | med | Odd/even header alignment shipped |
27
+ | 77 | done | adapter | low | Sub/sup baseline, note label suffix, hyperlink underline |
28
+ | 78 | done | adapter | low | Bibliography hanging indent + tag formatting |
29
+ | 79 | in progress | text | high | KP badness, emergency stretch, Glue merge, TOLERANCE |
30
+ | 80 | done | harness | critical | rake parity:check CI task |
31
+ | 81 | in progress | adapter | high | Conditional XSL style refinements (heading margins, term spacing) |
32
+ | 82 | done | adapter | high | Semantic/presentation element deduplication (xref, eref, identifier) || 83 | done | adapter | high | Nested list support (adapter + flow builder) |
33
+ | 84 | done | render | critical | Inline SVG extraction + rsvg-convert rasterization (parity 53→57%) |
34
+ | 85 | done | adapter | medium | Locality reference formatting (clause=X → clause X) |
35
+ | 86 | done | text | high | KP emergency pass, run-group styles, word-level justify (text loss fixed) |
36
+ | 87 | pending | engine | medium | Odd/even page template variants |
37
+ | 88 | pending | text | medium | Hyphenation support for long words |
38
+ | 89 | done | flowable | high | Rowspan via Table::Grid (welded groups, valign) |
39
+ | 90 | in progress | flowable | medium | Operator-math stacking shipped (1.9x); mfrac/msqrt open |
40
+ | 91 | done | harness | high | Parity diff diagnostics (rake parity:diff PAGE=N) |
41
+ | 92 | done | flowable | high | Table geometry config, footnotes, px→pt SVG, caption-in-table |
42
+ | 93 | pending | render | medium | Figure text searchability (SVG text overlay for pdftotext) |
43
+ | 94 | done | engine | critical | FO space resolution (max of consecutive spaces) — 28 pages! |
44
+ | 95 | done | engine | high | Title block as header static content |
45
+ | 96 | in progress | layout | high | Math stacking shipped (64.53%); intro + 3.6-4 region open |
36
46
 
37
- ## Status legend
47
+ ## Architectural quality (audited 2026-08-10)
38
48
 
39
- - `pending` — not started
40
- - `in_progress` — actively being worked
41
- - `done` — complete, specs pass, ready for review
42
- - `deferred` — intentionally out of scope for this milestone
49
+ - Zero `require_relative` in lib/
50
+ - Zero internal `require 'arrolio/...'` in lib/ (autoload only)
51
+ - Zero `respond_to?` in lib/
52
+ - Zero `instance_variable_set/get` in lib/
53
+ - Zero `send` to private methods in lib/
54
+ - OCP: `CONVERTER_REGISTRY` (adapter), `ELEMENT_HANDLERS` (MathML)
55
+ - All value objects frozen with `==`/`eql?`/`hash`
56
+ - All internal code uses autoload (no require_relative)
57
+ - `public_send` only for dynamic dispatch on EXTERNAL Mml objects
58
+
59
+ ## Critical path to 90%+
60
+
61
+ ```
62
+ 96 (drift map, region by region) → 71 (cover) → 88 (hyphenation) → 90 (mfrac)
63
+ ```
43
64
 
44
65
  ## Verification
45
66
 
46
- After each TODO: `bundle exec rake` (rspec + rubocop) must be green,
47
- and `bundle exec ruby exe/oiml-diff out.pdf <reference>` similarity
48
- must not regress.
67
+ `bundle exec rake` (rspec + rubocop) must be green.
68
+ `bundle exec rake parity:check` similarity must not regress.