markform 0.1.0 → 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.
- package/DOCS.md +546 -0
- package/README.md +484 -45
- package/SPEC.md +2779 -0
- package/dist/ai-sdk.d.mts +2 -2
- package/dist/ai-sdk.mjs +5 -3
- package/dist/{apply-C0vjijlP.mjs → apply-BfAGTHMh.mjs} +1044 -593
- package/dist/bin.mjs +6 -3
- package/dist/cli-B3NVm6zL.mjs +3904 -0
- package/dist/cli.mjs +6 -3
- package/dist/{coreTypes-T7dAuewt.d.mts → coreTypes-BXhhz9Iq.d.mts} +2795 -685
- package/dist/coreTypes-Dful87E0.mjs +537 -0
- package/dist/index.d.mts +196 -18
- package/dist/index.mjs +5 -3
- package/dist/session-Bqnwi9wp.mjs +110 -0
- package/dist/session-DdAtY2Ni.mjs +4 -0
- package/dist/shared-D7gf27Tr.mjs +3 -0
- package/dist/shared-N_s1M-_K.mjs +176 -0
- package/dist/src-BXRkGFpG.mjs +7587 -0
- package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
- package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
- package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
- package/examples/movie-research/movie-research-basic.form.md +164 -0
- package/examples/movie-research/movie-research-deep.form.md +486 -0
- package/examples/movie-research/movie-research-minimal.form.md +73 -0
- package/examples/simple/simple-mock-filled.form.md +52 -12
- package/examples/simple/simple-skipped-filled.form.md +170 -0
- package/examples/simple/simple-with-skips.session.yaml +189 -0
- package/examples/simple/simple.form.md +34 -12
- package/examples/simple/simple.session.yaml +80 -37
- package/examples/startup-deep-research/startup-deep-research.form.md +456 -0
- package/examples/startup-research/startup-research-mock-filled.form.md +307 -0
- package/examples/startup-research/startup-research.form.md +211 -0
- package/package.json +11 -5
- package/dist/cli-9fvFySww.mjs +0 -2564
- package/dist/src-DBD3Dt4f.mjs +0 -1785
- package/examples/political-research/political-research.form.md +0 -233
- 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
|
+
~~~
|