Skip to content

Commit eda6679

Browse files
author
merge-queue-bot
committed
Merge PR #286: plan(156): section schema — unify entry shape under heading:
2 parents 6852ca2 + b6ddaf8 commit eda6679

9 files changed

Lines changed: 594 additions & 6 deletions

File tree

.claude/skills/markdown-audit/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ where: 'nature: "directive"'
129129
sort: id
130130
row: "- `internal/rules/{id}-{name}/pattern/` ({name})"
131131
?>
132+
132133
- `internal/rules/MDS019-catalog/pattern/` (catalog)
133134
- `internal/rules/MDS021-include/pattern/` (include)
134135
- `internal/rules/MDS038-toc/pattern/` (toc)
135136
- `internal/rules/MDS039-build/pattern/` (build)
137+
136138
<?/catalog?>
137139

138140
Do not paraphrase directive syntax from memory.

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ row: "- [{summary}](../{filename})"
7878
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](../docs/reference/conventions.md)
7979
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](../docs/reference/globs.md)
8080
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](../docs/reference/schema-types.md)
81+
- [Section-schema reference: the entry-shape vocabulary used in inline `kinds.<name>.schema:` blocks and `proto.md` files. Covers the `heading:` discriminator, the `regex:` matcher (a CUE expression with `digits` and `fmvar` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm.](../docs/reference/section-schema.md)
8182
<?/catalog?>
8283

8384
### Development Workflow

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ row: "- [{summary}]({filename})"
8484
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
8585
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
8686
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
87+
- [Section-schema reference: the entry-shape vocabulary used in inline `kinds.<name>.schema:` blocks and `proto.md` files. Covers the `heading:` discriminator, the `regex:` matcher (a CUE expression with `digits` and `fmvar` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm.](docs/reference/section-schema.md)
8788
<?/catalog?>
8889

8990
## Development Workflow

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ row: "- [{summary}]({filename})"
7070
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
7171
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
7272
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
73+
- [Section-schema reference: the entry-shape vocabulary used in inline `kinds.<name>.schema:` blocks and `proto.md` files. Covers the `heading:` discriminator, the `regex:` matcher (a CUE expression with `digits` and `fmvar` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm.](docs/reference/section-schema.md)
7374
<?/catalog?>
7475

7576
## Development Workflow

PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,5 @@ footer: |
8686
| 155 || sonnet | [arch-fix: relocate convention types out of markdownflavor](plan/155_arch-fix-convention-config-ownership.md) |
8787
| 156 | 🔲 | opus | [Composable required-structure schemas across multiple kinds](plan/156_kind-schema-composition.md) |
8888
| 157 | 🔳 | sonnet | [Catalog filter by front matter property](plan/157_catalog-where-filter.md) |
89+
| 156 | 🔲 | opus | [Section schema — unify entry shape under `heading:` discriminator](plan/156_schema-entry-unification.md) |
8990
<?/catalog?>

docs/reference/section-schema.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
---
2+
summary: >-
3+
Section-schema reference: the entry-shape
4+
vocabulary used in inline `kinds.<name>.schema:`
5+
blocks and `proto.md` files. Covers the
6+
`heading:` discriminator, the `regex:` matcher
7+
(a CUE expression with `digits` and `fmvar`
8+
helpers), the `repeat: {min, max}` cardinality
9+
field, and the matching algorithm.
10+
---
11+
# Section schema
12+
13+
> **Status: upcoming.** This page documents the
14+
> shape defined by
15+
> [plan 156](../../plan/156_schema-entry-unification.md).
16+
> It is not yet implemented. The current parser
17+
> accepts the older shape documented in the
18+
> [schema guide](../guides/schemas.md). When plan
19+
> 156 lands, this notice is removed and the guide
20+
> is rewritten to drop every reference to the old
21+
> shape.
22+
23+
A **section schema** describes the heading
24+
structure mdsmith expects in a document. It
25+
pairs with frontmatter and filename constraints
26+
to form a kind's required-structure schema.
27+
28+
## At a glance
29+
30+
```yaml
31+
schema:
32+
filename: "RFC-[0-9][0-9][0-9][0-9].md"
33+
frontmatter:
34+
id: '=~"^RFC-[0-9]{4}$"'
35+
status: '"draft" | "ratified"'
36+
sections:
37+
- heading: null # preamble
38+
- heading: "Overview" # exact-match
39+
- heading:
40+
regex: 'Intro|Getting Started' # disjunction
41+
- heading:
42+
regex: 'Step \#(digits)' # numeric pattern
43+
repeat: { min: 1 } # one or more
44+
sequential: true # digits ordered
45+
sections: [...]
46+
content: [...]
47+
- heading:
48+
regex: '\#(fmvar(id)): \#(fmvar(name))' # frontmatter interpolation
49+
- heading:
50+
regex: '.+'
51+
repeat: { min: 0 } # zero or more (slot)
52+
- heading: "References"
53+
```
54+
55+
Three orthogonal axes describe each entry:
56+
57+
- **Discriminator** — what kind of section it
58+
is (`heading:` value).
59+
- **Matcher** — what text it accepts
60+
(`regex:`).
61+
- **Cardinality** — how many headings it claims
62+
(`repeat:`).
63+
64+
## Entry shapes
65+
66+
Every entry in `sections:` sets exactly one
67+
`heading:` key. Its value's YAML type
68+
discriminates the form.
69+
70+
### `heading: null` — no-heading section
71+
72+
```yaml
73+
- heading: null
74+
```
75+
76+
Represents the preamble. At the top level, the
77+
preamble is the content before any heading. In a
78+
nested `sections:`, it is the content between
79+
the parent heading and the first child.
80+
81+
Only valid as the first entry of its `sections:`
82+
list. Any later position parse-errors.
83+
84+
A null entry accepts `content:` and `rules:`. It
85+
rejects `regex:` and `repeat:` — those live
86+
inside the `heading:` mapping form.
87+
88+
### `heading: <string>` — exact-match sugar
89+
90+
```yaml
91+
- heading: "Overview"
92+
```
93+
94+
Sugar for the mapping form with the string
95+
regex-escaped into `regex:`. Equivalent to:
96+
97+
```yaml
98+
- heading:
99+
regex: 'Overview'
100+
```
101+
102+
The bare string is the most common form. Use it
103+
when the heading text is fixed and you want
104+
exactly one occurrence.
105+
106+
### `heading: { regex, repeat?, sequential? }` — full form
107+
108+
```yaml
109+
- heading:
110+
regex: 'Step \#(digits)'
111+
repeat: { min: 1, max: 5 }
112+
sequential: true
113+
```
114+
115+
The full form makes regex, cardinality, and
116+
ordering explicit. `regex:` is required when the
117+
value is a mapping.
118+
119+
## The regex matcher
120+
121+
`regex:` is a CUE expression evaluating to a
122+
string. The string is compiled as Go RE2.
123+
124+
The YAML value is the body of a CUE
125+
raw-interpolation string. mdsmith wraps it in
126+
`#"..."#` before evaluating. Two consequences:
127+
128+
- **Backslash is literal.** Write `\d`, `\w`,
129+
`\.`, `\(` directly — no doubling. Plain
130+
RE2 patterns work as-is.
131+
- **Interpolation is `\#(expr)`.** Inside the
132+
string, `\#(x)` evaluates `x` in the CUE
133+
scope (frontmatter fields plus mdsmith
134+
helpers) and substitutes the result.
135+
136+
**Anchoring.** Whole-string. `regex: 'Overview'`
137+
matches a heading whose text is exactly
138+
`Overview`. The bare-string sugar behaves the
139+
same way. For a substring, write
140+
`regex: '.*Overview.*'`.
141+
142+
**Match target.** The regex sees the heading's
143+
rendered plain text, not the raw source.
144+
Rendering strips inline emphasis, link wrappers
145+
(keeping link text), code-span backticks
146+
(keeping contents), heading attribute lists
147+
(`{#id}`), and trailing ATX `#`s.
148+
149+
**Case.** Sensitive. Use `(?i)` for
150+
insensitive.
151+
152+
## Helpers
153+
154+
Two helpers are in the `regex:` evaluation
155+
scope alongside the document's frontmatter
156+
fields.
157+
158+
**`digits`** — string constant
159+
`(?P<n>[0-9]+)`. A named numeric capture group
160+
on `n`. Use it for sequenced headings like
161+
`## Step 1` / `## Step 2`. Limit: one `digits`
162+
per pattern.
163+
164+
**`fmvar(name)`** — looks up the frontmatter
165+
field `name`, regex-escapes its value, and
166+
returns it. Use it whenever the heading text
167+
must equal a frontmatter value. The escape is
168+
needed because field values can contain RE2
169+
metacharacters.
170+
171+
```yaml
172+
- heading:
173+
regex: 'Step \#(digits)'
174+
repeat: { min: 1 }
175+
sequential: true
176+
- heading:
177+
regex: '\#(fmvar(id)): \#(fmvar(name))'
178+
```
179+
180+
`sequential: true` is a sibling field on the
181+
entry. Only meaningful with `digits` in the
182+
regex; asserts the captured `n` values are
183+
increasing with no gaps. Without `digits` it
184+
parse-errors.
185+
186+
## The repeat field
187+
188+
`repeat: { min: int, max: int }` bounds how
189+
many consecutive headings the entry claims.
190+
Both fields are optional within the mapping;
191+
both must be ≥ 0.
192+
193+
### Defaults
194+
195+
| `repeat:` | Meaning |
196+
|----------------------|-------------------|
197+
| absent | exactly one |
198+
| `{ min: 0 }` | zero or more |
199+
| `{ min: 1 }` | one or more |
200+
| `{ min: 0, max: 1 }` | optional (0 or 1) |
201+
| `{ min: N, max: M }` | bounded N..M |
202+
203+
`min:` omitted (when `repeat:` is set) defaults
204+
to 0. `max:` omitted defaults to unbounded.
205+
206+
Parse-time rejection: `repeat: {}` (empty),
207+
`max: 0`, `min > max` (both set).
208+
`repeat:` on a `heading: null` entry is
209+
structurally impossible — `repeat:` is a key
210+
inside the `heading:` mapping, not a sibling.
211+
212+
## Matching
213+
214+
Entries match the document's heading sequence
215+
as a positional quantified regex. Each entry
216+
consumes a contiguous run, sized within its
217+
`repeat:` bounds. The walker is greedy by
218+
default and backtracks if a later literal entry
219+
would otherwise be starved.
220+
221+
A heading whose text matches a later literal
222+
entry's `regex:` is claimed for that entry, not
223+
by an earlier wildcard slot. Mirrors plan 146's
224+
slot semantics.
225+
226+
## Sibling fields
227+
228+
Each entry can carry:
229+
230+
- `sections:` — nested entries one heading
231+
level deeper. Recursive.
232+
- `content:` — AST-node constraints inside the
233+
section body. See plan 149.
234+
- `rules:` — per-scope rule-config overrides.
235+
- `closed:` — strictness shorthand. When
236+
`true`, an unlisted heading inside this
237+
scope produces a diagnostic. Default
238+
`false`. Express positional flex by listing
239+
a wildcard slot instead.
240+
241+
## Schema-level fields
242+
243+
```yaml
244+
schema:
245+
filename: "<glob>"
246+
frontmatter:
247+
<key>: <cue-expression>
248+
"<key>?": <cue-expression>
249+
sections: [...]
250+
closed: <bool>
251+
```
252+
253+
- `filename:` — a glob the document basename
254+
must match. Top-level; no `require:` wrapper.
255+
- `frontmatter:` — per-key CUE constraints.
256+
Trailing `?` on a key marks it optional.
257+
- `sections:` — the top-level section list.
258+
- `closed:` — strictness for the root scope.
259+
Valid only on schemas that declare
260+
`sections:`. A frontmatter-only kind that
261+
sets `closed:` parse-errors.
262+
263+
## `proto.md` file syntax
264+
265+
Proto.md files use a literal-template surface
266+
distinct from the inline `regex:` form. Heading
267+
rows in the body act as the schema's
268+
`sections:` list. `{n}` and `{field}` survive
269+
here as template placeholders; they desugar to
270+
the same matcher the inline form produces with
271+
`digits` and `fmvar`.
272+
273+
| Row syntax | Equivalent inline entry |
274+
|-------------------|------------------------------------------------|
275+
| `## Literal text` | `heading: "Literal text"` |
276+
| `## ?` | `heading: { regex: '.+' }` |
277+
| `## ...` | `heading: { regex: '.+', repeat: { min: 0 } }` |
278+
| `## Step {n}` | `heading: { regex: 'Step \#(digits)' }` |
279+
| `## {id}` | `heading: { regex: '\#(fmvar(id))' }` |
280+
281+
Proto.md cannot express `repeat: { min, max }`
282+
or `sequential:`. Callers needing those switch
283+
to the inline-YAML form on a kind in
284+
`.mdsmith.yml`.
285+
286+
The `<?require filename: "..."?>` directive in
287+
proto.md bodies is unchanged.
288+
289+
## Migration from the old shape
290+
291+
Hard cutover. Old-shape keys parse-error with
292+
a "removed; see plan 156" diagnostic naming
293+
the replacement.
294+
295+
| Old shape | New shape |
296+
|------------------------------------|------------------------------------------------------|
297+
| `aliases: [A, B]` | `regex: 'A\|B'` |
298+
| `required: true` | default (omit `repeat:`) |
299+
| `required: false` | `repeat: { min: 0, max: 1 }` |
300+
| `heading: { unlisted: true }` | `heading: { regex: '.+', repeat: { min: 0 } }` |
301+
| Scope-level `repeats: true` | `repeat: { min: 1 }` |
302+
| Scope-level `min:` / `max:` | `repeat: { min, max }` |
303+
| `require: { filename: "..." }` | top-level `filename: "..."` |
304+
| `closed:` on frontmatter-only kind | dropped (no `sections:` → strictness has no meaning) |

editors/claude-code-audit/skills/markdown-audit/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,12 @@ where: 'nature: "directive"'
134134
sort: id
135135
row: "- `internal/rules/{id}-{name}/pattern/` ({name})"
136136
?>
137+
137138
- `internal/rules/MDS019-catalog/pattern/` (catalog)
138139
- `internal/rules/MDS021-include/pattern/` (include)
139140
- `internal/rules/MDS038-toc/pattern/` (toc)
140141
- `internal/rules/MDS039-build/pattern/` (build)
142+
141143
<?/catalog?>
142144

143145
Do not paraphrase directive syntax from memory.

0 commit comments

Comments
 (0)