md-verified 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/dist/check.d.ts +2 -0
- package/dist/check.js +342 -0
- package/dist/src/assertions.d.ts +25 -0
- package/dist/src/assertions.js +84 -0
- package/dist/src/coerce.d.ts +19 -0
- package/dist/src/coerce.js +122 -0
- package/dist/src/covers.d.ts +38 -0
- package/dist/src/covers.js +59 -0
- package/dist/src/framework.d.ts +64 -0
- package/dist/src/framework.js +73 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +13 -0
- package/dist/src/mdast-gfm.d.ts +22 -0
- package/dist/src/mdast-gfm.js +90 -0
- package/dist/src/mermaid.d.ts +17 -0
- package/dist/src/mermaid.js +304 -0
- package/dist/src/parser.d.ts +26 -0
- package/dist/src/parser.js +423 -0
- package/dist/src/references.d.ts +33 -0
- package/dist/src/references.js +198 -0
- package/dist/src/report.d.ts +52 -0
- package/dist/src/report.js +278 -0
- package/dist/src/reviews.d.ts +19 -0
- package/dist/src/reviews.js +137 -0
- package/dist/src/runner.d.ts +94 -0
- package/dist/src/runner.js +353 -0
- package/dist/src/symbols.d.ts +15 -0
- package/dist/src/symbols.js +112 -0
- package/dist/src/types.d.ts +278 -0
- package/dist/src/types.js +27 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kristian Dupont
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# md-verified
|
|
2
|
+
|
|
3
|
+
Executable specifications from Markdown that nobody has to learn to read.
|
|
4
|
+
|
|
5
|
+
There is no Given/When/Then, no feature-file dialect, no custom renderer. A
|
|
6
|
+
specification is an ordinary `.md` file with ordinary tables, lists and Mermaid
|
|
7
|
+
diagrams. It looks native on GitHub, in VS Code, and in any Markdown viewer you
|
|
8
|
+
already use. A blockquote above each asset registers it with the test runner:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
> 🛠️ **Verified Data:** `orderTotals`
|
|
12
|
+
> **Schema:** `[itemsTotal: Currency, shipping: Currency, tax: Percentage, total: Currency]`
|
|
13
|
+
|
|
14
|
+
| Items Total | Shipping | Tax Rate | Total Owed |
|
|
15
|
+
| ----------- | -------- | -------- | ---------- |
|
|
16
|
+
| $10.00 | $5.00 | 10% | $16.00 |
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The blockquote renders as a callout. The table renders as a table. Nothing in
|
|
20
|
+
the document is inert markup that only a tool understands.
|
|
21
|
+
|
|
22
|
+
### Install
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
bun add -d md-verified # or: npm install -D md-verified
|
|
26
|
+
bunx md-verified 'docs/**/*.md' # or: npx md-verified 'docs/**/*.md'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Runs on **Bun** and **Node 24+**. The package itself uses only `node:`
|
|
30
|
+
builtins, so there is one code path rather than a compatibility layer.
|
|
31
|
+
|
|
32
|
+
Or from a clone:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
bun install
|
|
36
|
+
bun run check.ts examples/spec.md
|
|
37
|
+
bun test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The point is documents that stay *true*, which is not the same as documents
|
|
41
|
+
that are fully verified — see [writing documents that stay
|
|
42
|
+
true](./docs/writing.md) for what earns an anchor and what should stay prose.
|
|
43
|
+
There is an agent skill at
|
|
44
|
+
[`.claude/skills/verified-docs`](./.claude/skills/verified-docs/SKILL.md).
|
|
45
|
+
|
|
46
|
+
## How binding works
|
|
47
|
+
|
|
48
|
+
An anchor is a blockquote whose first line matches:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
> [glyph] **Verified <Label>:** `<id>`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The runner then takes the **immediately following block node** and binds it.
|
|
55
|
+
The label says what you expect; the node says what is actually there, and a
|
|
56
|
+
disagreement is reported rather than guessed at:
|
|
57
|
+
|
|
58
|
+
`Data` and `Table` bind to a GFM table, `Flow` and `Diagram` to a
|
|
59
|
+
` ```mermaid ` block, `Rules` and `Checklist` to a list. The full set of labels
|
|
60
|
+
is listed — and verified against the source — in
|
|
61
|
+
[docs/anchor-reference.md](./docs/anchor-reference.md).
|
|
62
|
+
|
|
63
|
+
Lookahead skips only the HTML comments this tool writes itself, so a document
|
|
64
|
+
that has already been annotated with failures still binds correctly next run.
|
|
65
|
+
Anything else between the anchor and the asset — a stray paragraph — is an
|
|
66
|
+
error, not something to search past.
|
|
67
|
+
|
|
68
|
+
Blockquotes that are not anchors are left alone, so your existing callouts keep
|
|
69
|
+
working.
|
|
70
|
+
|
|
71
|
+
### Schemas
|
|
72
|
+
|
|
73
|
+
The optional `**Schema:**` line names and types the columns positionally.
|
|
74
|
+
Handlers then receive real values instead of strings:
|
|
75
|
+
|
|
76
|
+
```markdown
|
|
77
|
+
> 🛠️ **Verified Data:** `orderTotals`
|
|
78
|
+
> **Schema:** `[itemsTotal: Currency, shipping: Currency, tax: Percentage, total: Currency]`
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`$10.00` arrives as `10`, `8.5%` as `0.085`. Each value is reachable by column
|
|
82
|
+
header *and* by schema field name, with the author's original text preserved on
|
|
83
|
+
`row.$raw`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
row['Items Total'] // 10
|
|
87
|
+
row.itemsTotal // 10
|
|
88
|
+
row.$raw['Items Total'] // '$10.00'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Built-in types cover currency, percentages, numbers, booleans, dates, JSON and
|
|
92
|
+
comma-separated lists; the complete table, with a worked example per type, is in
|
|
93
|
+
[docs/anchor-reference.md](./docs/anchor-reference.md). A trailing `?`
|
|
94
|
+
(`discount?: Currency`) lets a blank cell through as `null`. Register your own
|
|
95
|
+
with `verify.type()`.
|
|
96
|
+
|
|
97
|
+
Without a `Schema:` line, cells arrive as the raw text — which is what you want
|
|
98
|
+
when the document's exact formatting is part of the contract.
|
|
99
|
+
|
|
100
|
+
## Runtimes
|
|
101
|
+
|
|
102
|
+
The tool imports your `.verify.ts` glue at runtime, so what matters is how each
|
|
103
|
+
runtime handles TypeScript.
|
|
104
|
+
|
|
105
|
+
| | Glue TypeScript |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| Bun | Fully transformed. Everything works. |
|
|
108
|
+
| Node 24+ | Type **stripping** only — see below. |
|
|
109
|
+
|
|
110
|
+
Node strips types rather than transforming them, so a few TypeScript features
|
|
111
|
+
do not survive **in glue, or in anything glue imports as `.ts`**:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
enum, namespace, parameter properties (constructor(readonly x: string)), decorators
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Types, interfaces, generics, `as const`, `satisfies` and type-only exports are
|
|
118
|
+
all fine. In practice glue is plain functions, so this rarely bites — the case
|
|
119
|
+
that does is glue importing an `enum` from your application code.
|
|
120
|
+
|
|
121
|
+
The fix is one line:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
NODE_OPTIONS=--experimental-transform-types npx md-verified docs/thing.md
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The published bin uses a `node` shebang so `npx` works out of the box. To run it
|
|
128
|
+
under Bun instead — which has none of the above limits — use `bunx --bun
|
|
129
|
+
md-verified` or `bun node_modules/md-verified/dist/check.js`.
|
|
130
|
+
|
|
131
|
+
Deno is untested. It should work in principle, via `node:` compatibility and an
|
|
132
|
+
`npm:` specifier, but nothing here verifies that.
|
|
133
|
+
|
|
134
|
+
## Project layout
|
|
135
|
+
|
|
136
|
+
Put documents wherever you like — beside the code they describe, or in a `docs/`
|
|
137
|
+
tree. The tool does not care. Your `tsconfig.json` does, and it fails in a
|
|
138
|
+
different way for each.
|
|
139
|
+
|
|
140
|
+
**The rule: treat `.verify.ts` exactly like `.test.ts`.** It is TypeScript that
|
|
141
|
+
should be *checked* but not *shipped*, which is a problem your project has
|
|
142
|
+
already solved once.
|
|
143
|
+
|
|
144
|
+
Concretely, two configs — a wide one for checking and the editor, a narrow one
|
|
145
|
+
for building:
|
|
146
|
+
|
|
147
|
+
```jsonc
|
|
148
|
+
// tsconfig.json — what gets typechecked
|
|
149
|
+
{ "compilerOptions": { "noEmit": true }, "include": ["src", "docs"] }
|
|
150
|
+
|
|
151
|
+
// tsconfig.build.json — what gets compiled
|
|
152
|
+
{
|
|
153
|
+
"extends": "./tsconfig.json",
|
|
154
|
+
"compilerOptions": { "noEmit": false, "outDir": "dist", "rootDir": "src" },
|
|
155
|
+
"include": ["src"],
|
|
156
|
+
"exclude": ["**/*.verify.ts", "**/*.test.ts"]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Without that split you hit one of these:
|
|
161
|
+
|
|
162
|
+
| Layout | What goes wrong |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `docs/` beside `src/`, `"include": ["src"]` | Glue is **never typechecked**. A real type error in a handler is invisible — Bun strips types, so the document still passes. |
|
|
165
|
+
| `docs/` added to `include`, with `"rootDir": "src"` | `TS6059: File 'docs/x.verify.ts' is not under 'rootDir'`. |
|
|
166
|
+
| Co-located `src/**/*.verify.ts` | Typechecked correctly, but the glue **compiles into your production build** (`dist/billing/billing.verify.js`). |
|
|
167
|
+
|
|
168
|
+
The first is the dangerous one, because nothing tells you.
|
|
169
|
+
|
|
170
|
+
Glue can import application code however the rest of your project does —
|
|
171
|
+
`tsconfig` path aliases work, since Bun reads them.
|
|
172
|
+
|
|
173
|
+
## Glue code
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { verify, assert } from './src/index.ts';
|
|
177
|
+
|
|
178
|
+
// Once per data row.
|
|
179
|
+
verify.table('orderTotals', (row) => {
|
|
180
|
+
const actual = calculateTotal(row.itemsTotal, row.shipping, row.tax);
|
|
181
|
+
assert(actual === row.total, `expected ${row.total}, got ${actual}`);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Once per edge: { from, to, label, style, directed }.
|
|
185
|
+
verify.mermaid.edges('checkoutFlow', async (edge) => {
|
|
186
|
+
assert(await checkNavigation(edge.from, edge.to),
|
|
187
|
+
`illegal transition: ${edge.from} -> ${edge.to}`);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Once per list item, nested items included.
|
|
191
|
+
verify.list('settlementRules', (item) => {
|
|
192
|
+
assert(item.checked === settlesImmediately(item.text), 'drifted');
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Return normally to pass, throw to fail. Any assertion library works, including
|
|
197
|
+
none.
|
|
198
|
+
|
|
199
|
+
### Assertions
|
|
200
|
+
|
|
201
|
+
A failure message here is not a test log — it is **written into the Markdown
|
|
202
|
+
file** and read as documentation. Terminal-shaped output reads badly there:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
<!-- ERROR: row 1: expect(received).toBe(expected)
|
|
206
|
+
Expected: 15
|
|
207
|
+
Received: 16 -->
|
|
208
|
+
|
|
209
|
+
<!-- ERROR: row 1: total: expected 15, got 16 -->
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
So the built-ins stay few, and each produces one self-contained line phrased in
|
|
213
|
+
terms of the claim:
|
|
214
|
+
|
|
215
|
+
| | |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| `assert(cond, message)` | the escape hatch — use it whenever you can say it better |
|
|
218
|
+
| `equals(actual, expected, what?)` | `total: expected 16, got 15` |
|
|
219
|
+
| `oneOf(value, allowed, what?)` | `status: "archived" is not one of active, paused` |
|
|
220
|
+
| `covers(documented, actual, opts?)` | see [Completeness](#completeness) |
|
|
221
|
+
|
|
222
|
+
Third-party libraries keep working — a handler fails by throwing and that is
|
|
223
|
+
not going away. Multi-line messages keep their structure in the document rather
|
|
224
|
+
than being flattened onto one line.
|
|
225
|
+
|
|
226
|
+
| Registration | Handler receives |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `verify.table(id, fn)` | one `TableRow` per data row |
|
|
229
|
+
| `verify.table.all(id, fn)` | the whole `ParsedTable` |
|
|
230
|
+
| `verify.mermaid(id, fn)` | the whole `MermaidGraph` |
|
|
231
|
+
| `verify.mermaid.edges(id, fn)` | one `MermaidEdge` per edge |
|
|
232
|
+
| `verify.list(id, fn)` | one `ListItem` per item |
|
|
233
|
+
| `verify.list.all(id, fn)` | the whole `ParsedList` |
|
|
234
|
+
| `verify.type(name, fn)` | — registers a `Schema:` value type |
|
|
235
|
+
|
|
236
|
+
`MermaidGraph` carries `nodes`, `edges` and `subgraphs`, plus `node(id)`,
|
|
237
|
+
`from(id)`, `to(id)`, `hasEdge(a, b)`, `hasPath(a, b)`, `roots()` and
|
|
238
|
+
`leaves()`.
|
|
239
|
+
|
|
240
|
+
One anchor may carry both an `each` and an `all` handler — they answer
|
|
241
|
+
different questions about the same asset. Registering the same mode twice is
|
|
242
|
+
still an error, so typos are still caught.
|
|
243
|
+
|
|
244
|
+
Glue is located by, in order: `--glue`, a `<!-- verify: ./x.verify.ts -->` hint
|
|
245
|
+
in the document, then `<name>.verify.ts` beside the Markdown file.
|
|
246
|
+
|
|
247
|
+
## Completeness
|
|
248
|
+
|
|
249
|
+
Per-element handlers only ever check elements that exist. If the code grows a
|
|
250
|
+
fifth payment method and nobody adds a row, every row still passes and the
|
|
251
|
+
document is quietly wrong. `covers()` is the assertion that catches it:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
verify.mermaid('checkoutFlow', (graph) => {
|
|
255
|
+
covers(graph.edges.map((e) => `${e.from} -> ${e.to}`), allowedTransitions(), {
|
|
256
|
+
noun: 'transition',
|
|
257
|
+
missing: (t) => `${t} is allowed by checkNavigation but is not drawn`,
|
|
258
|
+
extra: false, // the per-edge handler already owns this direction
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
It throws once, listing every gap, so a single run tells you the whole story.
|
|
264
|
+
Options: `missing` / `extra` take a message function or `false` to allow that
|
|
265
|
+
direction, `duplicates` (default on) flags a key the document lists twice, and
|
|
266
|
+
`noun` names the thing in default messages.
|
|
267
|
+
|
|
268
|
+
This is the check worth reaching for first. A *missing* element is
|
|
269
|
+
machine-identifiable in a way a wrong one is not — the runner knows exactly
|
|
270
|
+
which row should exist, which is what makes the annotation actionable.
|
|
271
|
+
|
|
272
|
+
## Reference checking
|
|
273
|
+
|
|
274
|
+
Anchors verify the assets. A second pass verifies the prose around them: links
|
|
275
|
+
to files that have moved, in-document anchors that no longer resolve, and —
|
|
276
|
+
where you ask for it with a fragment — symbols that no longer exist.
|
|
277
|
+
|
|
278
|
+
```markdown
|
|
279
|
+
Computed by [`calculateTotal`](./checkout.ts#calculateTotal).
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
That renders as an ordinary link, and it carries everything needed to check it:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
examples/broken.md
|
|
286
|
+
✖ 12:24 broken symbol: ./checkout.ts#calculateTotals (no export named
|
|
287
|
+
`calculateTotals`) (did you mean `calculateTotal`?)
|
|
288
|
+
✖ 13:19 broken link: ./appendix.md (no such file)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Checked automatically: every link and image path, `#heading` anchors within the
|
|
292
|
+
document and into other Markdown files, and link definitions. Nothing implicit
|
|
293
|
+
is ever checked — bare inline code is not treated as a symbol, because
|
|
294
|
+
`$10.00`, `--write` and `[itemsTotal: Currency]` are all inline code in a
|
|
295
|
+
perfectly healthy spec. A document opts in by linking.
|
|
296
|
+
|
|
297
|
+
Only files you fragment-link are imported, and only to read their export names.
|
|
298
|
+
`--no-symbols` keeps the link checks but imports nothing; `--no-links` skips the
|
|
299
|
+
pass entirely.
|
|
300
|
+
|
|
301
|
+
## Reviews: the parts that cannot be executed
|
|
302
|
+
|
|
303
|
+
Most of a good document is prose — rationale, context, the reason a rule exists
|
|
304
|
+
at all. That is usually the part worth reading, and it is the part that rots
|
|
305
|
+
silently.
|
|
306
|
+
|
|
307
|
+
A review does not try to verify prose. It records which code a section
|
|
308
|
+
describes, and a digest of that code at the moment someone last read the two
|
|
309
|
+
together:
|
|
310
|
+
|
|
311
|
+
```markdown
|
|
312
|
+
> 👁️ **Reviewed:** `settlement`
|
|
313
|
+
> **Covers:** `../src/checkout.ts#paymentMethod`
|
|
314
|
+
> **Digest:** `1:3aaced165261`
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
When `paymentMethod` changes, the digest stops matching and the section is
|
|
318
|
+
flagged for a human to re-read. That is an attestation, not a proof — the
|
|
319
|
+
weaker claim, deliberately, because the alternative is either checking nothing
|
|
320
|
+
or pretending prose can be executed.
|
|
321
|
+
|
|
322
|
+
`--stamp` records the digest. It is **separate from `--write` on purpose**: a
|
|
323
|
+
stamp applied as a side effect of a normal run would attest to nothing.
|
|
324
|
+
|
|
325
|
+
The `1:` prefix is the digest format version. It exists so that a future change
|
|
326
|
+
to the algorithm can be reported as "re-stamp needed" rather than as "the code
|
|
327
|
+
changed", which would be a lie and would train people to stamp blindly.
|
|
328
|
+
|
|
329
|
+
Digests ignore line endings, so a mixed Windows/Unix team does not see
|
|
330
|
+
everything go stale.
|
|
331
|
+
|
|
332
|
+
Point `Covers:` at a **symbol** rather than a whole file. A file-level target is
|
|
333
|
+
invalidated by every unrelated edit in that file, and a review that cries wolf
|
|
334
|
+
gets stamped without being read. Symbol targets ignore edits elsewhere in the
|
|
335
|
+
file, and ignore changes to leading comments.
|
|
336
|
+
|
|
337
|
+
### Which documents describe this code?
|
|
338
|
+
|
|
339
|
+
The mapping from prose to code already lives in the documents, so there is no
|
|
340
|
+
need for a marker in the source:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
$ bun run check.ts docs/*.md --covering src/parser.ts
|
|
344
|
+
Reviews covering src/parser.ts:
|
|
345
|
+
docs/anchor-reference.md:14 binding ../src/parser.ts#parseMarkdown
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Run it on the files a change touched. Anything listed describes code that just
|
|
349
|
+
moved.
|
|
350
|
+
|
|
351
|
+
## Bi-directional state
|
|
352
|
+
|
|
353
|
+
`--write` folds the result of a run back into the document. The glyph changes,
|
|
354
|
+
and each failure is recorded as an HTML comment directly above the asset that
|
|
355
|
+
failed:
|
|
356
|
+
|
|
357
|
+
```markdown
|
|
358
|
+
> ❌ **Verified Flow:** `checkoutFlow` (Failed: 1 of 4)
|
|
359
|
+
|
|
360
|
+
<!-- ERROR: Cart -> Payment: illegal transition: Cart -> Payment -->
|
|
361
|
+
|
|
362
|
+
```mermaid
|
|
363
|
+
graph TD
|
|
364
|
+
Cart[Cart Page] --> Shipping[Shipping Info]
|
|
365
|
+
Cart --> Payment[Payment Info]
|
|
366
|
+
```
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Comments are invisible in every renderer, so the page still reads as prose. For
|
|
370
|
+
an agent, the failure text sits at exactly the place that has to change — no
|
|
371
|
+
separate log to correlate against the document.
|
|
372
|
+
|
|
373
|
+
Three properties this relies on, all covered by the test suite:
|
|
374
|
+
|
|
375
|
+
- **Surgical.** Rewriting splices the original source; it never re-serialises
|
|
376
|
+
the AST. Prose, table alignment and diagram indentation survive byte for byte.
|
|
377
|
+
- **Idempotent.** Annotating an annotated document is a no-op. (This is why the
|
|
378
|
+
comments carry case names rather than line numbers — writing the comments
|
|
379
|
+
shifts the lines they would otherwise cite. Exact lines are in `--json`.)
|
|
380
|
+
- **Reversible.** A later green run clears the marks; `--reset` returns the file
|
|
381
|
+
to its unrun state exactly.
|
|
382
|
+
|
|
383
|
+
## CLI
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
md-verified <file.md|glob> [...] [options]
|
|
387
|
+
|
|
388
|
+
--glue <path> Glue module to load
|
|
389
|
+
--write, -w Fold results back into the file
|
|
390
|
+
--report Print the annotated Markdown to stdout instead
|
|
391
|
+
--reset Return anchors to their unrun state
|
|
392
|
+
--json Machine-readable results, for agents and CI
|
|
393
|
+
--stamp Record reviews as read (never implied by --write)
|
|
394
|
+
--covering <p> List the reviews that cover a source file
|
|
395
|
+
--no-links Skip link, anchor and symbol checking
|
|
396
|
+
--no-symbols Skip symbol checking
|
|
397
|
+
--no-reviews Skip review staleness checking
|
|
398
|
+
--only <id> Run one anchor (repeatable)
|
|
399
|
+
--bail Stop at the first failure
|
|
400
|
+
--timeout <ms> Per-case timeout (default 5000, 0 disables)
|
|
401
|
+
--verbose, -v Show passing cases and stack frames
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Exit code is 0 only when every anchor passed, every anchor bound cleanly, every
|
|
405
|
+
reference resolved, and every review is current.
|
|
406
|
+
|
|
407
|
+
### How things fail
|
|
408
|
+
|
|
409
|
+
| | Reported as | Annotated into the document |
|
|
410
|
+
| --- | --- | --- |
|
|
411
|
+
| Handler throws | a failed case | yes |
|
|
412
|
+
| A cell will not coerce | a failed case, that row only | yes |
|
|
413
|
+
| Schema or diagram malformed | a failed anchor | yes |
|
|
414
|
+
| No handler registered | a skipped anchor | yes |
|
|
415
|
+
| Broken link or symbol | a problem, with `line:col` | no — it is prose, not an anchor |
|
|
416
|
+
| Covered code changed | a stale review | yes |
|
|
417
|
+
|
|
418
|
+
A row whose cell will not coerce never reaches your handler, and a whole-asset
|
|
419
|
+
handler is never run against a table that is silently missing rows.
|
|
420
|
+
|
|
421
|
+
## Under `bun test`
|
|
422
|
+
|
|
423
|
+
`loadDocument()` expands a document into cases without running them, so Bun's
|
|
424
|
+
test runner can own scheduling and reporting — one native test per table row:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { describe, test, expect } from 'bun:test';
|
|
428
|
+
import { loadDocument } from 'md-verified';
|
|
429
|
+
|
|
430
|
+
for (const file of ['docs/pricing.md', 'docs/limits.md']) {
|
|
431
|
+
const doc = await loadDocument(file);
|
|
432
|
+
|
|
433
|
+
describe(doc.file, () => {
|
|
434
|
+
test('references resolve', () => expect(doc.problems).toEqual([]));
|
|
435
|
+
test('reviews current', () =>
|
|
436
|
+
expect(doc.reviews.filter((r) => r.status === 'failed')).toEqual([]));
|
|
437
|
+
|
|
438
|
+
for (const suite of doc.suites) {
|
|
439
|
+
describe(suite.id, () => {
|
|
440
|
+
for (const c of suite.cases) test(c.name, () => c.run());
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Use `loadDocument` rather than importing glue files directly. Anchor ids are
|
|
448
|
+
unique per *document*, and Bun shares module state across test files, so two
|
|
449
|
+
documents that both use `prices` would otherwise collide. `loadDocument`
|
|
450
|
+
isolates the registry per document; cases keep their own handler afterwards.
|
|
451
|
+
|
|
452
|
+
`planCases()` is the lower-level primitive if you need it. See
|
|
453
|
+
[`spec.test.ts`](./spec.test.ts).
|
|
454
|
+
|
|
455
|
+
## Layout
|
|
456
|
+
|
|
457
|
+
| File | |
|
|
458
|
+
| --- | --- |
|
|
459
|
+
| [`src/parser.ts`](./src/parser.ts) | Markdown → anchors: the AST walk, lookahead binding, schemas |
|
|
460
|
+
| [`src/mermaid.ts`](./src/mermaid.ts) | Mermaid flowcharts → nodes and edges |
|
|
461
|
+
| [`src/framework.ts`](./src/framework.ts) | The `verify` registry |
|
|
462
|
+
| [`src/runner.ts`](./src/runner.ts) | Execution, case planning, glue resolution |
|
|
463
|
+
| [`src/report.ts`](./src/report.ts) | Terminal output and the Markdown rewrite |
|
|
464
|
+
| [`src/references.ts`](./src/references.ts) | Link, anchor and symbol checking |
|
|
465
|
+
| [`src/covers.ts`](./src/covers.ts) | Set assertions for completeness |
|
|
466
|
+
| [`src/assertions.ts`](./src/assertions.ts) | `assert`, `equals`, `oneOf` |
|
|
467
|
+
| [`src/reviews.ts`](./src/reviews.ts) | Review staleness and digests |
|
|
468
|
+
| [`src/symbols.ts`](./src/symbols.ts) | Static symbol lookup, via the TS compiler API |
|
|
469
|
+
| [`src/coerce.ts`](./src/coerce.ts) | `Schema:` value types |
|
|
470
|
+
| [`check.ts`](./check.ts) | CLI |
|
|
471
|
+
| [`examples/spec.md`](./examples/spec.md) | A specification that passes |
|
|
472
|
+
| [`examples/broken.md`](./examples/broken.md) | The same spec, drifted, for the failure path |
|
|
473
|
+
| [`docs/writing.md`](./docs/writing.md) | What earns an anchor, and what does not |
|
|
474
|
+
| [`docs/anchor-reference.md`](./docs/anchor-reference.md) | The vocabulary, verified against the source |
|
|
475
|
+
|
|
476
|
+
## Prototype limits
|
|
477
|
+
|
|
478
|
+
- Mermaid support covers the flowchart/graph family. Sequence and class
|
|
479
|
+
diagrams parse only as far as their `A --> B` statements go.
|
|
480
|
+
- Glue modules are loaded with a cache-busting query string, so a long-lived
|
|
481
|
+
process re-registering the same file will accumulate module instances.
|
|
482
|
+
- Anchor ids must be unique per document. The CLI clears the registry between
|
|
483
|
+
files; in-process, use `loadDocument()`.
|
|
484
|
+
- Node's type stripping cannot handle `enum`, `namespace`, parameter properties
|
|
485
|
+
or decorators in glue. See [Runtimes](#runtimes); `NODE_OPTIONS=--experimental-transform-types`
|
|
486
|
+
lifts the restriction.
|
|
487
|
+
- Deno is untested.
|
|
488
|
+
- Symbol lookup reads files rather than importing them, so nothing in the
|
|
489
|
+
checked project is executed and type-only exports are visible. The trade-off
|
|
490
|
+
is that `export * from './x'` is not followed.
|
|
491
|
+
- A reference with no definition (`[text][missing]`) cannot be flagged:
|
|
492
|
+
CommonMark leaves it as literal text, so there is no node in the tree. The
|
|
493
|
+
reader does see the broken brackets.
|
package/dist/check.d.ts
ADDED