Skip to content

Commit 6a6abf4

Browse files
author
merge-queue-bot
committed
Merge PR #536: docs(extract): make worked examples runnable and outputs verbatim
2 parents e4a929e + 8d78487 commit 6a6abf4

8 files changed

Lines changed: 761 additions & 43 deletions

PLAN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,10 @@ footer: |
170170
| 240 | 🔲 | sonnet | [cuelite phase 4 — drop cuelang.org and enable tinygo](plan/240_cuelite-drop-cue.md) |
171171
| 241 | 🔲 | opus | [Schema-per-file config under `.mdsmith/schemas/`](plan/241_schema-files.md) |
172172
| 242 | 🔲 | sonnet | [Recover from panics in the LSP lint pipeline](plan/242_lsp-panic-recovery.md) |
173+
| 242 | 🔲 | opus | [proto.md schemas declare content entries via `<?content?>`](plan/242_proto-content-entries.md) |
174+
| 243 | 🔲 | sonnet | [`mdsmith extract` projects the document H1 as `title`](plan/243_extract-h1-title.md) |
173175
| 243 | 🔲 | sonnet | [Security hardening batch — 2026-06-09 audit](plan/243_secreview-2026-06-09-hardening.md) |
176+
| 244 | 🔲 | opus | [Structured list projection; fix nested-item text corruption](plan/244_structured-list-projection.md) |
177+
| 245 | 🔲 | sonnet | [Table projection modes: `records` and `rows`](plan/245_table-projection-modes.md) |
178+
| 246 | 🔲 | opus | [Typed block projection and full-document extract](plan/246_block-projection-full-extract.md) |
174179
<?/catalog?>

docs/guides/extract-markdown-as-data.md

Lines changed: 151 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ YAML scalar.
3838
The trap is to reach for frontmatter for everything
3939
because it's structured. A 60-character `tagline` in
4040
frontmatter and the same 60 characters in a
41-
`## Tagline` body section produce identical JSON;
42-
the body version is shorter to edit, diffs cleanly
43-
when wrapped, and is lintable as Markdown.
41+
`## Tagline` body section project the same string;
42+
only the key moves, from `frontmatter.tagline` to
43+
`tagline.text`. The body version is shorter to edit,
44+
diffs cleanly when wrapped, and is lintable as
45+
Markdown.
4446

4547
## Worked example
4648

47-
A product-copy file with a tagline, a lead, and one
48-
per-surface description.
49+
A product-copy file at `docs/copy/product.md` with a
50+
tagline, a lead, and one per-surface description.
4951

5052
### Frontmatter-heavy (the trap)
5153

@@ -101,33 +103,58 @@ Inline diagnostics, fix-on-save, and instant
101103
navigation for Markdown in VS Code.
102104
```
103105

104-
With a matching schema in `.mdsmith.yml`:
106+
With a matching schema and kind assignment in
107+
`.mdsmith.yml`:
105108

106109
```yaml
107110
kinds:
108111
product-copy:
109112
schema:
110113
sections:
111114
- heading: { regex: '^Tagline$' }
115+
content:
116+
- { kind: paragraph }
112117
- heading: { regex: '^Lead$' }
118+
content:
119+
- { kind: paragraph }
113120
- heading: { regex: '^VS Code$' }
114121
bind: vscode-description
122+
content:
123+
- { kind: paragraph }
124+
kind-assignment:
125+
- glob: ["docs/copy/product.md"]
126+
kinds: [product-copy]
115127
```
116128
117-
`mdsmith extract product-copy --format json` emits
118-
the same shape both encodings would produce:
129+
Each `content:` entry declares the paragraph its
130+
section projects. A section without one projects as
131+
an empty object — the schema, not the body, decides
132+
what `extract` emits.
133+
134+
`mdsmith extract product-copy --format json docs/copy/product.md`
135+
emits:
119136

120137
```json
121138
{
122-
"frontmatter": { "title": "Product copy" },
123-
"tagline": { "text": "Mark down your ideas; smith them into shipping docs." },
124-
"lead": { "text": "A lint-and-fix tool that keeps your Markdown consistent across every surface — READMEs, docs site, editor extensions." },
125-
"vscode-description": { "text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code." }
139+
"frontmatter": {
140+
"title": "Product copy"
141+
},
142+
"lead": {
143+
"text": "A lint-and-fix tool that keeps your Markdown consistent across every surface — READMEs, docs site, editor extensions."
144+
},
145+
"tagline": {
146+
"text": "Mark down your ideas; smith them into shipping docs."
147+
},
148+
"vscode-description": {
149+
"text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code."
150+
}
126151
}
127152
```
128153

129-
The body version costs nothing at the projection
130-
layer and is the editable artifact.
154+
Keys come out sorted, not in document order. The
155+
consumer reads the same strings the frontmatter
156+
version held, and the body version is the editable
157+
artifact.
131158

132159
## Projecting inline structure
133160

@@ -165,21 +192,36 @@ kinds:
165192
- { kind: paragraph, projection: inline, required: true }
166193
```
167194

