markform 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +110 -70
  2. package/dist/ai-sdk.d.mts +2 -2
  3. package/dist/ai-sdk.mjs +5 -5
  4. package/dist/{apply-00UmzDKL.mjs → apply-BCCiJzQr.mjs} +371 -26
  5. package/dist/bin.mjs +6 -6
  6. package/dist/{cli-D--Lel-e.mjs → cli-D469amuk.mjs} +386 -96
  7. package/dist/cli.mjs +6 -6
  8. package/dist/{coreTypes-BXhhz9Iq.d.mts → coreTypes-9XZSNOv6.d.mts} +1878 -325
  9. package/dist/{coreTypes-Dful87E0.mjs → coreTypes-pyctKRgc.mjs} +79 -5
  10. package/dist/index.d.mts +142 -5
  11. package/dist/index.mjs +5 -5
  12. package/dist/session-B_stoXQn.mjs +4 -0
  13. package/dist/{session-Bqnwi9wp.mjs → session-uF0e6m6k.mjs} +9 -5
  14. package/dist/{shared-N_s1M-_K.mjs → shared-BqPnYXrn.mjs} +82 -1
  15. package/dist/shared-CZsyShck.mjs +3 -0
  16. package/dist/{src-Dm8jZ5dl.mjs → src-Df0XX7UB.mjs} +818 -125
  17. package/docs/markform-apis.md +194 -0
  18. package/{DOCS.md → docs/markform-reference.md} +130 -69
  19. package/{SPEC.md → docs/markform-spec.md} +359 -108
  20. package/examples/earnings-analysis/earnings-analysis.form.md +88 -800
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +16 -148
  22. package/examples/movie-research/movie-research-basic.form.md +41 -37
  23. package/examples/movie-research/movie-research-deep.form.md +110 -98
  24. package/examples/movie-research/movie-research-minimal.form.md +29 -15
  25. package/examples/simple/simple-mock-filled.form.md +105 -41
  26. package/examples/simple/simple-skipped-filled.form.md +103 -41
  27. package/examples/simple/simple-with-skips.session.yaml +93 -25
  28. package/examples/simple/simple.form.md +86 -32
  29. package/examples/simple/simple.session.yaml +98 -25
  30. package/examples/startup-deep-research/startup-deep-research.form.md +130 -103
  31. package/examples/startup-research/startup-research-mock-filled.form.md +55 -55
  32. package/examples/startup-research/startup-research.form.md +36 -36
  33. package/package.json +18 -19
  34. package/dist/session-DdAtY2Ni.mjs +0 -4
  35. package/dist/shared-D7gf27Tr.mjs +0 -3
  36. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +0 -912
@@ -143,7 +143,7 @@ Data Model section for complete type definitions.
143
143
 
144
144
  IDs are organized into **two scoping levels** with different uniqueness requirements:
145
145
 
146
- **1. Structural IDs** (form, field-group, field):
146
+ **1. Structural IDs** (form, group, field):
147
147
 
148
148
  - *required:* Must be globally unique across the entire document
149
149
 
@@ -190,40 +190,44 @@ Markform defines its own scoping rules where option IDs are field-scoped.
190
190
 
191
191
  - IDs use `snake_case` (e.g., `company_info`, `ten_k`)
192
192
 
193
- - Tag names use `kebab-case` (Markdoc convention, e.g., `string-field`)
193
+ - The `field` tag uses a `kind` attribute to specify the field kind
194
194
 
195
195
  #### Structural Tags
196
196
 
197
197
  - `form` — the root container
198
198
 
199
- - `field-group` — containers for fields or nested groups
199
+ - `group` — containers for fields or nested groups
200
200
 
201
201
  #### Field Tags
202
202
 
203
203
  Custom tags are defined following [Markdoc tag conventions][markdoc-tags]. See
204
204
  [Markdoc Config][markdoc-config] for how to register custom tags.
205
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.
206
+ All fields use the unified `{% field kind="..." %}` syntax. The `kind` attribute
207
+ identifies what type of field a `Field` or `FieldValue` represents.
209
208
  (In TypeScript, the type is `FieldKind`.)
