markform 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Markform
2
2
 
3
- [![CI](https://github.com/jlevy/markform/actions/workflows/ci.yml/badge.svg)](https://github.com/jlevy/markform/actions/runs/21430430567)
4
- [![Coverage](https://raw.githubusercontent.com/jlevy/markform/main/badges/packages/markform/coverage-total.svg)](https://github.com/jlevy/markform/actions/runs/21430430567)
3
+ [![CI](https://github.com/jlevy/markform/actions/workflows/ci.yml/badge.svg)](https://github.com/jlevy/markform/actions/runs/21506874671)
4
+ [![Coverage](https://raw.githubusercontent.com/jlevy/markform/main/badges/packages/markform/coverage-total.svg)](https://github.com/jlevy/markform/actions/runs/21506874671)
5
5
  [![npm version](https://img.shields.io/npm/v/markform)](https://www.npmjs.com/package/markform)
6
6
  [![X Follow](https://img.shields.io/twitter/follow/ojoshe)](https://x.com/ojoshe)
7
7
 
@@ -467,6 +467,8 @@ markform fill my-form.form.md --interactive
467
467
  markform fill my-form.form.md --model=anthropic/claude-sonnet-4-5
468
468
  # Mock agent for testing (uses pre-filled form as source)
469
469
  markform fill my-form.form.md --mock --mock-source filled.form.md
470
+ # Record fill session to JSON sidecar (tokens, timing, tool calls)
471
+ markform fill my-form.form.md --model=openai/gpt-5-mini --record-fill
470
472
  ```
471
473
 
472
474
  ### Export and Transform
@@ -565,6 +567,29 @@ console.log(form.responsesByFieldId);
565
567
  const output = serialize(form);
566
568
  ```
567
569
 
570
+ ### High-Level Fill API
571
+
572
+ The simplest way to fill a form programmatically:
573
+
574
+ ```typescript
575
+ import { fillForm } from "markform";
576
+
577
+ const result = await fillForm({
578
+ form: markdownContent,
579
+ model: "anthropic/claude-sonnet-4-5",
580
+ enableWebSearch: true,
581
+ recordFill: true, // Capture tokens, timing, tool calls
582
+ });
583
+
584
+ if (result.status.ok) {
585
+ console.log("Values:", result.values);
586
+ console.log("Tokens used:", result.record?.llm.inputTokens);
587
+ }
588
+ ```
589
+
590
+ See the [API documentation](https://github.com/jlevy/markform/blob/main/docs/markform-apis.md)
591
+ for options like parallel execution, callbacks, and checkpointing.
592
+
568
593
  ### AI SDK Integration
569
594
 
570
595
  Markform provides tools compatible with the [Vercel AI SDK](https://sdk.vercel.ai/):
package/dist/ai-sdk.d.mts CHANGED
@@ -1,9 +1,8 @@
1
1
 
2
- import { At as Patch, Mt as PatchSchema, U as FieldResponse, Y as FormSchema, at as InspectResult, et as Id, kt as ParsedForm, mr as ValidatorRegistry, r as ApplyResult } from "./coreTypes-BMEs8h_2.mjs";
2
+ import { At as Patch, Mt as PatchSchema, U as FieldResponse, Y as FormSchema, at as InspectResult, et as Id, kt as ParsedForm, mr as ValidatorRegistry, r as ApplyResult } from "./coreTypes-CkxML8g2.mjs";
3
3
  import { z } from "zod";
4
4
 
5
5
  //#region src/integrations/toolTypes.d.ts
6
-
7
6
  /**
8
7
  * Result from markform_inspect tool.
9
8
  */
package/dist/ai-sdk.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
- import { L as PatchSchema } from "./coreTypes-SDB3KRRJ.mjs";
3
- import { d as serializeForm, i as inspect, t as applyPatches } from "./apply-BYgtU64w.mjs";
2
+ import { L as PatchSchema } from "./coreTypes-CPKXf2dc.mjs";
3
+ import { d as serializeForm, i as inspect, t as applyPatches } from "./apply-Dalpt-D6.mjs";
4
4
  import { z } from "zod";
5
5
 
6
6
  //#region src/integrations/vercelAiSdkTools.ts
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk.mjs","names":["toolset: MarkformToolSet"],"sources":["../src/integrations/vercelAiSdkTools.ts"],"sourcesContent":["/**\n * AI SDK Integration for Markform.\n *\n * Provides Vercel AI SDK compatible tools for agent-driven form filling.\n * Use createMarkformTools() to create a toolset that wraps the Markform engine.\n *\n * @example\n * ```typescript\n * import { createMarkformTools, MarkformSessionStore } from 'markform/ai-sdk';\n * import { generateText } from 'ai';\n *\n * const store = new MarkformSessionStore(parsedForm);\n * const tools = createMarkformTools({ sessionStore: store });\n *\n * const { text } = await generateText({\n * model: openai('gpt-4'),\n * prompt: 'Fill out this form...',\n * tools,\n * });\n * ```\n */\n\nimport { z } from 'zod';\nimport type { ParsedForm, Patch, ValidatorRegistry } from '../engine/coreTypes.js';\nimport { inspect } from '../engine/inspect.js';\nimport { applyPatches } from '../engine/apply.js';\nimport { serializeForm } from '../engine/serialize.js';\nimport { PatchSchema } from '../engine/coreTypes.js';\n\n// =============================================================================\n// Session Store\n// =============================================================================\n\n/**\n * Session store for managing form state during AI interactions.\n *\n * The AI SDK tools operate on a shared form instance through this store.\n * Create one store per form-filling session.\n */\nexport class MarkformSessionStore {\n private form: ParsedForm;\n private validatorRegistry: ValidatorRegistry;\n\n constructor(form: ParsedForm, validatorRegistry?: ValidatorRegistry) {\n this.form = form;\n this.validatorRegistry = validatorRegistry ?? {};\n }\n\n /**\n * Get the current form.\n */\n getForm(): ParsedForm {\n return this.form;\n }\n\n /**\n * Get the validator registry.\n */\n getValidatorRegistry(): ValidatorRegistry {\n return this.validatorRegistry;\n }\n\n /**\n * Update the form values.\n */\n updateForm(form: ParsedForm): void {\n this.form = form;\n }\n}\n\n// =============================================================================\n// Tool Creation Options\n// =============================================================================\n\n/**\n * Options for creating Markform AI SDK tools.\n */\nexport interface CreateMarkformToolsOptions {\n /**\n * Session store managing the form state.\n */\n sessionStore: MarkformSessionStore;\n\n /**\n * Whether to include the markform_get_markdown tool.\n * Defaults to true.\n */\n includeGetMarkdown?: boolean;\n}\n\n// =============================================================================\n// Tool Types (imported from toolTypes.ts)\n// =============================================================================\n\nimport type {\n ApplyToolResult,\n ExportToolResult,\n GetMarkdownToolResult,\n InspectToolResult,\n MarkformTool,\n MarkformToolSet,\n} from './toolTypes.js';\n\n// Re-export types for backwards compatibility\nexport type {\n ApplyToolResult,\n ExportToolResult,\n GetMarkdownToolResult,\n InspectToolResult,\n MarkformTool,\n MarkformToolSet,\n} from './toolTypes.js';\n\n// =============================================================================\n// Zod Schemas for Tool Inputs\n// =============================================================================\n\n/**\n * Input schema for markform_inspect tool (no parameters).\n */\nconst InspectInputSchema = z\n .object({})\n .describe('No input parameters required. Call this tool to inspect the current form state.');\n\n/**\n * Input schema for markform_apply tool.\n */\nconst ApplyInputSchema = z\n .object({\n patches: z\n .array(PatchSchema)\n .min(1)\n .max(20)\n .describe(\n 'Array of patches to apply to the form. Each patch sets or clears a field value. ' +\n 'Operations: set_string, set_number, set_string_list, set_single_select, set_multi_select, ' +\n 'set_checkboxes, set_url, set_url_list, set_date, set_year, set_table, clear_field, skip_field, abort_field. ' +\n 'Example: [{ \"op\": \"set_string\", \"fieldId\": \"name\", \"value\": \"Alice\" }]',\n ),\n })\n .describe('Apply patches to update form field values.');\n\n/**\n * Input schema for markform_export tool (no parameters).\n */\nconst ExportInputSchema = z\n .object({})\n .describe(\n 'No input parameters required. Call this tool to export the form schema and current values as JSON.',\n );\n\n/**\n * Input schema for markform_get_markdown tool (no parameters).\n */\nconst GetMarkdownInputSchema = z\n .object({})\n .describe(\n 'No input parameters required. Call this tool to get the canonical Markdown representation of the current form.',\n );\n\n// =============================================================================\n// Tool Factory\n// =============================================================================\n\n/**\n * Create Markform AI SDK tools for agent-driven form filling.\n *\n * Returns a toolset compatible with Vercel AI SDK's generateText and streamText.\n *\n * @param options - Tool creation options including session store\n * @returns MarkformToolSet containing all tools\n *\n * @example\n * ```typescript\n * import { parseForm } from 'markform';\n * import { createMarkformTools, MarkformSessionStore } from 'markform/ai-sdk';\n *\n * const form = parseForm(markdownContent);\n * const store = new MarkformSessionStore(form);\n * const tools = createMarkformTools({ sessionStore: store });\n *\n * // Use with AI SDK\n * const result = await generateText({\n * model: yourModel,\n * tools,\n * prompt: 'Fill out this form based on the user information...',\n * });\n * ```\n */\nexport function createMarkformTools(options: CreateMarkformToolsOptions): MarkformToolSet {\n const { sessionStore, includeGetMarkdown = true } = options;\n\n // markform_inspect - Get current form state with issues\n const markform_inspect: MarkformTool<Record<string, never>, InspectToolResult> = {\n description:\n 'Inspect the current form state. Returns structure summary, progress summary, validation issues, ' +\n 'and completion status. Use this to understand what fields need to be filled and what issues exist. ' +\n \"Issues are sorted by priority (1 = highest). Focus on 'required' severity issues first.\",\n inputSchema: InspectInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n const result = inspect(form);\n\n const requiredCount = result.issues.filter((i) => i.severity === 'required').length;\n const message = result.isComplete\n ? 'Form is complete. All required fields are filled.'\n : `Form has ${requiredCount} required issue(s) to resolve.`;\n\n return Promise.resolve({\n success: true,\n data: result,\n message,\n });\n },\n };\n\n // markform_apply - Apply patches to update form values\n const markform_apply: MarkformTool<{ patches: Patch[] }, ApplyToolResult> = {\n description:\n 'Apply patches to update form field values. Valid patches are applied even if some fail. ' +\n 'Single values are automatically coerced to arrays for list fields. ' +\n 'Returns applied patches, warnings for coerced values, and rejected patches separately. ' +\n 'Patch operations: set_string, set_number, set_string_list, set_single_select, set_multi_select, ' +\n 'set_checkboxes, set_url, set_url_list, set_date, set_year, set_table, clear_field, skip_field, abort_field.',\n inputSchema: ApplyInputSchema,\n execute: ({ patches }) => {\n const form = sessionStore.getForm();\n const result = applyPatches(form, patches);\n\n // Update the store with the modified form\n sessionStore.updateForm(form);\n\n const warningNote = result.warnings.length > 0 ? ` (${result.warnings.length} coerced)` : '';\n const remaining = result.issues.filter((i) => i.severity === 'required').length;\n\n const message =\n result.applyStatus === 'applied'\n ? `Applied ${patches.length} patch(es)${warningNote}. ${\n result.isComplete\n ? 'Form is now complete!'\n : `${remaining} required issue(s) remaining.`\n }`\n : result.applyStatus === 'partial'\n ? `Applied ${result.appliedPatches.length}/${patches.length} patches${warningNote}. ` +\n `${result.rejectedPatches.length} rejected.`\n : `All patches rejected. Check field IDs and value types.`;\n\n return Promise.resolve({\n success: result.applyStatus !== 'rejected',\n data: result,\n message,\n });\n },\n };\n\n // markform_export - Export schema and values as JSON\n const markform_export: MarkformTool<Record<string, never>, ExportToolResult> = {\n description:\n 'Export the form schema and current values as JSON. Use this to get a machine-readable ' +\n 'representation of the form structure and all field values. Useful for processing or analysis.',\n inputSchema: ExportInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n\n // Count answered fields\n const answeredCount = Object.values(form.responsesByFieldId).filter(\n (response) => response.state === 'answered',\n ).length;\n\n return Promise.resolve({\n success: true,\n data: {\n schema: form.schema,\n values: form.responsesByFieldId,\n },\n message: `Exported form with ${form.schema.groups.length} group(s) and ${answeredCount} answered field(s).`,\n });\n },\n };\n\n // Build the toolset\n const toolset: MarkformToolSet = {\n markform_inspect,\n markform_apply,\n markform_export,\n };\n\n // Optionally include markform_get_markdown\n if (includeGetMarkdown) {\n const markform_get_markdown: MarkformTool<Record<string, never>, GetMarkdownToolResult> = {\n description:\n 'Get the canonical Markdown representation of the current form. ' +\n 'Use this to see the complete form with all current values in Markform format. ' +\n 'The output is deterministic and round-trip safe.',\n inputSchema: GetMarkdownInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n const markdown = serializeForm(form);\n\n return Promise.resolve({\n success: true,\n data: {\n markdown,\n },\n message: `Generated Markdown (${markdown.length} characters).`,\n });\n },\n };\n\n toolset.markform_get_markdown = markform_get_markdown;\n }\n\n return toolset;\n}\n\n// =============================================================================\n// Convenience Exports\n// =============================================================================\n\nexport { PatchSchema } from '../engine/coreTypes.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,uBAAb,MAAkC;CAChC,AAAQ;CACR,AAAQ;CAER,YAAY,MAAkB,mBAAuC;AACnE,OAAK,OAAO;AACZ,OAAK,oBAAoB,qBAAqB,EAAE;;;;;CAMlD,UAAsB;AACpB,SAAO,KAAK;;;;;CAMd,uBAA0C;AACxC,SAAO,KAAK;;;;;CAMd,WAAW,MAAwB;AACjC,OAAK,OAAO;;;;;;AAsDhB,MAAM,qBAAqB,EACxB,OAAO,EAAE,CAAC,CACV,SAAS,kFAAkF;;;;AAK9F,MAAM,mBAAmB,EACtB,OAAO,EACN,SAAS,EACN,MAAM,YAAY,CAClB,IAAI,EAAE,CACN,IAAI,GAAG,CACP,SACC,2WAID,EACJ,CAAC,CACD,SAAS,6CAA6C;;;;AAKzD,MAAM,oBAAoB,EACvB,OAAO,EAAE,CAAC,CACV,SACC,qGACD;;;;AAKH,MAAM,yBAAyB,EAC5B,OAAO,EAAE,CAAC,CACV,SACC,iHACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BH,SAAgB,oBAAoB,SAAsD;CACxF,MAAM,EAAE,cAAc,qBAAqB,SAAS;CA2FpD,MAAMA,UAA2B;EAC/B,kBAzF+E;GAC/E,aACE;GAGF,aAAa;GACb,eAAe;IAEb,MAAM,SAAS,QADF,aAAa,SAAS,CACP;IAE5B,MAAM,gBAAgB,OAAO,OAAO,QAAQ,MAAM,EAAE,aAAa,WAAW,CAAC;IAC7E,MAAM,UAAU,OAAO,aACnB,sDACA,YAAY,cAAc;AAE9B,WAAO,QAAQ,QAAQ;KACrB,SAAS;KACT,MAAM;KACN;KACD,CAAC;;GAEL;EAqEC,gBAlE0E;GAC1E,aACE;GAKF,aAAa;GACb,UAAU,EAAE,cAAc;IACxB,MAAM,OAAO,aAAa,SAAS;IACnC,MAAM,SAAS,aAAa,MAAM,QAAQ;AAG1C,iBAAa,WAAW,KAAK;IAE7B,MAAM,cAAc,OAAO,SAAS,SAAS,IAAI,KAAK,OAAO,SAAS,OAAO,aAAa;IAC1F,MAAM,YAAY,OAAO,OAAO,QAAQ,MAAM,EAAE,aAAa,WAAW,CAAC;IAEzE,MAAM,UACJ,OAAO,gBAAgB,YACnB,WAAW,QAAQ,OAAO,YAAY,YAAY,IAChD,OAAO,aACH,0BACA,GAAG,UAAU,mCAEnB,OAAO,gBAAgB,YACrB,WAAW,OAAO,eAAe,OAAO,GAAG,QAAQ,OAAO,UAAU,YAAY,IAC7E,OAAO,gBAAgB,OAAO,cACjC;AAER,WAAO,QAAQ,QAAQ;KACrB,SAAS,OAAO,gBAAgB;KAChC,MAAM;KACN;KACD,CAAC;;GAEL;EA+BC,iBA5B6E;GAC7E,aACE;GAEF,aAAa;GACb,eAAe;IACb,MAAM,OAAO,aAAa,SAAS;IAGnC,MAAM,gBAAgB,OAAO,OAAO,KAAK,mBAAmB,CAAC,QAC1D,aAAa,SAAS,UAAU,WAClC,CAAC;AAEF,WAAO,QAAQ,QAAQ;KACrB,SAAS;KACT,MAAM;MACJ,QAAQ,KAAK;MACb,QAAQ,KAAK;MACd;KACD,SAAS,sBAAsB,KAAK,OAAO,OAAO,OAAO,gBAAgB,cAAc;KACxF,CAAC;;GAEL;EAOA;AAGD,KAAI,mBAqBF,SAAQ,wBApBkF;EACxF,aACE;EAGF,aAAa;EACb,eAAe;GAEb,MAAM,WAAW,cADJ,aAAa,SAAS,CACC;AAEpC,UAAO,QAAQ,QAAQ;IACrB,SAAS;IACT,MAAM,EACJ,UACD;IACD,SAAS,uBAAuB,SAAS,OAAO;IACjD,CAAC;;EAEL;AAKH,QAAO"}
1
+ {"version":3,"file":"ai-sdk.mjs","names":[],"sources":["../src/integrations/vercelAiSdkTools.ts"],"sourcesContent":["/**\n * AI SDK Integration for Markform.\n *\n * Provides Vercel AI SDK compatible tools for agent-driven form filling.\n * Use createMarkformTools() to create a toolset that wraps the Markform engine.\n *\n * @example\n * ```typescript\n * import { createMarkformTools, MarkformSessionStore } from 'markform/ai-sdk';\n * import { generateText } from 'ai';\n *\n * const store = new MarkformSessionStore(parsedForm);\n * const tools = createMarkformTools({ sessionStore: store });\n *\n * const { text } = await generateText({\n * model: openai('gpt-4'),\n * prompt: 'Fill out this form...',\n * tools,\n * });\n * ```\n */\n\nimport { z } from 'zod';\nimport type { ParsedForm, Patch, ValidatorRegistry } from '../engine/coreTypes.js';\nimport { inspect } from '../engine/inspect.js';\nimport { applyPatches } from '../engine/apply.js';\nimport { serializeForm } from '../engine/serialize.js';\nimport { PatchSchema } from '../engine/coreTypes.js';\n\n// =============================================================================\n// Session Store\n// =============================================================================\n\n/**\n * Session store for managing form state during AI interactions.\n *\n * The AI SDK tools operate on a shared form instance through this store.\n * Create one store per form-filling session.\n */\nexport class MarkformSessionStore {\n private form: ParsedForm;\n private validatorRegistry: ValidatorRegistry;\n\n constructor(form: ParsedForm, validatorRegistry?: ValidatorRegistry) {\n this.form = form;\n this.validatorRegistry = validatorRegistry ?? {};\n }\n\n /**\n * Get the current form.\n */\n getForm(): ParsedForm {\n return this.form;\n }\n\n /**\n * Get the validator registry.\n */\n getValidatorRegistry(): ValidatorRegistry {\n return this.validatorRegistry;\n }\n\n /**\n * Update the form values.\n */\n updateForm(form: ParsedForm): void {\n this.form = form;\n }\n}\n\n// =============================================================================\n// Tool Creation Options\n// =============================================================================\n\n/**\n * Options for creating Markform AI SDK tools.\n */\nexport interface CreateMarkformToolsOptions {\n /**\n * Session store managing the form state.\n */\n sessionStore: MarkformSessionStore;\n\n /**\n * Whether to include the markform_get_markdown tool.\n * Defaults to true.\n */\n includeGetMarkdown?: boolean;\n}\n\n// =============================================================================\n// Tool Types (imported from toolTypes.ts)\n// =============================================================================\n\nimport type {\n ApplyToolResult,\n ExportToolResult,\n GetMarkdownToolResult,\n InspectToolResult,\n MarkformTool,\n MarkformToolSet,\n} from './toolTypes.js';\n\n// Re-export types for backwards compatibility\nexport type {\n ApplyToolResult,\n ExportToolResult,\n GetMarkdownToolResult,\n InspectToolResult,\n MarkformTool,\n MarkformToolSet,\n} from './toolTypes.js';\n\n// =============================================================================\n// Zod Schemas for Tool Inputs\n// =============================================================================\n\n/**\n * Input schema for markform_inspect tool (no parameters).\n */\nconst InspectInputSchema = z\n .object({})\n .describe('No input parameters required. Call this tool to inspect the current form state.');\n\n/**\n * Input schema for markform_apply tool.\n */\nconst ApplyInputSchema = z\n .object({\n patches: z\n .array(PatchSchema)\n .min(1)\n .max(20)\n .describe(\n 'Array of patches to apply to the form. Each patch sets or clears a field value. ' +\n 'Operations: set_string, set_number, set_string_list, set_single_select, set_multi_select, ' +\n 'set_checkboxes, set_url, set_url_list, set_date, set_year, set_table, clear_field, skip_field, abort_field. ' +\n 'Example: [{ \"op\": \"set_string\", \"fieldId\": \"name\", \"value\": \"Alice\" }]',\n ),\n })\n .describe('Apply patches to update form field values.');\n\n/**\n * Input schema for markform_export tool (no parameters).\n */\nconst ExportInputSchema = z\n .object({})\n .describe(\n 'No input parameters required. Call this tool to export the form schema and current values as JSON.',\n );\n\n/**\n * Input schema for markform_get_markdown tool (no parameters).\n */\nconst GetMarkdownInputSchema = z\n .object({})\n .describe(\n 'No input parameters required. Call this tool to get the canonical Markdown representation of the current form.',\n );\n\n// =============================================================================\n// Tool Factory\n// =============================================================================\n\n/**\n * Create Markform AI SDK tools for agent-driven form filling.\n *\n * Returns a toolset compatible with Vercel AI SDK's generateText and streamText.\n *\n * @param options - Tool creation options including session store\n * @returns MarkformToolSet containing all tools\n *\n * @example\n * ```typescript\n * import { parseForm } from 'markform';\n * import { createMarkformTools, MarkformSessionStore } from 'markform/ai-sdk';\n *\n * const form = parseForm(markdownContent);\n * const store = new MarkformSessionStore(form);\n * const tools = createMarkformTools({ sessionStore: store });\n *\n * // Use with AI SDK\n * const result = await generateText({\n * model: yourModel,\n * tools,\n * prompt: 'Fill out this form based on the user information...',\n * });\n * ```\n */\nexport function createMarkformTools(options: CreateMarkformToolsOptions): MarkformToolSet {\n const { sessionStore, includeGetMarkdown = true } = options;\n\n // markform_inspect - Get current form state with issues\n const markform_inspect: MarkformTool<Record<string, never>, InspectToolResult> = {\n description:\n 'Inspect the current form state. Returns structure summary, progress summary, validation issues, ' +\n 'and completion status. Use this to understand what fields need to be filled and what issues exist. ' +\n \"Issues are sorted by priority (1 = highest). Focus on 'required' severity issues first.\",\n inputSchema: InspectInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n const result = inspect(form);\n\n const requiredCount = result.issues.filter((i) => i.severity === 'required').length;\n const message = result.isComplete\n ? 'Form is complete. All required fields are filled.'\n : `Form has ${requiredCount} required issue(s) to resolve.`;\n\n return Promise.resolve({\n success: true,\n data: result,\n message,\n });\n },\n };\n\n // markform_apply - Apply patches to update form values\n const markform_apply: MarkformTool<{ patches: Patch[] }, ApplyToolResult> = {\n description:\n 'Apply patches to update form field values. Valid patches are applied even if some fail. ' +\n 'Single values are automatically coerced to arrays for list fields. ' +\n 'Returns applied patches, warnings for coerced values, and rejected patches separately. ' +\n 'Patch operations: set_string, set_number, set_string_list, set_single_select, set_multi_select, ' +\n 'set_checkboxes, set_url, set_url_list, set_date, set_year, set_table, clear_field, skip_field, abort_field.',\n inputSchema: ApplyInputSchema,\n execute: ({ patches }) => {\n const form = sessionStore.getForm();\n const result = applyPatches(form, patches);\n\n // Update the store with the modified form\n sessionStore.updateForm(form);\n\n const warningNote = result.warnings.length > 0 ? ` (${result.warnings.length} coerced)` : '';\n const remaining = result.issues.filter((i) => i.severity === 'required').length;\n\n const message =\n result.applyStatus === 'applied'\n ? `Applied ${patches.length} patch(es)${warningNote}. ${\n result.isComplete\n ? 'Form is now complete!'\n : `${remaining} required issue(s) remaining.`\n }`\n : result.applyStatus === 'partial'\n ? `Applied ${result.appliedPatches.length}/${patches.length} patches${warningNote}. ` +\n `${result.rejectedPatches.length} rejected.`\n : `All patches rejected. Check field IDs and value types.`;\n\n return Promise.resolve({\n success: result.applyStatus !== 'rejected',\n data: result,\n message,\n });\n },\n };\n\n // markform_export - Export schema and values as JSON\n const markform_export: MarkformTool<Record<string, never>, ExportToolResult> = {\n description:\n 'Export the form schema and current values as JSON. Use this to get a machine-readable ' +\n 'representation of the form structure and all field values. Useful for processing or analysis.',\n inputSchema: ExportInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n\n // Count answered fields\n const answeredCount = Object.values(form.responsesByFieldId).filter(\n (response) => response.state === 'answered',\n ).length;\n\n return Promise.resolve({\n success: true,\n data: {\n schema: form.schema,\n values: form.responsesByFieldId,\n },\n message: `Exported form with ${form.schema.groups.length} group(s) and ${answeredCount} answered field(s).`,\n });\n },\n };\n\n // Build the toolset\n const toolset: MarkformToolSet = {\n markform_inspect,\n markform_apply,\n markform_export,\n };\n\n // Optionally include markform_get_markdown\n if (includeGetMarkdown) {\n const markform_get_markdown: MarkformTool<Record<string, never>, GetMarkdownToolResult> = {\n description:\n 'Get the canonical Markdown representation of the current form. ' +\n 'Use this to see the complete form with all current values in Markform format. ' +\n 'The output is deterministic and round-trip safe.',\n inputSchema: GetMarkdownInputSchema,\n execute: () => {\n const form = sessionStore.getForm();\n const markdown = serializeForm(form);\n\n return Promise.resolve({\n success: true,\n data: {\n markdown,\n },\n message: `Generated Markdown (${markdown.length} characters).`,\n });\n },\n };\n\n toolset.markform_get_markdown = markform_get_markdown;\n }\n\n return toolset;\n}\n\n// =============================================================================\n// Convenience Exports\n// =============================================================================\n\nexport { PatchSchema } from '../engine/coreTypes.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,uBAAb,MAAkC;CAChC,AAAQ;CACR,AAAQ;CAER,YAAY,MAAkB,mBAAuC;AACnE,OAAK,OAAO;AACZ,OAAK,oBAAoB,qBAAqB,EAAE;;;;;CAMlD,UAAsB;AACpB,SAAO,KAAK;;;;;CAMd,uBAA0C;AACxC,SAAO,KAAK;;;;;CAMd,WAAW,MAAwB;AACjC,OAAK,OAAO;;;;;;AAsDhB,MAAM,qBAAqB,EACxB,OAAO,EAAE,CAAC,CACV,SAAS,kFAAkF;;;;AAK9F,MAAM,mBAAmB,EACtB,OAAO,EACN,SAAS,EACN,MAAM,YAAY,CAClB,IAAI,EAAE,CACN,IAAI,GAAG,CACP,SACC,2WAID,EACJ,CAAC,CACD,SAAS,6CAA6C;;;;AAKzD,MAAM,oBAAoB,EACvB,OAAO,EAAE,CAAC,CACV,SACC,qGACD;;;;AAKH,MAAM,yBAAyB,EAC5B,OAAO,EAAE,CAAC,CACV,SACC,iHACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BH,SAAgB,oBAAoB,SAAsD;CACxF,MAAM,EAAE,cAAc,qBAAqB,SAAS;CA2FpD,MAAM,UAA2B;EAC/B,kBAzF+E;GAC/E,aACE;GAGF,aAAa;GACb,eAAe;IAEb,MAAM,SAAS,QADF,aAAa,SAAS,CACP;IAE5B,MAAM,gBAAgB,OAAO,OAAO,QAAQ,MAAM,EAAE,aAAa,WAAW,CAAC;IAC7E,MAAM,UAAU,OAAO,aACnB,sDACA,YAAY,cAAc;AAE9B,WAAO,QAAQ,QAAQ;KACrB,SAAS;KACT,MAAM;KACN;KACD,CAAC;;GAEL;EAqEC,gBAlE0E;GAC1E,aACE;GAKF,aAAa;GACb,UAAU,EAAE,cAAc;IACxB,MAAM,OAAO,aAAa,SAAS;IACnC,MAAM,SAAS,aAAa,MAAM,QAAQ;AAG1C,iBAAa,WAAW,KAAK;IAE7B,MAAM,cAAc,OAAO,SAAS,SAAS,IAAI,KAAK,OAAO,SAAS,OAAO,aAAa;IAC1F,MAAM,YAAY,OAAO,OAAO,QAAQ,MAAM,EAAE,aAAa,WAAW,CAAC;IAEzE,MAAM,UACJ,OAAO,gBAAgB,YACnB,WAAW,QAAQ,OAAO,YAAY,YAAY,IAChD,OAAO,aACH,0BACA,GAAG,UAAU,mCAEnB,OAAO,gBAAgB,YACrB,WAAW,OAAO,eAAe,OAAO,GAAG,QAAQ,OAAO,UAAU,YAAY,IAC7E,OAAO,gBAAgB,OAAO,cACjC;AAER,WAAO,QAAQ,QAAQ;KACrB,SAAS,OAAO,gBAAgB;KAChC,MAAM;KACN;KACD,CAAC;;GAEL;EA+BC,iBA5B6E;GAC7E,aACE;GAEF,aAAa;GACb,eAAe;IACb,MAAM,OAAO,aAAa,SAAS;IAGnC,MAAM,gBAAgB,OAAO,OAAO,KAAK,mBAAmB,CAAC,QAC1D,aAAa,SAAS,UAAU,WAClC,CAAC;AAEF,WAAO,QAAQ,QAAQ;KACrB,SAAS;KACT,MAAM;MACJ,QAAQ,KAAK;MACb,QAAQ,KAAK;MACd;KACD,SAAS,sBAAsB,KAAK,OAAO,OAAO,OAAO,gBAAgB,cAAc;KACxF,CAAC;;GAEL;EAOA;AAGD,KAAI,mBAqBF,SAAQ,wBApBkF;EACxF,aACE;EAGF,aAAa;EACb,eAAe;GAEb,MAAM,WAAW,cADJ,aAAa,SAAS,CACC;AAEpC,UAAO,QAAQ,QAAQ;IACrB,SAAS;IACT,MAAM,EACJ,UACD;IACD,SAAS,uBAAuB,SAAS,OAAO;IACjD,CAAC;;EAEL;AAKH,QAAO"}
@@ -2,7 +2,7 @@
2
2
  import YAML from "yaml";
3
3
 
4
4
  //#region src/errors.ts
5
- const VERSION = "0.1.18";
5
+ const VERSION = "0.1.19";
6
6
  /**
7
7
  * Base error class for all markform errors.
8
8
  * Consumers can catch this to handle any markform error.
@@ -472,6 +472,327 @@ function deriveSchemaPath(basePath) {
472
472
  return base + SCHEMA_EXTENSION;
473
473
  }
474
474
 
475
+ //#endregion
476
+ //#region src/engine/parseHelpers.ts
477
+ /** Map checkbox marker to state value */
478
+ const CHECKBOX_MARKERS = {
479
+ "[ ]": "todo",
480
+ "[x]": "done",
481
+ "[X]": "done",
482
+ "[/]": "incomplete",
483
+ "[*]": "active",
484
+ "[-]": "na",
485
+ "[y]": "yes",
486
+ "[Y]": "yes",
487
+ "[n]": "no",
488
+ "[N]": "no"
489
+ };
490
+ const OPTION_TEXT_PATTERN = /^(\[[^\]]\])\s*(.*?)\s*$/;
491
+ /**
492
+ * Parse option text to extract marker and label.
493
+ * Text is like "[ ] Label" or "[x] Label".
494
+ */
495
+ function parseOptionText(text) {
496
+ const match = OPTION_TEXT_PATTERN.exec(text);
497
+ if (!match) return null;
498
+ return {
499
+ marker: match[1] ?? "",
500
+ label: (match[2] ?? "").trim()
501
+ };
502
+ }
503
+ /**
504
+ * Check if a node is a tag node with specific name.
505
+ * Works with raw AST nodes (not transformed Tags).
506
+ */
507
+ function isTagNode(node, name) {
508
+ if (typeof node !== "object" || node === null) return false;
509
+ if (node.type === "tag" && node.tag) return name === void 0 || node.tag === name;
510
+ return false;
511
+ }
512
+ /**
513
+ * Get string attribute value or undefined.
514
+ */
515
+ function getStringAttr(node, name) {
516
+ const value = node.attributes?.[name];
517
+ return typeof value === "string" ? value : void 0;
518
+ }
519
+ /**
520
+ * Get number attribute value or undefined.
521
+ */
522
+ function getNumberAttr(node, name) {
523
+ const value = node.attributes?.[name];
524
+ return typeof value === "number" ? value : void 0;
525
+ }
526
+ /**
527
+ * Get boolean attribute value or undefined.
528
+ */
529
+ function getBooleanAttr(node, name) {
530
+ const value = node.attributes?.[name];
531
+ return typeof value === "boolean" ? value : void 0;
532
+ }
533
+ /**
534
+ * Get validator references from validate attribute.
535
+ * Handles both single string and array formats.
536
+ */
537
+ function getValidateAttr(node) {
538
+ const value = node.attributes?.validate;
539
+ if (value === void 0 || value === null) return;
540
+ if (Array.isArray(value)) return value;
541
+ if (typeof value === "string") return [value];
542
+ if (typeof value === "object") return [value];
543
+ }
544
+ /**
545
+ * Get string array attribute value or undefined.
546
+ * Handles both single string (converts to array) and array formats.
547
+ */
548
+ function getStringArrayAttr(node, name) {
549
+ const value = node.attributes?.[name];
550
+ if (value === void 0 || value === null) return;
551
+ if (Array.isArray(value)) {
552
+ const strings = value.filter((v) => typeof v === "string");
553
+ return strings.length > 0 ? strings : void 0;
554
+ }
555
+ if (typeof value === "string") return [value];
556
+ }
557
+ /**
558
+ * Extract option items from node children (for option lists).
559
+ * Works with raw AST nodes. Collects text and ID from list items.
560
+ */
561
+ function extractOptionItems(node) {
562
+ const items = [];
563
+ /**
564
+ * Collect all text content from a node tree into a single string.
565
+ */
566
+ function collectText(n) {
567
+ let text = "";
568
+ if (n.type === "text" && typeof n.attributes?.content === "string") text += n.attributes.content;
569
+ if (n.type === "softbreak") text += "\n";
570
+ if (n.children && Array.isArray(n.children)) for (const c of n.children) text += collectText(c);
571
+ return text;
572
+ }
573
+ /**
574
+ * Traverse to find list items and extract their content.
575
+ */
576
+ function traverse(child) {
577
+ if (!child || typeof child !== "object") return;
578
+ if (child.type === "item") {
579
+ const text = collectText(child);
580
+ let id = null;
581
+ if (typeof child.attributes?.id === "string") id = child.attributes.id;
582
+ else if (child.children && Array.isArray(child.children) && child.children.length > 0 && typeof child.children[0]?.attributes?.id === "string") id = child.children[0].attributes.id;
583
+ if (text.trim()) items.push({
584
+ id,
585
+ text: text.trim()
586
+ });
587
+ return;
588
+ }
589
+ if (child.children && Array.isArray(child.children)) for (const c of child.children) traverse(c);
590
+ }
591
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) traverse(child);
592
+ return items;
593
+ }
594
+ /**
595
+ * Extract fence value from node children.
596
+ * Looks for ```value code blocks.
597
+ */
598
+ function extractFenceValue(node) {
599
+ function traverse(child) {
600
+ if (!child || typeof child !== "object") return null;
601
+ if (child.type === "fence") {
602
+ if (child.attributes?.language === "value") return typeof child.attributes?.content === "string" ? child.attributes.content : null;
603
+ }
604
+ if (child.children && Array.isArray(child.children)) for (const c of child.children) {
605
+ const result = traverse(c);
606
+ if (result !== null) return result;
607
+ }
608
+ return null;
609
+ }
610
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) {
611
+ const result = traverse(child);
612
+ if (result !== null) return result;
613
+ }
614
+ return null;
615
+ }
616
+ /**
617
+ * Extract table content from node children.
618
+ * Handles both raw text and Markdoc-parsed table nodes.
619
+ * Reconstructs markdown table format from the AST.
620
+ */
621
+ function extractTableContent(node) {
622
+ const lines = [];
623
+ function extractTextFromNode(n) {
624
+ if (!n || typeof n !== "object") return "";
625
+ if (n.type === "text" && typeof n.attributes?.content === "string") return n.attributes.content;
626
+ if (n.children && Array.isArray(n.children)) return n.children.map(extractTextFromNode).join("");
627
+ return "";
628
+ }
629
+ function extractTableRow(trNode) {
630
+ if (!trNode.children || !Array.isArray(trNode.children)) return "";
631
+ return `| ${trNode.children.filter((c) => c.type === "th" || c.type === "td").map((c) => extractTextFromNode(c).trim()).join(" | ")} |`;
632
+ }
633
+ function processNode(child) {
634
+ if (!child || typeof child !== "object") return;
635
+ if (child.type === "paragraph" || child.type === "inline") {
636
+ const text = extractTextFromNode(child).trim();
637
+ if (text) lines.push(text);
638
+ return;
639
+ }
640
+ if (child.type === "text" && typeof child.attributes?.content === "string") {
641
+ const text = child.attributes.content.trim();
642
+ if (text) lines.push(text);
643
+ return;
644
+ }
645
+ if (child.type === "table") {
646
+ const thead = child.children?.find((c) => c.type === "thead");
647
+ if (thead?.children) for (const tr of thead.children.filter((c) => c.type === "tr")) lines.push(extractTableRow(tr));
648
+ if (thead?.children?.length) {
649
+ const firstTr = thead.children.find((c) => c.type === "tr");
650
+ if (firstTr?.children) {
651
+ const colCount = firstTr.children.filter((c) => c.type === "th" || c.type === "td").length;
652
+ const separatorCells = Array(colCount).fill("----");
653
+ lines.push(`| ${separatorCells.join(" | ")} |`);
654
+ }
655
+ }
656
+ const tbody = child.children?.find((c) => c.type === "tbody");
657
+ if (tbody?.children) for (const tr of tbody.children.filter((c) => c.type === "tr")) lines.push(extractTableRow(tr));
658
+ return;
659
+ }
660
+ if (child.children && Array.isArray(child.children)) for (const c of child.children) processNode(c);
661
+ }
662
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) processNode(child);
663
+ return lines.join("\n").trim() || null;
664
+ }
665
+
666
+ //#endregion
667
+ //#region src/engine/parseSentinels.ts
668
+ /** Sentinel values for text fields */
669
+ const SENTINEL_SKIP = "%SKIP%";
670
+ const SENTINEL_ABORT = "%ABORT%";
671
+ /**
672
+ * Detect if a value contains a sentinel pattern (%SKIP% or %ABORT%).
673
+ *
674
+ * This is the shared low-level detection function used for:
675
+ * - Form parsing validation (strict format)
676
+ * - Patch value validation (reject embedded sentinels)
677
+ * - Table cell parsing (convert to skipped/aborted state)
678
+ *
679
+ * Supports multiple formats that LLMs might generate:
680
+ * - `%SKIP%` or `%skip%` (case-insensitive)
681
+ * - `%SKIP% (reason)` - canonical format
682
+ * - `%SKIP:reason%` or `%SKIP(reason)%` - compact formats
683
+ *
684
+ * @param value - The value to check (returns null for non-strings)
685
+ * @returns Detected sentinel type and optional reason, or null if no sentinel
686
+ */
687
+ function detectSentinel(value) {
688
+ if (value == null || typeof value !== "string") return null;
689
+ const trimmed = value.trim();
690
+ const compactSkipMatch = /^%SKIP(?:[:(](.*?))?[)]?%$/i.exec(trimmed);
691
+ if (compactSkipMatch) {
692
+ const reason = compactSkipMatch[1]?.trim();
693
+ return {
694
+ type: "skip",
695
+ ...reason && { reason }
696
+ };
697
+ }
698
+ const compactAbortMatch = /^%ABORT(?:[:(](.*?))?[)]?%$/i.exec(trimmed);
699
+ if (compactAbortMatch) {
700
+ const reason = compactAbortMatch[1]?.trim();
701
+ return {
702
+ type: "abort",
703
+ ...reason && { reason }
704
+ };
705
+ }
706
+ const upper = trimmed.toUpperCase();
707
+ if (upper.startsWith("%SKIP%")) {
708
+ const rest = trimmed.slice(6).trim();
709
+ if (rest === "") return { type: "skip" };
710
+ const reasonMatch = /^\((.+)\)$/s.exec(rest);
711
+ if (reasonMatch?.[1]) return {
712
+ type: "skip",
713
+ reason: reasonMatch[1].trim()
714
+ };
715
+ return { type: "skip" };
716
+ }
717
+ if (upper.startsWith("%ABORT%")) {
718
+ const rest = trimmed.slice(7).trim();
719
+ if (rest === "") return { type: "abort" };
720
+ const reasonMatch = /^\((.+)\)$/s.exec(rest);
721
+ if (reasonMatch?.[1]) return {
722
+ type: "abort",
723
+ reason: reasonMatch[1].trim()
724
+ };
725
+ return { type: "abort" };
726
+ }
727
+ return null;
728
+ }
729
+ /**
730
+ * Parse a sentinel value with optional parenthesized reason (strict format).
731
+ *
732
+ * This is the strict parser used during form parsing, where we want to
733
+ * validate the exact format. For patch validation, use detectSentinel().
734
+ *
735
+ * Formats: `%SKIP%`, `%SKIP% (reason text)`, `%ABORT%`, `%ABORT% (reason text)`
736
+ * Returns null if the content is not a valid sentinel format.
737
+ */
738
+ function parseSentinel(content) {
739
+ if (!content) return null;
740
+ const trimmed = content.trim();
741
+ const reasonPattern = /^\((.+)\)$/s;
742
+ if (trimmed.startsWith(SENTINEL_SKIP)) {
743
+ const rest = trimmed.slice(6).trim();
744
+ if (rest === "") return { type: "skip" };
745
+ const match = reasonPattern.exec(rest);
746
+ if (match?.[1]) return {
747
+ type: "skip",
748
+ reason: match[1].trim()
749
+ };
750
+ return null;
751
+ }
752
+ if (trimmed.startsWith(SENTINEL_ABORT)) {
753
+ const rest = trimmed.slice(7).trim();
754
+ if (rest === "") return { type: "abort" };
755
+ const match = reasonPattern.exec(rest);
756
+ if (match?.[1]) return {
757
+ type: "abort",
758
+ reason: match[1].trim()
759
+ };
760
+ return null;
761
+ }
762
+ return null;
763
+ }
764
+ /**
765
+ * Check for sentinel values in fence content and validate against state attribute.
766
+ * Handles the common pattern of checking for %SKIP% and %ABORT% sentinels in field values.
767
+ *
768
+ * @param node - The field node to check
769
+ * @param fieldId - The field ID for error messages
770
+ * @param required - Whether the field is required (skip not allowed on required fields)
771
+ * @returns A FieldResponse if a sentinel is found, null otherwise
772
+ */
773
+ function tryParseSentinelResponse(node, fieldId, required) {
774
+ const fenceContent = extractFenceValue(node);
775
+ const stateAttr = getStringAttr(node, "state");
776
+ const sentinel = parseSentinel(fenceContent);
777
+ if (!sentinel) return null;
778
+ if (sentinel.type === "skip") {
779
+ if (stateAttr !== void 0 && stateAttr !== "skipped") throw new MarkformParseError(`Field '${fieldId}' has conflicting state='${stateAttr}' with %SKIP% sentinel`);
780
+ if (required) throw new MarkformParseError(`Field '${fieldId}' is required but has %SKIP% sentinel. Cannot skip required fields.`);
781
+ return {
782
+ state: "skipped",
783
+ ...sentinel.reason && { reason: sentinel.reason }
784
+ };
785
+ }
786
+ if (sentinel.type === "abort") {
787
+ if (stateAttr !== void 0 && stateAttr !== "aborted") throw new MarkformParseError(`Field '${fieldId}' has conflicting state='${stateAttr}' with %ABORT% sentinel`);
788
+ return {
789
+ state: "aborted",
790
+ ...sentinel.reason && { reason: sentinel.reason }
791
+ };
792
+ }
793
+ return null;
794
+ }
795
+
475
796
  //#endregion
476
797
  //#region src/engine/preprocess.ts
477
798
  const MARKFORM_TAGS = new Set([
@@ -508,10 +829,10 @@ function isValidFormTag(content) {
508
829
  return trimmed.includes("=") && /\bid\s*=/.test(trimmed);
509
830
  }
510
831
  /** Parser state for tracking code blocks */
511
- var State = /* @__PURE__ */ function(State$1) {
512
- State$1[State$1["NORMAL"] = 0] = "NORMAL";
513
- State$1[State$1["FENCED_CODE"] = 1] = "FENCED_CODE";
514
- return State$1;
832
+ var State = /* @__PURE__ */ function(State) {
833
+ State[State["NORMAL"] = 0] = "NORMAL";
834
+ State[State["FENCED_CODE"] = 1] = "FENCED_CODE";
835
+ return State;
515
836
  }(State || {});
516
837
  /**
517
838
  * Check if position is at the start of a line (or at position 0).
@@ -992,6 +1313,33 @@ function friendlyUrlAbbrev(url, maxPathChars = 12) {
992
1313
  function formatUrlAsMarkdownLink(url) {
993
1314
  return `[${friendlyUrlAbbrev(url)}](${url})`;
994
1315
  }
1316
+ /**
1317
+ * Format bare URLs in text as HTML links with abbreviated display text.
1318
+ * Also handles markdown-style links [text](url) for consistency.
1319
+ *
1320
+ * Processing order:
1321
+ * 1. Escape all HTML to prevent XSS
1322
+ * 2. Convert markdown links [text](url) to <a> tags
1323
+ * 3. Convert bare URLs (not already in links) to <a> tags with abbreviated display
1324
+ *
1325
+ * @param text - The raw text containing URLs (will be HTML-escaped)
1326
+ * @param escapeHtml - Function to escape HTML entities
1327
+ * @returns HTML-safe text with URLs converted to <a> tags
1328
+ */
1329
+ function formatBareUrlsAsHtmlLinks(text, escapeHtml) {
1330
+ let result = escapeHtml(text);
1331
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
1332
+ const cleanUrl = url.replace(/&amp;/g, "&");
1333
+ return `<a href="${escapeHtml(cleanUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(cleanUrl)}">${linkText}</a>`;
1334
+ });
1335
+ result = result.replace(/(?<!href="|data-url="|">)(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
1336
+ const cleanUrl = url.replace(/&amp;/g, "&");
1337
+ const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
1338
+ const display = friendlyUrlAbbrev(fullUrl);
1339
+ return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
1340
+ });
1341
+ return result;
1342
+ }
995
1343
 
996
1344
  //#endregion
997
1345
  //#region src/engine/serialize.ts
@@ -3549,6 +3897,16 @@ function typeMismatchError(index, op, field) {
3549
3897
  };
3550
3898
  }
3551
3899
  /**
3900
+ * Create an error for embedded sentinel in patch value.
3901
+ */
3902
+ function embeddedSentinelError(index, fieldId, sentinelType) {
3903
+ return {
3904
+ patchIndex: index,
3905
+ message: `Value contains ${sentinelType === "skip" ? "%SKIP%" : "%ABORT%"} sentinel for field "${fieldId}". Use ${sentinelType === "skip" ? "skip_field" : "abort_field"} operation instead of embedding sentinel in value.`,
3906
+ fieldId
3907
+ };
3908
+ }
3909
+ /**
3552
3910
  * Validate a single patch against the form schema.
3553
3911
  */
3554
3912
  function validatePatch(form, patch, index) {
@@ -3573,6 +3931,10 @@ function validatePatch(form, patch, index) {
3573
3931
  };
3574
3932
  const expectedKind = PATCH_OP_TO_FIELD_KIND[patch.op];
3575
3933
  if (expectedKind && field.kind !== expectedKind) return typeMismatchError(index, patch.op, field);
3934
+ if (patch.op === "set_string" || patch.op === "set_url" || patch.op === "set_date") {
3935
+ const sentinel = detectSentinel(patch.value);
3936
+ if (sentinel) return embeddedSentinelError(index, field.id, sentinel.type);
3937
+ }
3576
3938
  if (patch.op === "set_string_list" && field.kind === "string_list") {
3577
3939
  if (!Array.isArray(patch.value)) return {
3578
3940
  patchIndex: index,
@@ -3580,6 +3942,10 @@ function validatePatch(form, patch, index) {
3580
3942
  fieldId: field.id,
3581
3943
  fieldKind: field.kind
3582
3944
  };
3945
+ for (const item of patch.value) {
3946
+ const sentinel = detectSentinel(item);
3947
+ if (sentinel) return embeddedSentinelError(index, field.id, sentinel.type);
3948
+ }
3583
3949
  } else if (patch.op === "set_single_select" && field.kind === "single_select") {
3584
3950
  if (patch.value !== null) {
3585
3951
  if (!new Set(field.options.map((o) => o.id)).has(patch.value)) return {
@@ -3618,6 +3984,10 @@ function validatePatch(form, patch, index) {
3618
3984
  fieldId: field.id,
3619
3985
  fieldKind: field.kind
3620
3986
  };
3987
+ for (const item of patch.value) {
3988
+ const sentinel = detectSentinel(item);
3989
+ if (sentinel) return embeddedSentinelError(index, field.id, sentinel.type);
3990
+ }
3621
3991
  } else if (patch.op === "set_table" && field.kind === "table") {
3622
3992
  const columnIds = field.columns.map((c) => c.id);
3623
3993
  if (!Array.isArray(patch.value)) return {
@@ -3702,23 +4072,18 @@ function setMultiSelectValue(responses, fieldId, selected) {
3702
4072
  */
3703
4073
  function patchValueToCell(value) {
3704
4074
  if (value === null || value === void 0) return { state: "skipped" };
3705
- if (typeof value === "string") {
3706
- const trimmed = value.trim();
3707
- const skipMatch = /^%SKIP(?:[:(](.*))?[)]?%$/i.exec(trimmed);
3708
- if (skipMatch) return {
3709
- state: "skipped",
3710
- reason: skipMatch[1]
3711
- };
3712
- const abortMatch = /^%ABORT(?:[:(](.*))?[)]?%$/i.exec(trimmed);
3713
- if (abortMatch) return {
3714
- state: "aborted",
3715
- reason: abortMatch[1]
3716
- };
3717
- return {
3718
- state: "answered",
3719
- value: trimmed
3720
- };
3721
- }
4075
+ const sentinel = detectSentinel(value);
4076
+ if (sentinel) return sentinel.type === "skip" ? {
4077
+ state: "skipped",
4078
+ ...sentinel.reason && { reason: sentinel.reason }
4079
+ } : {
4080
+ state: "aborted",
4081
+ ...sentinel.reason && { reason: sentinel.reason }
4082
+ };
4083
+ if (typeof value === "string") return {
4084
+ state: "answered",
4085
+ value: value.trim()
4086
+ };
3722
4087
  return {
3723
4088
  state: "answered",
3724
4089
  value
@@ -3859,15 +4224,15 @@ function applyPatches(form, patches) {
3859
4224
  }
3860
4225
  }
3861
4226
  if (validPatches.length === 0 && errors.length > 0) {
3862
- const issues$1 = convertToInspectIssues(form);
3863
- const summaries$1 = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues$1);
4227
+ const issues = convertToInspectIssues(form);
4228
+ const summaries = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues);
3864
4229
  return {
3865
4230
  applyStatus: "rejected",
3866
- structureSummary: summaries$1.structureSummary,
3867
- progressSummary: summaries$1.progressSummary,
3868
- issues: issues$1,
3869
- isComplete: summaries$1.isComplete,
3870
- formState: summaries$1.formState,
4231
+ structureSummary: summaries.structureSummary,
4232
+ progressSummary: summaries.progressSummary,
4233
+ issues,
4234
+ isComplete: summaries.isComplete,
4235
+ formState: summaries.formState,
3871
4236
  appliedPatches: [],
3872
4237
  rejectedPatches: errors,
3873
4238
  warnings: []
@@ -3894,5 +4259,5 @@ function applyPatches(form, patches) {
3894
4259
  }
3895
4260
 
3896
4261
  //#endregion
3897
- export { isAbortError as $, DEFAULT_ROLE_INSTRUCTIONS as A, WEB_SEARCH_CONFIG as B, DEFAULT_MAX_STEPS_PER_TURN as C, DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN as D, DEFAULT_PRIORITY as E, deriveReportPath as F, MarkformAbortError as G, getWebSearchConfig as H, deriveSchemaPath as I, MarkformLlmError as J, MarkformConfigError as K, detectFileType as L, REPORT_EXTENSION as M, USER_ROLE as N, DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN as O, deriveExportPath as P, ParseError as Q, parseRolesFlag as R, DEFAULT_MAX_PATCHES_PER_TURN as S, DEFAULT_PORT as T, hasWebSearchSupport as U, formatSuggestedLlms as V, parseModelIdForDisplay as W, MarkformPatchError as X, MarkformParseError as Y, MarkformValidationError as Z, validateSyntaxConsistency as _, validate as a, isRetryableError as at, DEFAULT_MAX_ISSUES_PER_TURN as b, computeProgressSummary as c, serializeForm as d, isConfigError as et, serializeRawMarkdown as f, preprocessCommentSyntax as g, detectSyntaxStyle as h, inspect as i, isPatchError as it, MAX_FORMS_IN_MENU as j, DEFAULT_ROLES as k, computeStructureSummary as l, friendlyUrlAbbrev as m, getAllFields as n, isMarkformError as nt, computeAllSummaries as o, isValidationError as ot, serializeReport as p, MarkformError as q, getFieldsForRoles as r, isParseError as rt, computeFormState as s, applyPatches as t, isLlmError as tt, isFormComplete as u, AGENT_ROLE as v, DEFAULT_MAX_TURNS as w, DEFAULT_MAX_PARALLEL_AGENTS as x, DEFAULT_FORMS_DIR as y, SUGGESTED_LLMS as z };
3898
- //# sourceMappingURL=apply-BYgtU64w.mjs.map
4262
+ export { WEB_SEARCH_CONFIG as $, parseOptionText as A, DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN as B, extractTableContent as C, getStringAttr as D, getStringArrayAttr as E, DEFAULT_MAX_PATCHES_PER_TURN as F, REPORT_EXTENSION as G, DEFAULT_ROLES as H, DEFAULT_MAX_STEPS_PER_TURN as I, deriveReportPath as J, USER_ROLE as K, DEFAULT_MAX_TURNS as L, DEFAULT_FORMS_DIR as M, DEFAULT_MAX_ISSUES_PER_TURN as N, getValidateAttr as O, DEFAULT_MAX_PARALLEL_AGENTS as P, SUGGESTED_LLMS as Q, DEFAULT_PORT as R, extractOptionItems as S, getNumberAttr as T, DEFAULT_ROLE_INSTRUCTIONS as U, DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN as V, MAX_FORMS_IN_MENU as W, detectFileType as X, deriveSchemaPath as Y, parseRolesFlag as Z, preprocessCommentSyntax as _, isPatchError as _t, validate as a, MarkformConfigError as at, CHECKBOX_MARKERS as b, computeProgressSummary as c, MarkformParseError as ct, serializeForm as d, ParseError as dt, formatSuggestedLlms as et, serializeRawMarkdown as f, isAbortError as ft, detectSyntaxStyle as g, isParseError as gt, friendlyUrlAbbrev as h, isMarkformError as ht, inspect as i, MarkformAbortError as it, AGENT_ROLE as j, isTagNode as k, computeStructureSummary as l, MarkformPatchError as lt, formatBareUrlsAsHtmlLinks as m, isLlmError as mt, getAllFields as n, hasWebSearchSupport as nt, computeAllSummaries as o, MarkformError as ot, serializeReport as p, isConfigError as pt, deriveExportPath as q, getFieldsForRoles as r, parseModelIdForDisplay as rt, computeFormState as s, MarkformLlmError as st, applyPatches as t, getWebSearchConfig as tt, isFormComplete as u, MarkformValidationError as ut, validateSyntaxConsistency as v, isRetryableError as vt, getBooleanAttr as w, extractFenceValue as x, tryParseSentinelResponse as y, isValidationError as yt, DEFAULT_PRIORITY as z };
4263
+ //# sourceMappingURL=apply-Dalpt-D6.mjs.map