168-
`mdsmith extract product-copy --format json` emits the
169-
headline as a span listtext, then the level-1
170-
emphasis span with its own `children`, then the
171-
trailing text:
195+
`mdsmith extract product-copy --format json docs/copy/product.md`
196+
emits the headline as a span list: text, then the
197+
level-1 emphasis span with its own `children`, then
198+
the trailing text:
172199

173200
```json
174201
{
175-
"frontmatter": { "title": "Product copy" },
202+
"frontmatter": {
203+
"title": "Product copy"
204+
},
176205
"headline": {
177206
"inline": [
178-
{ "span": "text", "value": "Mark" },
179-
{ "span": "emphasis", "level": 1, "children": [
180-
{ "span": "text", "value": "down" }
181-
]},
182-
{ "span": "text", "value": ", smithed." }
207+
{
208+
"span": "text",
209+
"value": "Mark"
210+
},
211+
{
212+
"children": [
213+
{
214+
"span": "text",
215+
"value": "down"
216+
}
217+
],
218+
"level": 1,
219+
"span": "emphasis"
220+
},
221+
{
222+
"span": "text",
223+
"value": ", smithed."
224+
}
183225
]
184226
}
185227
}
@@ -194,9 +236,11 @@ recursive mode switch:
194236
```json
195237
"inline": [
196238
{ "span": "text", "value": "run " },
197-
{ "span": "strong", "level": 2, "children": [
198-
{ "span": "code", "value": "mdsmith fix" }
199-
]},
239+
{
240+
"children": [{ "span": "code", "value": "mdsmith fix" }],
241+
"level": 2,
242+
"span": "strong"
243+
},
200244
{ "span": "text", "value": " daily" }
201245
]
202246
```
@@ -237,6 +281,81 @@ than one line, and anything that benefits from
237281
Markdown formatting (code, emphasis, links) all
238282
belong in the body.
239283

284+
## Frontmatter `title` and the H1
285+
286+
The worked example carries the same string twice:
287+
`title: Product copy` in frontmatter and
288+
`# Product copy` as the H1. Nothing checks the two
289+
against each other by default, so they can drift
290+
apart edit by edit.
291+
292+
The test from the previous section decides it. When
293+
no catalog row, site template, or release script
294+
reads `frontmatter.title`, delete the field; the H1
295+
alone is the title. When a tool does read the
296+
field, keep it and let MDS020 enforce the match.
297+
298+
Enforcement needs a file-based schema. An inline
299+
`schema:` starts matching at H2 — the H1 belongs to
300+
[first-line-heading][mds004] — so the kind switches
301+
to a `proto.md` whose first row is the `{title}`
302+
placeholder:
303+
304+
```markdown
305+
# {title}
306+
307+
## ...
308+
```
309+
310+
```yaml
311+
kinds:
312+
product-copy:
313+
rules:
314+
required-structure:
315+
schema: copy-proto.md
316+
```
317+
318+
The `{title}` row requires the frontmatter field
319+
and checks the H1 text against its value. A drifted
320+
H1 fails `mdsmith check`:
321+
322+
```text
323+
docs/copy/product.md:4:1 MDS020 heading does not match frontmatter: expected "Product copy" (from title), got "Product page copy"
324+
```
325+
326+
The synced H1 also becomes data. `mdsmith extract`
327+
projects the H1 scope under a `title` key, with the
328+
captured heading text inside:
329+
330+
```json
331+
{
332+
"frontmatter": {
333+
"title": "Product copy"
334+
},
335+
"title": {
336+
"title": "Product copy"
337+
}
338+
}
339+
```
340+
341+
Weigh two limits before switching. Every schema
342+
source on a file must declare the same root level,
343+
so an H1-rooted `proto.md` cannot compose with an
344+
H2-rooted inline schema on the same file. And a
345+
`proto.md` declares heading rows only, not
346+
`content:` entries, so the worked example's
347+
paragraph projections (`tagline.text`, …) drop out
348+
of the tree.
349+
350+
When the kind's main job is extraction, keep the
351+
inline schema and delete the frontmatter field
352+
instead. mdsmith cannot project the H1 text without
353+
a frontmatter field behind it: a `{title}` row with
354+
no `title` field matches any heading, and `extract`
355+
skips wildcard scopes.
356+
357+
[mds004]: ../../internal/rules/MDS004-first-line-heading/README.md
358+
240359
## `bind:` patterns
241360

