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
@@ -1,5 +1,6 @@
1
- import { N as PatchSchema } from "./coreTypes-Dful87E0.mjs";
2
- import { C as DEFAULT_ROLE_INSTRUCTIONS, P as getWebSearchConfig, S as DEFAULT_ROLES, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, n as getFieldsForRoles, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, y as DEFAULT_PRIORITY } from "./apply-00UmzDKL.mjs";
1
+ import { L as PatchSchema } from "./coreTypes-pyctKRgc.mjs";
2
+ import { C as DEFAULT_ROLE_INSTRUCTIONS, P as getWebSearchConfig, S as DEFAULT_ROLES, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, n as getFieldsForRoles, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, y as DEFAULT_PRIORITY } from "./apply-BCCiJzQr.mjs";
3
+ import { createRequire } from "node:module";
3
4
  import { z } from "zod";
4
5
  import Markdoc from "@markdoc/markdoc";
5
6
  import YAML from "yaml";
@@ -12,6 +13,27 @@ import { ZodFirstPartyTypeKind } from "zod/v3";
12
13
  import { google } from "@ai-sdk/google";
13
14
  import { xai } from "@ai-sdk/xai";
14
15
 
16
+ //#region src/engine/fieldRegistry.ts
17
+ /**
18
+ * All field kinds as a const tuple - the single source of truth.
19
+ * Adding a new kind here will cause TypeScript errors until all
20
+ * corresponding types and handlers are added.
21
+ */
22
+ const FIELD_KINDS = [
23
+ "string",
24
+ "number",
25
+ "string_list",
26
+ "checkboxes",
27
+ "single_select",
28
+ "multi_select",
29
+ "url",
30
+ "url_list",
31
+ "date",
32
+ "year",
33
+ "table"
34
+ ];
35
+
36
+ //#endregion
15
37
  //#region src/engine/parseHelpers.ts
16
38
  /** Parse error with source location info */
17
39
  var ParseError = class extends Error {
@@ -90,6 +112,19 @@ function getValidateAttr(node) {
90
112
  if (typeof value === "object") return [value];
91
113
  }
92
114
  /**
115
+ * Get string array attribute value or undefined.
116
+ * Handles both single string (converts to array) and array formats.
117
+ */
118
+ function getStringArrayAttr(node, name$2) {
119
+ const value = node.attributes?.[name$2];
120
+ if (value === void 0 || value === null) return;
121
+ if (Array.isArray(value)) {
122
+ const strings = value.filter((v) => typeof v === "string");
123
+ return strings.length > 0 ? strings : void 0;
124
+ }
125
+ if (typeof value === "string") return [value];
126
+ }
127
+ /**
93
128
  * Extract option items from node children (for option lists).
94
129
  * Works with raw AST nodes. Collects text and ID from list items.
95
130
  */
@@ -146,6 +181,55 @@ function extractFenceValue(node) {
146
181
  }
147
182
  return null;
148
183
  }
184
+ /**
185
+ * Extract table content from node children.
186
+ * Handles both raw text and Markdoc-parsed table nodes.
187
+ * Reconstructs markdown table format from the AST.
188
+ */
189
+ function extractTableContent(node) {
190
+ const lines = [];
191
+ function extractTextFromNode(n) {
192
+ if (!n || typeof n !== "object") return "";
193
+ if (n.type === "text" && typeof n.attributes?.content === "string") return n.attributes.content;
194
+ if (n.children && Array.isArray(n.children)) return n.children.map(extractTextFromNode).join("");
195
+ return "";
196
+ }
197
+ function extractTableRow(trNode) {
198
+ if (!trNode.children || !Array.isArray(trNode.children)) return "";
199
+ return `| ${trNode.children.filter((c) => c.type === "th" || c.type === "td").map((c) => extractTextFromNode(c).trim()).join(" | ")} |`;
200
+ }
201
+ function processNode(child) {
202
+ if (!child || typeof child !== "object") return;
203
+ if (child.type === "paragraph" || child.type === "inline") {
204
+ const text = extractTextFromNode(child).trim();
205
+ if (text) lines.push(text);
206
+ return;
207
+ }
208
+ if (child.type === "text" && typeof child.attributes?.content === "string") {
209
+ const text = child.attributes.content.trim();
210
+ if (text) lines.push(text);
211
+ return;
212
+ }
213
+ if (child.type === "table") {
214
+ const thead = child.children?.find((c) => c.type === "thead");
215
+ if (thead?.children) for (const tr of thead.children.filter((c) => c.type === "tr")) lines.push(extractTableRow(tr));
216
+ if (thead?.children?.length) {
217
+ const firstTr = thead.children.find((c) => c.type === "tr");
218
+ if (firstTr?.children) {
219
+ const colCount = firstTr.children.filter((c) => c.type === "th" || c.type === "td").length;
220
+ const separatorCells = Array(colCount).fill("----");
221
+ lines.push(`| ${separatorCells.join(" | ")} |`);
222
+ }
223
+ }
224
+ const tbody = child.children?.find((c) => c.type === "tbody");
225
+ if (tbody?.children) for (const tr of tbody.children.filter((c) => c.type === "tr")) lines.push(extractTableRow(tr));
226
+ return;
227
+ }
228
+ if (child.children && Array.isArray(child.children)) for (const c of child.children) processNode(c);
229
+ }
230
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) processNode(child);
231
+ return lines.join("\n").trim() || null;
232
+ }
149
233
 
150
234
  //#endregion
151
235
  //#region src/engine/parseSentinels.ts
@@ -157,7 +241,7 @@ const SENTINEL_ABORT = "%ABORT%";
157
241
  * Formats: `%SKIP%`, `%SKIP% (reason text)`, `%ABORT%`, `%ABORT% (reason text)`
158
242
  * Returns null if the content is not a sentinel.
159
243
  */
