markform 0.1.1 → 0.1.2

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.
Files changed (36) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +338 -71
  3. package/SPEC.md +2779 -0
  4. package/dist/ai-sdk.d.mts +2 -2
  5. package/dist/ai-sdk.mjs +5 -3
  6. package/dist/{apply-BQdd-fdx.mjs → apply-BfAGTHMh.mjs} +837 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-B3NVm6zL.mjs} +1349 -422
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +116 -19
  13. package/dist/index.mjs +5 -3
  14. package/dist/session-Bqnwi9wp.mjs +110 -0
  15. package/dist/session-DdAtY2Ni.mjs +4 -0
  16. package/dist/shared-D7gf27Tr.mjs +3 -0
  17. package/dist/shared-N_s1M-_K.mjs +176 -0
  18. package/dist/src-BXRkGFpG.mjs +7587 -0
  19. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
  20. package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
  22. package/examples/movie-research/movie-research-basic.form.md +164 -0
  23. package/examples/movie-research/movie-research-deep.form.md +486 -0
  24. package/examples/movie-research/movie-research-minimal.form.md +73 -0
  25. package/examples/simple/simple-mock-filled.form.md +17 -13
  26. package/examples/simple/simple-skipped-filled.form.md +32 -9
  27. package/examples/simple/simple-with-skips.session.yaml +102 -143
  28. package/examples/simple/simple.form.md +13 -13
  29. package/examples/simple/simple.session.yaml +80 -69
  30. package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
  31. package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
  32. package/examples/startup-research/startup-research.form.md +1 -1
  33. package/package.json +9 -5
  34. package/dist/src-Cs4_9lWP.mjs +0 -2151
  35. package/examples/political-research/political-research.form.md +0 -233
  36. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