242361
`bind:` renames the JSON key that a heading or
@@ -281,13 +400,14 @@ embed reads the tagline directly:
281400
file: docs/copy/product.md
282401
extract: tagline.text
283402
?>
284-
Mark down your ideas; smith them into shipping
285-
docs.
403+
Mark down your ideas; smith them into shipping docs.
286404
<?/include?>
287405
```
288406

289-
The directive runs the included file through the
290-
same projection rules `mdsmith extract` produces,
407+
The spliced text lands on one line: a `text`
408+
projection joins a soft-wrapped paragraph with
409+
spaces. The directive runs the included file through
410+
the same projection rules `mdsmith extract` uses,
291411
walks the dotted path, and splices the leaf. There
292412
is no intermediate "fragment" file to keep in sync —
293413
the README reads the source of truth on every lint.

docs/reference/cli/extract.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ Content entries project under default keys:
5959
- `paragraph``text` (plain text), or `inline` when
6060
the entry sets `projection: inline` (see below).
6161

62-
Two sibling projections that resolve to the same key
63-
are a schema error. It is reported at extract time.
62+
Sibling keys are emitted in sorted order, not document
63+
order. Two sibling projections that resolve to the same
64+
key are a schema error. It is reported at extract time.
6465
Optional sections that did not match are omitted, not
6566
emitted as null.
6667

@@ -110,9 +111,11 @@ For the headline `Mark*down*, smithed.`:
110111
"headline": {
111112
"inline": [
112113
{ "span": "text", "value": "Mark" },
113-
{ "span": "emphasis", "level": 1, "children": [
114-
{ "span": "text", "value": "down" }
115-
]},
114+
{
115+
"children": [{ "span": "text", "value": "down" }],
116+
"level": 1,
117+
"span": "emphasis"
118+
},
116119
{ "span": "text", "value": ", smithed." }
117120
]
118121
}
@@ -122,9 +125,11 @@ A nested example — a strong span wrapping a code span,
122125
``**`mdsmith fix`**`` — projects with no mode switch:
123126

124127
```json
125-
{ "span": "strong", "level": 2, "children": [
126-
{ "span": "code", "value": "mdsmith fix" }
127-
] }
128+
{
129+
"children": [{ "span": "code", "value": "mdsmith fix" }],
130+
"level": 2,
131+
"span": "strong"
132+
}
128133
```
129134

130135
Each content kind constrains its projection at schema-
@@ -175,7 +180,10 @@ mdsmith extract plan --format msgpack plan/166_x.md > plan.mp
175180

176181
### Worked example
177182

178-
A two-section kind schema and a conformant file:
183+
A two-section kind schema, a kind assignment, and a
184+
conformant file. Each section declares a `content:`
185+
entry. A section without one projects as an empty
186+
object:
179187

180188
```yaml
181189
# .mdsmith.yml
@@ -184,8 +192,15 @@ kinds:
184192
schema:
185193
sections:
186194
- heading: { regex: '^Tagline$' }
195+
content:
196+
- { kind: paragraph }
187197
- heading: { regex: '^VS Code$' }
188198
bind: vscode-description
199+
content:
200+
- { kind: paragraph }
201+
kind-assignment:
202+
- glob: ["docs/copy.md"]
203+
kinds: [product-copy]
189204
```
190205

191206
```markdown
@@ -210,9 +225,15 @@ emits:
210225

211226
```json
212227
{
213-
"frontmatter": { "title": "Product copy" },
214-
"tagline": { "text": "Mark down your ideas; smith them into shipping docs." },
215-
"vscode-description": { "text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code." }
228+
"frontmatter": {
229+
"title": "Product copy"
230+
},
231+
"tagline": {
232+
"text": "Mark down your ideas; smith them into shipping docs."
233+
},
234+
"vscode-description": {
235+
"text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code."
236+
}
216237
}
217238
```
218239

0 commit comments

Comments
 (0)