@@ -37,24 +37,20 @@ templated body content.
3737kinds :
3838 rfc :
3939 schema :
40+ filename : " RFC-[0-9][0-9][0-9][0-9].md"
4041 frontmatter :
4142 id : ' =~"^RFC-[0-9]{4}$"'
4243 status : ' "draft" | "ratified" | "deprecated"'
4344 authors : ' [...string] & len(authors) >= 1'
44- require :
45- filename : " RFC-[0-9][0-9][0-9][0-9].md"
4645 closed : true
4746 sections :
4847 - heading : null
49- required : false
5048 - heading : " Overview"
51- required : true
5249 - heading : " Decision"
53- required : true
5450 - heading :
55- unlisted : true
51+ regex : ' .+'
52+ repeat : { min: 0 }
5653 - heading : " References"
57- required : true
5854` ` `
5955
6056The ` frontmatter:` mapping reuses CUE expressions per
@@ -68,40 +64,83 @@ the ISO regex; see
6864[Schema field types](../reference/schema-types.md)
6965for the registered names and how they are matched.
7066
71- ` require.filename:` is a glob the document basename
72- must match.
67+ ` filename:` is a glob the document basename must
68+ match. It sits at the top of the schema block (no
69+ ` require:` wrapper).
7370
7471`closed : true` makes the scope strict — unlisted
7572headings produce a diagnostic. `closed : false` (the
7673default) tolerates unlisted headings between listed
77- sections.
74+ sections. `closed:` is only meaningful when the
75+ schema declares `sections:`; setting it on a
76+ frontmatter-only kind is a parse error.
7877
79- # ## The `heading:` field
78+ # ## The `heading:` discriminator
8079
8180Every section-array entry sets `heading:`. The value
8281takes one of three shapes :
8382
84- - **string** — literal heading text. Example:
85- `heading : " Overview" ` . The doc must have a heading
86- whose text equals that string (or an alias).
8783- **`null`** — the preamble: content from line 1 up
8884 to the first heading. Only valid as the first entry
89- in a section list. Carries `required:` /
90- ` closed:` / `rules:` for that range; cannot carry
91- ` aliases:` or nested `sections:`.
92- - **mapping** — typed match. Today only
93- `{unlisted : true}` is accepted, declaring a slot
94- that absorbs zero or more sections the schema did
95- not list by name. The slot is positional, but
96- out-of-order detection still claims a heading whose
97- text matches a later listed scope, so the slot only
98- absorbs truly-unlisted sections. Slots are
99- positional-only — they cannot carry `aliases:`,
100- ` sections:` , `rules:`, `closed:`, or `required:`;
101- the parser rejects those keys. Future work can
102- extend the mapping form with shapes like
103- `{any : true}` (match any heading text) or
104- `{pattern : " ..." }` (match a placeholder pattern).
85+ in a section list. Carries `closed:` / `rules:` /
86+ ` content:` for that range; rejects `sections:`.
87+ - **string** — sugar for a literal match. The string
88+ is regex-escaped and used as the matcher's pattern,
89+ so `heading : " (WIP)" ` matches a heading whose text
90+ is exactly ` (WIP)` with the parens taken literally.
91+ Cardinality is one.
92+ - **mapping** — the full form:
93+ ` { regex, repeat?, sequential? }` . `regex:` is
94+ required and is the body of a CUE
95+ raw-interpolation string. `repeat:` bounds the
96+ run; `sequential:` (with the `digits` helper)
97+ asserts ordering.
98+
99+ # ## The matcher mapping
100+
101+ ` ` ` yaml
102+ sections:
103+ - heading:
104+ regex: 'Step \# (digits)'
105+ repeat: { min: 1, max: 5 }
106+ sequential: true
107+ sections: [...]
108+ content: [...]
109+ - heading:
110+ regex: '\# (fmvar(id)): \# (fmvar(name))'
111+ - heading:
112+ regex: '.+'
113+ repeat: { min: 0 }
114+ ` ` `
115+
116+ ` regex:` is whole-string anchored against the
117+ heading's rendered plain text (inline emphasis stripped,
118+ link wrappers unwrapped, code-span backticks dropped).
119+ Backslashes pass through to RE2; interpolation uses
120+ ` \# (expr)` . Two helpers are in scope:
121+
122+ - **`digits`** — expands to the named capture
123+ ` (?P<n>[0-9]+)` . One per pattern. With
124+ `sequential : true` the validator asserts the
125+ captured numbers are strictly increasing without
126+ gaps.
127+ - **`fmvar(name)`** — looks up the document's
128+ frontmatter field `name`, regex-escapes its value,
129+ and substitutes it.
130+
131+ ` repeat:` bounds how many consecutive matching
132+ headings the matcher claims. Omitting `repeat:`
133+ means exactly one; `{ min : 0 }` is zero-or-more;
134+ `{ min : 0, max: 1 }` is optional; `{ min: 1 }` is
135+ one-or-more; bounded forms enforce both bounds.
136+ `repeat : { max: 0 }` and `repeat: { min > max }`
137+ each parse-error.
138+
139+ The wildcard-slot shape — `regex : ' .+' ` with
140+ ` repeat: { min: 0 }` — is positional: it absorbs
141+ zero or more unlisted sections at its slot. A
142+ heading whose text matches a later listed entry is
143+ claimed for that entry, not absorbed by the slot.
105144
106145# ## Nested sections
107146
@@ -112,29 +151,25 @@ expresses that as:
112151
113152` ` ` yaml
114153sections:
115- - heading: "Symptoms"
116- required: true
117- aliases: ["Indicators"]
154+ - heading:
155+ regex: 'Symptoms|Indicators'
118156 - heading: "Diagnosis"
119- required: true
120157 sections:
121158 - heading: "Step"
122- required: true
123159 sections:
124160 - heading: "Check"
125- required: true
126161 - heading: "Expected"
127- required: true
128- - heading: "If different"
129- required: false
130- - heading: "References"
131- required: false
162+ - heading:
163+ regex: 'If different'
164+ repeat: { min: 0, max: 1 }
165+ - heading:
166+ regex: 'References'
167+ repeat: { min: 0, max: 1 }
132168` ` `
133169
134- ` aliases:` lets a heading match alternate texts. A
135- required scope that lists `aliases : ["Indicators"]`
136- matches both `## Symptoms` and `## Indicators` in a
137- document.
170+ A scope that accepts alternate heading texts encodes
171+ the disjunction in its regex : ` regex: 'A|B'` matches
172+ a heading whose text is `A` or `B`.
138173
139174# ## Section content
140175
@@ -179,10 +214,10 @@ ordered/min/max) emit their own diagnostics but
179214still consume the slot. Missing required entries
180215anchor at the section's heading line.
181216
182- ` content:` is not accepted on slot scopes
183- (`heading : {unlisted: true}`) or on the `?`
184- wildcard heading — the parser rejects those
185- shapes today .
217+ ` content:` is rejected on a slot scope (the
218+ wildcard-slot shape has no fixed identity to
219+ constrain). Set `content:` only on entries that
220+ match named sections .
186221
187222# ## Per-scope rule overrides
188223
@@ -197,7 +232,6 @@ document" without scattering glob overrides.
197232` ` ` yaml
198233sections:
199234 - heading: "Decision"
200- required: true
201235 rules:
202236 paragraph-readability:
203237 max-index: 12.0
@@ -364,9 +398,11 @@ filename: "MDS*-*.md"
364398The `# ?` (or `# {field}: {field}` form) acts as the
365399title placeholder. `## ...` rows mark wildcard slots.
366400Front-matter keys map directly to CUE expressions.
367- ` <?require?>` declares the filename pattern. See
368- [enforcing document structure](directives/enforcing-structure.md)
369- for the full file-based reference.
401+ ` <?require?>` declares the filename pattern. The
402+ heading-row tokens desugar to the same matcher
403+ shape the inline form uses; see the
404+ [section-schema reference](../reference/section-schema.md#protomd-file-syntax)
405+ for the row-by-row mapping.
370406
371407# # Choosing a source
372408
@@ -394,6 +430,8 @@ mismatch`, and `out of order` mean.
394430
395431# # See also
396432
433+ - [Section schema reference](../reference/section-schema.md)
434+ — the entry-shape grammar in full.
397435- [File kinds](file-kinds.md) — how kinds attach
398436 schemas (and other rule config) to file groups.
399437- [Enforcing document structure with schemas](directives/enforcing-structure.md)
0 commit comments