210
209
 
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` |
210
+ | Kind | Description |
211
+ | --- | --- |
212
+ | `string` | String value; optional `required`, `pattern`, `minLength`, `maxLength` |
213
+ | `number` | Numeric value; optional `min`, `max`, `integer` |
214
+ | `date` | ISO 8601 date (YYYY-MM-DD); optional `min`, `max` date constraints |
215
+ | `year` | Integer year; optional `min`, `max` year constraints |
216
+ | `string_list` | Array of strings (open-ended list); supports `minItems`, `maxItems`, `itemMinLength`, `itemMaxLength`, `uniqueItems` |
217
+ | `single_select` | Select one option from enumerated list |
218
+ | `multi_select` | Select multiple options; supports `minSelections`, `maxSelections` constraints |
219
+ | `checkboxes` | Stateful checklist; supports `checkboxMode` with values `multi` (5 states), `simple` (2 states), or `explicit` (yes/no); optional `minDone` for completion threshold |
220
+ | `url` | Single URL value with built-in format validation |
221
+ | `url_list` | Array of URLs (for citations, sources, references); supports `minItems`, `maxItems`, `uniqueItems` |
222
+ | `table` | Structured tabular data with typed columns; supports `columnIds`, `columnLabels`, `columnTypes`, `minRows`, `maxRows` |
223
+
224
+ **Syntax:** `{% field kind="<kind>" id="..." label="..." %}...{% /field %}`
221
225
 
222
226
  **Note on `pattern`:** The `pattern` attribute accepts a JavaScript-compatible regular
223
227
  expression string (without delimiters).
224
228
  Example: `pattern="^[A-Z]{1,5}$"` for a ticker symbol.
225
229
 
226
- **Common attributes (all field types):**
230
+ **Common attributes (all field kinds):**
227
231
 
228
232
  | Attribute | Type | Description |
229
233
  | --- | --- | --- |
@@ -234,6 +238,32 @@ Example: `pattern="^[A-Z]{1,5}$"` for a ticker symbol.
234
238
 
235
239
  The `role` attribute enables multi-actor workflows where different fields are assigned
236
240
  to different actors.
241
+
242
+ **Text-entry field attributes (string, number, string-list, url, url-list only):**
243
+
244
+ | Attribute | Type | Description |
245
+ | --- | --- | --- |
246
+ | `placeholder` | string | Hint text shown in empty fields (displayed in UI) |
247
+ | `examples` | string[] | Example values for the field (helps LLMs, shown in prompts) |
248
+
249
+ These attributes are only valid on text-entry field kinds. Using them on chooser fields
250
+ (single-select, multi-select, checkboxes) will result in a parse error.
251
+
252
+ **Example with placeholder and examples:**
253
+ ```md
254
+ {% field kind="string" id="company_name" label="Company name" placeholder="Enter company name" examples=["ACME Corp", "Globex Inc"] %}{% /field %}
255
+ ```
256
+
257
+ For number fields, examples must be valid numbers:
258
+ ```md
259
+ {% field kind="number" id="revenue" label="Revenue (USD)" placeholder="1000000" examples=["500000", "1000000", "5000000"] %}{% /field %}
260
+ ```
261
+
262
+ For URL fields, examples must be valid URLs:
263
+ ```md
264
+ {% field kind="url" id="website" label="Website" placeholder="https://example.com" examples=["https://example.com", "https://github.com"] %}{% /field %}
265
+ ```
266
+
237
267
  When running the fill harness with `targetRoles`, only fields matching those roles are
238
268
  considered for completion.
239
269
  See **Role-filtered completion** in the ProgressState Definitions section.
@@ -243,10 +273,10 @@ See **Role-filtered completion** in the ProgressState Definitions section.
243
273
  Markdoc does **not** natively support GFM-style task list checkbox syntax.
244
274
  The `[ ]` and `[x]` markers are **Markform-specific syntax** parsed within tag content.
245
275
 
246
- All selection field types use checkbox-style markers for broad markdown renderer
276
+ All selection field kinds use checkbox-style markers for broad markdown renderer
247
277
  compatibility:
248
278
 
249
- | Field Type | Marker | Meaning | Example |
279
+ | Field Kind | Marker | Meaning | Example |
250
280
  | --- | --- | --- | --- |
251
281
  | `checkboxes` | `[ ]` | Unchecked / todo / unfilled | `- [ ] Item {% #item_id %}` |
252
282
  | `checkboxes` | `[x]` | Checked / done | `- [x] Item {% #item_id %}` |
@@ -255,13 +285,13 @@ compatibility:
255
285
  | `checkboxes` | `[-]` | Not applicable (multi only) | `- [-] Item {% #item_id %}` |
256
286
  | `checkboxes` | `[y]` | Yes (explicit only) | `- [y] Item {% #item_id %}` |
257
287
  | `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 %}` |
288
+ | `single_select` | `[ ]` | Unselected | `- [ ] Option {% #opt_id %}` |
289
+ | `single_select` | `[x]` | Selected (exactly one) | `- [x] Option {% #opt_id %}` |
290
+ | `multi_select` | `[ ]` | Unselected | `- [ ] Option {% #opt_id %}` |
291
+ | `multi_select` | `[x]` | Selected | `- [x] Option {% #opt_id %}` |
262
292
 
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.
293
+ **Note:** `single_select` enforces that exactly one option has `[x]`. The distinction
294
+ between `single_select` and `multi_select` is in the `kind` attribute, not the marker syntax.
265
295
 
266
296
  The `{% #id %}` annotation **is** native Markdoc syntax (see
267
297
  [Attributes][markdoc-attributes]).
@@ -325,11 +355,11 @@ Type: `integer`, default: `-1` (require all).
325
355
 
326
356
  Example with partial completion allowed:
327
357
  ```md
328
- {% checkboxes id="optional_tasks" label="Optional tasks" required=true minDone=1 %}
358
+ {% field kind="checkboxes" id="optional_tasks" label="Optional tasks" required=true minDone=1 %}
329
359
  - [ ] Task A {% #task_a %}
330
360
  - [ ] Task B {% #task_b %}
331
361
  - [ ] Task C {% #task_c %}
332
- {% /checkboxes %}
362
+ {% /field %}
333
363
  ```
334
364
 
335
365
  **Note:** `minDone` only applies to `simple` mode.
@@ -361,6 +391,79 @@ The parser enforces this:
361
391
  - `active` (`[*]`): This item is the current focus right now (useful for showing where
362
392
  an agent is in a multi-step workflow)
363
393
 
394
+ #### Table Fields
395
+
396
+ Table fields enable structured tabular data collection with typed columns.
397
+ They use standard markdown table syntax for values and support validation per column
398
+ type.
399
+
400
+ **Table field attributes:**
401
+
402
+ | Attribute | Type | Required | Description |
403
+ | --- | --- | --- | --- |
404
+ | `columnIds` | string[] | Yes | Array of snake_case column identifiers |
405
+ | `columnLabels` | string[] | No | Array of display labels (backfilled from table header row if omitted) |
406
+ | `columnTypes` | string[] | No | Array of column types (defaults to all `'string'`) |
407
+ | `minRows` | number | No | Minimum row count (default: 0) |
408
+ | `maxRows` | number | No | Maximum row count (default: unlimited) |
409
+
410
+ **Column types and validation:**
411
+
412
+ | Type | Description | Validation |
413
+ | --- | --- | --- |
414
+ | `string` | Any text value | None |
415
+ | `number` | Numeric value | Integer or float |
416
+ | `url` | URL value | Valid URL format |
417
+ | `date` | Date value | ISO 8601 format (YYYY-MM-DD) |
418
+ | `year` | Year value | Integer (1000-9999) |
419
+
420
+ **Per-column required setting:** Column types can optionally specify a `required` flag:
421
+ ```md
422
+ columnTypes=[{type: "string", required: true}, "number", "url"]
423
+ ```
424
+
425
+ **Basic table-field (columnLabels backfilled from header row):**
426
+ ```md
427
+ {% field kind="table" id="team" label="Team Members"
428
+ columnIds=["name", "title", "department"] %}
429
+ | Name | Title | Department |
430
+ |------|-------|------------|
431
+ {% /field %}
432
+ ```
433
+
434
+ **Explicit column labels (when different from header row):**
435
+ ```md
436
+ {% field kind="table" id="team" label="Team Members"
437
+ columnIds=["name", "title", "department"]
438
+ columnLabels=["Full Name", "Job Title", "Department"]
439
+ columnTypes=["string", "string", "string"] %}
440
+ | Full Name | Job Title | Department |
441
+ |-----------|-----------|------------|
442
+ {% /field %}
443
+ ```
444
+
445
+ **Complete example with types and data:**
446
+ ```md
447
+ {% field kind="table" id="films" label="Notable Films" required=true
448
+ columnIds=["release_year", "title", "rt_score", "box_office_m"]
449
+ columnLabels=["Year", "Title", "RT Score", "Box Office ($M)"]
450
+ columnTypes=["year", "string", "number", "number"]
451
+ minRows=1 maxRows=10 %}
452
+ | Year | Title | RT Score | Box Office ($M) |
453
+ |------|-------|----------|-----------------|
454
+ | 2023 | Barbie | 88 | 1441.8 |
455
+ | 2019 | Once Upon a Time in Hollywood | 85 | 374.3 |
456
+ {% /field %}
457
+ ```
458
+
459
+ **Sentinel values in table cells:**
460
+ Cells can use `%SKIP%` and `%ABORT%` sentinels with optional reasons:
461
+ ```md
462
+ | 2017 | I, Tonya | 90 | %SKIP% (Box office not tracked) |
463
+ ```
464
+
465
+ **Cell escaping:** Use `\|` for literal pipe characters in cell values.
466
+
364
467
  #### Documentation Blocks
365
468
 
366
469
  Documentation blocks provide contextual help attached to form elements and each has its
@@ -391,7 +494,7 @@ Example values or usage...
391
494
 
392
495
  **Placement rules (MF/0.1):**
393
496
 
394
- - Doc blocks MAY appear inside `form` and `field-group` as direct children
497
+ - Doc blocks MAY appear inside `form` and `group` as direct children
395
498
 
396
499
  - *required:* Parser will reject doc blocks that appear inside field tag bodies (doc
397
500
  blocks MUST NOT be nested inside a field tag)
@@ -416,7 +519,7 @@ without filtering out nested doc blocks.
416
519
 
417
520
  #### Field Values
418
521
 
419
- Values are encoded differently based on field type.
522
+ Values are encoded differently based on field kind.
420
523
  The `fence` node with `language="value"` is used for scalar values (see
421
524
  [Markdoc Nodes][markdoc-nodes] for fence handling).
422
525
 
@@ -424,32 +527,32 @@ The `fence` node with `language="value"` is used for scalar values (see
424
527
 
425
528
  **Empty:** Omit the body entirely:
426
529
  ```md
427
- {% string-field id="company_name" label="Company name" required=true %}{% /string-field %}
530
+ {% field kind="string" id="company_name" label="Company name" required=true %}{% /field %}
428
531
  ```
429
532
 
430
533
  **Filled:** Value in a fenced code block with language `value`:
431
534
  ````md
432
- {% string-field id="company_name" label="Company name" required=true %}
535
+ {% field kind="string" id="company_name" label="Company name" required=true %}
433
536
  ```value
434
537
  ACME Corp
435
538
  ````
436
- {% /string-field %}
539
+ {% /field %}
437
540
  ````
438
541
 
439
542
  ##### Number Fields
440
543
 
441
544
  **Empty:**
442
545
  ```md
443
- {% number-field id="revenue_m" label="Revenue (millions)" %}{% /number-field %}
546
+ {% field kind="number" id="revenue_m" label="Revenue (millions)" %}{% /field %}
444
547
  ````
445
548
 
446
549
  **Filled:** Numeric value as string in fence (parsed to number):
447
550
  ````md
448
- {% number-field id="revenue_m" label="Revenue (millions)" %}
551
+ {% field kind="number" id="revenue_m" label="Revenue (millions)" %}
449
552
  ```value
450
553
  1234.56
451
554
  ````
452
- {% /number-field %}
555
+ {% /field %}
453
556
  ````
454
557
 
455
558
  ##### Single-Select Fields
@@ -457,11 +560,11 @@ ACME Corp
457
560
  Values are encoded **inline** via `[x]` marker—at most one option may be selected (if
458
561
  `required=true`, exactly one must be selected):
459
562
  ```md
460
- {% single-select id="rating" label="Rating" %}
563
+ {% field kind="single_select" id="rating" label="Rating" %}
461
564
  - [ ] Bullish {% #bullish %}
462
565
  - [x] Neutral {% #neutral %}
463
566
  - [ ] Bearish {% #bearish %}
464
- {% /single-select %}
567
+ {% /field %}
465
568
  ````
466
569
 
467
570
  Option IDs are scoped to the field—reference as `rating.bullish`, `rating.neutral`, etc.
@@ -470,41 +573,41 @@ Option IDs are scoped to the field—reference as `rating.bullish`, `rating.neut
470
573
 
471
574
  Values are encoded **inline** via `[x]` markers—no separate value fence:
472
575
  ```md
473
- {% multi-select id="categories" label="Categories" %}
576
+ {% field kind="multi_select" id="categories" label="Categories" %}
474
577
  - [x] Technology {% #tech %}
475
578
  - [ ] Healthcare {% #health %}
476
579
  - [x] Finance {% #finance %}
477
- {% /multi-select %}
580
+ {% /field %}
478
581
  ```
479
582
 
480
583
  ##### Checkboxes Fields
481
584
 
482
585
  Values are encoded **inline** via state markers—no separate value fence:
483
586
  ```md
484
- {% checkboxes id="tasks" label="Tasks" %}
587
+ {% field kind="checkboxes" id="tasks" label="Tasks" %}
485
588
  - [x] Review docs {% #review %}
486
589
  - [/] Write tests {% #tests %}
487
590
  - [*] Run CI {% #ci %}
488
591
  - [ ] Deploy {% #deploy %}
489
592
  - [-] Manual QA {% #manual_qa %}
490
- {% /checkboxes %}
593
+ {% /field %}
491
594
  ```
492
595
 
493
596
  For simple two-state checkboxes:
494
597
  ```md
495
- {% checkboxes id="agreements" label="Agreements" checkboxMode="simple" %}
598
+ {% field kind="checkboxes" id="agreements" label="Agreements" checkboxMode="simple" %}
496
599
  - [x] I agree to terms {% #terms %}
497
600
  - [ ] Subscribe to newsletter {% #newsletter %}
498
- {% /checkboxes %}
601
+ {% /field %}
499
602
  ```
500
603
 
501
604
  For explicit yes/no checkboxes (requires answer for each):
502
605
  ```md
503
- {% checkboxes id="risk_factors" label="Risk Assessment" checkboxMode="explicit" required=true %}
606
+ {% field kind="checkboxes" id="risk_factors" label="Risk Assessment" checkboxMode="explicit" required=true %}
504
607
  - [y] Market volatility risk assessed {% #market %}
505
608
  - [n] Regulatory risk assessed {% #regulatory %}
506
609
  - [ ] Currency risk assessed {% #currency %}
507
- {% /checkboxes %}
610
+ {% /field %}
508
611
  ```
509
612
 
510
613
  In this example, `risk_factors.currency` is unfilled (`[ ]`) and will fail validation
@@ -518,18 +621,18 @@ Items do not have individual IDs—the field has an ID and items are positional
518
621
 
519
622
  **Empty:**
520
623
  ```md
521
- {% string-list id="key_commitments" label="Key commitments" minItems=1 %}{% /string-list %}
624
+ {% field kind="string_list" id="key_commitments" label="Key commitments" minItems=1 %}{% /field %}
522
625
  ```
523
626
 
524
627
  **Filled:** One item per non-empty line in the value fence:
525
628
  ````md
526
- {% string-list id="key_commitments" label="Key commitments" minItems=1 %}
629
+ {% field kind="string_list" id="key_commitments" label="Key commitments" minItems=1 %}
527
630
  ```value
528
631
  Ship v1.0 by end of Q1
529
632
  Complete security audit
530
633
  Migrate legacy users to new platform
531
634
  ````
532
- {% /string-list %}
635
+ {% /field %}
533
636
  ````
534
637
 
535
638
  **Parsing rules:**
@@ -558,7 +661,7 @@ Regulatory changes in EU market
558
661
  Competitor launching similar feature in Q2
559
662
  Customer concentration risk (top 3 = 60% revenue)
560
663
  ````
561
- {% /string-list %}
664
+ {% /field %}
562
665
 
563
666
  {% instructions ref="top_risks" %} One risk per line.
564
667
  Be specific (company- or product-specific), not generic.
@@ -575,12 +678,12 @@ serialized on the opening tag:
575
678
 
576
679
  **Skipped field (no reason):**
577
680
  ```md
578
- {% string-field id="optional_notes" label="Optional notes" state="skipped" %}{% /string-field %}
681
+ {% field kind="string" id="optional_notes" label="Optional notes" state="skipped" %}{% /field %}
579
682
  ````
580
683
 
581
684
  **Aborted field (no reason):**
582
685
  ```md
583
- {% string-field id="company_name" label="Company name" required=true state="aborted" %}{% /string-field %}
686
+ {% field kind="string" id="company_name" label="Company name" required=true state="aborted" %}{% /field %}
584
687
  ```
585
688
 
586
689
  **State attribute values:**
@@ -595,19 +698,19 @@ When a field has a skip or abort state AND a reason was provided via the patch,
595
698
  reason is embedded in the sentinel value using parentheses:
596
699
 
597
700
  ````md
598
- {% string-field id="competitor_analysis" label="Competitor analysis" state="skipped" %}
701
+ {% field kind="string" id="competitor_analysis" label="Competitor analysis" state="skipped" %}
599
702
  ```value
600
703
  %SKIP% (Information not publicly available)
601
704
  ````
602
- {% /string-field %}
705
+ {% /field %}
603
706
  ````
604
707
 
605
708
  ```md
606
- {% number-field id="projected_revenue" label="Projected revenue" state="aborted" %}
709
+ {% field kind="number" id="projected_revenue" label="Projected revenue" state="aborted" %}
607
710
  ```value
608
711
  %ABORT% (Financial projections cannot be determined from available data)
609
712
  ````
610
- {% /number-field %}
713
+ {% /field %}
611
714
  ````
612
715
 
613
716
  **Parsing rules:**
@@ -667,12 +770,12 @@ Analysis completed with partial data due to API limitations.
667
770
  ````md
668
771
  {% form id="quarterly_earnings" title="Quarterly Earnings Analysis" %}
669
772
 
670
- {% field-group id="company_info" title="Company Info" %}
671
- {% string-field id="company_name" label="Company name" state="skipped" %}
773
+ {% group id="company_info" title="Company Info" %}
774
+ {% field kind="string" id="company_name" label="Company name" state="skipped" %}
672
775
  ```value
673
776
  %SKIP% (Not applicable for this analysis type)
674
777
  ````
675
- {% /string-field %} {% /field-group %}
778
+ {% /field %} {% /group %}
676
779
 
677
780
  {% note id="n1" ref="quarterly_earnings" role="agent" %} Analysis completed with partial
678
781
  data due to API limitations.
@@ -709,20 +812,20 @@ function containsMarkdocSyntax(value: string): boolean {
709
812
 
710
813
  **Example (process=false required):**
711
814
  ````md
712
- {% string-field id="notes" label="Notes" %}
815
+ {% field kind="string" id="notes" label="Notes" %}
713
816
  ```value {% process=false %}
714
817
  Use {% tag %} for special formatting.
715
818
  ````
716
- {% /string-field %}
819
+ {% /field %}
717
820
  ````
718
821
 
719
822
  **Example (process=false not needed):**
720
823
  ```md
721
- {% string-field id="name" label="Name" %}
824
+ {% field kind="string" id="name" label="Name" %}
722
825
  ```value
723
826
  Alice Johnson
724
827
  ````
725
- {% /string-field %}
828
+ {% /field %}
726
829
  ````
727
830
 
728
831
  See [GitHub Discussion #261][markdoc-process-false] for background on the attribute.
@@ -743,35 +846,35 @@ markform:
743
846
  Prepare an earnings-call brief by extracting key financials and writing a thesis.
744
847
  {% /description %}
745
848
 
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 %}
849
+ {% group id="company_info" title="Company Info" %}
850
+ {% field kind="string" id="company_name" label="Company name" required=true %}{% /field %}
851
+ {% field kind="string" id="ticker" label="Ticker" required=true %}{% /field %}
852
+ {% field kind="string" id="fiscal_period" label="Fiscal period" required=true %}{% /field %}
853
+ {% /group %}
751
854
 
752
- {% field-group id="source_docs" title="Source Documents" %}
753
- {% checkboxes id="docs_reviewed" label="Documents reviewed" required=true %}
855
+ {% group id="source_docs" title="Source Documents" %}
856
+ {% field kind="checkboxes" id="docs_reviewed" label="Documents reviewed" required=true %}
754
857
  - [ ] 10-K {% #ten_k %}
755
858
  - [ ] 10-Q {% #ten_q %}
756
859
  - [ ] Earnings release {% #earnings_release %}
757
860
  - [ ] Earnings call transcript {% #call_transcript %}
758
- {% /checkboxes %}
759
- {% /field-group %}
861
+ {% /field %}
862
+ {% /group %}
760
863
 
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 %}
864
+ {% group id="financials" title="Key Financials" %}
865
+ {% field kind="number" id="revenue_m" label="Revenue (USD millions)" required=true %}{% /field %}
866
+ {% field kind="number" id="gross_margin_pct" label="Gross margin (%)" %}{% /field %}
867
+ {% field kind="number" id="eps_diluted" label="Diluted EPS" required=true %}{% /field %}
868
+ {% /group %}
766
869
 
767
- {% field-group id="analysis" title="Analysis" %}
768
- {% single-select id="rating" label="Overall rating" required=true %}
870
+ {% group id="analysis" title="Analysis" %}
871
+ {% field kind="single_select" id="rating" label="Overall rating" required=true %}
769
872
  - [ ] Bullish {% #bullish %}
770
873
  - [ ] Neutral {% #neutral %}
771
874
  - [ ] Bearish {% #bearish %}
772
- {% /single-select %}
773
- {% string-field id="thesis" label="Investment thesis" required=true %}{% /string-field %}
774
- {% /field-group %}
875
+ {% /field %}
876
+ {% field kind="string" id="thesis" label="Investment thesis" required=true %}{% /field %}
877
+ {% /group %}
775
878
 
776
879
  {% /form %}
777
880
  ````
@@ -783,19 +886,19 @@ Hand-authored forms only need the `spec` field.
783
886
  #### Example: Incomplete Form
784
887
 
785
888
  ````md
786
- {% field-group id="company_info" title="Company Info" %}
787
- {% string-field id="company_name" label="Company name" required=true %}
889
+ {% group id="company_info" title="Company Info" %}
890
+ {% field kind="string" id="company_name" label="Company name" required=true %}
788
891
  ```value
789
892
  ACME Corp
790
893
  ````
791
- {% /string-field %} {% string-field id="ticker" label="Ticker" required=true %}
894
+ {% /field %} {% field kind="string" id="ticker" label="Ticker" required=true %}
792
895
  ```value
793
896
  ACME
794
897
  ```
795
- {% /string-field %} {% string-field id="fiscal_period" label="Fiscal period"
796
- required=true %}{% /string-field %} {% /field-group %}
898
+ {% /field %} {% field kind="string" id="fiscal_period" label="Fiscal period"
899
+ required=true %}{% /field %} {% /group %}
797
900
 
798
- {% field-group id="source_docs" title="Source Documents" %} {% checkboxes
901
+ {% group id="source_docs" title="Source Documents" %} {% checkboxes
799
902
  id="docs_reviewed" label="Documents reviewed" required=true %}
800
903
 
801
904
  - [x] 10-K {% #ten_k %}
@@ -804,7 +907,7 @@ id="docs_reviewed" label="Documents reviewed" required=true %}
804
907
 
805
908
  - [/] Earnings release {% #earnings_release %}
806
909
 
807
- - [ ] Earnings call transcript {% #call_transcript %} {% /checkboxes %} {% /field-group
910
+ - [ ] Earnings call transcript {% #call_transcript %} {% /field %} {% /group
808
911
  %}
809
912
  ````
810
913
 
@@ -912,7 +1015,7 @@ would create ambiguous or malformed output.
912
1015
  **Example—Markdown documentation inside a value:**
913
1016
 
914
1017
  ```md
915
- {% string-field id="setup_guide" label="Setup Guide" %}
1018
+ {% field kind="string" id="setup_guide" label="Setup Guide" %}
916
1019
  ~~~value
917
1020
  ## Installation
918
1021
 
@@ -930,7 +1033,7 @@ Then configure:
930
1033
  }
931
1034
  ```
932
1035
  ~~~
933
- {% /string-field %}
1036
+ {% /field %}
934
1037
  ```
935
1038
 
936
1039
  Here the serializer chose tildes (`~~~`) because the content contains backticks. The
@@ -953,6 +1056,107 @@ language (e.g., Pydantic for Python, JSON Schema for language-agnostic interchan
953
1056
  The schemas below are normative—conforming implementations must support equivalent
954
1057
  data structures.
955
1058
 
1059
+ #### Type System
1060
+
1061
+ This section formalizes the distinction between **field kinds** (Markform's field
1062
+ classification) and **data types** (the underlying value representation).
1063
+
1064
+ ##### Terminology
1065
+
1066
+ | Term | Definition | Examples |
1067
+ | --- | --- | --- |
1068
+ | **Field Kind** | Markform field classification. Determines syntax, validation, and behavior. | `string`, `single_select`, `table` |
1069
+ | **Data Type** | TypeScript/JSON type of the value. | `string`, `number`, `string[]` |
1070
+ | **Value Type** | Complete type expression including nullability. | `string \| null`, `OptionId[]` |
1071
+ | **Scalar Type** | Single atomic value (optionally format-constrained). | `string`, `url`, `date` |
1072
+ | **Column Type** | Type of a cell in a table field (subset of scalar types). | `string`, `number`, `url` |
1073
+
1074
+ ##### Data Type Taxonomy
1075
+
1076
+ **Primitive Types** — Base JSON types:
1077
+
1078
+ | Primitive | Description |
1079
+ | --- | --- |
1080
+ | `string` | UTF-8 text |
1081
+ | `number` | IEEE 754 float (includes integers) |
1082
+ | `boolean` | true/false |
1083
+ | `null` | Absence of value |
1084
+
1085
+ **Scalar Types** — Primitives with optional format constraints:
1086
+
1087
+ | Scalar Type | Base Primitive | Format Constraint |
1088
+ | --- | --- | --- |
1089
+ | `string` | `string` | — |
1090
+ | `number` | `number` | — |
1091
+ | `url` | `string` | Valid URL |
1092
+ | `date` | `string` | ISO 8601 (YYYY-MM-DD) |
1093
+ | `year` | `number` | Integer in valid year range |
1094
+
1095
+ **Enum Types** — Values constrained to a defined set:
1096
+
1097
+ | Enum Type | Base Primitive | Values |
1098
+ | --- | --- | --- |
1099
+ | `OptionId` | `string` | One of the field's defined option IDs |
1100
+ | `CheckboxValue` | `string` | State tokens based on `checkboxMode` |
1101
+
1102
+ **Collection Types** — Compound types:
1103
+
1104
+ | Collection Type | Structure |
1105
+ | --- | --- |
1106
+ | `Array<T>` | Ordered list of `T` |
1107
+ | `Record<K, V>` | Key-value map |
1108
+
1109
+ **Structured Types** — Complex domain objects:
1110
+
1111
+ | Structured Type | Definition |
1112
+ | --- | --- |
1113
+ | `TableRow` | `Record<ColumnId, CellValue>` |
1114
+ | `CellValue` | Scalar type determined by column's type |
1115
+
1116
+ ##### Field Kind Taxonomy
1117
+
1118
+ Field kinds are organized into four categories:
1119
+
1120
+ ```
1121
+ Field Kinds
1122
+ ├── Simple Kinds ──────── Single scalar value (nullable)
1123
+ │ ├── string Also usable as column types
1124
+ │ ├── number in table fields
1125
+ │ ├── url
1126
+ │ ├── date
1127
+ │ └── year
1128
+
1129
+ ├── List Kinds ────────── Ordered array of scalars
1130
+ │ ├── string_list Open-ended (user provides items)
1131
+ │ └── url_list
1132
+
1133
+ ├── Chooser Kinds ─────── Selection from predefined options
1134
+ │ ├── single_select Pick one
1135
+ │ ├── multi_select Pick many
1136
+ │ └── checkboxes State per option
1137
+
1138
+ └── Structured Kinds ──── Complex nested data
1139
+ └── table Rows × typed columns
1140
+ ```
1141
+
1142
+ **Simple Kinds** can also be used as column types in table fields.
1143
+
1144
+ ##### Kind → Type Mapping
1145
+
1146
+ | Field Kind | Category | Value Type | Base Type | Notes |
1147
+ | --- | --- | --- | --- | --- |
1148
+ | `string` | Simple | `string \| null` | `string` | Plain text |
1149
+ | `number` | Simple | `number \| null` | `number` | Integer or float |
1150
+ | `url` | Simple | `string \| null` | `string` | URL format validated |
1151
+ | `date` | Simple | `string \| null` | `string` | ISO 8601 format |
1152
+ | `year` | Simple | `number \| null` | `number` | Integer year |
1153
+ | `string_list` | List | `string[]` | `Array<string>` | Empty = `[]` |
1154
+ | `url_list` | List | `string[]` | `Array<string>` | URL format validated |
1155
+ | `single_select` | Chooser | `OptionId \| null` | `string` (enum) | One of defined options |
1156
+ | `multi_select` | Chooser | `OptionId[]` | `Array<string>` (enum) | Subset of options |
1157
+ | `checkboxes` | Chooser | `Record<OptionId, CheckboxValue>` | `Record<string, string>` | State per option |
1158
+ | `table` | Structured | `TableRow[]` | `Array<Record<string, CellValue>>` | Typed columns |
1159
+
956
1160
  #### Canonical TypeScript Types
957
1161
 
958
1162
  ```ts
@@ -961,7 +1165,7 @@ type Id = string; // validated snake_case, e.g., /^[a-z][a-z0-9_]*$/
961
1165
  // Validator reference: simple string ID or parameterized object
962
1166
  type ValidatorRef = string | { id: string; [key: string]: unknown };
963
1167
 
964
- // Answer state for a field - orthogonal to field type
1168
+ // Answer state for a field - orthogonal to field kind
965
1169
  // Any field can be in any answer state
966
1170
  type AnswerState = 'unanswered' | 'answered' | 'skipped' | 'aborted';
967
1171
 
@@ -1195,11 +1399,11 @@ Describes the static structure of the form schema:
1195
1399
  type FieldKind = 'string' | 'number' | 'string_list' | 'checkboxes' | 'single_select' | 'multi_select' | 'url' | 'url_list';
1196
1400
 
1197
1401
  interface StructureSummary {
1198
- groupCount: number; // total field-groups
1199
- fieldCount: number; // total fields (all types)
1402
+ groupCount: number; // total groups
1403
+ fieldCount: number; // total fields (all kinds)
1200
1404
  optionCount: number; // total options across all select/checkbox fields
1201
1405
 
1202
- fieldCountByKind: Record<FieldKind, number>; // breakdown by field type
1406
+ fieldCountByKind: Record<FieldKind, number>; // breakdown by field kind
1203
1407
 
1204
1408
  /** Map of group ID -> 'field_group' (for completeness; groups have one kind) */
1205
1409
  groupsById: Record<Id, 'field_group'>;
@@ -1237,7 +1441,7 @@ Tracks filling progress per field without exposing actual values:
1237
1441
  type ProgressState = 'empty' | 'incomplete' | 'invalid' | 'complete';
1238
1442
 
1239
1443
  interface FieldProgress {
1240
- kind: FieldKind; // field type
1444
+ kind: FieldKind; // field kind
1241
1445
  required: boolean; // whether field has required=true
1242
1446
 
1243
1447
  answerState: AnswerState; // unified answer state (unanswered/answered/skipped/aborted)
@@ -1390,7 +1594,7 @@ else:
1390
1594
  For form completion purposes, fields with constraints are treated as implicitly
1391
1595
  required:
1392
1596
 
1393
- | Field Type | Implicit Required When |
1597
+ | Field Kind | Implicit Required When |
1394
1598
  | --- | --- |
1395
1599
  | `string-list` | `minItems > 0` |
1396
1600
  | `multi-select` | `minSelections > 0` |
@@ -1577,10 +1781,10 @@ const MarkformFrontmatterSchema = z.object({
1577
1781
  Use a `toSnakeCaseDeep()` helper for deterministic conversion at the frontmatter
1578
1782
  boundary.
1579
1783
 
1580
- #### Comprehensive Field Type Reference
1784
+ #### Comprehensive Field Kind Reference
1581
1785
 
1582
1786
  This section provides a complete mapping between Markdoc syntax, TypeScript types, and
1583
- schema representations for all field types.
1787
+ schema representations for all field kinds.
1584
1788
 
1585
1789
  ##### Naming Conventions
1586
1790
 
@@ -1593,7 +1797,7 @@ schema representations for all field types.
1593
1797
  | JSON Schema keywords | camelCase | `minItems`, `maxLength`, `uniqueItems` |
1594
1798
  | IDs (values) | snake_case | `company_name`, `ten_k`, `quarterly_earnings` |
1595
1799
  | YAML keys (frontmatter, session transcripts) | snake_case | `spec`, `form_summary`, `field_count_by_kind` |
1596
- | Kind values (field types) | snake_case | `'string'`, `'single_select'` |
1800
+ | Kind values (field kinds) | snake_case | `'string'`, `'single_select'` |
1597
1801
  | Patch operations | snake_case | `set_string`, `set_single_select` |
1598
1802
 
1599
1803
  **Rationale:** Using camelCase for Markdoc attributes aligns with JSON Schema keywords
@@ -1605,11 +1809,11 @@ YAML keys use snake_case for readability and consistency with common YAML conven
1605
1809
 
1606
1810
  | Property | Used on | Values | Notes |
1607
1811
  | --- | --- | --- | --- |
1608
- | `kind` | `Field`, `FieldValue` | `FieldKind` values | Reserved for field type discrimination only |
1812
+ | `kind` | `Field`, `FieldValue` | `FieldKind` values | Reserved for field kind discrimination only |
1609
1813
  | `tag` | `DocumentationBlock` | `DocumentationTag` values | Identifies doc block type |
1610
1814
  | `nodeType` | `IdIndexEntry` | `'form' \| 'group' \| 'field'` | Identifies structural element type |
1611
1815
 
1612
- ##### Field Type Mappings
1816
+ ##### Field Kind Mappings
1613
1817
 
1614
1818
  **`string-field`** — Single string value
1615
1819
 
@@ -1715,6 +1919,45 @@ YAML keys use snake_case for readability and consistency with common YAML conven
1715
1919
  | Zod | `z.array(z.string().url()).min(n).max(m)` |
1716
1920
  | JSON Schema | `{ type: "array", items: { type: "string", format: "uri" }, minItems, maxItems, uniqueItems }` |
1717
1921
 
1922
+ **`date-field`** — ISO 8601 date value (YYYY-MM-DD)
1923
+
1924
+ | Aspect | Value |
1925
+ | --- | --- |
1926
+ | Markdoc tag | `date-field` |
1927
+ | TypeScript interface | `DateField` |
1928
+ | TypeScript kind | `'date'` |
1929
+ | Attributes | `id`, `label`, `required`, `min`, `max` |
1930
+ | FieldValue | `{ kind: 'date'; value: string \| null }` |
1931
+ | Patch operation | `{ op: 'set_date'; fieldId: Id; value: string \| null }` |
1932
+ | Zod | `z.string().regex(/^\d{4}-\d{2}-\d{2}$/)` |
1933
+ | JSON Schema | `{ type: "string", format: "date" }` |
1934
+
1935
+ **`year-field`** — Integer year value
1936
+
1937
+ | Aspect | Value |
1938
+ | --- | --- |
1939
+ | Markdoc tag | `year-field` |
1940
+ | TypeScript interface | `YearField` |
1941
+ | TypeScript kind | `'year'` |
1942
+ | Attributes | `id`, `label`, `required`, `min`, `max` |
1943
+ | FieldValue | `{ kind: 'year'; value: number \| null }` |
1944
+ | Patch operation | `{ op: 'set_year'; fieldId: Id; value: number \| null }` |
1945
+ | Zod | `z.number().int().min(min).max(max)` |
1946
+ | JSON Schema | `{ type: "integer", minimum: min, maximum: max }` |
1947
+
1948
+ **`table-field`** — Structured tabular data with typed columns
1949
+
1950
+ | Aspect | Value |
1951
+ | --- | --- |
1952
+ | Markdoc tag | `table-field` |
1953
+ | TypeScript interface | `TableField` |
1954
+ | TypeScript kind | `'table'` |
1955
+ | Attributes | `id`, `label`, `required`, `columnIds`, `columnLabels`, `columnTypes`, `minRows`, `maxRows` |
1956
+ | FieldValue | `{ kind: 'table'; rows: TableRowResponse[] }` |
1957
+ | Patch operation | `{ op: 'set_table'; fieldId: Id; rows: PatchTableRow[] }` |
1958
+ | Zod | `z.object({ kind: z.literal('table'), rows: z.array(TableRowResponseSchema) })` |
1959
+ | JSON Schema | `{ type: "object", properties: { kind: { const: "table" }, rows: { type: "array" } } }` |
1960
+
1718
1961
  **Note:** `OptionId` values are local to the field (e.g., `"ten_k"`, `"bullish"`). They
1719
1962
  are NOT qualified with the field ID in patches or FieldValue—the field context is
1720
1963
  implicit.
@@ -1742,12 +1985,16 @@ Validation happens at two levels: Markdoc syntax validation (see
1742
1985
 
1743
1986
  Schema checks (always available, deterministic):
1744
1987
 
1745
- | Check | Field Type | Constraint Source |
1988
+ | Check | Field Kind | Constraint Source |
1746
1989
  | --- | --- | --- |
1747
1990
  | Required fields present | All | `required=true` attribute |
1748
1991
  | Number parsing success | `number-field` | Built-in |
1749
1992
  | Min/max value range | `number-field` | `min`, `max` attributes |
1750
1993
  | Integer constraint | `number-field` | `integer=true` attribute |
1994
+ | Date format validation | `date-field` | Built-in (ISO 8601 YYYY-MM-DD) |
1995
+ | Min/max date range | `date-field` | `min`, `max` attributes |
1996
+ | Year integer validation | `year-field` | Built-in (integer) |
1997
+ | Min/max year range | `year-field` | `min`, `max` attributes |
1751
1998
  | Pattern match | `string-field` | `pattern` attribute (JS regex) |
1752
1999
  | Min/max length | `string-field` | `minLength`, `maxLength` attributes |
1753
2000
  | Min/max item count | `string-list` | `minItems`, `maxItems` attributes |
@@ -1762,13 +2009,17 @@ Output: `ValidationIssue[]`
1762
2009
 
1763
2010
  #### Required Field Semantics
1764
2011
 
1765
- The `required` attribute has specific semantics for each field type.
2012
+ The `required` attribute has specific semantics for each field kind.
1766
2013
  This section provides normative definitions:
1767
2014
 
1768
- | Field Type | `required=true` means | `required=false` (or omitted) means |
2015
+ | Field Kind | `required=true` means | `required=false` (or omitted) means |
1769
2016
  | --- | --- | --- |
1770
2017
  | `string-field` | `value !== null && value.trim() !== ""` | Value may be null or empty |
1771
2018
  | `number-field` | `value !== null` (and parseable as number) | Value may be null |
2019
+ | `date-field` | `value !== null && isValidDate(value)` | Value may be null |
2020
+ | `year-field` | `value !== null` (and valid integer) | Value may be null |
2021
+ | `url-field` | `value !== null && isValidUrl(value)` | Value may be null |
2022
+ | `url-list` | `items.length >= max(1, minItems)` | Empty array is valid (unless `minItems` constraint) |
1772
2023
  | `string-list` | `items.length >= max(1, minItems)` | Empty array is valid (unless `minItems` constraint) |
1773
2024
  | `single-select` | Exactly one option must be selected | Zero or one option selected (never >1) |
1774
2025
  | `multi-select` | `selected.length >= max(1, minSelections)` | Empty selection valid (unless `minSelections` constraint) |
@@ -1800,7 +2051,7 @@ A completed form must have all checkbox options resolved to either `done` or `na
1800
2051
 
1801
2052
  **Field group `required` attribute:**
1802
2053
 
1803
- The `required` attribute on `field-group` is **not supported in MF/0.1**. Groups may
2054
+ The `required` attribute on `group` is **not supported in MF/0.1**. Groups may
1804
2055
  have `validate` references for custom validation, but the `required` attribute should
1805
2056
  not be used on groups.
1806
2057
  If present, it is ignored with a warning.
@@ -1906,15 +2157,15 @@ export const validators: Record<string, (ctx: ValidatorContext) => ValidationIss
1906
2157
 
1907
2158
  <!-- Parameterized: pass min word count as parameter -->
1908
2159
 
1909
- {% string-field id="thesis" label="Investment thesis" validate=[{id: "min_words", min: 50}] %}{% /string-field %}
2160
+ {% field kind="string" id="thesis" label="Investment thesis" validate=[{id: "min_words", min: 50}] %}{% /field %}
1910
2161
 
1911
2162
  <!-- Multiple validators with different params -->
1912
2163
 
1913
- {% string-field id="summary" label="Summary" validate=[{id: "min_words", min: 25}, {id: "max_words", max: 100}] %}{% /string-field %}
2164
+ {% field kind="string" id="summary" label="Summary" validate=[{id: "min_words", min: 25}, {id: "max_words", max: 100}] %}{% /field %}
1914
2165
 
1915
2166
  <!-- Sum-to validator with configurable target -->
1916
2167
 
1917
- {% field-group id="scenarios" validate=[{id: "sum_to", fields: ["base_prob", "bull_prob", "bear_prob"], target: 100}] %}
2168
+ {% group id="scenarios" validate=[{id: "sum_to", fields: ["base_prob", "bull_prob", "bear_prob"], target: 100}] %}
1918
2169
  ```
1919
2170
 
1920
2171
  **Runtime loading (engine):**