160
- function parseSentinel(content) {
244
+ function parseSentinel$1(content) {
161
245
  if (!content) return null;
162
246
  const trimmed = content.trim();
163
247
  const reasonPattern = /^\((.+)\)$/s;
@@ -195,7 +279,7 @@ function parseSentinel(content) {
195
279
  function tryParseSentinelResponse(node, fieldId, required) {
196
280
  const fenceContent = extractFenceValue(node);
197
281
  const stateAttr = getStringAttr(node, "state");
198
- const sentinel = parseSentinel(fenceContent);
282
+ const sentinel = parseSentinel$1(fenceContent);
199
283
  if (!sentinel) return null;
200
284
  if (sentinel.type === "skip") {
201
285
  if (stateAttr !== void 0 && stateAttr !== "skipped") throw new ParseError(`Field '${fieldId}' has conflicting state='${stateAttr}' with %SKIP% sentinel`);
@@ -215,6 +299,216 @@ function tryParseSentinelResponse(node, fieldId, required) {
215
299
  return null;
216
300
  }
217
301
 
302
+ //#endregion
303
+ //#region src/engine/table/parseTable.ts
304
+ /** Sentinel pattern: %SKIP% or %SKIP:reason% or %SKIP(reason)% */
305
+ const SKIP_PATTERN = /^%SKIP(?:[:(](.*))?[)]?%$/i;
306
+ /** Sentinel pattern: %ABORT% or %ABORT:reason% or %ABORT(reason)% */
307
+ const ABORT_PATTERN = /^%ABORT(?:[:(](.*))?[)]?%$/i;
308
+ /**
309
+ * Detect if a cell value is a sentinel.
310
+ */
311
+ function parseSentinel(value) {
312
+ const trimmed = value.trim();
313
+ const skipMatch = SKIP_PATTERN.exec(trimmed);
314
+ if (skipMatch) return {
315
+ type: "skip",
316
+ reason: skipMatch[1]
317
+ };
318
+ const abortMatch = ABORT_PATTERN.exec(trimmed);
319
+ if (abortMatch) return {
320
+ type: "abort",
321
+ reason: abortMatch[1]
322
+ };
323
+ return null;
324
+ }
325
+ /**
326
+ * Parse a cell value according to its column type.
327
+ * Returns a CellResponse with appropriate state.
328
+ */
329
+ function parseCellValue(rawValue, columnType) {
330
+ const trimmed = rawValue.trim();
331
+ if (!trimmed) return { state: "skipped" };
332
+ const sentinel = parseSentinel(trimmed);
333
+ if (sentinel) return {
334
+ state: sentinel.type === "skip" ? "skipped" : "aborted",
335
+ reason: sentinel.reason
336
+ };
337
+ switch (columnType) {
338
+ case "string": return {
339
+ state: "answered",
340
+ value: trimmed
341
+ };
342
+ case "number": {
343
+ const num = parseFloat(trimmed);
344
+ if (isNaN(num)) return {
345
+ state: "answered",
346
+ value: trimmed
347
+ };
348
+ return {
349
+ state: "answered",
350
+ value: num
351
+ };
352
+ }
353
+ case "url": return {
354
+ state: "answered",
355
+ value: trimmed
356
+ };
357
+ case "date": return {
358
+ state: "answered",
359
+ value: trimmed
360
+ };
361
+ case "year": {
362
+ const year = parseInt(trimmed, 10);
363
+ if (isNaN(year) || !Number.isInteger(year)) return {
364
+ state: "answered",
365
+ value: trimmed
366
+ };
367
+ return {
368
+ state: "answered",
369
+ value: year
370
+ };
371
+ }
372
+ }
373
+ }
374
+ /**
375
+ * Parse a table row into cell values.
376
+ * Handles leading/trailing pipes and cell trimming.
377
+ */
378
+ function parseTableRow(line) {
379
+ let trimmed = line.trim();
380
+ if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
381
+ if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
382
+ return trimmed.split("|").map((cell) => cell.trim());
383
+ }
384
+ /**
385
+ * Extract header labels from table content.
386
+ * Returns array of header labels from the first row, or empty array if no valid header.
387
+ */
388
+ function extractTableHeaderLabels(content) {
389
+ if (!content || content.trim() === "") return [];
390
+ const lines = content.trim().split("\n").filter((line) => line.trim());
391
+ if (lines.length === 0) return [];
392
+ return parseTableRow(lines[0]);
393
+ }
394
+ /**
395
+ * Check if a line is a valid table separator row.
396
+ * Each cell should contain only dashes and optional colons for alignment.
397
+ */
398
+ function isValidSeparator(line, expectedCols) {
399
+ const cells = parseTableRow(line);
400
+ if (cells.length !== expectedCols) return false;
401
+ const separatorPattern = /^:?-+:?$/;
402
+ return cells.every((cell) => separatorPattern.test(cell.trim()));
403
+ }
404
+ /**
405
+ * Parse a markdown table with column schema for type coercion.
406
+ *
407
+ * @param content - The markdown table content
408
+ * @param columns - Column definitions from the table field schema
409
+ * @param dataStartLine - Optional line index where data rows start (skips header validation)
410
+ * @returns Parsed table value with typed cells
411
+ */
412
+ function parseMarkdownTable(content, columns, dataStartLine) {
413
+ const lines = content.trim().split("\n").filter((line) => line.trim());
414
+ if (lines.length === 0) return {
415
+ ok: true,
416
+ value: {
417
+ kind: "table",
418
+ rows: []
419
+ }
420
+ };
421
+ if (lines.length < 2) return {
422
+ ok: false,
423
+ error: "Table must have at least a header and separator row"
424
+ };
425
+ const headerLine = lines[0];
426
+ const headers = parseTableRow(headerLine);
427
+ const separatorLine = lines[1];
428
+ if (!isValidSeparator(separatorLine, headers.length)) return {
429
+ ok: false,
430
+ error: "Invalid table separator row"
431
+ };
432
+ if (dataStartLine !== void 0) {
433
+ const rows$1 = [];
434
+ for (let i = dataStartLine; i < lines.length; i++) {
435
+ const rawCells = parseTableRow(lines[i]);
436
+ const row = {};
437
+ for (let j = 0; j < columns.length; j++) {
438
+ const column = columns[j];
439
+ const rawValue = rawCells[j] ?? "";
440
+ row[column.id] = parseCellValue(rawValue, column.type);
441
+ }
442
+ rows$1.push(row);
443
+ }
444
+ return {
445
+ ok: true,
446
+ value: {
447
+ kind: "table",
448
+ rows: rows$1
449
+ }
450
+ };
451
+ }
452
+ const columnIdToIndex = /* @__PURE__ */ new Map();
453
+ for (let i = 0; i < headers.length; i++) {
454
+ const header = headers[i];
455
+ const column = columns.find((c) => c.id === header || c.label === header);
456
+ if (column) columnIdToIndex.set(column.id, i);
457
+ }
458
+ const rows = [];
459
+ for (let i = 2; i < lines.length; i++) {
460
+ const rawCells = parseTableRow(lines[i]);
461
+ const row = {};
462
+ for (const column of columns) {
463
+ const cellIndex = columnIdToIndex.get(column.id);
464
+ const rawValue = cellIndex !== void 0 ? rawCells[cellIndex] ?? "" : "";
465
+ row[column.id] = parseCellValue(rawValue, column.type);
466
+ }
467
+ rows.push(row);
468
+ }
469
+ return {
470
+ ok: true,
471
+ value: {
472
+ kind: "table",
473
+ rows
474
+ }
475
+ };
476
+ }
477
+ /**
478
+ * Parse just the raw table structure without schema.
479
+ * Useful for validation and error reporting.
480
+ */
481
+ function parseRawTable(content) {
482
+ const lines = content.trim().split("\n").filter((line) => line.trim());
483
+ if (lines.length === 0) return {
484
+ ok: true,
485
+ headers: [],
486
+ rows: []
487
+ };
488
+ if (lines.length < 2) return {
489
+ ok: false,
490
+ error: "Table must have at least a header and separator row"
491
+ };
492
+ const headers = parseTableRow(lines[0]);
493
+ const separatorLine = lines[1];
494
+ if (!isValidSeparator(separatorLine, headers.length)) return {
495
+ ok: false,
496
+ error: "Invalid table separator row"
497
+ };
498
+ const rows = [];
499
+ for (let i = 2; i < lines.length; i++) {
500
+ const row = parseTableRow(lines[i]);
501
+ while (row.length < headers.length) row.push("");
502
+ if (row.length > headers.length) row.length = headers.length;
503
+ rows.push(row);
504
+ }
505
+ return {
506
+ ok: true,
507
+ headers,
508
+ rows
509
+ };
510
+ }
511
+
218
512
  //#endregion
219
513
  //#region src/engine/parseFields.ts
220
514
  /**
@@ -233,6 +527,7 @@ function isValueEmpty(value) {
233
527
  case "single_select": return value.selected === null;
234
528
  case "multi_select": return value.selected.length === 0;
235
529
  case "checkboxes": return Object.values(value.values).every((v) => v === "todo" || v === "unfilled");
530
+ case "table": return value.rows.length === 0;
236
531
  }
237
532
  }
238
533
  /**
@@ -281,27 +576,86 @@ function getPriorityAttr(node) {
281
576
  return DEFAULT_PRIORITY;
282
577
  }
283
578
  /**
284
- * Parse a string-field tag.
579
+ * Parse and validate base field attributes (id, label, required).
580
+ * Throws ParseError if id or label is missing.
285
581
  */
286
- function parseStringField(node) {
582
+ function parseBaseFieldAttrs(node, kind) {
287
583
  const id = getStringAttr(node, "id");
288
584
  const label = getStringAttr(node, "label");
289
- if (!id) throw new ParseError("string-field missing required 'id' attribute");
290
- if (!label) throw new ParseError(`string-field '${id}' missing required 'label' attribute`);
291
- const required = getBooleanAttr(node, "required") ?? false;
585
+ if (!id) throw new ParseError(`field kind="${kind}" missing required 'id' attribute`);
586
+ if (!label) throw new ParseError(`field '${id}' missing required 'label' attribute`);
587
+ return {
588
+ id,
589
+ label,
590
+ required: getBooleanAttr(node, "required") ?? false
591
+ };
592
+ }
593
+ /**
594
+ * Get common field attributes (priority, role, validate, report).
595
+ */
596
+ function getCommonFieldAttrs(node) {
597
+ return {
598
+ priority: getPriorityAttr(node),
599
+ role: getStringAttr(node, "role") ?? AGENT_ROLE,
600
+ validate: getValidateAttr(node),
601
+ report: getBooleanAttr(node, "report")
602
+ };
603
+ }
604
+ /**
605
+ * Validate that placeholder/examples are not used on chooser fields.
606
+ * Throws ParseError if either attribute is present.
607
+ */
608
+ function validateNoPlaceholderExamples(node, fieldType, fieldId) {
609
+ const placeholder = getStringAttr(node, "placeholder");
610
+ const examples = getStringArrayAttr(node, "examples");
611
+ if (placeholder !== void 0) throw new ParseError(`${fieldType} '${fieldId}' has 'placeholder' attribute, but placeholder is only valid on text-entry fields (string, number, string-list, url, url-list)`);
612
+ if (examples !== void 0) throw new ParseError(`${fieldType} '${fieldId}' has 'examples' attribute, but examples is only valid on text-entry fields (string, number, string-list, url, url-list)`);
613
+ }
614
+ /**
615
+ * Check if a string is a valid URL.
616
+ */
617
+ function isValidUrl(str) {
618
+ try {
619
+ new URL(str);
620
+ return true;
621
+ } catch {
622
+ return false;
623
+ }
624
+ }
625
+ /**
626
+ * Validate examples for number fields - all must parse as numbers.
627
+ */
628
+ function validateNumberExamples(examples, fieldId) {
629
+ if (!examples) return;
630
+ for (const example of examples) {
631
+ const parsed = Number(example);
632
+ if (Number.isNaN(parsed)) throw new ParseError(`number-field '${fieldId}' has invalid example '${example}' - must be a valid number`);
633
+ }
634
+ }
635
+ /**
636
+ * Validate examples for URL fields - all must be valid URLs.
637
+ */
638
+ function validateUrlExamples(examples, fieldId) {
639
+ if (!examples) return;
640
+ for (const example of examples) if (!isValidUrl(example)) throw new ParseError(`url-field '${fieldId}' has invalid example '${example}' - must be a valid URL`);
641
+ }
642
+ /**
643
+ * Parse a string-field tag.
644
+ */
645
+ function parseStringField(node) {
646
+ const { id, label, required } = parseBaseFieldAttrs(node, "string");
292
647
  const field = {
293
648
  kind: "string",
294
649
  id,
295
650
  label,
296
651
  required,
297
- priority: getPriorityAttr(node),
298
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
652
+ ...getCommonFieldAttrs(node),
299
653
  multiline: getBooleanAttr(node, "multiline"),
300
654
  pattern: getStringAttr(node, "pattern"),
301
655
  minLength: getNumberAttr(node, "minLength"),
302
656
  maxLength: getNumberAttr(node, "maxLength"),
303
- validate: getValidateAttr(node),
304
- report: getBooleanAttr(node, "report")
657
+ placeholder: getStringAttr(node, "placeholder"),
658
+ examples: getStringArrayAttr(node, "examples")
305
659
  };
306
660
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
307
661
  if (sentinelResponse) return {
@@ -321,23 +675,21 @@ function parseStringField(node) {
321
675
  * Parse a number-field tag.
322
676
  */
323
677
  function parseNumberField(node) {
324
- const id = getStringAttr(node, "id");
325
- const label = getStringAttr(node, "label");
326
- if (!id) throw new ParseError("number-field missing required 'id' attribute");
327
- if (!label) throw new ParseError(`number-field '${id}' missing required 'label' attribute`);
328
- const required = getBooleanAttr(node, "required") ?? false;
678
+ const { id, label, required } = parseBaseFieldAttrs(node, "number");
679
+ const placeholder = getStringAttr(node, "placeholder");
680
+ const examples = getStringArrayAttr(node, "examples");
681
+ validateNumberExamples(examples, id);
329
682
  const field = {
330
683
  kind: "number",
331
684
  id,
332
685
  label,
333
686
  required,
334
- priority: getPriorityAttr(node),
335
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
687
+ ...getCommonFieldAttrs(node),
336
688
  min: getNumberAttr(node, "min"),
337
689
  max: getNumberAttr(node, "max"),
338
690
  integer: getBooleanAttr(node, "integer"),
339
- validate: getValidateAttr(node),
340
- report: getBooleanAttr(node, "report")
691
+ placeholder,
692
+ examples
341
693
  };
342
694
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
343
695
  if (sentinelResponse) return {
@@ -363,25 +715,20 @@ function parseNumberField(node) {
363
715
  * Parse a string-list tag.
364
716
  */
365
717
  function parseStringListField(node) {
366
- const id = getStringAttr(node, "id");
367
- const label = getStringAttr(node, "label");
368
- if (!id) throw new ParseError("string-list missing required 'id' attribute");
369
- if (!label) throw new ParseError(`string-list '${id}' missing required 'label' attribute`);
370
- const required = getBooleanAttr(node, "required") ?? false;
718
+ const { id, label, required } = parseBaseFieldAttrs(node, "string_list");
371
719
  const field = {
372
720
  kind: "string_list",
373
721
  id,
374
722
  label,
375
723
  required,
376
- priority: getPriorityAttr(node),
377
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
724
+ ...getCommonFieldAttrs(node),
378
725
  minItems: getNumberAttr(node, "minItems"),
379
726
  maxItems: getNumberAttr(node, "maxItems"),
380
727
  itemMinLength: getNumberAttr(node, "itemMinLength"),
381
728
  itemMaxLength: getNumberAttr(node, "itemMaxLength"),
382
729
  uniqueItems: getBooleanAttr(node, "uniqueItems"),
383
- validate: getValidateAttr(node),
384
- report: getBooleanAttr(node, "report")
730
+ placeholder: getStringAttr(node, "placeholder"),
731
+ examples: getStringArrayAttr(node, "examples")
385
732
  };
386
733
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
387
734
  if (sentinelResponse) return {
@@ -435,22 +782,16 @@ function parseOptions(node, fieldId) {
435
782
  * Parse a single-select tag.
436
783
  */
437
784
  function parseSingleSelectField(node) {
438
- const id = getStringAttr(node, "id");
439
- const label = getStringAttr(node, "label");
440
- if (!id) throw new ParseError("single-select missing required 'id' attribute");
441
- if (!label) throw new ParseError(`single-select '${id}' missing required 'label' attribute`);
442
- const required = getBooleanAttr(node, "required") ?? false;
785
+ const { id, label, required } = parseBaseFieldAttrs(node, "single_select");
786
+ validateNoPlaceholderExamples(node, "single-select", id);
443
787
  const { options, selected } = parseOptions(node, id);
444
788
  const field = {
445
789
  kind: "single_select",
446
790
  id,
447
791
  label,
448
792
  required,
449
- priority: getPriorityAttr(node),
450
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
451
- options,
452
- validate: getValidateAttr(node),
453
- report: getBooleanAttr(node, "report")
793
+ ...getCommonFieldAttrs(node),
794
+ options
454
795
  };
455
796
  let selectedOption = null;
456
797
  for (const [optId, state] of Object.entries(selected)) if (state === "done") {
@@ -469,24 +810,18 @@ function parseSingleSelectField(node) {
469
810
  * Parse a multi-select tag.
470
811
  */
471
812
  function parseMultiSelectField(node) {
472
- const id = getStringAttr(node, "id");
473
- const label = getStringAttr(node, "label");
474
- if (!id) throw new ParseError("multi-select missing required 'id' attribute");
475
- if (!label) throw new ParseError(`multi-select '${id}' missing required 'label' attribute`);
476
- const required = getBooleanAttr(node, "required") ?? false;
813
+ const { id, label, required } = parseBaseFieldAttrs(node, "multi_select");
814
+ validateNoPlaceholderExamples(node, "multi-select", id);
477
815
  const { options, selected } = parseOptions(node, id);
478
816
  const field = {
479
817
  kind: "multi_select",
480
818
  id,
481
819
  label,
482
820
  required,
483
- priority: getPriorityAttr(node),
484
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
821
+ ...getCommonFieldAttrs(node),
485
822
  options,
486
823
  minSelections: getNumberAttr(node, "minSelections"),
487
- maxSelections: getNumberAttr(node, "maxSelections"),
488
- validate: getValidateAttr(node),
489
- report: getBooleanAttr(node, "report")
824
+ maxSelections: getNumberAttr(node, "maxSelections")
490
825
  };
491
826
  const selectedOptions = [];
492
827
  for (const [optId, state] of Object.entries(selected)) if (state === "done") selectedOptions.push(optId);
@@ -504,8 +839,9 @@ function parseMultiSelectField(node) {
504
839
  function parseCheckboxesField(node) {
505
840
  const id = getStringAttr(node, "id");
506
841
  const label = getStringAttr(node, "label");
507
- if (!id) throw new ParseError("checkboxes missing required 'id' attribute");
508
- if (!label) throw new ParseError(`checkboxes '${id}' missing required 'label' attribute`);
842
+ if (!id) throw new ParseError("field kind=\"checkboxes\" missing required 'id' attribute");
843
+ if (!label) throw new ParseError(`field '${id}' missing required 'label' attribute`);
844
+ validateNoPlaceholderExamples(node, "checkboxes", id);
509
845
  const { options, selected } = parseOptions(node, id);
510
846
  const checkboxModeStr = getStringAttr(node, "checkboxMode");
511
847
  let checkboxMode = "multi";
@@ -524,14 +860,11 @@ function parseCheckboxesField(node) {
524
860
  id,
525
861
  label,
526
862
  required,
527
- priority: getPriorityAttr(node),
528
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
863
+ ...getCommonFieldAttrs(node),
529
864
  checkboxMode,
530
865
  minDone: getNumberAttr(node, "minDone"),
531
866
  options,
532
- approvalMode,
533
- validate: getValidateAttr(node),
534
- report: getBooleanAttr(node, "report")
867
+ approvalMode
535
868
  };
536
869
  const values = {};
537
870
  for (const opt of options) {
@@ -551,20 +884,18 @@ function parseCheckboxesField(node) {
551
884
  * Parse a url-field tag.
552
885
  */
553
886
  function parseUrlField(node) {
554
- const id = getStringAttr(node, "id");
555
- const label = getStringAttr(node, "label");
556
- if (!id) throw new ParseError("url-field missing required 'id' attribute");
557
- if (!label) throw new ParseError(`url-field '${id}' missing required 'label' attribute`);
558
- const required = getBooleanAttr(node, "required") ?? false;
887
+ const { id, label, required } = parseBaseFieldAttrs(node, "url");
888
+ const placeholder = getStringAttr(node, "placeholder");
889
+ const examples = getStringArrayAttr(node, "examples");
890
+ validateUrlExamples(examples, id);
559
891
  const field = {
560
892
  kind: "url",
561
893
  id,
562
894
  label,
563
895
  required,
564
- priority: getPriorityAttr(node),
565
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
566
- validate: getValidateAttr(node),
567
- report: getBooleanAttr(node, "report")
896
+ ...getCommonFieldAttrs(node),
897
+ placeholder,
898
+ examples
568
899
  };
569
900
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
570
901
  if (sentinelResponse) return {
@@ -584,23 +915,21 @@ function parseUrlField(node) {
584
915
  * Parse a url-list tag.
585
916
  */
586
917
  function parseUrlListField(node) {
587
- const id = getStringAttr(node, "id");
588
- const label = getStringAttr(node, "label");
589
- if (!id) throw new ParseError("url-list missing required 'id' attribute");
590
- if (!label) throw new ParseError(`url-list '${id}' missing required 'label' attribute`);
591
- const required = getBooleanAttr(node, "required") ?? false;
918
+ const { id, label, required } = parseBaseFieldAttrs(node, "url_list");
919
+ const placeholder = getStringAttr(node, "placeholder");
920
+ const examples = getStringArrayAttr(node, "examples");
921
+ validateUrlExamples(examples, id);
592
922
  const field = {
593
923
  kind: "url_list",
594
924
  id,
595
925
  label,
596
926
  required,
597
- priority: getPriorityAttr(node),
598
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
927
+ ...getCommonFieldAttrs(node),
599
928
  minItems: getNumberAttr(node, "minItems"),
600
929
  maxItems: getNumberAttr(node, "maxItems"),
601
930
  uniqueItems: getBooleanAttr(node, "uniqueItems"),
602
- validate: getValidateAttr(node),
603
- report: getBooleanAttr(node, "report")
931
+ placeholder,
932
+ examples
604
933
  };
605
934
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
606
935
  if (sentinelResponse) return {
@@ -628,22 +957,15 @@ function parseUrlListField(node) {
628
957
  * Parse a date-field tag.
629
958
  */
630
959
  function parseDateField(node) {
631
- const id = getStringAttr(node, "id");
632
- const label = getStringAttr(node, "label");
633
- if (!id) throw new ParseError("date-field missing required 'id' attribute");
634
- if (!label) throw new ParseError(`date-field '${id}' missing required 'label' attribute`);
635
- const required = getBooleanAttr(node, "required") ?? false;
960
+ const { id, label, required } = parseBaseFieldAttrs(node, "date");
636
961
  const field = {
637
962
  kind: "date",
638
963
  id,
639
964
  label,
640
965
  required,
641
- priority: getPriorityAttr(node),
642
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
966
+ ...getCommonFieldAttrs(node),
643
967
  min: getStringAttr(node, "min"),
644
- max: getStringAttr(node, "max"),
645
- validate: getValidateAttr(node),
646
- report: getBooleanAttr(node, "report")
968
+ max: getStringAttr(node, "max")
647
969
  };
648
970
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
649
971
  if (sentinelResponse) return {
@@ -668,22 +990,15 @@ function parseDateField(node) {
668
990
  * Parse a year-field tag.
669
991
  */
670
992
  function parseYearField(node) {
671
- const id = getStringAttr(node, "id");
672
- const label = getStringAttr(node, "label");
673
- if (!id) throw new ParseError("year-field missing required 'id' attribute");
674
- if (!label) throw new ParseError(`year-field '${id}' missing required 'label' attribute`);
675
- const required = getBooleanAttr(node, "required") ?? false;
993
+ const { id, label, required } = parseBaseFieldAttrs(node, "year");
676
994
  const field = {
677
995
  kind: "year",
678
996
  id,
679
997
  label,
680
998
  required,
681
- priority: getPriorityAttr(node),
682
- role: getStringAttr(node, "role") ?? AGENT_ROLE,
999
+ ...getCommonFieldAttrs(node),
683
1000
  min: getNumberAttr(node, "min"),
684
- max: getNumberAttr(node, "max"),
685
- validate: getValidateAttr(node),
686
- report: getBooleanAttr(node, "report")
1001
+ max: getNumberAttr(node, "max")
687
1002
  };
688
1003
  const sentinelResponse = tryParseSentinelResponse(node, id, required);
689
1004
  if (sentinelResponse) return {
@@ -706,23 +1021,145 @@ function parseYearField(node) {
706
1021
  };
707
1022
  }
708
1023
  /**
1024
+ * Validate column type string.
1025
+ */
1026
+ function isValidColumnType(type) {
1027
+ return type === "string" || type === "number" || type === "url" || type === "date" || type === "year";
1028
+ }
1029
+ /**
1030
+ * Parse column definitions from attributes.
1031
+ * columnIds is required. columnLabels is optional (backfilled from tableHeaderLabels if provided).
1032
+ */
1033
+ function parseColumnsFromAttributes(node, fieldId, tableHeaderLabels) {
1034
+ const columnIds = getStringArrayAttr(node, "columnIds");
1035
+ const columnLabels = getStringArrayAttr(node, "columnLabels");
1036
+ const columnTypesRaw = node.attributes?.columnTypes;
1037
+ if (!columnIds || columnIds.length === 0) throw new ParseError(`table-field '${fieldId}' requires 'columnIds' attribute. Example: columnIds=["name", "title", "department"]`);
1038
+ const seenIds = /* @__PURE__ */ new Set();
1039
+ for (const id of columnIds) {
1040
+ if (seenIds.has(id)) throw new ParseError(`table-field '${fieldId}' has duplicate column ID '${id}'`);
1041
+ seenIds.add(id);
1042
+ }
1043
+ const columns = [];
1044
+ for (let i = 0; i < columnIds.length; i++) {
1045
+ const id = columnIds[i];
1046
+ const label = columnLabels?.[i] ?? tableHeaderLabels?.[i] ?? id;
1047
+ const typeSpec = columnTypesRaw?.[i];
1048
+ let type = "string";
1049
+ let required = false;
1050
+ if (typeSpec !== void 0) {
1051
+ if (typeof typeSpec === "string") {
1052
+ if (!isValidColumnType(typeSpec)) throw new ParseError(`table-field '${fieldId}' has invalid column type '${String(typeSpec)}' for column '${id}'. Valid types: string, number, url, date, year`);
1053
+ type = typeSpec;
1054
+ } else if (typeof typeSpec === "object" && typeSpec !== null) {
1055
+ const typeObj = typeSpec;
1056
+ if (!isValidColumnType(typeObj.type)) throw new ParseError(`table-field '${fieldId}' has invalid column type '${String(typeObj.type)}' for column '${id}'. Valid types: string, number, url, date, year`);
1057
+ type = typeObj.type;
1058
+ required = typeObj.required ?? false;
1059
+ }
1060
+ }
1061
+ columns.push({
1062
+ id,
1063
+ label,
1064
+ type,
1065
+ required
1066
+ });
1067
+ }
1068
+ return columns;
1069
+ }
1070
+ /**
1071
+ * Parse a table-field tag.
1072
+ *
1073
+ * Column definitions come from attributes:
1074
+ * - columnIds (required): array of snake_case column identifiers
1075
+ * - columnLabels (optional): array of display labels (backfilled from table header row if omitted)
1076
+ * - columnTypes (optional): array of column types (defaults to all 'string')
1077
+ *
1078
+ * Table content is a raw markdown table inside the tag (NOT a value fence).
1079
+ */
1080
+ function parseTableField(node) {
1081
+ const { id, label, required } = parseBaseFieldAttrs(node, "table");
1082
+ const sentinelResponse = tryParseSentinelResponse(node, id, required);
1083
+ const tableContent = extractTableContent(node);
1084
+ const columns = parseColumnsFromAttributes(node, id, extractTableHeaderLabels(tableContent));
1085
+ const dataStartLine = 2;
1086
+ const field = {
1087
+ kind: "table",
1088
+ id,
1089
+ label,
1090
+ required,
1091
+ ...getCommonFieldAttrs(node),
1092
+ columns,
1093
+ minRows: getNumberAttr(node, "minRows"),
1094
+ maxRows: getNumberAttr(node, "maxRows")
1095
+ };
1096
+ if (sentinelResponse) return {
1097
+ field,
1098
+ response: sentinelResponse
1099
+ };
1100
+ if (tableContent === null || tableContent.trim() === "") return {
1101
+ field,
1102
+ response: parseFieldResponse(node, {
1103
+ kind: "table",
1104
+ rows: []
1105
+ }, id, required)
1106
+ };
1107
+ const parseResult = parseMarkdownTable(tableContent, columns, dataStartLine);
1108
+ if (!parseResult.ok) throw new ParseError(`table-field '${id}': ${parseResult.error}`);
1109
+ return {
1110
+ field,
1111
+ response: parseFieldResponse(node, parseResult.value, id, required)
1112
+ };
1113
+ }
1114
+ /**
1115
+ * Map legacy tag names to field kinds for error messages.
1116
+ */
1117
+ const LEGACY_TAG_TO_KIND = {
1118
+ "string-field": "string",
1119
+ "number-field": "number",
1120
+ "string-list": "string_list",
1121
+ "single-select": "single_select",
1122
+ "multi-select": "multi_select",
1123
+ checkboxes: "checkboxes",
1124
+ "url-field": "url",
1125
+ "url-list": "url_list",
1126
+ "date-field": "date",
1127
+ "year-field": "year",
1128
+ "table-field": "table"
1129
+ };
1130
+ /**
1131
+ * Parse a unified field tag: {% field kind="..." ... %}
1132
+ */
1133
+ function parseUnifiedField(node) {
1134
+ const kind = getStringAttr(node, "kind");
1135
+ if (!kind) throw new ParseError("field tag missing required 'kind' attribute");
1136
+ if (!FIELD_KINDS.includes(kind)) throw new ParseError(`field tag has invalid kind '${kind}'. Valid kinds: ${FIELD_KINDS.join(", ")}`);
1137
+ switch (kind) {
1138
+ case "string": return parseStringField(node);
1139
+ case "number": return parseNumberField(node);
1140
+ case "string_list": return parseStringListField(node);
1141
+ case "single_select": return parseSingleSelectField(node);
1142
+ case "multi_select": return parseMultiSelectField(node);
1143
+ case "checkboxes": return parseCheckboxesField(node);
1144
+ case "url": return parseUrlField(node);
1145
+ case "url_list": return parseUrlListField(node);
1146
+ case "date": return parseDateField(node);
1147
+ case "year": return parseYearField(node);
1148
+ case "table": return parseTableField(node);
1149
+ }
1150
+ }
1151
+ /**
709
1152
  * Parse a field tag and return field schema and response.
1153
+ * Accepts both unified field syntax {% field kind="..." %} and legacy tags.
710
1154
  */
711
1155
  function parseField(node) {
712
1156
  if (!isTagNode(node)) return null;
713
- switch (node.tag) {
714
- case "string-field": return parseStringField(node);
715
- case "number-field": return parseNumberField(node);
716
- case "string-list": return parseStringListField(node);
717
- case "single-select": return parseSingleSelectField(node);
718
- case "multi-select": return parseMultiSelectField(node);
719
- case "checkboxes": return parseCheckboxesField(node);
720
- case "url-field": return parseUrlField(node);
721
- case "url-list": return parseUrlListField(node);
722
- case "date-field": return parseDateField(node);
723
- case "year-field": return parseYearField(node);
724
- default: return null;
1157
+ if (node.tag === "field") return parseUnifiedField(node);
1158
+ if (node.tag) {
1159
+ const kind = LEGACY_TAG_TO_KIND[node.tag];
1160
+ if (kind !== void 0) throw new ParseError(`Legacy field tag '${node.tag}' is no longer supported. Use {% field kind="${kind}" %} instead`);
725
1161
  }
1162
+ return null;
726
1163
  }
727
1164
 
728
1165
  //#endregion
@@ -732,6 +1169,18 @@ function parseField(node) {
732
1169
  *
733
1170
  * Parses Markdoc documents and extracts form schema, values, and documentation blocks.
734
1171
  */
1172
+ /**
1173
+ * Valid tag names inside a form.
1174
+ * Any other tag will produce a ParseError.
1175
+ */
1176
+ const VALID_FORM_TAGS = new Set([
1177
+ "group",
1178
+ "field",
1179
+ "note",
1180
+ "description",
1181
+ "instructions",
1182
+ "documentation"
1183
+ ]);
735
1184
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
736
1185
  /**
737
1186
  * Parse harness configuration from frontmatter.
@@ -790,14 +1239,14 @@ function extractFrontmatter(content) {
790
1239
  }
791
1240
  }
792
1241
  /**
793
- * Parse a field-group tag.
1242
+ * Parse a group tag.
794
1243
  */
795
1244
  function parseFieldGroup(node, responsesByFieldId, orderIndex, idIndex, parentId) {
796
1245
  const id = getStringAttr(node, "id");
797
1246
  const title = getStringAttr(node, "title");
798
- if (!id) throw new ParseError("field-group missing required 'id' attribute");
1247
+ if (!id) throw new ParseError("group missing required 'id' attribute");
799
1248
  if (idIndex.has(id)) throw new ParseError(`Duplicate ID '${id}'`);
800
- if (getStringAttr(node, "state") !== void 0) throw new ParseError(`Field-group '${id}' has state attribute. state attribute is not allowed on field-groups.`);
1249
+ if (getStringAttr(node, "state") !== void 0) throw new ParseError(`Field-group '${id}' has state attribute. state attribute is not allowed on groups.`);
801
1250
  idIndex.set(id, {
802
1251
  nodeType: "group",
803
1252
  parentId
@@ -838,6 +1287,7 @@ function parseFieldGroup(node, responsesByFieldId, orderIndex, idIndex, parentId
838
1287
  }
839
1288
  /**
840
1289
  * Parse a form tag.
1290
+ * Handles both explicit groups and fields placed directly under the form.
841
1291
  */
842
1292
  function parseFormTag(node, responsesByFieldId, orderIndex, idIndex) {
843
1293
  const id = getStringAttr(node, "id");
@@ -846,16 +1296,52 @@ function parseFormTag(node, responsesByFieldId, orderIndex, idIndex) {
846
1296
  if (idIndex.has(id)) throw new ParseError(`Duplicate ID '${id}'`);
847
1297
  idIndex.set(id, { nodeType: "form" });
848
1298
  const groups = [];
849
- function findFieldGroups(child) {
1299
+ const ungroupedFields = [];
1300
+ function processContent(child) {
850
1301
  if (!child || typeof child !== "object") return;
851
- if (isTagNode(child, "field-group")) {
1302
+ if (isTagNode(child) && !VALID_FORM_TAGS.has(child.tag)) throw new ParseError(`Unknown tag '${child.tag}' inside form`);
1303
+ if (isTagNode(child, "group")) {
852
1304
  const group = parseFieldGroup(child, responsesByFieldId, orderIndex, idIndex, id);
853
1305
  groups.push(group);
854
1306
  return;
855
1307
  }
856
- if (child.children && Array.isArray(child.children)) for (const c of child.children) findFieldGroups(c);
1308
+ const result = parseField(child);
1309
+ if (result) {
1310
+ if (idIndex.has(result.field.id)) throw new ParseError(`Duplicate ID '${result.field.id}'`);
1311
+ idIndex.set(result.field.id, {
1312
+ nodeType: "field",
1313
+ parentId: id
1314
+ });
1315
+ ungroupedFields.push(result.field);
1316
+ responsesByFieldId[result.field.id] = result.response;
1317
+ orderIndex.push(result.field.id);
1318
+ if ("options" in result.field) for (const opt of result.field.options) {
1319
+ const qualifiedRef = `${result.field.id}.${opt.id}`;
1320
+ if (idIndex.has(qualifiedRef)) throw new ParseError(`Duplicate option ref '${qualifiedRef}'`);
1321
+ idIndex.set(qualifiedRef, {
1322
+ nodeType: "option",
1323
+ parentId: id,
1324
+ fieldId: result.field.id
1325
+ });
1326
+ }
1327
+ return;
1328
+ }
1329
+ if (child.children && Array.isArray(child.children)) for (const c of child.children) processContent(c);
1330
+ }
1331
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) processContent(child);
1332
+ if (ungroupedFields.length > 0) {
1333
+ const implicitGroupId = `_default`;
1334
+ if (idIndex.has(implicitGroupId)) throw new ParseError(`ID '${implicitGroupId}' is reserved for implicit field groups. Please use a different ID for your field or group.`);
1335
+ idIndex.set(implicitGroupId, {
1336
+ nodeType: "group",
1337
+ parentId: id
1338
+ });
1339
+ groups.push({
1340
+ id: implicitGroupId,
1341
+ children: ungroupedFields,
1342
+ implicit: true
1343
+ });
857
1344
  }
858
- if (node.children && Array.isArray(node.children)) for (const child of node.children) findFieldGroups(child);
859
1345
  return {
860
1346
  id,
861
1347
  title,
@@ -982,6 +1468,137 @@ function parseForm(markdown) {
982
1468
  };
983
1469
  }
984
1470
 
1471
+ //#endregion
1472
+ //#region src/engine/scopeRef.ts
1473
+ /**
1474
+ * Pattern for cell reference: fieldId[row].columnId
1475
+ * - fieldId: identifier (letters, digits, underscores, hyphens)
1476
+ * - row: non-negative integer
1477
+ * - columnId: identifier
1478
+ */
1479
+ const CELL_REF_PATTERN = /^([a-zA-Z_][a-zA-Z0-9_-]*)\[(\d+)\]\.([a-zA-Z_][a-zA-Z0-9_-]*)$/;
1480
+ /**
1481
+ * Pattern for qualified reference: fieldId.qualifierId
1482
+ * - fieldId: identifier
1483
+ * - qualifierId: identifier (optionId or columnId)
1484
+ */
1485
+ const QUALIFIED_REF_PATTERN = /^([a-zA-Z_][a-zA-Z0-9_-]*)\.([a-zA-Z_][a-zA-Z0-9_-]*)$/;
1486
+ /**
1487
+ * Pattern for simple field reference: fieldId
1488
+ * - fieldId: identifier
1489
+ */
1490
+ const FIELD_REF_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
1491
+ /**
1492
+ * Parse a scope reference string into a structured format.
1493
+ *
1494
+ * @param refStr - The scope reference string to parse
1495
+ * @returns Parsed scope reference or error
1496
+ *
1497
+ * @example
1498
+ * parseScopeRef('myField')
1499
+ * // => { ok: true, ref: { type: 'field', fieldId: 'myField' } }
1500
+ *
1501
+ * parseScopeRef('myField.optA')
1502
+ * // => { ok: true, ref: { type: 'qualified', fieldId: 'myField', qualifierId: 'optA' } }
1503
+ *
1504
+ * parseScopeRef('myTable[2].name')
1505
+ * // => { ok: true, ref: { type: 'cell', fieldId: 'myTable', row: 2, columnId: 'name' } }
1506
+ */
1507
+ function parseScopeRef(refStr) {
1508
+ const trimmed = refStr.trim();
1509
+ if (!trimmed) return {
1510
+ ok: false,
1511
+ error: "Empty scope reference"
1512
+ };
1513
+ const cellMatch = CELL_REF_PATTERN.exec(trimmed);
1514
+ if (cellMatch) {
1515
+ const fieldId = cellMatch[1];
1516
+ const rowStr = cellMatch[2];
1517
+ const columnId = cellMatch[3];
1518
+ const row = parseInt(rowStr, 10);
1519
+ if (row < 0 || !Number.isInteger(row)) return {
1520
+ ok: false,
1521
+ error: `Invalid row index: ${rowStr}`
1522
+ };
1523
+ return {
1524
+ ok: true,
1525
+ ref: {
1526
+ type: "cell",
1527
+ fieldId,
1528
+ row,
1529
+ columnId
1530
+ }
1531
+ };
1532
+ }
1533
+ const qualifiedMatch = QUALIFIED_REF_PATTERN.exec(trimmed);
1534
+ if (qualifiedMatch) return {
1535
+ ok: true,
1536
+ ref: {
1537
+ type: "qualified",
1538
+ fieldId: qualifiedMatch[1],
1539
+ qualifierId: qualifiedMatch[2]
1540
+ }
1541
+ };
1542
+ if (FIELD_REF_PATTERN.test(trimmed)) return {
1543
+ ok: true,
1544
+ ref: {
1545
+ type: "field",
1546
+ fieldId: trimmed
1547
+ }
1548
+ };
1549
+ return {
1550
+ ok: false,
1551
+ error: `Invalid scope reference format: ${refStr}`
1552
+ };
1553
+ }
1554
+ /**
1555
+ * Serialize a parsed scope reference back to a string.
1556
+ *
1557
+ * @param ref - The parsed scope reference
1558
+ * @returns Serialized string
1559
+ *
1560
+ * @example
1561
+ * serializeScopeRef({ type: 'field', fieldId: 'myField' })
1562
+ * // => 'myField'
1563
+ *
1564
+ * serializeScopeRef({ type: 'qualified', fieldId: 'myField', qualifierId: 'optA' })
1565
+ * // => 'myField.optA'
1566
+ *
1567
+ * serializeScopeRef({ type: 'cell', fieldId: 'myTable', row: 2, columnId: 'name' })
1568
+ * // => 'myTable[2].name'
1569
+ */
1570
+ function serializeScopeRef(ref) {
1571
+ switch (ref.type) {
1572
+ case "field": return ref.fieldId;
1573
+ case "qualified": return `${ref.fieldId}.${ref.qualifierId}`;
1574
+ case "cell": return `${ref.fieldId}[${ref.row}].${ref.columnId}`;
1575
+ }
1576
+ }
1577
+ /**
1578
+ * Check if a scope reference is a cell reference.
1579
+ */
1580
+ function isCellRef(ref) {
1581
+ return ref.type === "cell";
1582
+ }
1583
+ /**
1584
+ * Check if a scope reference is a qualified reference.
1585
+ */
1586
+ function isQualifiedRef(ref) {
1587
+ return ref.type === "qualified";
1588
+ }
1589
+ /**
1590
+ * Check if a scope reference is a simple field reference.
1591
+ */
1592
+ function isFieldRef(ref) {
1593
+ return ref.type === "field";
1594
+ }
1595
+ /**
1596
+ * Extract the field ID from any scope reference.
1597
+ */
1598
+ function getFieldId(ref) {
1599
+ return ref.fieldId;
1600
+ }
1601
+
985
1602
  //#endregion
986
1603
  //#region src/engine/valueCoercion.ts
987
1604
  /**
@@ -1387,6 +2004,51 @@ function coerceToYear(fieldId, rawValue) {
1387
2004
  };
1388
2005
  }
1389
2006
  /**
2007
+ * Coerce raw value to SetTablePatch.
2008
+ * Accepts:
2009
+ * - Array of row objects: [{ col1: value1, col2: value2 }, ...]
2010
+ * - Empty array: [] (valid for optional tables or minRows=0)
2011
+ */
2012
+ function coerceToTable(fieldId, rawValue) {
2013
+ if (rawValue === null) return {
2014
+ ok: true,
2015
+ patch: {
2016
+ op: "set_table",
2017
+ fieldId,
2018
+ rows: []
2019
+ }
2020
+ };
2021
+ if (!Array.isArray(rawValue)) return {
2022
+ ok: false,
2023
+ error: `Table value for field '${fieldId}' must be an array of rows, got ${typeof rawValue}`
2024
+ };
2025
+ if (rawValue.length === 0) return {
2026
+ ok: true,
2027
+ patch: {
2028
+ op: "set_table",
2029
+ fieldId,
2030
+ rows: []
2031
+ }
2032
+ };
2033
+ const rows = [];
2034
+ for (let i = 0; i < rawValue.length; i++) {
2035
+ const row = rawValue[i];
2036
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return {
2037
+ ok: false,
2038
+ error: `Row ${i} for table field '${fieldId}' must be an object, got ${Array.isArray(row) ? "array" : typeof row}`
2039
+ };
2040
+ rows.push(row);
2041
+ }
2042
+ return {
2043
+ ok: true,
2044
+ patch: {
2045
+ op: "set_table",
2046
+ fieldId,
2047
+ rows
2048
+ }
2049
+ };
2050
+ }
2051
+ /**
1390
2052
  * Coerce a raw value to a Patch for a specific field.
1391
2053
  */
1392
2054
  function coerceToFieldPatch(form, fieldId, rawValue) {
@@ -1406,6 +2068,7 @@ function coerceToFieldPatch(form, fieldId, rawValue) {
1406
2068
  case "url_list": return coerceToUrlList(fieldId, rawValue);
1407
2069
  case "date": return coerceToDate(fieldId, rawValue);
1408
2070
  case "year": return coerceToYear(fieldId, rawValue);
2071
+ case "table": return coerceToTable(fieldId, rawValue);
1409
2072
  }
1410
2073
  }
1411
2074
  /**
@@ -1758,6 +2421,9 @@ var MockAgent = class {
1758
2421
  case "checkboxes": return true;
1759
2422
  case "url": return value.value !== null && value.value !== "";
1760
2423
  case "url_list": return value.items.length > 0;
2424
+ case "date": return value.value !== null;
2425
+ case "year": return value.value !== null;
2426
+ case "table": return value.rows.length > 0;
1761
2427
  default: return false;
1762
2428
  }
1763
2429
  }
@@ -1806,6 +2472,25 @@ var MockAgent = class {
1806
2472
  fieldId,
1807
2473
  items: value.items
1808
2474
  };
2475
+ case "date": return {
2476
+ op: "set_date",
2477
+ fieldId,
2478
+ value: value.value
2479
+ };
2480
+ case "year": return {
2481
+ op: "set_year",
2482
+ fieldId,
2483
+ value: value.value
2484
+ };
2485
+ case "table": return {
2486
+ op: "set_table",
2487
+ fieldId,
2488
+ rows: value.rows.map((row) => {
2489
+ const patchRow = {};
2490
+ for (const [colId, cellResponse] of Object.entries(row)) patchRow[colId] = cellResponse.value ?? null;
2491
+ return patchRow;
2492
+ })
2493
+ };
1809
2494
  default: return null;
1810
2495
  }
1811
2496
  }
@@ -6886,12 +7571,12 @@ function getIssuesIntro(maxPatches) {
6886
7571
  /**
6887
7572
  * Instructions section for the context prompt.
6888
7573
  *
6889
- * This explains the patch format for each field type.
7574
+ * This explains the patch format for each field kind.
6890
7575
  */
6891
7576
  const PATCH_FORMAT_INSTRUCTIONS = `# Instructions
6892
7577
 
6893
7578
  Use the generatePatches tool to submit patches for the fields above.
6894
- Each patch should match the field type:
7579
+ Each patch should match the field kind:
6895
7580
  - string: { op: "set_string", fieldId: "...", value: "..." }
6896
7581
  - number: { op: "set_number", fieldId: "...", value: 123 }
6897
7582
  - string_list: { op: "set_string_list", fieldId: "...", items: ["...", "..."] }
@@ -6927,13 +7612,15 @@ var LiveAgent = class {
6927
7612
  provider;
6928
7613
  enableWebSearch;
6929
7614
  webSearchTools = null;
7615
+ additionalTools;
6930
7616
  constructor(config) {
6931
7617
  this.model = config.model;
6932
7618
  this.maxStepsPerTurn = config.maxStepsPerTurn ?? 3;
6933
7619
  this.systemPromptAddition = config.systemPromptAddition;
6934
7620
  this.targetRole = config.targetRole ?? AGENT_ROLE;
6935
7621
  this.provider = config.provider;
6936
- this.enableWebSearch = config.enableWebSearch ?? true;
7622
+ this.enableWebSearch = config.enableWebSearch;
7623
+ this.additionalTools = config.additionalTools ?? {};
6937
7624
  if (this.enableWebSearch && this.provider) this.webSearchTools = loadWebSearchTools(this.provider);
6938
7625
  }
6939
7626
  /**
@@ -6943,7 +7630,8 @@ var LiveAgent = class {
6943
7630
  getAvailableToolNames() {
6944
7631
  const tools = ["generatePatches"];
6945
7632
  if (this.webSearchTools) tools.push(...Object.keys(this.webSearchTools));
6946
- return tools;
7633
+ tools.push(...Object.keys(this.additionalTools));
7634
+ return [...new Set(tools)];
6947
7635
  }
6948
7636
  /**
6949
7637
  * Generate patches using the LLM.
@@ -6963,7 +7651,8 @@ var LiveAgent = class {
6963
7651
  description: GENERATE_PATCHES_TOOL_DESCRIPTION,
6964
7652
  inputSchema: zodSchema(z.object({ patches: z.array(PatchSchema).max(maxPatches).describe("Array of patches. Each patch sets a value for one field.") }))
6965
7653
  },
6966
- ...this.webSearchTools
7654
+ ...this.webSearchTools,
7655
+ ...this.additionalTools
6967
7656
  };
6968
7657
  const result = await generateText({
6969
7658
  model: this.model,
@@ -7389,7 +8078,8 @@ async function fillForm(options) {
7389
8078
  systemPromptAddition: options.systemPromptAddition,
7390
8079
  targetRole: targetRoles[0] ?? AGENT_ROLE,
7391
8080
  provider,
7392
- enableWebSearch: true
8081
+ enableWebSearch: options.enableWebSearch,
8082
+ additionalTools: options.additionalTools
7393
8083
  });
7394
8084
  let turnCount = 0;
7395
8085
  let stepResult = harness.step();
@@ -7488,7 +8178,9 @@ async function runResearch(form, options) {
7488
8178
  const agent = createLiveAgent({
7489
8179
  model,
7490
8180
  provider,
7491
- targetRole: config.targetRoles?.[0] ?? AGENT_ROLE
8181
+ targetRole: config.targetRoles?.[0] ?? AGENT_ROLE,
8182
+ enableWebSearch: options.enableWebSearch,
8183
+ additionalTools: options.additionalTools
7492
8184
  });
7493
8185
  const availableTools = agent.getAvailableToolNames();
7494
8186
  let totalInputTokens = 0;
@@ -7580,8 +8272,9 @@ function validateResearchForm(form) {
7580
8272
  * This is the main library entry point that exports the core engine,
7581
8273
  * types, and utilities for working with .form.md files.
7582
8274
  */
7583
- /** Markform version. */
7584
- const VERSION = "0.1.0";
8275
+ const pkg = createRequire(import.meta.url)("../package.json");
8276
+ /** Markform version (read from package.json). */
8277
+ const VERSION = pkg.version;
7585
8278
 
7586
8279
  //#endregion
7587
- export { findFieldById as _, resolveHarnessConfig as a, getProviderNames as c, MockAgent as d, createMockAgent as f, coerceToFieldPatch as g, coerceInputContext as h, runResearch as i, resolveModel as l, createHarness as m, isResearchForm as n, fillForm as o, FormHarness as p, validateResearchForm as r, getProviderInfo as s, VERSION as t, createLiveAgent as u, parseForm as v, ParseError as y };
8280
+ export { serializeScopeRef as C, parseRawTable as D, parseMarkdownTable as E, ParseError as O, parseScopeRef as S, parseCellValue as T, findFieldById as _, resolveHarnessConfig as a, isFieldRef as b, getProviderNames as c, MockAgent as d, createMockAgent as f, coerceToFieldPatch as g, coerceInputContext as h, runResearch as i, resolveModel as l, createHarness as m, isResearchForm as n, fillForm as o, FormHarness as p, validateResearchForm as r, getProviderInfo as s, VERSION as t, createLiveAgent as u, getFieldId as v, parseForm as w, isQualifiedRef as x, isCellRef as y };