1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Separation matrix — the single, named home for "how many blank lines must
//! sit between two adjacent sibling Blocks" in serialized Lex.
//!
//! In Lex a blank line is *load-bearing*: two adjacent text lines with no blank
//! between them parse as ONE two-line Paragraph, while the same lines with a
//! blank between them parse as TWO Paragraphs. Block Separation is therefore a
//! structural property of the grammar, not decoration (see CONTEXT.md and
//! `docs/adr/0001-lex-serializer-structural-block-separation.md`).
//!
//! The serializer emits this separation *structurally* — derived from the block
//! structure — rather than reading it out of `BlankLineGroup` nodes. Readers
//! other than lex-core (Markdown, RFC-XML, …) build ASTs with no
//! `BlankLineGroup`s, so a serializer that separated blocks only when it saw one
//! jammed their blocks together and re-parsed them wrong. Emitting from the
//! matrix fixes every reader at the one shared choke point.
//!
//! Composition is **max-composing** (`ensure_blank_lines`, an ensure-at-least-N,
//! never additive): a structural minimum of `n` alongside a `BlankLineGroup(k)`
//! yields `max(n, k)`. Because a lex-sourced AST already carries a
//! `BlankLineGroup` ≥ the structural minimum between blocks, `lex → lex` output
//! is byte-identical — the matrix only ever adds separation a reader omitted.
//!
//! # How the matrix was derived (lex#782)
//!
//! Every cell was derived from — and is verified against — the lex-core parser's
//! actual behavior, not the prose grammar (where the two disagree, the parser
//! wins; the disagreements are called out below). The verification lives in
//! `tests/lex_separation/matrix.rs::every_ordered_pair_reparses_as_two_blocks`,
//! which builds a minimal reader-shaped document for each ordered pair, serializes
//! it, re-parses, and asserts both blocks survive with the right types.
//!
//! The requirement is governed by two independent parser mechanisms:
//!
//! 1. **Forward absorption** — does `next`'s opening line merge into `prev`?
//! `prev` is *absorbing* only when it ends with an open line at the sibling
//! indent: a Paragraph (a bare text line) or a List (an item line). Blocks that
//! end with a dedent (Session, Definition body) or a `:: label ::` closer
//! (Verbatim, Table) present a hard boundary and absorb nothing. `next` is
//! *absorbable* when it opens with a plain line the paragraph look-ahead does
//! NOT yield before: a Paragraph (text), a Session (title line), or a
//! **marker-form Verbatim** (`subject:` with no indented body — the look-ahead
//! only yields before `subject:` *followed by an indent*). A List, a Definition,
//! a Table, and a block-form Verbatim all open with a construct the look-ahead
//! detects structurally, so they are NOT absorbed and need no blank.
//!
//! 2. **Multi-group verbatim absorption** — the verbatim/table matcher runs at the
//! highest precedence and pairs the *first* `subject:` line it sees with the
//! *next* `:: label ::` closer, spanning blanks and dedents. This is the
//! multi-group verbatim production (`comms/specs/grammar-core.lex` §4.5c): a
//! verbatim block is `<verbatim-group-with-content>+` sharing ONE closing
//! annotation. A **Definition** (`subject:` + indented body, no closer of its
//! own) placed immediately before any block that presents a `:: label ::` line —
//! a Verbatim, a Table, or an Annotation (`:: label ::` is a valid verbatim
//! closer) — is structurally identical to a `<verbatim-group-with-content>`, and
//! verbatim is tried before definition (§4.7), so it becomes that block's FIRST
//! group. No blank count changes this: blank lines do not separate groups. The
//! three cells (`Definition → Verbatim`, `Definition → Table`,
//! `Definition → Annotation`) all take the structural boundary minimum (0, the
//! definition's dedent) below, but they split into TWO cases:
//!
//! - `Definition → Verbatim` and `Definition → Annotation` are **intended and
//! lossless**: the definition's subject AND body survive as the verbatim's
//! first group. This is the group feature working as designed (lex#814 §4,
//! resolved as intended), not a separation gap.
//! - `Definition → Table` is the ONE exception and is **NOT intended**: a table
//! is single-group, so the definition cannot become a group. The merge instead
//! collapses to a degenerate table that keeps only the definition's subject and
//! DROPS the definition body AND the table's own rows — a real, pre-existing
//! **content-loss bug tracked in lex#819** (deferred, not fixed here). It is
//! explicitly NOT sanctioned; it merges lossily today, and when #819 is fixed
//! the pair should SEPARATE and Table drops out of `is_known_hijack`.
//!
//! The verification test characterizes each merge (regression guard for the
//! lossless pairs, known-bad-shape tripwire for Table) rather than asserting
//! faithfulness for those cells.
//!
//! ## Spec/parser disagreements found
//!
//! - `grammar-core.lex` says a blank before a list is *optional* (0). The parser
//! agrees: every `* → List` cell is 0 **except `List → List`**, where two
//! list blocks with no blank between them merge into one list (a blank
//! terminates the first list, per the grammar's own "blank between items
//! terminates the list" rule), so `List → List` = 1.
//! - `grammar-core.lex` frames the session title→body blank as the session's
//! internal separator. Independently, the parser needs a blank *before* a
//! session title when the preceding sibling ends with a `:: label ::` closer
//! (Verbatim, Table) or is an open Paragraph — otherwise the title line is not
//! recognized as a session start (it degrades to a paragraph). Hence
//! `Paragraph → Session`, `Verbatim → Session`, and `Table → Session` = 1,
//! while `List/Session/Definition → Session` = 0 (a dedent or item-line boundary
//! is enough).
//!
//! ## Annotations (two kinds by shape)
//!
//! Standalone block Annotations are a special case the matrix cannot fully own:
//! the parser attaches a floating annotation to a neighboring element (or, at the
//! document head, promotes it to a document-level annotation), so an Annotation
//! placed as a bare body sibling does not round-trip as a sibling regardless of
//! separation. What the matrix *does* own is the boundary the lex#682 band-aid
//! used to patch — but only for the shape that has it: an annotation with an
//! indented body needs a trailing blank so the following sibling is not pulled
//! into that body. A **marker-form** annotation (no body) ends with a closed
//! `:: label ::` and separates exactly like a Verbatim/Table closer. The two
//! shapes are therefore distinct `BlockKind`s: `AnnotationBody → *` = 1 (the
//! lex#682 boundary), while marker `Annotation → *` mirrors the Verbatim row
//! (only a following Session needs a blank). Getting this split right is what
//! keeps `lex → lex` byte-identical for documents that list several marker
//! annotations back-to-back (e.g. `elements/annotation.lex`, where the old
//! band-aid never fired because those annotations have no body).
/// A sibling Block that can appear in a Lex container body — the units that must
/// be *separated* from one another in surface syntax (CONTEXT.md, "Block").
///
/// Annotations split by shape — `Annotation` is the marker form (`:: label ::`,
/// no body) and `AnnotationBody` is the block form with an indented body —
/// because their *trailing* separation requirement differs (see the module docs).
pub
/// Minimum blank lines the grammar requires between an ordered pair of adjacent
/// sibling blocks `(prev, next)`, derived from and verified against the lex-core
/// parser (see the module docs for the derivation and the two governing
/// mechanisms). This is the separation matrix; every ordered pair of the eight
/// `BlockKind`s has an explicit, tested entry.
pub