package/SPEC.md ADDED
@@ -0,0 +1,2779 @@
1
+ # Markform Specification
2
+
3
+ **Version:** MF/0.1 (draft)
4
+
5
+ ## Overview
6
+
7
+ Markform is a format, data model, and editing API for agent-friendly, human-readable
8
+ text forms. The format is a superset of Markdown based on
9
+ [Markdoc](https://github.com/markdoc/markdoc) that is easily readable by agents and
10
+ humans.
11
+
12
+ Key design principles:
13
+
14
+ - **Form content, structure, and field values in one text file** for better context
15
+ engineering
16
+
17
+ - **Incremental filling** where agents or humans can iterate until the form is complete
18
+
19
+ - **Flexible validation** at multiple scopes (field/group/form), including declarative
20
+ constraints and external hook validators
21
+
22
+ This specification defines the portable, language-agnostic elements of Markform:
23
+
24
+ - **Layer 1: Syntax** — The `.form.md` file format
25
+
26
+ - **Layer 2: Form Data Model** — Precise data structures for forms, fields, and values
27
+
28
+ - **Layer 3: Validation & Form Filling** — Rules for validation and form manipulation
29
+
30
+ - **Layer 4: Tool API & Interfaces** — Operations for agents and humans to interact with
31
+ forms
32
+
33
+ ## Revision History
34
+
35
+ | Version | Date | Code Version | Summary |
36
+ | --- | --- | --- | --- |
37
+ | MF/0.1 (draft) | 2025-12-27 | v0.1.2 | Initial draft. Defines core syntax, data model, validation pipeline, and tool API. |
38
+
39
+ ## Specification Terminology
40
+
41
+ The keywords MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and
42
+ OPTIONAL are to be interpreted as described in
43
+ [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
44
+
45
+ In this specification we use these keywords:
46
+
47
+ | Term | Definition |
48
+ | --- | --- |
49
+ | *required* | A constraint that MUST be satisfied. Enforced by engine validation; violations produce errors. |
50
+ | *recommended* | A convention that SHOULD be followed for consistency and best practices. Not enforced by the engine; violations do not produce errors. |
51
+
52
+ ### Markform Terminology
53
+
54
+ **Form concepts:**
55
+
56
+ | Term | Definition |
57
+ | --- | --- |
58
+ | **Field** | A single data entry point within a form. Fields have a kind (type), label, and optional constraints. |
59
+ | **Kind** | The type of a field. One of: `string`, `number`, `string_list`, `checkboxes`, `single_select`, `multi_select`, `url`, `url_list`. Determines the field's value structure, input behavior, and validation rules. |
60
+ | **Field group** | A container that organizes related fields together. Groups have an id, optional title, and may have custom validators. Currently (MF/0.1), groups contain only fields (they are not nested groups). |
61
+ | **Template form** | A form with no values filled in (schema only). Starting point for filling. |
62
+ | **Incomplete form** | A form with some values but not yet complete or valid. |
63
+ | **Completed form** | A form with all required fields filled and passing validation. |
64
+
65
+ **Checkbox modes:**
66
+
67
+ | Term | Definition |
68
+ | --- | --- |
69
+ | **Simple checkbox** | Checkbox mode with 2 states: `todo` and `done`. GFM-compatible. |
70
+ | **Multi checkbox** | Checkbox mode with 5 states: `todo`, `done`, `incomplete`, `active`, `na`. Default mode. |
71
+ | **Explicit checkbox** | Checkbox mode requiring explicit `yes`/`no` answer for each option. No implicit "unchecked = no". |
72
+
73
+ **Field state concepts:**
74
+
75
+ | Term | Definition |
76
+ | --- | --- |
77
+ | **AnswerState** | The action taken on a field: `unanswered` (no action), `answered` (has value), `skipped` (explicitly bypassed), `aborted` (explicitly abandoned). |
78
+ | **ProgressState** | Form-level completion status: `empty`, `incomplete`, `invalid`, `complete`. |
79
+ | **ProgressCounts** | Rollup counts with three orthogonal dimensions: AnswerState (unanswered/answered/skipped/aborted), Validity (valid/invalid), Value presence (empty/filled). |
80
+ | **FieldProgress** | Per-field progress info including `answerState`, `valid`, `empty`, and optional `checkboxProgress`. |
81
+
82
+ **Execution concepts:**
83
+
84
+ | Term | Definition |
85
+ | --- | --- |
86
+ | **Harness loop** | The execution wrapper that manages step-by-step form filling, tracking state and suggesting next actions. Also called just "harness" or "loop" — these refer to the same component. |
87
+ | **Session** | A single execution run from template form to completed form (or abandonment). |
88
+ | **Turn** | One iteration of the harness loop: inspect → recommend → apply patches → validate. |
89
+ | **Patch** | A single atomic change operation applied to form values (e.g., setting a string field, toggling checkboxes). |
90
+
91
+ **Testing and files:**
92
+
93
+ | Term | Definition |
94
+ | --- | --- |
95
+ | **Session transcript** | YAML serialization of a session's turns for golden testing (`.session.yaml`). |
96
+ | **Completed mock** | A pre-filled completed form file used in mock mode to provide deterministic "correct" values for testing. |
97
+ | **Sidecar file** | A companion file with the same basename but different extension (e.g., `X.form.md` → `X.valid.ts`). |
98
+
99
+ * * *
100
+
101
+ ## Layer 1: Syntax
102
+
103
+ Defines the **Markform document format** (`.form.md`) containing the form schema and
104
+ current filled-in state.
105
+ Built on [Markdoc’s tag syntax specification][markdoc-spec] and
106
+ [syntax conventions][markdoc-syntax].
107
+
108
+ #### File Format
109
+
110
+ - **Extension:** `.form.md` (*required*)
111
+
112
+ - **Frontmatter:** (*required*) YAML with a top-level `markform` object containing
113
+ version and derived metadata (see [Markdoc Frontmatter][markdoc-frontmatter]).
114
+
115
+ **Frontmatter structure (MF/0.1):**
116
+
117
+ ```yaml
118
+ ---
119
+ markform:
120
+ spec: MF/0.1
121
+ form_summary: { ... } # derived: structure summary
122
+ form_progress: { ... } # derived: progress summary
123
+ form_state: complete|incomplete|invalid|empty # derived: overall progress state
124
+ ---
125
+ ```
126
+
127
+ **Behavioral rules (*required*):**
128
+
129
+ - *required:* `form_summary`, `form_progress`, and `form_state` are derived,
130
+ engine-owned metadata
131
+
132
+ - *required:* The engine recomputes and overwrites these on every serialize/canonicalize
133
+
134
+ - *required:* They are not authoritative—the source of truth is the body schema + values
135
+
136
+ - *required:* On parse, existing `form_summary`/`form_progress`/`form_state` values are
137
+ ignored; fresh summaries are computed from the parsed body
138
+
139
+ See [StructureSummary and ProgressSummary](#structuresummary-and-progresssummary) in the
140
+ Data Model section for complete type definitions.
141
+
142
+ #### ID Conventions
143
+
144
+ IDs are organized into **two scoping levels** with different uniqueness requirements:
145
+
146
+ **1. Structural IDs** (form, field-group, field):
147
+
148
+ - *required:* Must be globally unique across the entire document
149
+
150
+ - *required:* Enforced by engine validation at parse time (duplicate = error)
151
+
152
+ - *recommended:* Use `snake_case` (e.g., `company_info`, `revenue_m`)
153
+
154
+ **2. Option IDs** (within single-select, multi-select, checkboxes):
155
+
156
+ - *required:* Must be unique within the containing field (field-scoped)
157
+
158
+ - *required:* Enforced by engine validation at parse time
159
+
160
+ - *recommended:* Use a slugified version of the option label (e.g., `ten_k`, `bullish`)
161
+
162
+ - Use [Markdoc’s annotation shorthand][markdoc-attributes] `{% #my_id %}` after list
163
+ items
164
+
165
+ - This allows reusing common option patterns across multiple fields without renaming
166
+ (e.g., `[ ] 10-K {% #ten_k %}` can appear in multiple checkbox fields)
167
+
168
+ - When referencing an option externally (patches, doc blocks), use qualified form:
169
+ `{fieldId}.{optionId}` (e.g., `docs_reviewed.ten_k`)
170
+
171
+ **Markdoc compatibility:** Markdoc’s `{% #id %}` shorthand simply sets an `id` attribute
172
+ on the element—Markdoc itself does not enforce uniqueness (see [Markdoc Attributes]).
173
+ Markform defines its own scoping rules where option IDs are field-scoped.
174
+
175
+ **3. Documentation blocks:**
176
+
177
+ - Doc blocks do not have their own IDs
178
+
179
+ - *required:* Identified by `(ref, tag)` combination, which must be unique (e.g., only
180
+ one `{% instructions ref="foo" %}` allowed)
181
+
182
+ - Duplicate `(ref, tag)` pairs are an error
183
+
184
+ - Multiple doc blocks can reference the same target with different tags (e.g., both `{%
185
+ description ref="foo" %}` and `{% instructions ref="foo" %}` are allowed)
186
+
187
+ - To reference an option, use qualified form: `ref="{fieldId}.{optionId}"`
188
+
189
+ **General conventions (*recommended*, not enforced):**
190
+
191
+ - IDs use `snake_case` (e.g., `company_info`, `ten_k`)
192
+
193
+ - Tag names use `kebab-case` (Markdoc convention, e.g., `string-field`)
194
+
195
+ #### Structural Tags
196
+
197
+ - `form` — the root container
198
+
199
+ - `field-group` — containers for fields or nested groups
200
+
201
+ #### Field Tags
202
+
203
+ Custom tags are defined following [Markdoc tag conventions][markdoc-tags]. See
204
+ [Markdoc Config][markdoc-config] for how to register custom tags.
205
+
206
+ Each field tag maps to a **kind** value that identifies the field type.
207
+ The `kind` property is reserved exclusively for field types—it identifies what type of
208
+ field a `Field` or `FieldValue` represents.
209
+ (In TypeScript, the type is `FieldKind`.)
210
+
211
+ | Tag | Kind | Description |
212
+ | --- | --- | --- |
213
+ | `string-field` | `'string'` | String value; optional `required`, `pattern`, `minLength`, `maxLength` |
214
+ | `number-field` | `'number'` | Numeric value; optional `min`, `max`, `integer` |
215
+ | `string-list` | `'string_list'` | Array of strings (open-ended list); supports `minItems`, `maxItems`, `itemMinLength`, `itemMaxLength`, `uniqueItems` |
216
+ | `single-select` | `'single_select'` | Select one option from enumerated list |
217
+ | `multi-select` | `'multi_select'` | Select multiple options; supports `minSelections`, `maxSelections` constraints |
218
+ | `checkboxes` | `'checkboxes'` | Stateful checklist; supports `checkboxMode` with values `multi` (5 states), `simple` (2 states), or `explicit` (yes/no); optional `minDone` for completion threshold |
219
+ | `url-field` | `'url'` | Single URL value with built-in format validation |
220
+ | `url-list` | `'url_list'` | Array of URLs (for citations, sources, references); supports `minItems`, `maxItems`, `uniqueItems` |
221
+
222
+ **Note on `pattern`:** The `pattern` attribute accepts a JavaScript-compatible regular
223
+ expression string (without delimiters).
224
+ Example: `pattern="^[A-Z]{1,5}$"` for a ticker symbol.
225
+
226
+ **Common attributes (all field types):**
227
+
228
+ | Attribute | Type | Description |
229
+ | --- | --- | --- |
230
+ | `id` | string | Required. Unique identifier (snake_case) |
231
+ | `label` | string | Required. Human-readable field name |
232
+ | `required` | boolean | Whether field must be filled for form completion |
233
+ | `role` | string | Target actor (e.g., `"user"`, `"agent"`). See role-filtered completion |
234
+
235
+ The `role` attribute enables multi-actor workflows where different fields are assigned
236
+ to different actors.
237
+ When running the fill harness with `targetRoles`, only fields matching those roles are
238
+ considered for completion.
239
+ See **Role-filtered completion** in the ProgressState Definitions section.
240
+
241
+ #### Option Syntax (Markform-specific)
242
+
243
+ Markdoc does **not** natively support GFM-style task list checkbox syntax.
244
+ The `[ ]` and `[x]` markers are **Markform-specific syntax** parsed within tag content.
245
+
246
+ All selection field types use checkbox-style markers for broad markdown renderer
247
+ compatibility:
248
+
249
+ | Field Type | Marker | Meaning | Example |
250
+ | --- | --- | --- | --- |
251
+ | `checkboxes` | `[ ]` | Unchecked / todo / unfilled | `- [ ] Item {% #item_id %}` |
252
+ | `checkboxes` | `[x]` | Checked / done | `- [x] Item {% #item_id %}` |
253
+ | `checkboxes` | `[/]` | Incomplete (multi only) | `- [/] Item {% #item_id %}` |
254
+ | `checkboxes` | `[*]` | Active (multi only) | `- [*] Item {% #item_id %}` |
255
+ | `checkboxes` | `[-]` | Not applicable (multi only) | `- [-] Item {% #item_id %}` |
256
+ | `checkboxes` | `[y]` | Yes (explicit only) | `- [y] Item {% #item_id %}` |
257
+ | `checkboxes` | `[n]` | No (explicit only) | `- [n] Item {% #item_id %}` |
258
+ | `single-select` | `[ ]` | Unselected | `- [ ] Option {% #opt_id %}` |
259
+ | `single-select` | `[x]` | Selected (exactly one) | `- [x] Option {% #opt_id %}` |
260
+ | `multi-select` | `[ ]` | Unselected | `- [ ] Option {% #opt_id %}` |
261
+ | `multi-select` | `[x]` | Selected | `- [x] Option {% #opt_id %}` |
262
+
263
+ **Note:** `single-select` enforces that exactly one option has `[x]`. The distinction
264
+ between `single-select` and `multi-select` is in the tag name, not the marker syntax.
265
+
266
+ The `{% #id %}` annotation **is** native Markdoc syntax (see
267
+ [Attributes][markdoc-attributes]).
268
+
269
+ **Implementation note:** Markform’s parser extracts list items from the tag’s children,
270
+ then applies regex matching to detect markers.
271
+ This is custom parsing layered on top of Markdoc’s AST traversal.
272
+
273
+ #### Checkbox State Tokens
274
+
275
+ Markform supports three checkbox modes:
276
+
277
+ **`checkboxMode="multi"`** (default) — 5 states for rich workflow tracking:
278
+
279
+ | Token | State | Notes |
280
+ | --- | --- | --- |
281
+ | `[ ]` | todo | Not started. Standard GFM ([spec][gfm-tasklists], [GitHub docs][github-tasklists]) |
282
+ | `[x]` | done | Completed. Standard GFM |
283
+ | `[/]` | incomplete | Work started but not finished. Obsidian convention ([discussion][obsidian-tasks-discussion]) |
284
+ | `[*]` | active | Currently being worked on (current focus). Useful for agents to indicate which step they're executing |
285
+ | `[-]` | na | Not applicable / skipped. Obsidian convention ([guide][obsidian-tasks-guide]) |
286
+
287
+ **`checkboxMode="simple"`** — 2 states for GFM compatibility:
288
+
289
+ | Token | State | Notes |
290
+ | --- | --- | --- |
291
+ | `[ ]` | todo | Unchecked |
292
+ | `[x]` | done | Checked |
293
+
294
+ **`checkboxMode="explicit"`** — Requires explicit yes/no answer (no implicit “unchecked
295
+ = no”):
296
+
297
+ | Token | Value | Notes |
298
+ | --- | --- | --- |
299
+ | `[ ]` | unfilled | Not yet answered (invalid if required) |
300
+ | `[y]` | yes | Explicit affirmative |
301
+ | `[n]` | no | Explicit negative |
302
+
303
+ Use `checkboxMode` attribute to select mode:
304
+
305
+ - `checkboxMode="multi"` (default) — 5 states for workflow tracking
306
+
307
+ - `checkboxMode="simple"` — 2 states for GFM compatibility; use `minDone` to control
308
+ completion threshold
309
+
310
+ - `checkboxMode="explicit"` — Requires explicit yes/no, validates all options answered
311
+
312
+ **The `minDone` attribute (for `simple` mode):**
313
+
314
+ Controls how many options must be `done` for a required checkbox field to be complete.
315
+ Type: `integer`, default: `-1` (require all).
316
+
317
+ - **`minDone=-1` (default):** All options must be `done` (strict completion)
318
+
319
+ - **`minDone=0`:** No minimum; any state is valid (effectively optional even when
320
+ `required`)
321
+
322
+ - **`minDone=1`:** At least one option must be `done`
323
+
324
+ - **`minDone=N`:** At least N options must be `done`
325
+
326
+ Example with partial completion allowed:
327
+ ```md
328
+ {% checkboxes id="optional_tasks" label="Optional tasks" required=true minDone=1 %}
329
+ - [ ] Task A {% #task_a %}
330
+ - [ ] Task B {% #task_b %}
331
+ - [ ] Task C {% #task_c %}
332
+ {% /checkboxes %}
333
+ ```
334
+
335
+ **Note:** `minDone` only applies to `simple` mode.
336
+ For `multi` mode, completion requires all options in terminal states (`done` or `na`).
337
+ For `explicit` mode, all options must have explicit `yes` or `no` answers.
338
+
339
+ **Checkbox mode and `required` attribute:**
340
+
341
+ Fields with `checkboxMode="explicit"` are inherently required—every option must receive
342
+ an explicit `yes` or `no` answer.
343
+ The parser enforces this:
344
+
345
+ - Omitting `required` → defaults to `true`
346
+
347
+ - Setting `required=true` → redundant but valid
348
+
349
+ - Setting `required=false` → **parse error** (explicit mode cannot be optional)
350
+
351
+ | Checkbox Mode | `required` Effect |
352
+ | --- | --- |
353
+ | `simple` | Optional by default; when required, `minDone` threshold must be met |
354
+ | `multi` | Optional by default; when required, all options in terminal state (`done`/`na`) |
355
+ | `explicit` | Always required (enforced by parser; `required=false` is an error) |
356
+
357
+ **Distinction between `incomplete` and `active`:**
358
+
359
+ - `incomplete` (`[/]`): Work has started on this item (may be paused)
360
+
361
+ - `active` (`[*]`): This item is the current focus right now (useful for showing where
362
+ an agent is in a multi-step workflow)
363
+
364
+ #### Documentation Blocks
365
+
366
+ Documentation blocks provide contextual help attached to form elements and each has its
367
+ own tag:
368
+
369
+ ```md
370
+ {% description ref="<target_id>" %}
371
+ Markdown content here...
372
+ {% /description %}
373
+
374
+ {% instructions ref="<target_id>" %}
375
+ Step-by-step guidance...
376
+ {% /instructions %}
377
+
378
+ {% notes ref="<target_id>" %}
379
+ Additional notes...
380
+ {% /notes %}
381
+
382
+ {% examples ref="<target_id>" %}
383
+ Example values or usage...
384
+ {% /examples %}
385
+ ```
386
+
387
+ - `ref` (*required*): References the ID of a form, group, field, or option
388
+
389
+ - The Markdoc tag name determines the documentation `tag` property (`description`,
390
+ `instructions`, `notes`, `examples`, or `documentation` for general content)
391
+
392
+ **Placement rules (MF/0.1):**
393
+
394
+ - Doc blocks MAY appear inside `form` and `field-group` as direct children
395
+
396
+ - *required:* Parser will reject doc blocks that appear inside field tag bodies (doc
397
+ blocks MUST NOT be nested inside a field tag)
398
+
399
+ - For field-level docs: place immediately after the field block (as a sibling within the
400
+ group)
401
+
402
+ - Canonical serialization places doc blocks immediately after the referenced element
403
+
404
+ This keeps parsing simple: field value extraction only needs to find the `value` fence
405
+ without filtering out nested doc blocks.
406
+
407
+ **Identification:**
408
+
409
+ - Doc blocks do not have their own IDs
410
+
411
+ - *required:* `(ref, tag)` combination must be unique (e.g., only one `{% instructions
412
+ ref="foo" %}`)
413
+
414
+ - Multiple doc blocks with different tags can reference the same target (e.g., both `{%
415
+ description ref="foo" %}` and `{% instructions ref="foo" %}` are allowed)
416
+
417
+ #### Field Values
418
+
419
+ Values are encoded differently based on field type.
420
+ The `fence` node with `language="value"` is used for scalar values (see
421
+ [Markdoc Nodes][markdoc-nodes] for fence handling).
422
+
423
+ ##### String Fields
424
+
425
+ **Empty:** Omit the body entirely:
426
+ ```md
427
+ {% string-field id="company_name" label="Company name" required=true %}{% /string-field %}
428
+ ```
429
+
430
+ **Filled:** Value in a fenced code block with language `value`:
431
+ ````md
432
+ {% string-field id="company_name" label="Company name" required=true %}
433
+ ```value
434
+ ACME Corp
435
+ ````
436
+ {% /string-field %}
437
+ ````
438
+
439
+ ##### Number Fields
440
+
441
+ **Empty:**
442
+ ```md
443
+ {% number-field id="revenue_m" label="Revenue (millions)" %}{% /number-field %}
444
+ ````
445
+
446
+ **Filled:** Numeric value as string in fence (parsed to number):
447
+ ````md
448
+ {% number-field id="revenue_m" label="Revenue (millions)" %}
449
+ ```value
450
+ 1234.56
451
+ ````
452
+ {% /number-field %}
453
+ ````
454
+
455
+ ##### Single-Select Fields
456
+
457
+ Values are encoded **inline** via `[x]` marker—at most one option may be selected (if
458
+ `required=true`, exactly one must be selected):
459
+ ```md
460
+ {% single-select id="rating" label="Rating" %}
461
+ - [ ] Bullish {% #bullish %}
462
+ - [x] Neutral {% #neutral %}
463
+ - [ ] Bearish {% #bearish %}
464
+ {% /single-select %}
465
+ ````
466
+
467
+ Option IDs are scoped to the field—reference as `rating.bullish`, `rating.neutral`, etc.
468
+
469
+ ##### Multi-Select Fields
470
+
471
+ Values are encoded **inline** via `[x]` markers—no separate value fence:
472
+ ```md
473
+ {% multi-select id="categories" label="Categories" %}
474
+ - [x] Technology {% #tech %}
475
+ - [ ] Healthcare {% #health %}
476
+ - [x] Finance {% #finance %}
477
+ {% /multi-select %}
478
+ ```
479
+
480
+ ##### Checkboxes Fields
481
+
482
+ Values are encoded **inline** via state markers—no separate value fence:
483
+ ```md
484
+ {% checkboxes id="tasks" label="Tasks" %}
485
+ - [x] Review docs {% #review %}
486
+ - [/] Write tests {% #tests %}
487
+ - [*] Run CI {% #ci %}
488
+ - [ ] Deploy {% #deploy %}
489
+ - [-] Manual QA {% #manual_qa %}
490
+ {% /checkboxes %}
491
+ ```
492
+
493
+ For simple two-state checkboxes:
494
+ ```md
495
+ {% checkboxes id="agreements" label="Agreements" checkboxMode="simple" %}
496
+ - [x] I agree to terms {% #terms %}
497
+ - [ ] Subscribe to newsletter {% #newsletter %}
498
+ {% /checkboxes %}
499
+ ```
500
+
501
+ For explicit yes/no checkboxes (requires answer for each):
502
+ ```md
503
+ {% checkboxes id="risk_factors" label="Risk Assessment" checkboxMode="explicit" required=true %}
504
+ - [y] Market volatility risk assessed {% #market %}
505
+ - [n] Regulatory risk assessed {% #regulatory %}
506
+ - [ ] Currency risk assessed {% #currency %}
507
+ {% /checkboxes %}
508
+ ```
509
+
510
+ In this example, `risk_factors.currency` is unfilled (`[ ]`) and will fail validation
511
+ because `checkboxMode="explicit"` requires all options to have explicit `[y]` or `[n]`
512
+ answers.
513
+
514
+ ##### String-List Fields
515
+
516
+ String-list fields represent open-ended arrays of user-provided strings.
517
+ Items do not have individual IDs—the field has an ID and items are positional strings.
518
+
519
+ **Empty:**
520
+ ```md
521
+ {% string-list id="key_commitments" label="Key commitments" minItems=1 %}{% /string-list %}
522
+ ```
523
+
524
+ **Filled:** One item per non-empty line in the value fence:
525
+ ````md
526
+ {% string-list id="key_commitments" label="Key commitments" minItems=1 %}
527
+ ```value
528
+ Ship v1.0 by end of Q1
529
+ Complete security audit
530
+ Migrate legacy users to new platform
531
+ ````
532
+ {% /string-list %}
533
+ ````
534
+
535
+ **Parsing rules:**
536
+ - Split fence content by `\n`
537
+ - For each line: trim leading/trailing whitespace, ignore empty lines
538
+ - Result is `string[]`
539
+
540
+ **Serialization rules (canonical):**
541
+ - Emit one item per line (no bullets)
542
+ - Use `process=false` only if any item contains Markdoc syntax
543
+ - Empty arrays serialize as empty tag (no value fence)
544
+
545
+ **Example with constraints:**
546
+ ```md
547
+ {% string-list
548
+ id="top_risks"
549
+ label="Top 5 risks (specific, not generic)"
550
+ required=true
551
+ minItems=5
552
+ itemMinLength=10
553
+ %}
554
+ ```value
555
+ Supply chain disruption from single-source vendor
556
+ Key engineer departure risk (bus factor = 1)
557
+ Regulatory changes in EU market
558
+ Competitor launching similar feature in Q2
559
+ Customer concentration risk (top 3 = 60% revenue)
560
+ ````
561
+ {% /string-list %}
562
+
563
+ {% instructions ref="top_risks" %} One risk per line.
564
+ Be specific (company- or product-specific), not generic.
565
+ Minimum 5; include more if needed.
566
+ {% /instructions %}
567
+ ````
568
+
569
+ **Note:** The doc block is a sibling placed after the field, not nested inside it.
570
+
571
+ ##### Field State Attributes
572
+
573
+ Fields can have a `state` attribute to indicate skip or abort status. The attribute is
574
+ serialized on the opening tag:
575
+
576
+ **Skipped field (no reason):**
577
+ ```md
578
+ {% string-field id="optional_notes" label="Optional notes" state="skipped" %}{% /string-field %}
579
+ ````
580
+
581
+ **Aborted field (no reason):**
582
+ ```md
583
+ {% string-field id="company_name" label="Company name" required=true state="aborted" %}{% /string-field %}
584
+ ```
585
+
586
+ **State attribute values:**
587
+
588
+ - `state="skipped"`: Field was explicitly skipped via `skip_field` patch
589
+
590
+ - `state="aborted"`: Field was explicitly aborted via `abort_field` patch
591
+
592
+ **Serialization with sentinel values and reasons (markform-254):**
593
+
594
+ When a field has a skip or abort state AND a reason was provided via the patch, the
595
+ reason is embedded in the sentinel value using parentheses:
596
+
597
+ ````md
598
+ {% string-field id="competitor_analysis" label="Competitor analysis" state="skipped" %}
599
+ ```value
600
+ %SKIP% (Information not publicly available)
601
+ ````
602
+ {% /string-field %}
603
+ ````
604
+
605
+ ```md
606
+ {% number-field id="projected_revenue" label="Projected revenue" state="aborted" %}
607
+ ```value
608
+ %ABORT% (Financial projections cannot be determined from available data)
609
+ ````
610
+ {% /number-field %}
611
+ ````
612
+
613
+ **Parsing rules:**
614
+ - If `state="skipped"` is present, field's responseState is `'skipped'`
615
+ - If `state="aborted"` is present, field's responseState is `'aborted'`
616
+ - Otherwise, responseState is determined by value presence (`'answered'` or `'empty'`)
617
+ - Sentinel values (`%SKIP%` or `%ABORT%`) are parsed as metadata, not field values
618
+ - Text in parentheses after sentinel is extracted as `FieldResponse.reason`
619
+
620
+ **Serialization rules:**
621
+ - Only emit `state` attribute when responseState is `'skipped'` or `'aborted'`
622
+ - If skip/abort has a reason, serialize as sentinel value with parenthesized reason
623
+ - If skip/abort has no reason, omit the value fence entirely
624
+
625
+ ##### Note Serialization Format
626
+
627
+ Notes are general-purpose runtime annotations by agents/users, serialized at the end of
628
+ the form body (before `{% /form %}`). Notes are sorted numerically by ID for
629
+ deterministic output.
630
+
631
+ **Note tag syntax:**
632
+
633
+ ```md
634
+ {% note id="n1" ref="field_id" role="agent" %}
635
+ General observation about this field.
636
+ {% /note %}
637
+
638
+ {% note id="n2" ref="quarterly_earnings" role="agent" %}
639
+ Analysis completed with partial data due to API limitations.
640
+ {% /note %}
641
+ ````
642
+
643
+ **Note attributes:**
644
+
645
+ | Attribute | Required | Description |
646
+ | --- | --- | --- |
647
+ | `id` | Yes | Unique note identifier (implementation uses n1, n2, n3...) |
648
+ | `ref` | Yes | Target element ID (field, group, or form) |
649
+ | `role` | Yes | Who created the note (e.g., 'agent', 'user') |
650
+
651
+ > **Note (markform-254):** Notes no longer support a `state` attribute.
652
+ > Skip/abort reasons are now embedded directly in the field value using sentinel syntax
653
+ > like `%SKIP% (reason)`. Notes are purely for general annotations.
654
+
655
+ **Placement and ordering:**
656
+
657
+ - Notes appear at the end of the form, before `{% /form %}`
658
+
659
+ - Notes are sorted numerically by ID suffix (n1, n2, n10 not n1, n10, n2)
660
+
661
+ - Multiple notes can reference the same target element
662
+
663
+ - Notes are separated by blank lines for readability
664
+
665
+ **Example form with notes:**
666
+
667
+ ````md
668
+ {% form id="quarterly_earnings" title="Quarterly Earnings Analysis" %}
669
+
670
+ {% field-group id="company_info" title="Company Info" %}
671
+ {% string-field id="company_name" label="Company name" state="skipped" %}
672
+ ```value
673
+ %SKIP% (Not applicable for this analysis type)
674
+ ````
675
+ {% /string-field %} {% /field-group %}
676
+
677
+ {% note id="n1" ref="quarterly_earnings" role="agent" %} Analysis completed with partial
678
+ data due to API limitations.
679
+ {% /note %}
680
+
681
+ {% /form %}
682
+ ````
683
+
684
+ ##### The `process=false` Attribute
685
+
686
+ **Rule:** Only emit `process=false` when the value contains Markdoc syntax.
687
+
688
+ The `process=false` attribute prevents Markdoc from interpreting content as tags.
689
+ It is only required when the value contains Markdoc tag syntax:
690
+
691
+ - Tag syntax: `{% ... %}`
692
+
693
+ > **Note:** Markdoc uses HTML comments (`
694
+
695
+ <!-- ... -->
696
+
697
+ `), not `{# ... #}`. HTML comments in form values are plain text and don't require
698
+ `process=false`.
699
+
700
+ **Detection:** Check if the value matches the pattern `/\{%/`. A simple regex check is
701
+ sufficient since false positives are harmless (adding `process=false` when not needed
702
+ has no effect, but we prefer not to clutter the output).
703
+
704
+ ```ts
705
+ function containsMarkdocSyntax(value: string): boolean {
706
+ return /\{%/.test(value);
707
+ }
708
+ ````
709
+
710
+ **Example (process=false required):**
711
+ ````md
712
+ {% string-field id="notes" label="Notes" %}
713
+ ```value {% process=false %}
714
+ Use {% tag %} for special formatting.
715
+ ````
716
+ {% /string-field %}
717
+ ````
718
+
719
+ **Example (process=false not needed):**
720
+ ```md
721
+ {% string-field id="name" label="Name" %}
722
+ ```value
723
+ Alice Johnson
724
+ ````
725
+ {% /string-field %}
726
+ ````
727
+
728
+ See [GitHub Discussion #261][markdoc-process-false] for background on the attribute.
729
+
730
+ #### Example: Template Form
731
+
732
+ **Minimal frontmatter (hand-authored):**
733
+
734
+ ```md
735
+ ---
736
+ markform:
737
+ spec: MF/0.1
738
+ ---
739
+
740
+ {% form id="quarterly_earnings" title="Quarterly Earnings Analysis" %}
741
+
742
+ {% description ref="quarterly_earnings" %}
743
+ Prepare an earnings-call brief by extracting key financials and writing a thesis.
744
+ {% /description %}
745
+
746
+ {% field-group id="company_info" title="Company Info" %}
747
+ {% string-field id="company_name" label="Company name" required=true %}{% /string-field %}
748
+ {% string-field id="ticker" label="Ticker" required=true %}{% /string-field %}
749
+ {% string-field id="fiscal_period" label="Fiscal period" required=true %}{% /string-field %}
750
+ {% /field-group %}
751
+
752
+ {% field-group id="source_docs" title="Source Documents" %}
753
+ {% checkboxes id="docs_reviewed" label="Documents reviewed" required=true %}
754
+ - [ ] 10-K {% #ten_k %}
755
+ - [ ] 10-Q {% #ten_q %}
756
+ - [ ] Earnings release {% #earnings_release %}
757
+ - [ ] Earnings call transcript {% #call_transcript %}
758
+ {% /checkboxes %}
759
+ {% /field-group %}
760
+
761
+ {% field-group id="financials" title="Key Financials" %}
762
+ {% number-field id="revenue_m" label="Revenue (USD millions)" required=true %}{% /number-field %}
763
+ {% number-field id="gross_margin_pct" label="Gross margin (%)" %}{% /number-field %}
764
+ {% number-field id="eps_diluted" label="Diluted EPS" required=true %}{% /number-field %}
765
+ {% /field-group %}
766
+
767
+ {% field-group id="analysis" title="Analysis" %}
768
+ {% single-select id="rating" label="Overall rating" required=true %}
769
+ - [ ] Bullish {% #bullish %}
770
+ - [ ] Neutral {% #neutral %}
771
+ - [ ] Bearish {% #bearish %}
772
+ {% /single-select %}
773
+ {% string-field id="thesis" label="Investment thesis" required=true %}{% /string-field %}
774
+ {% /field-group %}
775
+
776
+ {% /form %}
777
+ ````
778
+
779
+ **Note:** When the engine serializes this form, it will add `form_summary`,
780
+ `form_progress`, and `form_state` to the `markform` block automatically.
781
+ Hand-authored forms only need the `spec` field.
782
+
783
+ #### Example: Incomplete Form
784
+
785
+ ````md
786
+ {% field-group id="company_info" title="Company Info" %}
787
+ {% string-field id="company_name" label="Company name" required=true %}
788
+ ```value
789
+ ACME Corp
790
+ ````
791
+ {% /string-field %} {% string-field id="ticker" label="Ticker" required=true %}
792
+ ```value
793
+ ACME
794
+ ```
795
+ {% /string-field %} {% string-field id="fiscal_period" label="Fiscal period"
796
+ required=true %}{% /string-field %} {% /field-group %}
797
+
798
+ {% field-group id="source_docs" title="Source Documents" %} {% checkboxes
799
+ id="docs_reviewed" label="Documents reviewed" required=true %}
800
+
801
+ - [x] 10-K {% #ten_k %}
802
+
803
+ - [x] 10-Q {% #ten_q %}
804
+
805
+ - [/] Earnings release {% #earnings_release %}
806
+
807
+ - [ ] Earnings call transcript {% #call_transcript %} {% /checkboxes %} {% /field-group
808
+ %}
809
+ ````
810
+
811
+ Notes:
812
+
813
+ - Option IDs use Markdoc annotation shorthand `#id` (field-scoped, slugified from label)
814
+
815
+ - Reference options externally using qualified form: `{fieldId}.{optionId}` (e.g., `docs_reviewed.ten_k`)
816
+
817
+ - Checkbox states: `[ ]` todo, `[x]` done, `[/]` incomplete, `[*]` active, `[-]` n/a
818
+
819
+ #### Parsing Strategy
820
+
821
+ Follows [Markdoc's render phases][markdoc-render] (parse → transform → render):
822
+
823
+ 1. Split frontmatter (YAML) from body
824
+
825
+ 2. `Markdoc.parse(body)` to get AST (see [Getting Started][markdoc-getting-started])
826
+
827
+ 3. `Markdoc.validate(ast, markformConfig)` for syntax-level issues (see [Validation][markdoc-validation])
828
+
829
+ 4. Traverse AST to extract:
830
+ - Form/group/field attributes from tags
831
+ - Option lists from list items within select/checkbox tags
832
+ - Values from `fence` nodes where `language === "value"`
833
+ - Documentation blocks
834
+
835
+ 5. Run **semantic** validation (Markform-specific, not Markdoc built-in):
836
+ - Globally-unique IDs for form/group/field (option IDs are field-scoped only)
837
+ - `ref` resolution (doc blocks reference valid targets)
838
+ - Checkbox mode enforcement (`checkboxMode="simple"` restricts to 2 states)
839
+ - Option marker parsing (`[ ]`, `[x]`, `[/]`, `[*]`, `[-]`, `[y]`, `[n]`, etc.)
840
+ - **Label requirement** (*required*): All fields must have a `label` attribute;
841
+ missing label is a parse error
842
+ - **Option ID annotation** (*required*): All options in select/checkbox fields must
843
+ have `{% #id %}` annotation; missing annotation is a parse error
844
+ - **Option ID uniqueness** (*required*): Option IDs must be unique within their
845
+ containing field; duplicates are a parse error
846
+
847
+ **Non-Markform content policy (*required*):**
848
+
849
+ Markform files may contain content outside of Markform tags. This content is handled as follows:
850
+
851
+ | Content Type | Policy |
852
+ |--------------|--------|
853
+ | HTML comments (`
854
+
855
+ <!-- ... -->
856
+
857
+ `) | Allowed, preserved verbatim on round-trip |
858
+ | Markdown headings/text between groups | Allowed, but NOT preserved on canonical serialize |
859
+ | Arbitrary Markdoc tags (non-Markform) | Parse warning, ignored |
860
+
861
+ **MF/0.1 scope:** Only HTML comments are guaranteed to be preserved. Do not rely on
862
+ non-Markform content surviving serialization. Future versions may support full
863
+ content preservation via raw slicing.
864
+
865
+ #### Serialization Strategy
866
+
867
+ Generate markdown string directly (not using `Markdoc.format()` due to canonicalization
868
+ requirements beyond what it provides—see [Formatting][markdoc-format]):
869
+
870
+ **MF/0.1 content restrictions for canonical serialization (*required*):**
871
+
872
+ To ensure deterministic round-tripping without building a full markdown serializer:
873
+
874
+ | Content type | Restriction |
875
+ |--------------|-------------|
876
+ | Option labels | Plain text only—no inline markdown, no nested tags. Validated at parse time. |
877
+ | Doc block bodies | Preserved verbatim—stored as raw text slice, re-emitted without reformatting. |
878
+ | Field labels | Plain text only—no inline markdown. |
879
+ | Group/form titles | Plain text only—no inline markdown. |
880
+
881
+ **Canonical formatting rules (*required*):**
882
+
883
+ | Rule | Specification |
884
+ |------|---------------|
885
+ | Attribute ordering | Alphabetical within each tag |
886
+ | Indentation | 0 spaces for top-level, no nested indentation |
887
+ | Blank lines | One blank line between adjacent blocks (fields, groups, doc blocks) for readability |
888
+ | Value fences | Omit entirely for empty fields |
889
+ | Fence character | Smart selection: pick backticks or tildes based on content to avoid collision with nested code blocks. Pick the character with smaller max-run at line start (indent ≤ 3); prefer backticks on tie. Length = max(3, maxRun + 1). |
890
+ | `process=false` | Emit only when value contains Markdoc tag syntax (`/{%/`) |
891
+ | Option ordering | Preserved as authored (order is significant) |
892
+ | Line endings | Unix (`\n`) only |
893
+ | Doc block placement | Immediately after the referenced element |
894
+
895
+ ##### Smart Fence Selection
896
+
897
+ When serializing field values, the fence character (backticks `` ` `` or tildes `~`) is
898
+ chosen dynamically to avoid collision with nested code blocks in the content.
899
+
900
+ **Algorithm:**
901
+
902
+ 1. Count the maximum consecutive run of each fence character at line starts (ignoring
903
+ lines indented 4+ spaces, which are inside indented code blocks per CommonMark)
904
+ 2. Pick the character with the smaller max-run
905
+ 3. On a tie, prefer backticks (more common convention)
906
+ 4. Use fence length = max(3, maxRun + 1) to ensure safe nesting
907
+
908
+ **Why this matters:** String field values may contain arbitrary Markdown, including
909
+ fenced code blocks. Without smart selection, a value containing triple backticks
910
+ would create ambiguous or malformed output.
911
+
912
+ **Example—Markdown documentation inside a value:**
913
+
914
+ ```md
915
+ {% string-field id="setup_guide" label="Setup Guide" %}
916
+ ~~~value
917
+ ## Installation
918
+
919
+ Install the package:
920
+
921
+ ```bash
922
+ npm install my-library
923
+ ````
924
+
925
+ Then configure:
926
+
927
+ ```json
928
+ {
929
+ "enabled": true
930
+ }
931
+ ```
932
+ ~~~
933
+ {% /string-field %}
934
+ ```
935
+
936
+ Here the serializer chose tildes (`~~~`) because the content contains backticks. The
937
+ content includes multiple fenced code blocks that are preserved exactly as authored.
938
+
939
+ * * *
940
+
941
+ ## Layer 2: Form Data Model
942
+
943
+ This layer defines the precise data structures for forms, fields, values, and
944
+ documentation. We use **Zod schemas** as the canonical notation because they provide:
945
+
946
+ - Precise, unambiguous type definitions
947
+ - Runtime validation built-in
948
+ - Easy mapping to JSON Schema (via `zod-to-json-schema`)
949
+ - Clear documentation of constraints and invariants
950
+
951
+ Alternative implementations may use equivalent schema definitions in their native
952
+ language (e.g., Pydantic for Python, JSON Schema for language-agnostic interchange).
953
+ The schemas below are normative—conforming implementations must support equivalent
954
+ data structures.
955
+
956
+ #### Canonical TypeScript Types
957
+
958
+ ```ts
959
+ type Id = string; // validated snake_case, e.g., /^[a-z][a-z0-9_]*$/
960
+
961
+ // Validator reference: simple string ID or parameterized object
962
+ type ValidatorRef = string | { id: string; [key: string]: unknown };
963
+
964
+ // Answer state for a field - orthogonal to field type
965
+ // Any field can be in any answer state
966
+ type AnswerState = 'unanswered' | 'answered' | 'skipped' | 'aborted';
967
+
968
+ // Field response: combines answer state with optional value
969
+ // Used in responsesByFieldId for all fields
970
+ interface FieldResponse {
971
+ state: AnswerState;
972
+ value?: FieldValue; // present only when state === 'answered'
973
+ reason?: string; // skip/abort reason (embedded in sentinel value)
974
+ }
975
+
976
+ // Note system for field/group/form annotations
977
+ type NoteId = string; // unique note ID (implementation uses n1, n2, n3...)
978
+
979
+ interface Note {
980
+ id: NoteId;
981
+ ref: Id; // target ID (field, group, or form)
982
+ role: string; // who created (agent, user, ...)
983
+ state?: 'skipped' | 'aborted'; // optional: links note to action
984
+ text: string; // markdown content
985
+ }
986
+
987
+ // Multi-checkbox states (checkboxMode="multi", default)
988
+ type MultiCheckboxState = 'todo' | 'done' | 'incomplete' | 'active' | 'na';
989
+
990
+ // Simple checkbox states (checkboxMode="simple")
991
+ type SimpleCheckboxState = 'todo' | 'done';
992
+
993
+ // Explicit checkbox values (checkboxMode="explicit")
994
+ type ExplicitCheckboxValue = 'unfilled' | 'yes' | 'no';
995
+
996
+ // Union type for all checkbox values (validated based on checkboxMode)
997
+ type CheckboxValue = MultiCheckboxState | ExplicitCheckboxValue;
998
+
999
+ type Field =
1000
+ | StringField
1001
+ | NumberField
1002
+ | StringListField
1003
+ | CheckboxesField
1004
+ | SingleSelectField
1005
+ | MultiSelectField
1006
+ | UrlField
1007
+ | UrlListField;
1008
+
1009
+ interface FormSchema {
1010
+ id: Id;
1011
+ title?: string;
1012
+ groups: FieldGroup[];
1013
+ }
1014
+
1015
+ interface FieldGroup {
1016
+ id: Id;
1017
+ title?: string;
1018
+ // Note: `required` on groups is not supported in MF/0.1 (ignored with warning)
1019
+ validate?: ValidatorRef[]; // validator references (string IDs or parameterized objects)
1020
+ children: Field[]; // MF/0.1/0.2: fields only; nested groups deferred (future)
1021
+ }
1022
+
1023
+ type FieldPriorityLevel = 'high' | 'medium' | 'low';
1024
+
1025
+ interface FieldBase {
1026
+ id: Id;
1027
+ label: string;
1028
+ required: boolean; // explicit: parser defaults to false if not specified
1029
+ priority: FieldPriorityLevel; // explicit: parser defaults to 'medium' if not specified
1030
+ validate?: ValidatorRef[]; // validator references (string IDs or parameterized objects)
1031
+ }
1032
+
1033
+ // NOTE: `required` and `priority` are explicit (not optional) in the data model.
1034
+ // The parser assigns defaults when not specified in markup. This ensures:
1035
+ // 1. Consumers don't need null/undefined checks or ?? fallbacks
1036
+ // 2. Intent is always explicit in parsed data - no silent "undefined means false" behavior
1037
+ // 3. Serialization can always emit these values for clarity
1038
+
1039
+ interface StringField extends FieldBase {
1040
+ kind: 'string';
1041
+ multiline?: boolean;
1042
+ pattern?: string; // JS regex without delimiters
1043
+ minLength?: number;
1044
+ maxLength?: number;
1045
+ }
1046
+
1047
+ interface NumberField extends FieldBase {
1048
+ kind: 'number';
1049
+ min?: number;
1050
+ max?: number;
1051
+ integer?: boolean;
1052
+ }
1053
+
1054
+ interface StringListField extends FieldBase {
1055
+ kind: 'string_list';
1056
+ minItems?: number;
1057
+ maxItems?: number;
1058
+ itemMinLength?: number;
1059
+ itemMaxLength?: number;
1060
+ uniqueItems?: boolean;
1061
+ }
1062
+
1063
+ interface Option {
1064
+ id: Id;
1065
+ label: string;
1066
+ }
1067
+
1068
+ type CheckboxMode = 'multi' | 'simple' | 'explicit';
1069
+
1070
+ interface CheckboxesField extends FieldBase {
1071
+ kind: 'checkboxes';
1072
+ checkboxMode: CheckboxMode; // explicit: parser defaults to 'multi' if not specified
1073
+ minDone?: number; // simple mode only: integer, default -1 (all)
1074
+ options: Option[];
1075
+ }
1076
+
1077
+ interface SingleSelectField extends FieldBase {
1078
+ kind: 'single_select';
1079
+ options: Option[];
1080
+ }
1081
+
1082
+ interface MultiSelectField extends FieldBase {
1083
+ kind: 'multi_select';
1084
+ options: Option[];
1085
+ minSelections?: number;
1086
+ maxSelections?: number;
1087
+ }
1088
+
1089
+ interface UrlField extends FieldBase {
1090
+ kind: 'url';
1091
+ // No additional constraints - URL format validation is built-in
1092
+ }
1093
+
1094
+ interface UrlListField extends FieldBase {
1095
+ kind: 'url_list';
1096
+ minItems?: number;
1097
+ maxItems?: number;
1098
+ uniqueItems?: boolean;
1099
+ }
1100
+
1101
+ // OptionId is local to the containing field (e.g., "ten_k", "bullish")
1102
+ type OptionId = string;
1103
+
1104
+ type FieldValue =
1105
+ | { kind: 'string'; value: string | null }
1106
+ | { kind: 'number'; value: number | null }
1107
+ | { kind: 'string_list'; items: string[] }
1108
+ | { kind: 'checkboxes'; values: Record<OptionId, CheckboxValue> } // keys are local option IDs
1109
+ | { kind: 'single_select'; selected: OptionId | null } // local option ID
1110
+ | { kind: 'multi_select'; selected: OptionId[] } // local option IDs
1111
+ | { kind: 'url'; value: string | null } // validated URL string
1112
+ | { kind: 'url_list'; items: string[] }; // array of URL strings
1113
+
1114
+ // QualifiedOptionRef is used when referencing options externally (e.g., in doc blocks)
1115
+ type QualifiedOptionRef = `${Id}.${OptionId}`; // e.g., "docs_reviewed.ten_k"
1116
+
1117
+ /** Documentation tag types (from Markdoc tag name) */
1118
+ type DocumentationTag = 'description' | 'instructions' | 'documentation';
1119
+
1120
+ interface DocumentationBlock {
1121
+ ref: Id | QualifiedOptionRef; // form/group/field ID, or qualified option ref
1122
+ tag: DocumentationTag; // the Markdoc tag name
1123
+ bodyMarkdown: string;
1124
+ }
1125
+
1126
+ /** Node type for ID index entries - identifies what structural element an ID refers to */
1127
+ type NodeType = 'form' | 'group' | 'field';
1128
+
1129
+ // IdIndexEntry: lookup entry for fast ID resolution and validation
1130
+ // NOTE: Options are NOT indexed here (they are field-scoped, not globally unique)
1131
+ // Use StructureSummary.optionsById for option lookup via QualifiedOptionRef
1132
+ interface IdIndexEntry {
1133
+ nodeType: NodeType; // what this ID refers to
1134
+ parentId?: Id; // parent group/form ID (undefined for form)
1135
+ }
1136
+
1137
+ // FormMetadata: form-level metadata from YAML frontmatter
1138
+ interface FormMetadata {
1139
+ markformVersion: string;
1140
+ roles: string[]; // available roles for field assignment
1141
+ roleInstructions: Record<string, string>; // instructions per role
1142
+ }
1143
+
1144
+ // ParsedForm: canonical internal representation returned by parseForm()
1145
+ interface ParsedForm {
1146
+ schema: FormSchema;
1147
+ responsesByFieldId: Record<Id, FieldResponse>; // unified response state + value
1148
+ notes: Note[]; // agent/user notes
1149
+ docs: DocumentationBlock[];
1150
+ orderIndex: Id[]; // fieldIds in document order (deterministic)
1151
+ idIndex: Map<Id, IdIndexEntry>; // fast lookup for form/group/field (NOT options)
1152
+ metadata?: FormMetadata; // optional for backward compat with forms without frontmatter
1153
+ }
1154
+
1155
+ // InspectIssue: unified type for inspect/apply API results
1156
+ // Derived from ValidationIssue[] but simplified for agent/UI consumption
1157
+ // Returned as a single list sorted by priority tier (ascending, P1 = highest)
1158
+ interface InspectIssue {
1159
+ ref: Id | QualifiedOptionRef; // target this issue relates to (field, group, or qualified option)
1160
+ scope: 'form' | 'group' | 'field' | 'option'; // scope of the issue target
1161
+ reason: IssueReason; // machine-readable reason code
1162
+ message: string; // human-readable description
1163
+ severity: 'required' | 'recommended'; // *required* = must fix; *recommended* = suggested
1164
+ priority: number; // tier 1-5 (P1-P5); computed from field priority + issue type score
1165
+ }
1166
+
1167
+ // Standard reason codes (keep stable for golden tests)
1168
+ type IssueReason =
1169
+ // Severity: *required* (must be resolved for form completion)
1170
+ | 'validation_error' // Field has validation errors (pattern, range, etc.)
1171
+ | 'required_missing' // Required field with no value
1172
+ | 'checkbox_incomplete' // Required checkboxes with non-terminal states
1173
+ | 'min_items_not_met' // String-list or multi-select below minimum
1174
+ // Severity: *recommended* (optional improvements)
1175
+ | 'optional_empty'; // Optional field with no value
1176
+
1177
+ // Mapping from ValidationIssue to InspectIssue:
1178
+ // - ValidationIssue.severity='error' → InspectIssue.severity='required'
1179
+ // - ValidationIssue.severity='warning'/'info' → InspectIssue.severity='recommended'
1180
+ // - Missing optional fields → severity='recommended', reason='optional_empty'
1181
+ ```
1182
+
1183
+ #### StructureSummary and ProgressSummary
1184
+
1185
+ These types model the derived metadata stored in frontmatter and exposed via
1186
+ tool/harness APIs. They provide a quick overview of form structure and filling progress
1187
+ without exposing actual field values.
1188
+
1189
+ ##### StructureSummary (form_summary)
1190
+
1191
+ Describes the static structure of the form schema:
1192
+
1193
+ ```ts
1194
+ // FieldKind matches the 'kind' discriminant on Field types
1195
+ type FieldKind = 'string' | 'number' | 'string_list' | 'checkboxes' | 'single_select' | 'multi_select' | 'url' | 'url_list';
1196
+
1197
+ interface StructureSummary {
1198
+ groupCount: number; // total field-groups
1199
+ fieldCount: number; // total fields (all types)
1200
+ optionCount: number; // total options across all select/checkbox fields
1201
+
1202
+ fieldCountByKind: Record<FieldKind, number>; // breakdown by field type
1203
+
1204
+ /** Map of group ID -> 'field_group' (for completeness; groups have one kind) */
1205
+ groupsById: Record<Id, 'field_group'>;
1206
+
1207
+ /** Map of field ID -> field kind */
1208
+ fieldsById: Record<Id, FieldKind>;
1209
+
1210
+ /**
1211
+ * Map of qualified option ref -> parent field info.
1212
+ * Keys use qualified form: "{fieldId}.{optionId}" (e.g., "docs_reviewed.ten_k")
1213
+ * This allows relating options back to their parent field.
1214
+ */
1215
+ optionsById: Record<QualifiedOptionRef, {
1216
+ parentFieldId: Id;
1217
+ parentFieldKind: FieldKind;
1218
+ }>;
1219
+ }
1220
+ ```
1221
+
1222
+ **Notes:**
1223
+
1224
+ - This is **schema-only**; it does not include values
1225
+
1226
+ - All ID maps are sorted alphabetically in YAML output for deterministic serialization
1227
+
1228
+ - `optionsById` uses qualified refs to avoid ambiguity between fields with same option
1229
+ IDs
1230
+
1231
+ ##### ProgressSummary (form_progress)
1232
+
1233
+ Tracks filling progress per field without exposing actual values:
1234
+
1235
+ ```ts
1236
+ // Progress state for a field or the whole form (derived from dimensions)
1237
+ type ProgressState = 'empty' | 'incomplete' | 'invalid' | 'complete';
1238
+
1239
+ interface FieldProgress {
1240
+ kind: FieldKind; // field type
1241
+ required: boolean; // whether field has required=true
1242
+
1243
+ answerState: AnswerState; // unified answer state (unanswered/answered/skipped/aborted)
1244
+ hasNotes: boolean; // whether field has any notes attached
1245
+ noteCount: number; // count of notes attached to this field
1246
+
1247
+ empty: boolean; // true if field has no value
1248
+ valid: boolean; // true iff no validation issues for this field
1249
+ issueCount: number; // count of ValidationIssues referencing this field
1250
+
1251
+ /**
1252
+ * Checkbox state rollup (only present for checkboxes fields).
1253
+ * Provides counts without exposing which specific options are in each state.
1254
+ */
1255
+ checkboxProgress?: CheckboxProgressCounts;
1256
+ }
1257
+
1258
+ /**
1259
+ * Checkbox progress counts, generalized for all checkbox modes.
1260
+ * Only the states valid for the field's checkboxMode will have non-zero values.
1261
+ */
1262
+ interface CheckboxProgressCounts {
1263
+ total: number; // total options in this checkbox field
1264
+
1265
+ // Multi mode states (checkboxMode="multi")
1266
+ todo: number;
1267
+ done: number;
1268
+ incomplete: number; // camelCase in TS, snake_case in YAML
1269
+ active: number;
1270
+ na: number;
1271
+
1272
+ // Explicit mode states (checkboxMode="explicit")
1273
+ unfilled: number;
1274
+ yes: number;
1275
+ no: number;
1276
+ }
1277
+
1278
+ interface ProgressSummary {
1279
+ counts: ProgressCounts;
1280
+
1281
+ /** Map of field ID -> field progress */
1282
+ fields: Record<Id, FieldProgress>;
1283
+ }
1284
+
1285
+ interface ProgressCounts {
1286
+ totalFields: number; // all fields in the form
1287
+ requiredFields: number; // fields with required=true
1288
+
1289
+ // Dimension 1: AnswerState (mutually exclusive, sum to totalFields)
1290
+ unansweredFields: number; // fields with answerState='unanswered'
1291
+ answeredFields: number; // fields with answerState='answered' (have values)
1292
+ skippedFields: number; // fields with answerState='skipped'
1293
+ abortedFields: number; // fields with answerState='aborted'
1294
+
1295
+ // Dimension 2: Validity (mutually exclusive, sum to totalFields)
1296
+ validFields: number; // fields with valid=true
1297
+ invalidFields: number; // fields with valid=false
1298
+
1299
+ // Dimension 3: Value presence (mutually exclusive, sum to totalFields)
1300
+ emptyFields: number; // fields with empty=true (no value)
1301
+ filledFields: number; // fields with empty=false (have value)
1302
+
1303
+ // Derived counts
1304
+ emptyRequiredFields: number; // required fields with no value
1305
+ totalNotes: number; // total notes across all fields/groups/form
1306
+ }
1307
+ ```
1308
+
1309
+ ##### ProgressState Definitions
1310
+
1311
+ The `ProgressState` is computed deterministically based on submission status, validation
1312
+ result, and completeness rules:
1313
+
1314
+ | State | Meaning |
1315
+ | --- | --- |
1316
+ | `empty` | No fields have values |
1317
+ | `incomplete` | Some fields have values but not all required fields are filled or valid |
1318
+ | `invalid` | Form has validation errors or aborted fields |
1319
+ | `complete` | All required fields are filled and all fields are valid |
1320
+
1321
+ **AnswerState rules (deterministic, per field):**
1322
+
1323
+ The `answerState` is determined by the field’s FieldResponse:
1324
+
1325
+ | AnswerState | When |
1326
+ | --- | --- |
1327
+ | `unanswered` | No value provided and not skipped/aborted |
1328
+ | `answered` | FieldResponse has a value (value !== undefined) |
1329
+ | `skipped` | Explicitly skipped via skip_field patch |
1330
+ | `aborted` | Explicitly aborted via abort_field patch |
1331
+
1332
+ For `answered` fields, value presence is determined per field kind:
1333
+
1334
+ | Field Kind | Has value when |
1335
+ | --- | --- |
1336
+ | `string` | `value !== null && value.trim().length > 0` |
1337
+ | `number` | `value !== null` |
1338
+ | `single_select` | `selected !== null` |
1339
+ | `multi_select` | `selected.length > 0` |
1340
+ | `string_list` | `items.length > 0` |
1341
+ | `url` | `value !== null && value.trim().length > 0` |
1342
+ | `url_list` | `items.length > 0` |
1343
+ | `checkboxes` | At least one option state differs from initial (`todo` for multi/simple, `unfilled` for explicit) |
1344
+
1345
+ **Completeness rules (for required fields):**
1346
+
1347
+ Completeness is relevant only when `required=true`. An answered field is complete if:
1348
+
1349
+ | Field Kind | Complete when |
1350
+ | --- | --- |
1351
+ | `string` | Submitted (non-empty after trim) |
1352
+ | `number` | Submitted (non-null) |
1353
+ | `single_select` | Submitted (exactly one selected) |
1354
+ | `multi_select` | `selected.length >= max(1, minSelections)` |
1355
+ | `string_list` | `items.length >= max(1, minItems)` |
1356
+ | `checkboxes` | All options in terminal state (see checkbox completion rules above) |
1357
+
1358
+ **State computation logic:**
1359
+
1360
+ ```
1361
+ if not answered AND (skipped OR aborted) AND has validation errors:
1362
+ state = 'invalid' // addressed but problematic
1363
+ elif not answered:
1364
+ state = 'empty'
1365
+ elif has validation errors:
1366
+ state = 'invalid'
1367
+ elif required and not complete:
1368
+ state = 'incomplete'
1369
+ else:
1370
+ state = 'complete'
1371
+ ```
1372
+
1373
+ **Form state computation (`form_state` in frontmatter):**
1374
+
1375
+ The overall `form_state: ProgressState` is derived from `ProgressSummary.counts`:
1376
+
1377
+ ```
1378
+ if counts.answeredFields == 0:
1379
+ form_state = 'empty'
1380
+ elif counts.invalidFields > 0:
1381
+ form_state = 'invalid'
1382
+ elif counts.incompleteFields > 0 or counts.emptyRequiredFields > 0:
1383
+ form_state = 'incomplete'
1384
+ else:
1385
+ form_state = 'complete'
1386
+ ```
1387
+
1388
+ **Implicit requiredness (*required*):**
1389
+
1390
+ For form completion purposes, fields with constraints are treated as implicitly
1391
+ required:
1392
+
1393
+ | Field Type | Implicit Required When |
1394
+ | --- | --- |
1395
+ | `string-list` | `minItems > 0` |
1396
+ | `multi-select` | `minSelections > 0` |
1397
+ | `checkboxes` | `minDone > 0` (simple mode) |
1398
+
1399
+ These fields contribute to `emptyRequiredFields` count even without explicit
1400
+ `required=true`. This ensures `form_state` accurately reflects whether all constraints
1401
+ are satisfied.
1402
+
1403
+ **Role-filtered completion (for multi-role forms):**
1404
+
1405
+ When running the fill harness with a specific `targetRoles` parameter (e.g., just the
1406
+ `agent` role), completion should be determined by only the fields assignable to those
1407
+ roles, not all fields in the form.
1408
+ The completion formula becomes:
1409
+
1410
+ ```
1411
+ Role-filtered completion for targetRoles:
1412
+ roleFilteredFields = fields.filter(f => targetRoles.includes(f.role) || !f.role)
1413
+
1414
+ isComplete =
1415
+ abortedFields == 0 AND
1416
+ for all f in roleFilteredFields:
1417
+ f.responseState == 'answered' (with f.state == 'complete') OR
1418
+ f.responseState == 'skipped'
1419
+ ```
1420
+
1421
+ This is critical for forms where the user fills some fields first, then the agent fills
1422
+ the remaining agent-role fields.
1423
+ Without role filtering, the harness would incorrectly report the form as incomplete even
1424
+ after all agent fields are filled, because user fields (intended for a different actor)
1425
+ would still be empty.
1426
+
1427
+ **Response states for completion purposes:**
1428
+
1429
+ | Response State | Counts as Complete | Notes |
1430
+ | --- | --- | --- |
1431
+ | `answered` (complete) | Yes | Field has valid value and passes validation |
1432
+ | `skipped` | Yes | Explicitly skipped via `skip_field` patch (optional fields only) |
1433
+ | `aborted` | No | Field marked as unable to complete—blocks all completion |
1434
+ | `empty` | No | Field has no response—must be answered or skipped |
1435
+ | `answered` (incomplete) | No | Partially filled (e.g., list with fewer items than `minItems`) |
1436
+ | `answered` (invalid) | No | Has validation errors |
1437
+
1438
+ **Note:** The completion formula requires:
1439
+
1440
+ 1. All fields to be either *answered* (with complete/valid value) or *skipped*
1441
+
1442
+ 2. No fields can be in *aborted* state (abortedFields == 0)
1443
+
1444
+ Simply leaving an optional field empty does NOT count toward completion—the agent must
1445
+ actively skip it or provide a value.
1446
+ This ensures agents acknowledge every field.
1447
+
1448
+ Aborted fields block completion entirely, requiring manual intervention to either fill
1449
+ the field or remove the abort state before the form can be completed.
1450
+
1451
+ **Naming convention note:** Markdoc attributes and TypeScript properties both use
1452
+ `camelCase` (e.g., `checkboxMode`, `minItems`). Only IDs use `snake_case`. This
1453
+ alignment with JSON Schema keywords reduces translation complexity.
1454
+
1455
+ #### Zod Schemas
1456
+
1457
+ Use [Zod][zod] as the canonical TypeScript-first schema layer.
1458
+ See [Zod API][zod-api] for primitives and constraints.
1459
+
1460
+ Implement Zod validators for all types, patch operations, session transcript schema, and
1461
+ tool inputs. Zod is used for:
1462
+
1463
+ - CLI validation of inputs
1464
+
1465
+ - AI SDK tool `inputSchema` definitions (see [AI SDK Tools][ai-sdk-tools])
1466
+
1467
+ - Deriving JSON Schema for MCP tool schemas via [zod-to-json-schema]
1468
+
1469
+ **Note:** The `zod-to-json-schema` library has a deprecation notice for some APIs—use
1470
+ the updated patterns documented in its README.
1471
+
1472
+ ##### Summary Zod Schemas
1473
+
1474
+ Zod schemas for the frontmatter summary types:
1475
+
1476
+ ```ts
1477
+ const FieldKindSchema = z.enum([
1478
+ 'string', 'number', 'string_list', 'checkboxes', 'single_select', 'multi_select', 'url', 'url_list'
1479
+ ]);
1480
+
1481
+ const AnswerStateSchema = z.enum(['unanswered', 'answered', 'skipped', 'aborted']);
1482
+
1483
+ const StructureSummarySchema = z.object({
1484
+ groupCount: z.number().int().nonnegative(),
1485
+ fieldCount: z.number().int().nonnegative(),
1486
+ optionCount: z.number().int().nonnegative(),
1487
+ fieldCountByKind: z.record(FieldKindSchema, z.number().int().nonnegative()),
1488
+ groupsById: z.record(z.string(), z.literal('field_group')),
1489
+ fieldsById: z.record(z.string(), FieldKindSchema),
1490
+ optionsById: z.record(z.string(), z.object({
1491
+ parentFieldId: z.string(),
1492
+ parentFieldKind: FieldKindSchema,
1493
+ })),
1494
+ });
1495
+
1496
+ const ProgressStateSchema = z.enum(['empty', 'incomplete', 'invalid', 'complete']);
1497
+
1498
+ const CheckboxProgressCountsSchema = z.object({
1499
+ total: z.number().int().nonnegative(),
1500
+ // Multi mode
1501
+ todo: z.number().int().nonnegative(),
1502
+ done: z.number().int().nonnegative(),
1503
+ incomplete: z.number().int().nonnegative(),
1504
+ active: z.number().int().nonnegative(),
1505
+ na: z.number().int().nonnegative(),
1506
+ // Explicit mode
1507
+ unfilled: z.number().int().nonnegative(),
1508
+ yes: z.number().int().nonnegative(),
1509
+ no: z.number().int().nonnegative(),
1510
+ });
1511
+
1512
+ const FieldProgressSchema = z.object({
1513
+ kind: FieldKindSchema,
1514
+ required: z.boolean(),
1515
+ answerState: AnswerStateSchema, // unified answer state
1516
+ hasNotes: z.boolean(),
1517
+ noteCount: z.number().int().nonnegative(),
1518
+ empty: z.boolean(), // true if field has no value
1519
+ valid: z.boolean(), // true if field has no validation errors
1520
+ issueCount: z.number().int().nonnegative(),
1521
+ checkboxProgress: CheckboxProgressCountsSchema.optional(),
1522
+ });
1523
+
1524
+ const ProgressCountsSchema = z.object({
1525
+ totalFields: z.number().int().nonnegative(),
1526
+ requiredFields: z.number().int().nonnegative(),
1527
+ // Dimension 1: AnswerState (mutually exclusive, sum to totalFields)
1528
+ unansweredFields: z.number().int().nonnegative(),
1529
+ answeredFields: z.number().int().nonnegative(),
1530
+ skippedFields: z.number().int().nonnegative(),
1531
+ abortedFields: z.number().int().nonnegative(),
1532
+ // Dimension 2: Validity (mutually exclusive, sum to totalFields)
1533
+ validFields: z.number().int().nonnegative(),
1534
+ invalidFields: z.number().int().nonnegative(),
1535
+ // Dimension 3: Value presence (mutually exclusive, sum to totalFields)
1536
+ emptyFields: z.number().int().nonnegative(),
1537
+ filledFields: z.number().int().nonnegative(),
1538
+ // Derived counts
1539
+ emptyRequiredFields: z.number().int().nonnegative(),
1540
+ totalNotes: z.number().int().nonnegative(),
1541
+ });
1542
+
1543
+ const ProgressSummarySchema = z.object({
1544
+ counts: ProgressCountsSchema,
1545
+ fields: z.record(z.string(), FieldProgressSchema),
1546
+ });
1547
+
1548
+ // Frontmatter schema for INPUT forms (templates)
1549
+ const MarkformInputFrontmatterSchema = z.object({
1550
+ markformVersion: z.string(), // Required: e.g., "0.1.0"
1551
+ // User metadata allowed but not validated
1552
+ });
1553
+
1554
+ // Frontmatter schema for OUTPUT forms (after processing/serialization)
1555
+ const MarkformFrontmatterSchema = z.object({
1556
+ markformVersion: z.string(),
1557
+ formSummary: StructureSummarySchema.optional(), // Derived on serialize
1558
+ formProgress: ProgressSummarySchema.optional(), // Derived on serialize
1559
+ formState: ProgressStateSchema.optional(), // Derived on serialize
1560
+ });
1561
+ ```
1562
+
1563
+ **YAML serialization:** When writing frontmatter, convert camelCase keys to snake_case:
1564
+
1565
+ - `specVersion` → `spec` (or use `MF_SPEC_VERSION` constant directly)
1566
+
1567
+ - `formSummary` → `form_summary`
1568
+
1569
+ - `formProgress` → `form_progress`
1570
+
1571
+ - `formState` → `form_state`
1572
+
1573
+ - `fieldCountByKind` → `field_count_by_kind`
1574
+
1575
+ - etc.
1576
+
1577
+ Use a `toSnakeCaseDeep()` helper for deterministic conversion at the frontmatter
1578
+ boundary.
1579
+
1580
+ #### Comprehensive Field Type Reference
1581
+
1582
+ This section provides a complete mapping between Markdoc syntax, TypeScript types, and
1583
+ schema representations for all field types.
1584
+
1585
+ ##### Naming Conventions
1586
+
1587
+ | Layer | Convention | Example |
1588
+ | --- | --- | --- |
1589
+ | Markdoc tag names | kebab-case | `string-field`, `multi-select` |
1590
+ | Markdoc attributes | camelCase | `minLength`, `checkboxMode`, `minItems` |
1591
+ | TypeScript interfaces | PascalCase | `StringField`, `MultiSelectField` |
1592
+ | TypeScript properties | camelCase | `minLength`, `checkboxMode` |
1593
+ | JSON Schema keywords | camelCase | `minItems`, `maxLength`, `uniqueItems` |
1594
+ | IDs (values) | snake_case | `company_name`, `ten_k`, `quarterly_earnings` |
1595
+ | YAML keys (frontmatter, session transcripts) | snake_case | `spec`, `form_summary`, `field_count_by_kind` |
1596
+ | Kind values (field types) | snake_case | `'string'`, `'single_select'` |
1597
+ | Patch operations | snake_case | `set_string`, `set_single_select` |
1598
+
1599
+ **Rationale:** Using camelCase for Markdoc attributes aligns with JSON Schema keywords
1600
+ and TypeScript conventions, eliminating translation overhead.
1601
+ IDs remain snake_case as they are data values, not code identifiers.
1602
+ YAML keys use snake_case for readability and consistency with common YAML conventions.
1603
+
1604
+ **Reserved property names:**
1605
+
1606
+ | Property | Used on | Values | Notes |
1607
+ | --- | --- | --- | --- |
1608
+ | `kind` | `Field`, `FieldValue` | `FieldKind` values | Reserved for field type discrimination only |
1609
+ | `tag` | `DocumentationBlock` | `DocumentationTag` values | Identifies doc block type |
1610
+ | `nodeType` | `IdIndexEntry` | `'form' \| 'group' \| 'field'` | Identifies structural element type |
1611
+
1612
+ ##### Field Type Mappings
1613
+
1614
+ **`string-field`** — Single string value
1615
+
1616
+ | Aspect | Value |
1617
+ | --- | --- |
1618
+ | Markdoc tag | `string-field` |
1619
+ | TypeScript interface | `StringField` |
1620
+ | TypeScript kind | `'string'` |
1621
+ | Attributes | `id`, `label`, `required`, `pattern`, `minLength`, `maxLength`, `multiline` |
1622
+ | FieldValue | `{ kind: 'string'; value: string \| null }` |
1623
+ | Patch operation | `{ op: 'set_string'; fieldId: Id; value: string \| null }` |
1624
+ | Zod | `z.string().min(n).max(m).regex(pattern)` |
1625
+ | JSON Schema | `{ type: "string", minLength, maxLength, pattern }` |
1626
+
1627
+ **`number-field`** — Numeric value
1628
+
1629
+ | Aspect | Value |
1630
+ | --- | --- |
1631
+ | Markdoc tag | `number-field` |
1632
+ | TypeScript interface | `NumberField` |
1633
+ | TypeScript kind | `'number'` |
1634
+ | Attributes | `id`, `label`, `required`, `min`, `max`, `integer` |
1635
+ | FieldValue | `{ kind: 'number'; value: number \| null }` |
1636
+ | Patch operation | `{ op: 'set_number'; fieldId: Id; value: number \| null }` |
1637
+ | Zod | `z.number().min(n).max(m).int()` |
1638
+ | JSON Schema | `{ type: "number"/"integer", minimum, maximum }` |
1639
+
1640
+ **`string-list`** — Array of strings (open-ended list)
1641
+
1642
+ | Aspect | Value |
1643
+ | --- | --- |
1644
+ | Markdoc tag | `string-list` |
1645
+ | TypeScript interface | `StringListField` |
1646
+ | TypeScript kind | `'string_list'` |
1647
+ | Attributes | `id`, `label`, `required`, `minItems`, `maxItems`, `itemMinLength`, `itemMaxLength`, `uniqueItems` |
1648
+ | FieldValue | `{ kind: 'string_list'; items: string[] }` |
1649
+ | Patch operation | `{ op: 'set_string_list'; fieldId: Id; items: string[] }` |
1650
+ | Zod | `z.array(z.string().min(itemMin).max(itemMax)).min(n).max(m)` |
1651
+ | JSON Schema | `{ type: "array", items: { type: "string" }, minItems, maxItems, uniqueItems }` |
1652
+
1653
+ **`single-select`** — Select exactly one option from enumerated list
1654
+
1655
+ | Aspect | Value |
1656
+ | --- | --- |
1657
+ | Markdoc tag | `single-select` |
1658
+ | TypeScript interface | `SingleSelectField` |
1659
+ | TypeScript kind | `'single_select'` |
1660
+ | Attributes | `id`, `label`, `required` + inline `options` via list syntax |
1661
+ | FieldValue | `{ kind: 'single_select'; selected: OptionId \| null }` |
1662
+ | Patch operation | `{ op: 'set_single_select'; fieldId: Id; selected: OptionId \| null }` |
1663
+ | Zod | `z.enum([...optionIds])` |
1664
+ | JSON Schema | `{ type: "string", enum: [...optionIds] }` |
1665
+
1666
+ **`multi-select`** — Select multiple options from enumerated list
1667
+
1668
+ | Aspect | Value |
1669
+ | --- | --- |
1670
+ | Markdoc tag | `multi-select` |
1671
+ | TypeScript interface | `MultiSelectField` |
1672
+ | TypeScript kind | `'multi_select'` |
1673
+ | Attributes | `id`, `label`, `required`, `minSelections`, `maxSelections` + inline `options` |
1674
+ | FieldValue | `{ kind: 'multi_select'; selected: OptionId[] }` |
1675
+ | Patch operation | `{ op: 'set_multi_select'; fieldId: Id; selected: OptionId[] }` |
1676
+ | Zod | `z.array(z.enum([...optionIds])).min(n).max(m)` |
1677
+ | JSON Schema | `{ type: "array", items: { enum: [...optionIds] }, minItems, maxItems }` |
1678
+
1679
+ **`checkboxes`** — Stateful checklist with configurable checkbox modes
1680
+
1681
+ | Aspect | Value |
1682
+ | --- | --- |
1683
+ | Markdoc tag | `checkboxes` |
1684
+ | TypeScript interface | `CheckboxesField` |
1685
+ | TypeScript kind | `'checkboxes'` |
1686
+ | Attributes | `id`, `label`, `required`, `checkboxMode` (`multi`/`simple`/`explicit`), `minDone` (simple only) + inline `options` |
1687
+ | FieldValue | `{ kind: 'checkboxes'; values: Record<OptionId, CheckboxValue> }` |
1688
+ | Patch operation | `{ op: 'set_checkboxes'; fieldId: Id; values: Record<OptionId, CheckboxValue> }` |
1689
+ | Zod | `z.record(z.enum([...states]))` |
1690
+ | JSON Schema | `{ type: "object", additionalProperties: { enum: [...states] } }` |
1691
+
1692
+ **`url-field`** — Single URL value
1693
+
1694
+ | Aspect | Value |
1695
+ | --- | --- |
1696
+ | Markdoc tag | `url-field` |
1697
+ | TypeScript interface | `UrlField` |
1698
+ | TypeScript kind | `'url'` |
1699
+ | Attributes | `id`, `label`, `required` |
1700
+ | FieldValue | `{ kind: 'url'; value: string \| null }` |
1701
+ | Patch operation | `{ op: 'set_url'; fieldId: Id; value: string \| null }` |
1702
+ | Zod | `z.string().url()` |
1703
+ | JSON Schema | `{ type: "string", format: "uri" }` |
1704
+
1705
+ **`url-list`** — Array of URLs (for citations, sources, references)
1706
+
1707
+ | Aspect | Value |
1708
+ | --- | --- |
1709
+ | Markdoc tag | `url-list` |
1710
+ | TypeScript interface | `UrlListField` |
1711
+ | TypeScript kind | `'url_list'` |
1712
+ | Attributes | `id`, `label`, `required`, `minItems`, `maxItems`, `uniqueItems` |
1713
+ | FieldValue | `{ kind: 'url_list'; items: string[] }` |
1714
+ | Patch operation | `{ op: 'set_url_list'; fieldId: Id; items: string[] }` |
1715
+ | Zod | `z.array(z.string().url()).min(n).max(m)` |
1716
+ | JSON Schema | `{ type: "array", items: { type: "string", format: "uri" }, minItems, maxItems, uniqueItems }` |
1717
+
1718
+ **Note:** `OptionId` values are local to the field (e.g., `"ten_k"`, `"bullish"`). They
1719
+ are NOT qualified with the field ID in patches or FieldValue—the field context is
1720
+ implicit.
1721
+
1722
+ ##### Checkbox Mode State Values
1723
+
1724
+ | Mode | States | Zod Enum |
1725
+ | --- | --- | --- |
1726
+ | `multi` (default) | `todo`, `done`, `incomplete`, `active`, `na` | `z.enum(['todo', 'done', 'incomplete', 'active', 'na'])` |
1727
+ | `simple` | `todo`, `done` | `z.enum(['todo', 'done'])` |
1728
+ | `explicit` | `unfilled`, `yes`, `no` | `z.enum(['unfilled', 'yes', 'no'])` |
1729
+
1730
+ * * *
1731
+
1732
+ ## Layer 3: Validation & Form Filling
1733
+
1734
+ This layer defines the rules for validating form data, computing progress state, and
1735
+ manipulating forms through patches.
1736
+ It covers both the constraints that must be satisfied and the mechanics of form filling.
1737
+
1738
+ Validation happens at two levels: Markdoc syntax validation (see
1739
+ [Markdoc Validation][markdoc-validation]) and Markform semantic validation.
1740
+
1741
+ #### Built-in Deterministic Validation
1742
+
1743
+ Schema checks (always available, deterministic):
1744
+
1745
+ | Check | Field Type | Constraint Source |
1746
+ | --- | --- | --- |
1747
+ | Required fields present | All | `required=true` attribute |
1748
+ | Number parsing success | `number-field` | Built-in |
1749
+ | Min/max value range | `number-field` | `min`, `max` attributes |
1750
+ | Integer constraint | `number-field` | `integer=true` attribute |
1751
+ | Pattern match | `string-field` | `pattern` attribute (JS regex) |
1752
+ | Min/max length | `string-field` | `minLength`, `maxLength` attributes |
1753
+ | Min/max item count | `string-list` | `minItems`, `maxItems` attributes |
1754
+ | Item length constraints | `string-list` | `itemMinLength`, `itemMaxLength` attributes |
1755
+ | Unique items | `string-list` | `uniqueItems=true` attribute |
1756
+ | Min/max selections | `multi-select` | `minSelections`, `maxSelections` (see [JSON Schema array][json-schema-array]) |
1757
+ | Exactly one selected | `single-select` | `required=true` |
1758
+ | Valid checkbox states | `checkboxes` | `checkboxMode` attribute (multi: 5 states, simple: 2 states, explicit: yes/no) |
1759
+ | Valid explicit states | `checkboxes` | `checkboxMode="explicit"` validates markers are `unfilled`, `yes`, or `no` |
1760
+
1761
+ Output: `ValidationIssue[]`
1762
+
1763
+ #### Required Field Semantics
1764
+
1765
+ The `required` attribute has specific semantics for each field type.
1766
+ This section provides normative definitions:
1767
+
1768
+ | Field Type | `required=true` means | `required=false` (or omitted) means |
1769
+ | --- | --- | --- |
1770
+ | `string-field` | `value !== null && value.trim() !== ""` | Value may be null or empty |
1771
+ | `number-field` | `value !== null` (and parseable as number) | Value may be null |
1772
+ | `string-list` | `items.length >= max(1, minItems)` | Empty array is valid (unless `minItems` constraint) |
1773
+ | `single-select` | Exactly one option must be selected | Zero or one option selected (never >1) |
1774
+ | `multi-select` | `selected.length >= max(1, minSelections)` | Empty selection valid (unless `minSelections` constraint) |
1775
+ | `checkboxes` | See checkbox completion rules below | No completion requirement |
1776
+
1777
+ **Checkbox completion rules by mode:**
1778
+
1779
+ When `required=true`, checkboxes must reach a “completion state” based on mode:
1780
+
1781
+ | Mode | Completion state | Non-terminal states (invalid when required) |
1782
+ | --- | --- | --- |
1783
+ | `simple` | Done count ≥ `minDone` (default `-1` = all) | `todo` (if below threshold) |
1784
+ | `multi` | All options in `{done, na}` | `todo`, `incomplete`, `active` |
1785
+ | `explicit` | All options in `{yes, no}` (no `unfilled`) | `unfilled` |
1786
+
1787
+ **`simple` mode completion with `minDone`:**
1788
+
1789
+ - If `minDone=-1` (default): All options must be `done` (strict completion)
1790
+
1791
+ - If `minDone=0`: Always complete (no minimum threshold)
1792
+
1793
+ - If `minDone=N` (where N > 0): At least N options must be `done`
1794
+
1795
+ - If `minDone` exceeds option count, it’s clamped to the option count
1796
+
1797
+ **Note:** For `multi` mode, `incomplete` and `active` are valid workflow states during
1798
+ form filling but are **not** terminal states.
1799
+ A completed form must have all checkbox options resolved to either `done` or `na`.
1800
+
1801
+ **Field group `required` attribute:**
1802
+
1803
+ The `required` attribute on `field-group` is **not supported in MF/0.1**. Groups may
1804
+ have `validate` references for custom validation, but the `required` attribute should
1805
+ not be used on groups.
1806
+ If present, it is ignored with a warning.
1807
+
1808
+ #### Hook Validators
1809
+
1810
+ Validators are referenced by **ID** from fields/groups/form via `validate=[...]`.
1811
+
1812
+ **Validate attribute syntax:**
1813
+
1814
+ The `validate` attribute accepts an array of validator references.
1815
+ Each reference can be:
1816
+
1817
+ 1. **String** — Simple validator ID with no parameters:
1818
+ ```md
1819
+ validate=["thesis_quality"]
1820
+ ```
1821
+
1822
+ 2. **Object** — Validator ID with parameters (Markdoc supports JSON object syntax):
1823
+ ```md
1824
+ validate=[{id: "min_words", min: 50}]
1825
+ validate=[{id: "sum_to", target: 100, tolerance: 0.1}]
1826
+ ```
1827
+
1828
+ 3. **Mixed** — Combine both in one array:
1829
+ ```md
1830
+ validate=["format_check", {id: "min_words", min: 25}]
1831
+ ```
1832
+
1833
+ **Code validators (`.valid.ts`):**
1834
+
1835
+ - Sidecar file with same basename: `X.form.md` → `X.valid.ts`
1836
+
1837
+ - Loaded at runtime via [jiti](https://github.com/unjs/jiti) (~150KB, zero dependencies)
1838
+
1839
+ - Exports a `validators` registry mapping `validatorId -> function`
1840
+
1841
+ **Validator contract:**
1842
+
1843
+ ```ts
1844
+ import type { ValidatorContext, ValidationIssue } from 'markform';
1845
+
1846
+ /**
1847
+ * A single validator reference from the validate attribute.
1848
+ */
1849
+ type ValidatorRef = string | { id: string; [key: string]: unknown };
1850
+
1851
+ /**
1852
+ * Context passed to each validator function.
1853
+ */
1854
+ interface ValidatorContext {
1855
+ schema: FormSchema;
1856
+ values: Record<Id, FieldValue>;
1857
+ targetId: Id; // field/group/form ID that referenced this validator
1858
+ targetSchema: Field | FieldGroup | Form; // schema of the target (for reading custom attrs)
1859
+ params: Record<string, unknown>; // parameters from validate ref (empty if string ref)
1860
+ }
1861
+
1862
+ /**
1863
+ * Sidecar file exports a validators registry.
1864
+ */
1865
+ export const validators: Record<string, (ctx: ValidatorContext) => ValidationIssue[]> = {
1866
+ // Parameterized validator: min word count from params
1867
+ min_words: (ctx) => {
1868
+ const min = ctx.params.min as number;
1869
+ if (typeof min !== 'number') {
1870
+ return [{ severity: 'error', message: 'min_words requires "min" parameter', ref: ctx.targetId, source: 'code' }];
1871
+ }
1872
+ const value = ctx.values[ctx.targetId];
1873
+ if (value?.kind === 'string' && value.value) {
1874
+ const wordCount = value.value.trim().split(/\s+/).length;
1875
+ if (wordCount < min) {
1876
+ return [{
1877
+ severity: 'error',
1878
+ message: `Field requires at least ${min} words (currently ${wordCount})`,
1879
+ ref: ctx.targetId,
1880
+ source: 'code',
1881
+ }];
1882
+ }
1883
+ }
1884
+ return [];
1885
+ },
1886
+
1887
+ // Simple validator with no params
1888
+ thesis_quality: (ctx) => {
1889
+ const thesis = ctx.values.thesis;
1890
+ if (thesis?.kind === 'string' && thesis.value && thesis.value.length < 50) {
1891
+ return [{
1892
+ severity: 'warning',
1893
+ message: 'Investment thesis should be more detailed',
1894
+ ref: 'thesis',
1895
+ source: 'code',
1896
+ }];
1897
+ }
1898
+ return [];
1899
+ },
1900
+ };
1901
+ ```
1902
+
1903
+ **Usage examples:**
1904
+
1905
+ ```md
1906
+
1907
+ <!-- Parameterized: pass min word count as parameter -->
1908
+
1909
+ {% string-field id="thesis" label="Investment thesis" validate=[{id: "min_words", min: 50}] %}{% /string-field %}
1910
+
1911
+ <!-- Multiple validators with different params -->
1912
+
1913
+ {% string-field id="summary" label="Summary" validate=[{id: "min_words", min: 25}, {id: "max_words", max: 100}] %}{% /string-field %}
1914
+
1915
+ <!-- Sum-to validator with configurable target -->
1916
+
1917
+ {% field-group id="scenarios" validate=[{id: "sum_to", fields: ["base_prob", "bull_prob", "bear_prob"], target: 100}] %}
1918
+ ```
1919
+
1920
+ **Runtime loading (engine):**
1921
+
1922
+ ```ts
1923
+ import { createJiti } from 'jiti';
1924
+
1925
+ const jiti = createJiti(import.meta.url);
1926
+
1927
+ export async function loadValidators(formPath: string): Promise<ValidatorRegistry> {
1928
+ const basePath = formPath.replace(/\.form\.md$/, '');
1929
+
1930
+ for (const ext of ['.valid.ts', '.valid.js']) {
1931
+ const validatorPath = basePath + ext;
1932
+ if (await fileExists(validatorPath)) {
1933
+ try {
1934
+ const mod = await jiti.import(validatorPath);
1935
+ return mod.validators ?? {};
1936
+ } catch (err) {
1937
+ // Return error as validation issue, don't crash
1938
+ return { __load_error__: () => [{
1939
+ severity: 'error',
1940
+ message: `Failed to load validators: ${err.message}`,
1941
+ source: 'code',
1942
+ }]};
1943
+ }
1944
+ }
1945
+ }
1946
+ return {};
1947
+ }
1948
+ ```
1949
+
1950
+ **Caching:** Jiti caches transpiled files in `node_modules/.cache/jiti`. First load
1951
+ transpiles (~~50-100ms); subsequent loads read from cache (~~5ms).
1952
+
1953
+ **Error handling:**
1954
+
1955
+ - Syntax errors in `.valid.ts` → reported as validation issue, form still loads
1956
+
1957
+ - Missing `validators` export → warning, continue with empty registry
1958
+
1959
+ - Validator throws at runtime → catch and convert to validation issue
1960
+
1961
+ **LLM validators (`.valid.md`) — MF/0.2:**
1962
+
1963
+ - Sidecar file: `X.valid.md`
1964
+
1965
+ - Contains prompts keyed by validator IDs
1966
+
1967
+ - Executed behind a flag (`--llm-validate`) with an injected model client
1968
+
1969
+ - Output as structured JSON issues
1970
+
1971
+ - Deferred to MF/0.2 to reduce scope
1972
+
1973
+ #### Validation Pipeline
1974
+
1975
+ 1. Built-ins first (fast, deterministic)
1976
+
1977
+ 2. Code validators (via jiti)
1978
+
1979
+ 3. LLM validators (optional; MF/0.2)
1980
+
1981
+ #### Validation Result Model
1982
+
1983
+ ```ts
1984
+ type Severity = 'error' | 'warning' | 'info';
1985
+
1986
+ // Source location types for CLI/tool integration
1987
+ interface SourcePosition {
1988
+ line: number; // 1-indexed line number
1989
+ col: number; // 1-indexed column number
1990
+ }
1991
+
1992
+ interface SourceRange {
1993
+ start: SourcePosition;
1994
+ end: SourcePosition;
1995
+ }
1996
+
1997
+ interface ValidationIssue {
1998
+ severity: Severity;
1999
+ message: string; // Human-readable, suitable for display
2000
+ code?: string; // Machine-readable error code (e.g., 'REQUIRED_MISSING')
2001
+ ref?: Id; // Field/group ID this issue relates to
2002
+ path?: string; // Field/group ID path (e.g., "company_info.ticker")
2003
+ range?: SourceRange; // Source location if available from Markdoc AST
2004
+ validatorId?: string; // Which validator produced this (for hook validators)
2005
+ source: 'builtin' | 'code' | 'llm';
2006
+ }
2007
+ ```
2008
+
2009
+ **Error message guidelines:**
2010
+
2011
+ - Messages should be actionable: “Field ‘ticker’ is required” not “Validation failed”
2012
+
2013
+ - Include the field label when available for human context
2014
+
2015
+ - Include the field ID in `ref` for programmatic access
2016
+
2017
+ - Use `code` for agents to handle specific error types programmatically
2018
+
2019
+ **Standard error codes (built-in):**
2020
+
2021
+ | Code | Meaning |
2022
+ | --- | --- |
2023
+ | `REQUIRED_MISSING` | Required field has no value |
2024
+ | `NUMBER_PARSE_ERROR` | Value cannot be parsed as number |
2025
+ | `NUMBER_OUT_OF_RANGE` | Number outside min/max bounds |
2026
+ | `NUMBER_NOT_INTEGER` | Number has decimal when integer required |
2027
+ | `PATTERN_MISMATCH` | Value doesn't match regex pattern |
2028
+ | `LENGTH_OUT_OF_RANGE` | String length outside min/max bounds |
2029
+ | `ITEM_COUNT_ERROR` | String-list item count outside minItems/maxItems bounds |
2030
+ | `ITEM_LENGTH_ERROR` | String-list item length outside itemMinLength/itemMaxLength bounds |
2031
+ | `DUPLICATE_ITEMS` | String-list contains duplicate items when uniqueItems=true |
2032
+ | `SELECTION_COUNT_ERROR` | Wrong number of selections in multi-select |
2033
+ | `INVALID_CHECKBOX_STATE` | Checkbox has disallowed state (e.g., `[*]` when `checkboxMode="simple"`) |
2034
+ | `EXPLICIT_CHECKBOX_UNFILLED` | Explicit checkbox has unfilled options (requires yes/no for all) |
2035
+ | `INVALID_OPTION_ID` | Selected option ID doesn't exist |
2036
+
2037
+ #### Error Taxonomy
2038
+
2039
+ Markform distinguishes between two fundamental error types:
2040
+
2041
+ **1. ParseError** — Syntax and structural errors
2042
+
2043
+ Parse errors occur when the markdown/Markdoc syntax is malformed or the form structure
2044
+ is invalid. These are detected during parsing and prevent the form from being loaded.
2045
+
2046
+ Examples:
2047
+
2048
+ - Invalid Markdoc syntax (unclosed tags, malformed attributes)
2049
+
2050
+ - Missing required attributes (e.g., field without `id` or `label`)
2051
+
2052
+ - Duplicate IDs within the form
2053
+
2054
+ - Invalid field state attribute value (not ‘skipped’ or ‘aborted’)
2055
+
2056
+ - Malformed sentinel values in value fences
2057
+
2058
+ **Type definition:**
2059
+ ```ts
2060
+ interface ParseError {
2061
+ type: 'parse';
2062
+ message: string;
2063
+ location?: {
2064
+ line?: number;
2065
+ column?: number;
2066
+ fieldId?: Id;
2067
+ noteId?: NoteId;
2068
+ };
2069
+ }
2070
+ ```
2071
+
2072
+ **Behavior:**
2073
+
2074
+ - Parse errors prevent form loading
2075
+
2076
+ - Returned from `parseForm()` as thrown exceptions or error results
2077
+
2078
+ - Must be fixed before the form can be used
2079
+
2080
+ **2. MarkformValidationError** — Model consistency errors
2081
+
2082
+ Validation errors occur when the parsed form model is inconsistent with Markform rules,
2083
+ even if the syntax is valid.
2084
+ These are semantic errors in the data model.
2085
+
2086
+ Examples:
2087
+
2088
+ - Option ID referenced in value doesn’t exist in field schema
2089
+
2090
+ - Field value type doesn’t match field kind
2091
+
2092
+ - Response state inconsistency (e.g., `state='answered'` but no value present)
2093
+
2094
+ - Invalid checkbox state for the field’s checkbox mode
2095
+
2096
+ **Type definition:**
2097
+ ```ts
2098
+ interface MarkformValidationError {
2099
+ type: 'validation';
2100
+ message: string;
2101
+ location?: {
2102
+ line?: number;
2103
+ column?: number;
2104
+ fieldId?: Id;
2105
+ noteId?: NoteId;
2106
+ };
2107
+ }
2108
+ ```
2109
+
2110
+ **Behavior:**
2111
+
2112
+ - Validation errors prevent form operations
2113
+
2114
+ - Returned from `parseForm()` or `applyPatches()` as errors
2115
+
2116
+ - Must be fixed before the form is in a valid state
2117
+
2118
+ **Distinction from ValidationIssue:**
2119
+
2120
+ `ValidationIssue` represents content validation (required fields, constraints, hook
2121
+ validators) and is part of normal form filling workflow.
2122
+ These issues don’t prevent form operations—they guide what needs to be filled next.
2123
+
2124
+ `ParseError` and `MarkformValidationError` represent structural problems that prevent
2125
+ the form from being used at all.
2126
+
2127
+ **Union type:**
2128
+ ```ts
2129
+ type MarkformError = ParseError | MarkformValidationError;
2130
+ ```
2131
+
2132
+ * * *
2133
+
2134
+ ## Layer 4: Tool API & Interfaces
2135
+
2136
+ This layer defines how agents and humans interact with forms.
2137
+ It specifies:
2138
+
2139
+ - **Tool operations** (inspect, apply, export) and their method signatures
2140
+
2141
+ - **Result types** for each operation
2142
+
2143
+ - **Import/export formats** for form values (JSON, YAML)
2144
+
2145
+ - **Priority scoring** for guiding agents on what to fill next
2146
+
2147
+ - **Abstract interface patterns** for console, web, and agent tools
2148
+
2149
+ Tool definitions follow [AI SDK tool conventions][ai-sdk-tools] and can be exposed via
2150
+ MCP (Model Context Protocol) for agent integration.
2151
+
2152
+ #### Core Operations
2153
+
2154
+ | Operation | Description | Returns |
2155
+ | --- | --- | --- |
2156
+ | **Inspect** | Get form state summary | `InspectResult` with summaries and unified issues list |
2157
+ | **Apply** | Apply patches to form values | `ApplyResult` with updated summaries and issues |
2158
+ | **Export** | Get structured data | `{ schema: FormSchemaJson, values: FormValuesJson }` |
2159
+ | **GetMarkdown** | Get canonical form source | Markdown string |
2160
+
2161
+ #### Inspect and Apply Result Types
2162
+
2163
+ ```ts
2164
+ interface InspectResult {
2165
+ structureSummary: StructureSummary; // form structure overview
2166
+ progressSummary: ProgressSummary; // filling progress per field
2167
+ issues: InspectIssue[]; // unified list sorted by priority (ascending, 1 = highest)
2168
+ isComplete: boolean; // see completion formula below
2169
+ formState: ProgressState; // overall progress state; mirrors frontmatter form_state
2170
+ }
2171
+
2172
+ interface ApplyResult {
2173
+ applyStatus: 'applied' | 'rejected'; // 'rejected' if structural validation failed
2174
+ structureSummary: StructureSummary;
2175
+ progressSummary: ProgressSummary;
2176
+ issues: InspectIssue[]; // unified list sorted by priority (ascending, 1 = highest)
2177
+ isComplete: boolean; // see completion formula below
2178
+ formState: ProgressState; // overall progress state; mirrors frontmatter form_state
2179
+ }
2180
+ ```
2181
+
2182
+ **Notes:**
2183
+
2184
+ - Both operations return the full summaries for client convenience
2185
+
2186
+ - `issues` is a single sorted list; filter by `severity: 'required'` to get blockers
2187
+
2188
+ - `structureSummary` is static (doesn’t change after patches) but included for
2189
+ consistency
2190
+
2191
+ - `progressSummary` is recomputed after each patch application
2192
+
2193
+ - Summaries are serialized to frontmatter on every form write
2194
+
2195
+ - `formState` is derived from `progressSummary.counts` (see ProgressState definitions)
2196
+
2197
+ **Completion formula:**
2198
+
2199
+ `isComplete` is true when all target-role fields are either answered or skipped, there
2200
+ are no aborted fields, and there are no issues with `severity: 'required'`:
2201
+
2202
+ ```
2203
+ isComplete = (answeredFields + skippedFields == totalFields for target roles)
2204
+ AND (abortedFields == 0)
2205
+ AND (no issues with severity == 'required')
2206
+ ```
2207
+
2208
+ This formula ensures:
2209
+
2210
+ - Agents must actively respond to every field (either fill it or explicitly skip it)
2211
+
2212
+ - Aborted fields block completion (they represent failures requiring intervention)
2213
+
2214
+ - Skipped fields won’t have values, but they won’t block completion
2215
+
2216
+ **Operation availability by interface:**
2217
+
2218
+ | Operation | CLI | AI SDK | MCP (MF/0.2) |
2219
+ | --- | --- | --- | --- |
2220
+ | inspect | `markform inspect` (prints YAML report) | `markform_inspect` | `markform.inspect` |
2221
+ | apply | `markform apply` | `markform_apply` | `markform.apply` |
2222
+ | export | `markform export --format=json` | `markform_export` | `markform.export` |
2223
+ | getMarkdown | `markform apply` (writes file) | `markform_get_markdown` | `markform.get_markdown` |
2224
+ | render | `markform render` (static HTML output) | — | — |
2225
+ | serve | `markform serve` (interactive web UI) | — | — |
2226
+
2227
+ #### Patch Schema
2228
+
2229
+ ```ts
2230
+ type Patch =
2231
+ | { op: 'set_string'; fieldId: Id; value: string | null }
2232
+ | { op: 'set_number'; fieldId: Id; value: number | null }
2233
+ | { op: 'set_string_list'; fieldId: Id; items: string[] }
2234
+ | { op: 'set_checkboxes'; fieldId: Id; values: Record<OptionId, CheckboxValue> }
2235
+ | { op: 'set_single_select'; fieldId: Id; selected: OptionId | null }
2236
+ | { op: 'set_multi_select'; fieldId: Id; selected: OptionId[] }
2237
+ | { op: 'set_url'; fieldId: Id; value: string | null }
2238
+ | { op: 'set_url_list'; fieldId: Id; items: string[] }
2239
+ | { op: 'clear_field'; fieldId: Id }
2240
+ | { op: 'skip_field'; fieldId: Id; role: string; reason?: string }
2241
+ | { op: 'abort_field'; fieldId: Id; role: string; reason?: string }
2242
+ | { op: 'add_note'; ref: Id; role: string; text: string; state?: 'skipped' | 'aborted' }
2243
+ | { op: 'remove_note'; noteId: NoteId };
2244
+
2245
+ // OptionId is just the local ID within the field (e.g., "ten_k", "bullish")
2246
+ // NOT the qualified form—the fieldId provides the scope
2247
+ type OptionId = string;
2248
+ ```
2249
+
2250
+ **Option ID scoping in patches:**
2251
+
2252
+ Option IDs in patches are **local to the field** specified by `fieldId`. You do NOT use
2253
+ the qualified `{fieldId}.{optionId}` form in patches—the `fieldId` already provides the
2254
+ scope. For example:
2255
+
2256
+ - `{ op: 'set_checkboxes', fieldId: 'docs_reviewed', values: { ten_k: 'done', ten_q:
2257
+ 'done' } }`
2258
+
2259
+ - `{ op: 'set_single_select', fieldId: 'rating', selected: 'bullish' }`
2260
+
2261
+ **Patch semantics:**
2262
+
2263
+ - `set_*` with `null` value: Clears the field (equivalent to `clear_field`)
2264
+
2265
+ - `clear_field`: Removes all values; behavior varies by field kind:
2266
+
2267
+ - **string/number fields:** Clear the value fence entirely
2268
+
2269
+ - **string_list field:** Clear to empty list (no value fence)
2270
+
2271
+ - **single_select field:** Reset all markers to `[ ]` (no selection)
2272
+
2273
+ - **multi_select field:** Reset all markers to `[ ]` (no selections)
2274
+
2275
+ - **checkboxes field:** Reset to default state based on mode:
2276
+
2277
+ - simple mode: all `[ ]`
2278
+
2279
+ - multi mode: all `[ ]` (todo)
2280
+
2281
+ - explicit mode: all `[ ]` (unfilled)
2282
+
2283
+ - `set_checkboxes`: Merges provided values with existing state (only specified options
2284
+ are updated)
2285
+
2286
+ - `set_multi_select`: Replaces entire selection array (not additive)
2287
+
2288
+ - `skip_field`: Explicitly skip an optional field without providing a value.
2289
+ Used when an agent cannot or should not fill a field (e.g., information not available,
2290
+ field not applicable).
2291
+ The optional `reason` field provides context.
2292
+ The required `role` field identifies who is skipping.
2293
+
2294
+ **Constraints:**
2295
+
2296
+ - Can only skip **optional** fields (required fields reject with error)
2297
+
2298
+ - Skipping a field clears any existing value
2299
+
2300
+ - A skipped field counts toward completion but has no value
2301
+
2302
+ **Behavior:**
2303
+
2304
+ - Response state changes to `'skipped'` in `responsesByFieldId`
2305
+
2306
+ - Skipped fields no longer appear in the issues list (not blocking completion)
2307
+
2308
+ - Setting a value on a skipped field clears the skip state (field becomes answered)
2309
+ and removes any notes with `state="skipped"` for that field (general notes are
2310
+ preserved)
2311
+
2312
+ - Skip state is serialized to markdown via `state="skipped"` attribute
2313
+
2314
+ **Completion semantics:** Form completion requires all fields to be in a terminal
2315
+ state (`answered`, `skipped`, or `aborted` for optional fields) AND `abortedFields ==
2316
+ 0`. This ensures agents actively respond to every field, even if just to skip it.
2317
+
2318
+ - `abort_field`: Mark a field as unable to be completed (for any reason).
2319
+ Used when a field cannot be answered and should not block form completion.
2320
+ The required `role` field identifies who is aborting.
2321
+ The optional `reason` field provides context.
2322
+
2323
+ **Constraints:**
2324
+
2325
+ - Can be used on both required and optional fields
2326
+
2327
+ - Aborting a field clears any existing value
2328
+
2329
+ - An aborted field does NOT count toward completion
2330
+
2331
+ **Behavior:**
2332
+
2333
+ - Response state changes to `'aborted'` in `responsesByFieldId`
2334
+
2335
+ - Aborted fields appear in the issues list as blocking completion
2336
+
2337
+ - Setting a value on an aborted field clears the abort state (field becomes answered)
2338
+ and removes any notes with `state="aborted"` for that field (general notes are
2339
+ preserved)
2340
+
2341
+ - Abort state is serialized to markdown via `state="aborted"` attribute
2342
+
2343
+ **Completion semantics:** Form completion requires `abortedFields == 0`. Any aborted
2344
+ field blocks completion, requiring manual intervention to either fill the field or
2345
+ remove the abort state.
2346
+
2347
+ - `add_note`: Attach a note to a field, group, or form.
2348
+ The `ref` parameter specifies the target ID. The required `role` field identifies who
2349
+ created the note. The `text` field contains markdown content.
2350
+ The optional `state` field links the note to a skip or abort action.
2351
+
2352
+ **Behavior:**
2353
+
2354
+ - Note is added to `ParsedForm.notes` array
2355
+
2356
+ - Note ID is auto-generated (n1, n2, n3 …)
2357
+
2358
+ - Notes are serialized to markdown as comment blocks with metadata
2359
+
2360
+ - Multiple notes can exist for the same ref
2361
+
2362
+ - `remove_note`: Remove a specific note by ID.
2363
+
2364
+ **Behavior:**
2365
+
2366
+ - Note with matching `noteId` is removed from `ParsedForm.notes`
2367
+
2368
+ - If note doesn’t exist, operation is silently ignored (idempotent)
2369
+
2370
+ **Patch validation layers (*required*):**
2371
+
2372
+ Patches go through two distinct validation phases:
2373
+
2374
+ **1. Structural validation (pre-apply):** Checked before any patches are applied:
2375
+
2376
+ - `fieldId` exists in schema
2377
+
2378
+ - `optionId` exists for the referenced field (for select/checkbox patches)
2379
+
2380
+ - Value shape matches expected type (e.g., `number` for `set_number`)
2381
+
2382
+ - **Unknown option IDs** (*required*): For `set_checkboxes`, `set_single_select`, and
2383
+ `set_multi_select` patches, any option ID not defined in the field’s schema produces
2384
+ an `INVALID_OPTION_ID` error.
2385
+ The entire patch batch is rejected—unknown keys are never silently dropped.
2386
+
2387
+ If *any* patch fails structural validation:
2388
+
2389
+ - *required:* Entire batch is rejected (transaction semantics)
2390
+
2391
+ - *required:* Form state is unchanged
2392
+
2393
+ - *required:* Response includes `applyStatus: "rejected"` and structural issues
2394
+
2395
+ **2. Semantic validation (post-apply):** Checked after all patches are applied:
2396
+
2397
+ - Required field constraints
2398
+
2399
+ - Pattern/range validation
2400
+
2401
+ - Selection count constraints
2402
+
2403
+ Semantic issues do **not** prevent patch application—they are returned as
2404
+ `ValidationIssue[]` for the caller to address.
2405
+ This is the normal inspect/apply/fix workflow.
2406
+
2407
+ **Patch conflict handling:**
2408
+
2409
+ - Patches are applied in array order within a single `apply` call
2410
+
2411
+ - Later patches to the same field overwrite earlier ones (last-write-wins)
2412
+
2413
+ #### Inspect Results
2414
+
2415
+ When `inspect` runs, it returns a **single list of `InspectIssue` objects** sorted by
2416
+ priority tier (ascending, where P1 = highest priority).
2417
+ Priority is computed using a tiered scoring system based on field importance and issue
2418
+ type.
2419
+
2420
+ ##### Priority Scoring System
2421
+
2422
+ **Field Priority Weight** (optional schema attribute, defaults to `medium`):
2423
+
2424
+ | Field Priority | Weight |
2425
+ | --- | --- |
2426
+ | `high` | 3 |
2427
+ | `medium` | 2 (default) |
2428
+ | `low` | 1 |
2429
+
2430
+ **Issue Type Score:**
2431
+
2432
+ | Issue Reason | Score |
2433
+ | --- | --- |
2434
+ | `required_missing` | 3 |
2435
+ | `checkbox_incomplete` | 3 (required) / 2 (recommended) |
2436
+ | `validation_error` | 2 |
2437
+ | `min_items_not_met` | 2 |
2438
+ | `optional_empty` | 1 |
2439
+
2440
+ **Total Score** = Field Priority Weight + Issue Type Score
2441
+
2442
+ **Priority Tier** mapping from total score:
2443
+
2444
+ | Tier | Score Threshold | Console Color |
2445
+ | --- | --- | --- |
2446
+ | P1 | ≥ 5 | Bold red |
2447
+ | P2 | ≥ 4 | Yellow |
2448
+ | P3 | ≥ 3 | Cyan |
2449
+ | P4 | ≥ 2 | Blue |
2450
+ | P5 | ≥ 1 | Dim/gray |
2451
+
2452
+ **Examples** (assuming default `medium` field priority):
2453
+
2454
+ | Issue Type | Field Priority | Total Score | Tier |
2455
+ | --- | --- | --- | --- |
2456
+ | Required field missing | medium (2) | 2 + 3 = 5 | P1 |
2457
+ | Validation error | medium (2) | 2 + 2 = 4 | P2 |
2458
+ | Optional field empty | medium (2) | 2 + 1 = 3 | P3 |
2459
+ | Required field missing | low (1) | 1 + 3 = 4 | P2 |
2460
+ | Validation error | low (1) | 1 + 2 = 3 | P3 |
2461
+ | Optional field empty | low (1) | 1 + 1 = 2 | P4 |
2462
+
2463
+ Within each tier, issues are sorted by:
2464
+
2465
+ 1. Severity (`required` before `recommended`)
2466
+
2467
+ 2. Score (higher scores first)
2468
+
2469
+ 3. Ref (alphabetically for deterministic output)
2470
+
2471
+ The harness config controls how many issues to return (`max_issues`).
2472
+
2473
+ **Completion check:** A form is complete when there are no issues with `severity:
2474
+ 'required'`.
2475
+
2476
+ #### Export Schema
2477
+
2478
+ The `export` operation returns a JSON object with `schema` and `values` properties.
2479
+
2480
+ **Schema format:**
2481
+
2482
+ ```ts
2483
+ interface ExportedSchema {
2484
+ id: string;
2485
+ title?: string;
2486
+ groups: ExportedGroup[];
2487
+ }
2488
+
2489
+ interface ExportedGroup {
2490
+ id: string;
2491
+ title?: string;
2492
+ children: ExportedField[];
2493
+ }
2494
+
2495
+ interface ExportedField {
2496
+ id: string;
2497
+ kind: FieldKind;
2498
+ label: string;
2499
+ required: boolean; // Always explicit: true or false
2500
+ options?: ExportedOption[]; // For single_select, multi_select, checkboxes
2501
+ }
2502
+
2503
+ interface ExportedOption {
2504
+ id: string;
2505
+ label: string;
2506
+ }
2507
+ ```
2508
+
2509
+ **Key design decisions:**
2510
+
2511
+ - **`required` is always explicit:** The `required` field is always present as `true` or
2512
+ `false`, never omitted.
2513
+ This makes the schema self-documenting for external consumers without requiring
2514
+ knowledge of default values.
2515
+
2516
+ - **Values are typed by kind:** The `values` object maps field IDs to typed value
2517
+ objects matching the field’s `kind`.
2518
+
2519
+ #### Value Export Formats
2520
+
2521
+ Value export supports two formats: **structured** (default) and **friendly** (optional).
2522
+
2523
+ **Structured format (default for JSON and YAML):**
2524
+
2525
+ The structured format uses explicit objects for each field response, eliminating
2526
+ ambiguity between actual values and sentinel strings:
2527
+
2528
+ ```json
2529
+ {
2530
+ "values": {
2531
+ "company_name": { "state": "skipped" },
2532
+ "revenue_m": { "state": "aborted" },
2533
+ "quarterly_growth": { "state": "answered", "value": 12.5 },
2534
+ "ticker": { "state": "answered", "value": "ACME" }
2535
+ },
2536
+ "notes": [
2537
+ {
2538
+ "id": "n1",
2539
+ "ref": "company_name",
2540
+ "role": "agent",
2541
+ "state": "skipped",
2542
+ "text": "Not applicable for this analysis type."
2543
+ }
2544
+ ]
2545
+ }
2546
+ ```
2547
+
2548
+ **YAML equivalent:**
2549
+
2550
+ ```yaml
2551
+ values:
2552
+ company_name:
2553
+ state: skipped
2554
+ revenue_m:
2555
+ state: aborted
2556
+ quarterly_growth:
2557
+ state: answered
2558
+ value: 12.5
2559
+ ticker:
2560
+ state: answered
2561
+ value: ACME
2562
+ notes:
2563
+ - id: n1
2564
+ ref: company_name
2565
+ role: agent
2566
+ state: skipped
2567
+ text: Not applicable for this analysis type.
2568
+ ```
2569
+
2570
+ **Friendly format (optional, with `--friendly` flag):**
2571
+
2572
+ The friendly format uses sentinel strings for human readability:
2573
+
2574
+ ```yaml
2575
+ values:
2576
+ company_name: "%SKIP%"
2577
+ revenue_m: "%ABORT%"
2578
+ quarterly_growth: 12.5
2579
+ ticker: ACME
2580
+ ```
2581
+
2582
+ **Format comparison:**
2583
+
2584
+ | Aspect | Structured (default) | Friendly |
2585
+ | --- | --- | --- |
2586
+ | Ambiguity | None | Possible collision with literal values |
2587
+ | Machine interchange | Recommended | Not recommended |
2588
+ | Human readability | Verbose | Concise |
2589
+ | Notes included | Yes | Yes (separate section) |
2590
+ | CLI flag | (default) | `--friendly` |
2591
+
2592
+ **Import behavior:**
2593
+
2594
+ When importing values (from YAML or JSON):
2595
+
2596
+ - Structured format: Parse `state` and `value` properties from each field
2597
+
2598
+ - Friendly format: Recognize `%SKIP%` and `%ABORT%` sentinel strings
2599
+
2600
+ **Export includes notes:**
2601
+
2602
+ All export formats include the `notes` array with all note attributes:
2603
+
2604
+ - `id`: Note identifier
2605
+
2606
+ - `ref`: Target element ID
2607
+
2608
+ - `role`: Who created the note
2609
+
2610
+ - `state`: Optional link to skip/abort action
2611
+
2612
+ - `text`: Note content
2613
+
2614
+ * * *
2615
+
2616
+ ## References
2617
+
2618
+ ### Markdoc
2619
+
2620
+ Core documentation for the Markdoc framework that powers Markform’s syntax layer:
2621
+
2622
+ - [What is Markdoc?][markdoc-overview] — Philosophy and “docs-as-data” rationale
2623
+
2624
+ - [Tag Syntax Specification][markdoc-spec] — Formal grammar for `{% tag %}` syntax
2625
+
2626
+ - [Syntax Guide][markdoc-syntax] — Practical syntax reference
2627
+
2628
+ - [Tags][markdoc-tags] — Custom tag composition and behavior
2629
+
2630
+ - [Attributes][markdoc-attributes] — Attribute typing and `#id` shorthand
2631
+
2632
+ - [Nodes][markdoc-nodes] — Node model including `fence` and `process` attribute
2633
+
2634
+ - [Validation][markdoc-validation] — Built-in `Markdoc.validate()` for syntax checks
2635
+
2636
+ - [Frontmatter][markdoc-frontmatter] — YAML frontmatter support
2637
+
2638
+ - [Formatting][markdoc-format] — `Markdoc.format()` for AST canonicalization
2639
+
2640
+ - [Render Phases][markdoc-render] — Parse → transform → render pipeline
2641
+
2642
+ - [Getting Started][markdoc-getting-started] — Minimal implementation reference
2643
+
2644
+ - [Config Objects][markdoc-config] — Registering custom tags/nodes
2645
+
2646
+ - [Common Examples][markdoc-examples] — Custom tag transform patterns
2647
+
2648
+ - [GitHub Repository][markdoc-github] — Source and AST type definitions
2649
+
2650
+ - [Language Server][markdoc-language-server] — Editor tooling reference
2651
+
2652
+ - [FAQ][markdoc-faq] — CommonMark/GFM compatibility notes
2653
+
2654
+ - [GitHub Discussion #261][markdoc-process-false] — `process=false` implementation
2655
+ details
2656
+
2657
+ Background:
2658
+
2659
+ - [How Stripe builds interactive docs with Markdoc][stripe-markdoc] — Production usage
2660
+ patterns
2661
+
2662
+ ### AI SDK (Vercel)
2663
+
2664
+ Tool calling and agentic loop patterns:
2665
+
2666
+ - [Tools Foundation][ai-sdk-tools] — Tool shape (`description`, `inputSchema`,
2667
+ `execute`)
2668
+
2669
+ - [Tool Calling][ai-sdk-tool-calling] — Canonical tool mechanics
2670
+
2671
+ - [MCP Tools][ai-sdk-mcp] — Connecting to MCP servers
2672
+
2673
+ - [AI SDK 5 Blog][ai-sdk-5] — `stopWhen`, `stepCountIs` for loop control
2674
+
2675
+ - [AI SDK 6 Blog][ai-sdk-6] — `ToolLoopAgent` and step limits
2676
+
2677
+ ### Model Context Protocol (MCP)
2678
+
2679
+ Server implementation references:
2680
+
2681
+ - [MCP Specification][mcp-spec] — Protocol definition (JSON-RPC, tools/resources)
2682
+
2683
+ - [MCP SDKs Overview][mcp-sdks] — Official SDK documentation
2684
+
2685
+ - [TypeScript SDK][mcp-typescript-sdk] — Implementation we build on
2686
+
2687
+ ### Checkbox/Task List Conventions
2688
+
2689
+ Precedent for checkbox syntax:
2690
+
2691
+ - [GFM Task List Spec][gfm-tasklists] — Standard `[ ]`/`[x]` definition
2692
+
2693
+ - [GitHub Task Lists Docs][github-tasklists] — Basic syntax reference
2694
+
2695
+ - [GitHub About Tasklists][github-about-tasklists] — Productized tasklist features
2696
+
2697
+ - [Obsidian Forum Discussion][obsidian-tasks-forum] — `[/]` for “in progress”
2698
+
2699
+ - [Obsidian Tasks Discussion #68][obsidian-tasks-discussion] — DOING/CANCELLED status
2700
+ mapping
2701
+
2702
+ - [Obsidian Tasks Guide][obsidian-tasks-guide] — Custom status UX patterns
2703
+
2704
+ ### Schema & Validation
2705
+
2706
+ Type system and validation vocabulary:
2707
+
2708
+ - [Zod Introduction][zod] — TypeScript-first schema library
2709
+
2710
+ - [Zod API Reference][zod-api] — Primitives and constraints
2711
+
2712
+ - [zod-to-json-schema] — JSON Schema generation (note deprecation warnings)
2713
+
2714
+ - [JSON Schema Validation (2020-12)][json-schema-validation] — Canonical validation
2715
+ keywords
2716
+
2717
+ - [JSON Schema Array Reference][json-schema-array] — `minItems`/`maxItems` for
2718
+ selections
2719
+
2720
+ * * *
2721
+
2722
+ <!-- Reference Link Definitions -->
2723
+
2724
+
2725
+
2726
+ <!-- Markdoc -->
2727
+
2728
+ [markdoc-overview]: https://markdoc.dev/docs/overview "What is Markdoc?"
2729
+ [markdoc-spec]: https://markdoc.dev/spec "Markdoc Tag Syntax Specification"
2730
+ [markdoc-syntax]: https://markdoc.dev/docs/syntax "The Markdoc Syntax"
2731
+ [markdoc-tags]: https://markdoc.dev/docs/tags "Markdoc Tags"
2732
+ [markdoc-attributes]: https://markdoc.dev/docs/attributes "Markdoc Attributes"
2733
+ [markdoc-nodes]: https://markdoc.dev/docs/nodes "Markdoc Nodes"
2734
+ [markdoc-validation]: https://markdoc.dev/docs/validation "Markdoc Validation"
2735
+ [markdoc-frontmatter]: https://markdoc.dev/docs/frontmatter "Markdoc Frontmatter"
2736
+ [markdoc-format]: https://markdoc.dev/docs/format "Markdoc Formatting"
2737
+ [markdoc-render]: https://markdoc.dev/docs/render "Markdoc Render Phases"
2738
+ [markdoc-getting-started]: https://markdoc.dev/docs/getting-started "Get Started with Markdoc"
2739
+ [markdoc-config]: https://markdoc.dev/docs/config "Markdoc Config Objects"
2740
+ [markdoc-examples]: https://markdoc.dev/docs/examples "Markdoc Common Examples"
2741
+ [markdoc-github]: https://github.com/markdoc/markdoc "markdoc/markdoc on GitHub"
2742
+ [markdoc-language-server]: https://github.com/markdoc/language-server "markdoc/language-server on GitHub"
2743
+ [markdoc-faq]: https://markdoc.dev/docs/faq "Markdoc FAQ"
2744
+ [markdoc-process-false]: https://github.com/markdoc/markdoc/discussions/261 "process=false Discussion"
2745
+ [stripe-markdoc]: https://stripe.com/blog/markdoc "How Stripe builds interactive docs with Markdoc"
2746
+
2747
+ <!-- AI SDK -->
2748
+
2749
+ [ai-sdk-tools]: https://ai-sdk.dev/docs/foundations/tools "AI SDK: Tools"
2750
+ [ai-sdk-tool-calling]: https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling "AI SDK: Tool Calling"
2751
+ [ai-sdk-mcp]: https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools "AI SDK: MCP Tools"
2752
+ [ai-sdk-5]: https://vercel.com/blog/ai-sdk-5 "AI SDK 5"
2753
+ [ai-sdk-6]: https://vercel.com/blog/ai-sdk-6 "AI SDK 6"
2754
+
2755
+ <!-- MCP -->
2756
+
2757
+ [mcp-spec]: https://modelcontextprotocol.io/specification/2025-11-25 "MCP Specification"
2758
+ [mcp-sdks]: https://modelcontextprotocol.io/docs/sdk "MCP SDKs"
2759
+ [mcp-typescript-sdk]: https://github.com/modelcontextprotocol/typescript-sdk "MCP TypeScript SDK"
2760
+
2761
+ <!-- Task Lists -->
2762
+
2763
+ [gfm-tasklists]: https://github.github.com/gfm/#task-list-items-extension- "GFM Task List Items"
2764
+ [github-tasklists]: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax "GitHub Basic Formatting"
2765
+ [github-about-tasklists]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-tasklists "GitHub About Tasklists"
2766
+ [obsidian-tasks-forum]: https://forum.obsidian.md/t/partially-completed-tasks/53258 "Obsidian: Partially Completed Tasks"
2767
+ [obsidian-tasks-discussion]: https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/68 "Obsidian Tasks: Add DOING Status"
2768
+ [obsidian-tasks-guide]: https://obsidian.rocks/power-features-of-tasks-in-obsidian/ "Power Features of Tasks in Obsidian"
2769
+
2770
+ <!-- Schema/Validation -->
2771
+
2772
+ [zod]: https://zod.dev/ "Zod"
2773
+ [zod-api]: https://zod.dev/api "Zod API"
2774
+ [zod-to-json-schema]: https://github.com/StefanTerdell/zod-to-json-schema "zod-to-json-schema"
2775
+ [json-schema-validation]: https://json-schema.org/draft/2020-12/json-schema-validation "JSON Schema Validation"
2776
+ [json-schema-array]: https://json-schema.org/understanding-json-schema/reference/array "JSON Schema: Array"
2777
+
2778
+ * * *
2779
+ ~~~