markform 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +484 -45
  3. package/SPEC.md +2779 -0
  4. package/dist/ai-sdk.d.mts +2 -2
  5. package/dist/ai-sdk.mjs +5 -3
  6. package/dist/{apply-C0vjijlP.mjs → apply-BfAGTHMh.mjs} +1044 -593
  7. package/dist/bin.mjs +6 -3
  8. package/dist/cli-B3NVm6zL.mjs +3904 -0
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes-T7dAuewt.d.mts → coreTypes-BXhhz9Iq.d.mts} +2795 -685
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +196 -18
  13. package/dist/index.mjs +5 -3
  14. package/dist/session-Bqnwi9wp.mjs +110 -0
  15. package/dist/session-DdAtY2Ni.mjs +4 -0
  16. package/dist/shared-D7gf27Tr.mjs +3 -0
  17. package/dist/shared-N_s1M-_K.mjs +176 -0
  18. package/dist/src-BXRkGFpG.mjs +7587 -0
  19. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
  20. package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
  22. package/examples/movie-research/movie-research-basic.form.md +164 -0
  23. package/examples/movie-research/movie-research-deep.form.md +486 -0
  24. package/examples/movie-research/movie-research-minimal.form.md +73 -0
  25. package/examples/simple/simple-mock-filled.form.md +52 -12
  26. package/examples/simple/simple-skipped-filled.form.md +170 -0
  27. package/examples/simple/simple-with-skips.session.yaml +189 -0
  28. package/examples/simple/simple.form.md +34 -12
  29. package/examples/simple/simple.session.yaml +80 -37
  30. package/examples/startup-deep-research/startup-deep-research.form.md +456 -0
  31. package/examples/startup-research/startup-research-mock-filled.form.md +307 -0
  32. package/examples/startup-research/startup-research.form.md +211 -0
  33. package/package.json +11 -5
  34. package/dist/cli-9fvFySww.mjs +0 -2564
  35. package/dist/src-DBD3Dt4f.mjs +0 -1785
  36. package/examples/political-research/political-research.form.md +0 -233
  37. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
@@ -1,385 +1,114 @@
1
- import { z } from "zod";
1
+ import { resolve } from "node:path";
2
2
 
3
- //#region src/engine/coreTypes.ts
3
+ //#region src/llms.ts
4
4
  /**
5
- * Core types and Zod schemas for Markform.
5
+ * LLM-related settings and configuration.
6
6
  *
7
- * This module defines all TypeScript types and their corresponding Zod schemas
8
- * for forms, fields, values, validation, patches, and session transcripts.
9
- */
10
- const IdSchema = z.string().min(1);
11
- const OptionIdSchema = z.string().min(1);
12
- const ValidatorRefSchema = z.union([z.string(), z.object({ id: z.string() }).passthrough()]);
13
- const MultiCheckboxStateSchema = z.enum([
14
- "todo",
15
- "done",
16
- "incomplete",
17
- "active",
18
- "na"
19
- ]);
20
- const SimpleCheckboxStateSchema = z.enum(["todo", "done"]);
21
- const ExplicitCheckboxValueSchema = z.enum([
22
- "unfilled",
23
- "yes",
24
- "no"
25
- ]);
26
- const CheckboxValueSchema = z.union([MultiCheckboxStateSchema, ExplicitCheckboxValueSchema]);
27
- const CheckboxModeSchema = z.enum([
28
- "multi",
29
- "simple",
30
- "explicit"
31
- ]);
32
- const FillModeSchema = z.enum(["continue", "overwrite"]);
33
- const ApprovalModeSchema = z.enum(["none", "blocking"]);
34
- const FieldKindSchema = z.enum([
35
- "string",
36
- "number",
37
- "string_list",
38
- "checkboxes",
39
- "single_select",
40
- "multi_select"
41
- ]);
42
- const FieldPriorityLevelSchema = z.enum([
43
- "high",
44
- "medium",
45
- "low"
46
- ]);
47
- const OptionSchema = z.object({
48
- id: IdSchema,
49
- label: z.string()
50
- });
51
- const FieldBaseSchemaPartial = {
52
- id: IdSchema,
53
- label: z.string(),
54
- required: z.boolean(),
55
- priority: FieldPriorityLevelSchema,
56
- role: z.string(),
57
- validate: z.array(ValidatorRefSchema).optional()
7
+ * This module centralizes LLM provider and model configuration,
8
+ * including suggested models and web search support.
9
+ */
10
+ /**
11
+ * Suggested LLM models for the fill command, organized by provider.
12
+ * These are shown in help/error messages and model selection prompts.
13
+ */
14
+ const SUGGESTED_LLMS = {
15
+ openai: [
16
+ "gpt-5-mini",
17
+ "gpt-5-nano",
18
+ "gpt-5.2",
19
+ "gpt-5.2-pro",
20
+ "o3",
21
+ "o3-mini"
22
+ ],
23
+ anthropic: [
24
+ "claude-opus-4-5",
25
+ "claude-sonnet-4-5",
26
+ "claude-haiku-4-5"
27
+ ],
28
+ google: [
29
+ "gemini-3-flash",
30
+ "gemini-3-pro-preview",
31
+ "gemini-2.5-flash"
32
+ ],
33
+ xai: ["grok-4", "grok-4.1-fast"],
34
+ deepseek: ["deepseek-chat", "deepseek-reasoner"]
58
35
  };
59
- const StringFieldSchema = z.object({
60
- ...FieldBaseSchemaPartial,
61
- kind: z.literal("string"),
62
- multiline: z.boolean().optional(),
63
- pattern: z.string().optional(),
64
- minLength: z.number().int().nonnegative().optional(),
65
- maxLength: z.number().int().nonnegative().optional()
66
- });
67
- const NumberFieldSchema = z.object({
68
- ...FieldBaseSchemaPartial,
69
- kind: z.literal("number"),
70
- min: z.number().optional(),
71
- max: z.number().optional(),
72
- integer: z.boolean().optional()
73
- });
74
- const StringListFieldSchema = z.object({
75
- ...FieldBaseSchemaPartial,
76
- kind: z.literal("string_list"),
77
- minItems: z.number().int().nonnegative().optional(),
78
- maxItems: z.number().int().nonnegative().optional(),
79
- itemMinLength: z.number().int().nonnegative().optional(),
80
- itemMaxLength: z.number().int().nonnegative().optional(),
81
- uniqueItems: z.boolean().optional()
82
- });
83
- const CheckboxesFieldSchema = z.object({
84
- ...FieldBaseSchemaPartial,
85
- kind: z.literal("checkboxes"),
86
- checkboxMode: CheckboxModeSchema,
87
- minDone: z.number().int().optional(),
88
- options: z.array(OptionSchema),
89
- approvalMode: ApprovalModeSchema
90
- });
91
- const SingleSelectFieldSchema = z.object({
92
- ...FieldBaseSchemaPartial,
93
- kind: z.literal("single_select"),
94
- options: z.array(OptionSchema)
95
- });
96
- const MultiSelectFieldSchema = z.object({
97
- ...FieldBaseSchemaPartial,
98
- kind: z.literal("multi_select"),
99
- options: z.array(OptionSchema),
100
- minSelections: z.number().int().nonnegative().optional(),
101
- maxSelections: z.number().int().nonnegative().optional()
102
- });
103
- const FieldSchema = z.discriminatedUnion("kind", [
104
- StringFieldSchema,
105
- NumberFieldSchema,
106
- StringListFieldSchema,
107
- CheckboxesFieldSchema,
108
- SingleSelectFieldSchema,
109
- MultiSelectFieldSchema
110
- ]);
111
- const FieldGroupSchema = z.object({
112
- kind: z.literal("field_group"),
113
- id: IdSchema,
114
- title: z.string().optional(),
115
- validate: z.array(ValidatorRefSchema).optional(),
116
- children: z.array(FieldSchema)
117
- });
118
- const FormSchemaSchema = z.object({
119
- id: IdSchema,
120
- title: z.string().optional(),
121
- groups: z.array(FieldGroupSchema)
122
- });
123
- const StringValueSchema = z.object({
124
- kind: z.literal("string"),
125
- value: z.string().nullable()
126
- });
127
- const NumberValueSchema = z.object({
128
- kind: z.literal("number"),
129
- value: z.number().nullable()
130
- });
131
- const StringListValueSchema = z.object({
132
- kind: z.literal("string_list"),
133
- items: z.array(z.string())
134
- });
135
- const CheckboxesValueSchema = z.object({
136
- kind: z.literal("checkboxes"),
137
- values: z.record(OptionIdSchema, CheckboxValueSchema)
138
- });
139
- const SingleSelectValueSchema = z.object({
140
- kind: z.literal("single_select"),
141
- selected: OptionIdSchema.nullable()
142
- });
143
- const MultiSelectValueSchema = z.object({
144
- kind: z.literal("multi_select"),
145
- selected: z.array(OptionIdSchema)
146
- });
147
- const FieldValueSchema = z.discriminatedUnion("kind", [
148
- StringValueSchema,
149
- NumberValueSchema,
150
- StringListValueSchema,
151
- CheckboxesValueSchema,
152
- SingleSelectValueSchema,
153
- MultiSelectValueSchema
154
- ]);
155
- const DocumentationTagSchema = z.enum([
156
- "description",
157
- "instructions",
158
- "documentation"
159
- ]);
160
- const DocumentationBlockSchema = z.object({
161
- tag: DocumentationTagSchema,
162
- ref: z.string(),
163
- bodyMarkdown: z.string()
164
- });
165
- const FormMetadataSchema = z.object({
166
- markformVersion: z.string(),
167
- roles: z.array(z.string()).min(1),
168
- roleInstructions: z.record(z.string(), z.string())
169
- });
170
- const SeveritySchema = z.enum([
171
- "error",
172
- "warning",
173
- "info"
174
- ]);
175
- const SourcePositionSchema = z.object({
176
- line: z.number().int().positive(),
177
- col: z.number().int().positive()
178
- });
179
- const SourceRangeSchema = z.object({
180
- start: SourcePositionSchema,
181
- end: SourcePositionSchema
182
- });
183
- const ValidationIssueSchema = z.object({
184
- severity: SeveritySchema,
185
- message: z.string(),
186
- code: z.string().optional(),
187
- ref: IdSchema.optional(),
188
- path: z.string().optional(),
189
- range: SourceRangeSchema.optional(),
190
- validatorId: z.string().optional(),
191
- source: z.enum([
192
- "builtin",
193
- "code",
194
- "llm"
195
- ])
196
- });
197
- const IssueReasonSchema = z.enum([
198
- "validation_error",
199
- "required_missing",
200
- "checkbox_incomplete",
201
- "min_items_not_met",
202
- "optional_empty"
203
- ]);
204
- const IssueScopeSchema = z.enum([
205
- "form",
206
- "group",
207
- "field",
208
- "option"
209
- ]);
210
- const InspectIssueSchema = z.object({
211
- ref: z.union([IdSchema, z.string()]),
212
- scope: IssueScopeSchema,
213
- reason: IssueReasonSchema,
214
- message: z.string(),
215
- severity: z.enum(["required", "recommended"]),
216
- priority: z.number().int().positive(),
217
- blockedBy: IdSchema.optional()
218
- });
219
- const ProgressStateSchema = z.enum([
220
- "empty",
221
- "incomplete",
222
- "invalid",
223
- "complete"
224
- ]);
225
- const CheckboxProgressCountsSchema = z.object({
226
- total: z.number().int().nonnegative(),
227
- todo: z.number().int().nonnegative(),
228
- done: z.number().int().nonnegative(),
229
- incomplete: z.number().int().nonnegative(),
230
- active: z.number().int().nonnegative(),
231
- na: z.number().int().nonnegative(),
232
- unfilled: z.number().int().nonnegative(),
233
- yes: z.number().int().nonnegative(),
234
- no: z.number().int().nonnegative()
235
- });
236
- const FieldProgressSchema = z.object({
237
- kind: FieldKindSchema,
238
- required: z.boolean(),
239
- submitted: z.boolean(),
240
- state: ProgressStateSchema,
241
- valid: z.boolean(),
242
- issueCount: z.number().int().nonnegative(),
243
- checkboxProgress: CheckboxProgressCountsSchema.optional()
244
- });
245
- const ProgressCountsSchema = z.object({
246
- totalFields: z.number().int().nonnegative(),
247
- requiredFields: z.number().int().nonnegative(),
248
- submittedFields: z.number().int().nonnegative(),
249
- completeFields: z.number().int().nonnegative(),
250
- incompleteFields: z.number().int().nonnegative(),
251
- invalidFields: z.number().int().nonnegative(),
252
- emptyRequiredFields: z.number().int().nonnegative(),
253
- emptyOptionalFields: z.number().int().nonnegative()
254
- });
255
- const ProgressSummarySchema = z.object({
256
- counts: ProgressCountsSchema,
257
- fields: z.record(IdSchema, FieldProgressSchema)
258
- });
259
- const StructureSummarySchema = z.object({
260
- groupCount: z.number().int().nonnegative(),
261
- fieldCount: z.number().int().nonnegative(),
262
- optionCount: z.number().int().nonnegative(),
263
- fieldCountByKind: z.record(FieldKindSchema, z.number().int().nonnegative()),
264
- groupsById: z.record(IdSchema, z.literal("field_group")),
265
- fieldsById: z.record(IdSchema, FieldKindSchema),
266
- optionsById: z.record(z.string(), z.object({
267
- parentFieldId: IdSchema,
268
- parentFieldKind: FieldKindSchema
269
- }))
270
- });
271
- const InspectResultSchema = z.object({
272
- structureSummary: StructureSummarySchema,
273
- progressSummary: ProgressSummarySchema,
274
- issues: z.array(InspectIssueSchema),
275
- isComplete: z.boolean(),
276
- formState: ProgressStateSchema
277
- });
278
- const ApplyResultSchema = z.object({
279
- applyStatus: z.enum(["applied", "rejected"]),
280
- structureSummary: StructureSummarySchema,
281
- progressSummary: ProgressSummarySchema,
282
- issues: z.array(InspectIssueSchema),
283
- isComplete: z.boolean(),
284
- formState: ProgressStateSchema
285
- });
286
- const SetStringPatchSchema = z.object({
287
- op: z.literal("set_string"),
288
- fieldId: IdSchema,
289
- value: z.string().nullable()
290
- });
291
- const SetNumberPatchSchema = z.object({
292
- op: z.literal("set_number"),
293
- fieldId: IdSchema,
294
- value: z.number().nullable()
295
- });
296
- const SetStringListPatchSchema = z.object({
297
- op: z.literal("set_string_list"),
298
- fieldId: IdSchema,
299
- items: z.array(z.string())
300
- });
301
- const SetCheckboxesPatchSchema = z.object({
302
- op: z.literal("set_checkboxes"),
303
- fieldId: IdSchema,
304
- values: z.record(OptionIdSchema, CheckboxValueSchema)
305
- });
306
- const SetSingleSelectPatchSchema = z.object({
307
- op: z.literal("set_single_select"),
308
- fieldId: IdSchema,
309
- selected: OptionIdSchema.nullable()
310
- });
311
- const SetMultiSelectPatchSchema = z.object({
312
- op: z.literal("set_multi_select"),
313
- fieldId: IdSchema,
314
- selected: z.array(OptionIdSchema)
315
- });
316
- const ClearFieldPatchSchema = z.object({
317
- op: z.literal("clear_field"),
318
- fieldId: IdSchema
319
- });
320
- const PatchSchema = z.discriminatedUnion("op", [
321
- SetStringPatchSchema,
322
- SetNumberPatchSchema,
323
- SetStringListPatchSchema,
324
- SetCheckboxesPatchSchema,
325
- SetSingleSelectPatchSchema,
326
- SetMultiSelectPatchSchema,
327
- ClearFieldPatchSchema
328
- ]);
329
- const StepResultSchema = z.object({
330
- structureSummary: StructureSummarySchema,
331
- progressSummary: ProgressSummarySchema,
332
- issues: z.array(InspectIssueSchema),
333
- stepBudget: z.number().int().nonnegative(),
334
- isComplete: z.boolean(),
335
- turnNumber: z.number().int().positive()
336
- });
337
- const HarnessConfigSchema = z.object({
338
- maxIssues: z.number().int().positive(),
339
- maxPatchesPerTurn: z.number().int().positive(),
340
- maxTurns: z.number().int().positive(),
341
- maxFieldsPerTurn: z.number().int().positive().optional(),
342
- maxGroupsPerTurn: z.number().int().positive().optional(),
343
- targetRoles: z.array(z.string()).optional(),
344
- fillMode: FillModeSchema.optional()
345
- });
346
- const SessionTurnSchema = z.object({
347
- turn: z.number().int().positive(),
348
- inspect: z.object({ issues: z.array(InspectIssueSchema) }),
349
- apply: z.object({ patches: z.array(PatchSchema) }),
350
- after: z.object({
351
- requiredIssueCount: z.number().int().nonnegative(),
352
- markdownSha256: z.string()
353
- })
354
- });
355
- const SessionFinalSchema = z.object({
356
- expectComplete: z.boolean(),
357
- expectedCompletedForm: z.string()
358
- });
359
- const SessionTranscriptSchema = z.object({
360
- sessionVersion: z.string(),
361
- mode: z.enum(["mock", "live"]),
362
- form: z.object({ path: z.string() }),
363
- validators: z.object({ code: z.string().optional() }).optional(),
364
- mock: z.object({ completedMock: z.string() }).optional(),
365
- live: z.object({ modelId: z.string() }).optional(),
366
- harness: HarnessConfigSchema,
367
- turns: z.array(SessionTurnSchema),
368
- final: SessionFinalSchema
369
- });
370
- const MarkformFrontmatterSchema = z.object({
371
- markformVersion: z.string(),
372
- formSummary: StructureSummarySchema,
373
- formProgress: ProgressSummarySchema,
374
- formState: ProgressStateSchema
375
- });
36
+ /**
37
+ * Format suggested LLMs for display in help/error messages.
38
+ */
39
+ function formatSuggestedLlms() {
40
+ const lines = ["Available providers and example models:"];
41
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
42
+ lines.push(` ${provider}/`);
43
+ for (const model of models) lines.push(` - ${provider}/${model}`);
44
+ }
45
+ return lines.join("\n");
46
+ }
47
+ /**
48
+ * Web search configuration per provider.
49
+ *
50
+ * Tool names are from Vercel AI SDK provider documentation:
51
+ * - openai: https://ai-sdk.dev/providers/ai-sdk-providers/openai
52
+ * - anthropic: https://ai-sdk.dev/providers/ai-sdk-providers/anthropic
53
+ * - google: https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
54
+ * - xai: https://ai-sdk.dev/providers/ai-sdk-providers/xai
55
+ * - deepseek: https://ai-sdk.dev/providers/ai-sdk-providers/deepseek (no tools)
56
+ */
57
+ const WEB_SEARCH_CONFIG = {
58
+ openai: {
59
+ supported: true,
60
+ toolName: "webSearch"
61
+ },
62
+ anthropic: {
63
+ supported: true,
64
+ toolName: "webSearch_20250305"
65
+ },
66
+ google: {
67
+ supported: true,
68
+ toolName: "googleSearch"
69
+ },
70
+ xai: {
71
+ supported: true,
72
+ toolName: "webSearch"
73
+ },
74
+ deepseek: { supported: false }
75
+ };
76
+ /**
77
+ * Check if a provider supports native web search.
78
+ */
79
+ function hasWebSearchSupport(provider) {
80
+ return WEB_SEARCH_CONFIG[provider]?.supported ?? false;
81
+ }
82
+ /**
83
+ * Get web search tool configuration for a provider.
84
+ * Returns undefined if provider doesn't support web search.
85
+ */
86
+ function getWebSearchConfig(provider) {
87
+ const config = WEB_SEARCH_CONFIG[provider];
88
+ return config?.supported ? config : void 0;
89
+ }
376
90
 
377
91
  //#endregion
378
92
  //#region src/settings.ts
93
+ /**
94
+ * Global settings and constants for Markform.
95
+ *
96
+ * This file consolidates non-changing default values that were previously
97
+ * scattered across the codebase. These are NOT runtime configurable - they
98
+ * are compile-time constants.
99
+ */
100
+ /**
101
+ * The current Markform spec version in full notation (e.g., "MF/0.1").
102
+ * This is distinct from npm package version and tracks the format that
103
+ * .form.md files conform to.
104
+ */
105
+ const MF_SPEC_VERSION = "MF/0.1";
379
106
  /** Default role for fields without explicit role attribute */
380
107
  const AGENT_ROLE = "agent";
381
108
  /** Role for human-filled fields in interactive mode */
382
109
  const USER_ROLE = "user";
110
+ /** Default roles list for forms without explicit roles in frontmatter */
111
+ const DEFAULT_ROLES = [USER_ROLE, AGENT_ROLE];
383
112
  /** Default instructions per role (used when form doesn't specify role_instructions) */
384
113
  const DEFAULT_ROLE_INSTRUCTIONS = {
385
114
  [USER_ROLE]: "Fill in the fields you have direct knowledge of.",
@@ -418,6 +147,22 @@ const DEFAULT_PRIORITY = "medium";
418
147
  */
419
148
  const DEFAULT_PORT = 3344;
420
149
  /**
150
+ * Default forms directory for CLI output (relative to cwd).
151
+ * Commands write form outputs here to avoid cluttering the workspace.
152
+ */
153
+ const DEFAULT_FORMS_DIR = "./forms";
154
+ /**
155
+ * Resolve the forms directory path to an absolute path.
156
+ * Uses the provided override or falls back to DEFAULT_FORMS_DIR.
157
+ *
158
+ * @param override Optional override path from CLI --forms-dir option
159
+ * @param cwd Base directory for resolving relative paths (defaults to process.cwd())
160
+ * @returns Absolute path to the forms directory
161
+ */
162
+ function getFormsDir(override, cwd = process.cwd()) {
163
+ return resolve(cwd, override ?? DEFAULT_FORMS_DIR);
164
+ }
165
+ /**
421
166
  * Default maximum turns for the fill harness.
422
167
  * Prevents runaway loops during agent execution.
423
168
  */
@@ -427,55 +172,129 @@ const DEFAULT_MAX_TURNS = 100;
427
172
  */
428
173
  const DEFAULT_MAX_PATCHES_PER_TURN = 20;
429
174
  /**
430
- * Default maximum issues to show per step.
175
+ * Default maximum issues to show per turn.
176
+ * Note: Renamed from DEFAULT_MAX_ISSUES for naming consistency with other per-turn limits.
431
177
  */
432
- const DEFAULT_MAX_ISSUES = 10;
178
+ const DEFAULT_MAX_ISSUES_PER_TURN = 10;
433
179
  /**
434
- * Suggested LLM models for the fill command, organized by provider.
435
- * These are shown in help/error messages. Only includes models from the
436
- * authoritative models.yaml configuration.
180
+ * Default maximum issues to show per turn in research mode.
181
+ * Lower than general fill to keep research responses focused.
437
182
  */
438
- const SUGGESTED_LLMS = {
439
- openai: [
440
- "gpt-5-mini",
441
- "gpt-5-nano",
442
- "gpt-5.1",
443
- "gpt-5-pro",
444
- "gpt-5.2",
445
- "gpt-5.2-pro"
446
- ],
447
- anthropic: [
448
- "claude-opus-4-5",
449
- "claude-opus-4-1",
450
- "claude-sonnet-4-5",
451
- "claude-sonnet-4-0",
452
- "claude-haiku-4-5"
453
- ],
454
- google: [
455
- "gemini-2.5-pro",
456
- "gemini-2.5-flash",
457
- "gemini-2.0-flash",
458
- "gemini-2.0-flash-lite",
459
- "gemini-3-pro-preview"
460
- ],
461
- xai: ["grok-4", "grok-4-fast"],
462
- deepseek: ["deepseek-chat", "deepseek-reasoner"]
183
+ const DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN = 5;
184
+ /**
185
+ * Default maximum patches per turn in research mode.
186
+ */
187
+ const DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN = 10;
188
+ /**
189
+ * Export format extensions used by the export command and exportMultiFormat.
190
+ * These are the primary output formats when exporting forms.
191
+ */
192
+ const EXPORT_EXTENSIONS = {
193
+ form: ".form.md",
194
+ raw: ".raw.md",
195
+ yaml: ".yml",
196
+ json: ".json"
463
197
  };
464
198
  /**
465
- * Format suggested LLMs for display in help/error messages.
199
+ * Report extension - generated by the report command.
200
+ * Separate from exports as it's a filtered human-readable output.
466
201
  */
467
- function formatSuggestedLlms() {
468
- const lines = ["Available providers and example models:"];
469
- for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
470
- lines.push(` ${provider}/`);
471
- for (const model of models) lines.push(` - ${provider}/${model}`);
202
+ const REPORT_EXTENSION = ".report.md";
203
+ /**
204
+ * All recognized markform file extensions.
205
+ * Combines export formats with report format.
206
+ */
207
+ const ALL_EXTENSIONS = {
208
+ ...EXPORT_EXTENSIONS,
209
+ report: REPORT_EXTENSION
210
+ };
211
+ /**
212
+ * Detect file type from path based on extension.
213
+ * Used by serve command to dispatch to appropriate renderer.
214
+ */
215
+ function detectFileType(filePath) {
216
+ if (filePath.endsWith(ALL_EXTENSIONS.form)) return "form";
217
+ if (filePath.endsWith(ALL_EXTENSIONS.raw)) return "raw";
218
+ if (filePath.endsWith(ALL_EXTENSIONS.report)) return "report";
219
+ if (filePath.endsWith(ALL_EXTENSIONS.yaml)) return "yaml";
220
+ if (filePath.endsWith(ALL_EXTENSIONS.json)) return "json";
221
+ if (filePath.endsWith(".md")) return "raw";
222
+ return "unknown";
223
+ }
224
+ /**
225
+ * Derive export path by replacing any known extension with the target format.
226
+ * Only works with export formats (form, raw, yaml, json), not report.
227
+ */
228
+ function deriveExportPath(basePath, format) {
229
+ let base = basePath;
230
+ for (const ext of Object.values(ALL_EXTENSIONS)) if (base.endsWith(ext)) {
231
+ base = base.slice(0, -ext.length);
232
+ break;
472
233
  }
473
- return lines.join("\n");
234
+ return base + EXPORT_EXTENSIONS[format];
474
235
  }
475
236
 
476
237
  //#endregion
477
238
  //#region src/engine/serialize.ts
478
239
  /**
240
+ * Find the maximum run of fence characters at line starts (indent ≤ 3 spaces).
241
+ * Lines with 4+ space indent are inside code blocks so don't break fences.
242
+ */
243
+ function maxRunAtLineStart(value, char) {
244
+ const escaped = char === "`" ? "`" : "~";
245
+ const pattern = new RegExp(`^( {0,3})${escaped}+`, "gm");
246
+ let maxRun = 0;
247
+ let match;
248
+ while ((match = pattern.exec(value)) !== null) {
249
+ const indent = match[1]?.length ?? 0;
250
+ const runLength = match[0].length - indent;
251
+ if (runLength > maxRun) maxRun = runLength;
252
+ }
253
+ return maxRun;
254
+ }
255
+ /**
256
+ * Pick the optimal fence character and length for a value.
257
+ * Also detects if process=false is needed for Markdoc tags.
258
+ */
259
+ function pickFence(value) {
260
+ const hasMarkdocTags = value.includes("{%");
261
+ const maxBackticks = maxRunAtLineStart(value, "`");
262
+ const maxTildes = maxRunAtLineStart(value, "~");
263
+ let char;
264
+ let maxRun;
265
+ if (maxBackticks <= maxTildes) {
266
+ char = "`";
267
+ maxRun = maxBackticks;
268
+ } else {
269
+ char = "~";
270
+ maxRun = maxTildes;
271
+ }
272
+ const len = Math.max(3, maxRun + 1);
273
+ return {
274
+ char,
275
+ len,
276
+ processFalse: hasMarkdocTags
277
+ };
278
+ }
279
+ /**
280
+ * Format a value fence block with the given content.
281
+ * Uses smart fence selection to avoid collision with code blocks in content.
282
+ */
283
+ function formatValueFence(content) {
284
+ const { char, len, processFalse } = pickFence(content);
285
+ const fence = char.repeat(len);
286
+ return `\n${fence}value${processFalse ? " {% process=false %}" : ""}\n${content}\n${fence}\n`;
287
+ }
288
+ /**
289
+ * Get sentinel value content for skipped/aborted fields with reason.
290
+ * Returns the fence block if there's a reason, empty string otherwise.
291
+ */
292
+ function getSentinelContent(response) {
293
+ if (response?.state === "skipped" && response.reason) return formatValueFence(`%SKIP% (${response.reason})`);
294
+ if (response?.state === "aborted" && response.reason) return formatValueFence(`%ABORT% (${response.reason})`);
295
+ return "";
296
+ }
297
+ /**
479
298
  * Serialize an attribute value to Markdoc format.
480
299
  */
481
300
  function serializeAttrValue(value) {
@@ -521,7 +340,7 @@ function getMarker(state) {
521
340
  /**
522
341
  * Serialize a string field.
523
342
  */
524
- function serializeStringField(field, value) {
343
+ function serializeStringField(field, response) {
525
344
  const attrs = {
526
345
  id: field.id,
527
346
  label: field.label
@@ -534,15 +353,22 @@ function serializeStringField(field, value) {
534
353
  if (field.minLength !== void 0) attrs.minLength = field.minLength;
535
354
  if (field.maxLength !== void 0) attrs.maxLength = field.maxLength;
536
355
  if (field.validate) attrs.validate = field.validate;
356
+ if (field.report !== void 0) attrs.report = field.report;
357
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
537
358
  const attrStr = serializeAttrs(attrs);
538
359
  let content = "";
539
- if (value?.value) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
360
+ if (response?.state === "answered" && response.value) {
361
+ const value = response.value;
362
+ if (value.value) content = formatValueFence(value.value);
363
+ }
364
+ const sentinelContent = getSentinelContent(response);
365
+ if (sentinelContent) content = sentinelContent;
540
366
  return `{% string-field ${attrStr} %}${content}{% /string-field %}`;
541
367
  }
542
368
  /**
543
369
  * Serialize a number field.
544
370
  */
545
- function serializeNumberField(field, value) {
371
+ function serializeNumberField(field, response) {
546
372
  const attrs = {
547
373
  id: field.id,
548
374
  label: field.label
@@ -554,15 +380,22 @@ function serializeNumberField(field, value) {
554
380
  if (field.max !== void 0) attrs.max = field.max;
555
381
  if (field.integer) attrs.integer = field.integer;
556
382
  if (field.validate) attrs.validate = field.validate;
383
+ if (field.report !== void 0) attrs.report = field.report;
384
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
557
385
  const attrStr = serializeAttrs(attrs);
558
386
  let content = "";
559
- if (value?.value !== null && value?.value !== void 0) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
387
+ if (response?.state === "answered" && response.value) {
388
+ const value = response.value;
389
+ if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
390
+ }
391
+ const sentinelContent = getSentinelContent(response);
392
+ if (sentinelContent) content = sentinelContent;
560
393
  return `{% number-field ${attrStr} %}${content}{% /number-field %}`;
561
394
  }
562
395
  /**
563
396
  * Serialize a string-list field.
564
397
  */
565
- function serializeStringListField(field, value) {
398
+ function serializeStringListField(field, response) {
566
399
  const attrs = {
567
400
  id: field.id,
568
401
  label: field.label
@@ -576,9 +409,16 @@ function serializeStringListField(field, value) {
576
409
  if (field.itemMaxLength !== void 0) attrs.itemMaxLength = field.itemMaxLength;
577
410
  if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
578
411
  if (field.validate) attrs.validate = field.validate;
412
+ if (field.report !== void 0) attrs.report = field.report;
413
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
579
414
  const attrStr = serializeAttrs(attrs);
580
415
  let content = "";
581
- if (value?.items && value.items.length > 0) content = `\n\`\`\`value\n${value.items.join("\n")}\n\`\`\`\n`;
416
+ if (response?.state === "answered" && response.value) {
417
+ const value = response.value;
418
+ if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
419
+ }
420
+ const sentinelContent = getSentinelContent(response);
421
+ if (sentinelContent) content = sentinelContent;
582
422
  return `{% string-list ${attrStr} %}${content}{% /string-list %}`;
583
423
  }
584
424
  /**
@@ -595,7 +435,7 @@ function serializeOptions(options, selected) {
595
435
  /**
596
436
  * Serialize a single-select field.
597
437
  */
598
- function serializeSingleSelectField(field, value) {
438
+ function serializeSingleSelectField(field, response) {
599
439
  const attrs = {
600
440
  id: field.id,
601
441
  label: field.label
@@ -604,7 +444,11 @@ function serializeSingleSelectField(field, value) {
604
444
  if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
605
445
  if (field.role !== AGENT_ROLE) attrs.role = field.role;
606
446
  if (field.validate) attrs.validate = field.validate;
447
+ if (field.report !== void 0) attrs.report = field.report;
448
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
607
449
  const attrStr = serializeAttrs(attrs);
450
+ let value;
451
+ if (response?.state === "answered" && response.value) value = response.value;
608
452
  const selected = {};
609
453
  for (const opt of field.options) selected[opt.id] = opt.id === value?.selected ? "done" : "todo";
610
454
  return `{% single-select ${attrStr} %}\n${serializeOptions(field.options, selected)}\n{% /single-select %}`;
@@ -612,7 +456,7 @@ function serializeSingleSelectField(field, value) {
612
456
  /**
613
457
  * Serialize a multi-select field.
614
458
  */
615
- function serializeMultiSelectField(field, value) {
459
+ function serializeMultiSelectField(field, response) {
616
460
  const attrs = {
617
461
  id: field.id,
618
462
  label: field.label
@@ -623,7 +467,11 @@ function serializeMultiSelectField(field, value) {
623
467
  if (field.minSelections !== void 0) attrs.minSelections = field.minSelections;
624
468
  if (field.maxSelections !== void 0) attrs.maxSelections = field.maxSelections;
625
469
  if (field.validate) attrs.validate = field.validate;
470
+ if (field.report !== void 0) attrs.report = field.report;
471
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
626
472
  const attrStr = serializeAttrs(attrs);
473
+ let value;
474
+ if (response?.state === "answered" && response.value) value = response.value;
627
475
  const selected = {};
628
476
  const selectedSet = new Set(value?.selected ?? []);
629
477
  for (const opt of field.options) selected[opt.id] = selectedSet.has(opt.id) ? "done" : "todo";
@@ -632,7 +480,7 @@ function serializeMultiSelectField(field, value) {
632
480
  /**
633
481
  * Serialize a checkboxes field.
634
482
  */
635
- function serializeCheckboxesField(field, value) {
483
+ function serializeCheckboxesField(field, response) {
636
484
  const attrs = {
637
485
  id: field.id,
638
486
  label: field.label
@@ -644,20 +492,132 @@ function serializeCheckboxesField(field, value) {
644
492
  if (field.minDone !== void 0) attrs.minDone = field.minDone;
645
493
  if (field.approvalMode !== "none") attrs.approvalMode = field.approvalMode;
646
494
  if (field.validate) attrs.validate = field.validate;
647
- return `{% checkboxes ${serializeAttrs(attrs)} %}\n${serializeOptions(field.options, value?.values ?? {})}\n{% /checkboxes %}`;
495
+ if (field.report !== void 0) attrs.report = field.report;
496
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
497
+ const attrStr = serializeAttrs(attrs);
498
+ let value;
499
+ if (response?.state === "answered" && response.value) value = response.value;
500
+ return `{% checkboxes ${attrStr} %}\n${serializeOptions(field.options, value?.values ?? {})}\n{% /checkboxes %}`;
501
+ }
502
+ /**
503
+ * Serialize a url-field.
504
+ */
505
+ function serializeUrlField(field, response) {
506
+ const attrs = {
507
+ id: field.id,
508
+ label: field.label
509
+ };
510
+ if (field.required) attrs.required = field.required;
511
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
512
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
513
+ if (field.validate) attrs.validate = field.validate;
514
+ if (field.report !== void 0) attrs.report = field.report;
515
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
516
+ const attrStr = serializeAttrs(attrs);
517
+ let content = "";
518
+ if (response?.state === "answered" && response.value) {
519
+ const value = response.value;
520
+ if (value.value) content = formatValueFence(value.value);
521
+ }
522
+ const sentinelContent = getSentinelContent(response);
523
+ if (sentinelContent) content = sentinelContent;
524
+ return `{% url-field ${attrStr} %}${content}{% /url-field %}`;
525
+ }
526
+ /**
527
+ * Serialize a url-list field.
528
+ */
529
+ function serializeUrlListField(field, response) {
530
+ const attrs = {
531
+ id: field.id,
532
+ label: field.label
533
+ };
534
+ if (field.required) attrs.required = field.required;
535
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
536
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
537
+ if (field.minItems !== void 0) attrs.minItems = field.minItems;
538
+ if (field.maxItems !== void 0) attrs.maxItems = field.maxItems;
539
+ if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
540
+ if (field.validate) attrs.validate = field.validate;
541
+ if (field.report !== void 0) attrs.report = field.report;
542
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
543
+ const attrStr = serializeAttrs(attrs);
544
+ let content = "";
545
+ if (response?.state === "answered" && response.value) {
546
+ const value = response.value;
547
+ if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
548
+ }
549
+ const sentinelContent = getSentinelContent(response);
550
+ if (sentinelContent) content = sentinelContent;
551
+ return `{% url-list ${attrStr} %}${content}{% /url-list %}`;
552
+ }
553
+ /**
554
+ * Serialize a date-field.
555
+ */
556
+ function serializeDateField(field, response) {
557
+ const attrs = {
558
+ id: field.id,
559
+ label: field.label
560
+ };
561
+ if (field.required) attrs.required = field.required;
562
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
563
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
564
+ if (field.min !== void 0) attrs.min = field.min;
565
+ if (field.max !== void 0) attrs.max = field.max;
566
+ if (field.validate) attrs.validate = field.validate;
567
+ if (field.report !== void 0) attrs.report = field.report;
568
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
569
+ const attrStr = serializeAttrs(attrs);
570
+ let content = "";
571
+ if (response?.state === "answered" && response.value) {
572
+ const value = response.value;
573
+ if (value.value) content = formatValueFence(value.value);
574
+ }
575
+ const sentinelContent = getSentinelContent(response);
576
+ if (sentinelContent) content = sentinelContent;
577
+ return `{% date-field ${attrStr} %}${content}{% /date-field %}`;
578
+ }
579
+ /**
580
+ * Serialize a year-field.
581
+ */
582
+ function serializeYearField(field, response) {
583
+ const attrs = {
584
+ id: field.id,
585
+ label: field.label
586
+ };
587
+ if (field.required) attrs.required = field.required;
588
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
589
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
590
+ if (field.min !== void 0) attrs.min = field.min;
591
+ if (field.max !== void 0) attrs.max = field.max;
592
+ if (field.validate) attrs.validate = field.validate;
593
+ if (field.report !== void 0) attrs.report = field.report;
594
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
595
+ const attrStr = serializeAttrs(attrs);
596
+ let content = "";
597
+ if (response?.state === "answered" && response.value) {
598
+ const value = response.value;
599
+ if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
600
+ }
601
+ const sentinelContent = getSentinelContent(response);
602
+ if (sentinelContent) content = sentinelContent;
603
+ return `{% year-field ${attrStr} %}${content}{% /year-field %}`;
648
604
  }
649
605
  /**
650
606
  * Serialize a field to Markdoc format.
651
607
  */
652
- function serializeField(field, values) {
653
- const value = values[field.id];
608
+ function serializeField(field, responses) {
609
+ const response = responses[field.id];
654
610
  switch (field.kind) {
655
- case "string": return serializeStringField(field, value);
656
- case "number": return serializeNumberField(field, value);
657
- case "string_list": return serializeStringListField(field, value);
658
- case "single_select": return serializeSingleSelectField(field, value);
659
- case "multi_select": return serializeMultiSelectField(field, value);
660
- case "checkboxes": return serializeCheckboxesField(field, value);
611
+ case "string": return serializeStringField(field, response);
612
+ case "number": return serializeNumberField(field, response);
613
+ case "string_list": return serializeStringListField(field, response);
614
+ case "single_select": return serializeSingleSelectField(field, response);
615
+ case "multi_select": return serializeMultiSelectField(field, response);
616
+ case "checkboxes": return serializeCheckboxesField(field, response);
617
+ case "url": return serializeUrlField(field, response);
618
+ case "url_list": return serializeUrlListField(field, response);
619
+ case "date": return serializeDateField(field, response);
620
+ case "year": return serializeYearField(field, response);
661
621
  }
662
622
  }
663
623
  /**
@@ -665,16 +625,39 @@ function serializeField(field, values) {
665
625
  * Uses the semantic tag name (description, instructions, documentation).
666
626
  */
667
627
  function serializeDocBlock(doc) {
668
- const attrStr = serializeAttrs({ ref: doc.ref });
628
+ const attrs = { ref: doc.ref };
629
+ if (doc.report !== void 0) attrs.report = doc.report;
630
+ const attrStr = serializeAttrs(attrs);
669
631
  return `{% ${doc.tag} ${attrStr} %}\n${doc.bodyMarkdown}\n{% /${doc.tag} %}`;
670
632
  }
671
633
  /**
634
+ * Serialize notes in sorted order.
635
+ * Notes are sorted numerically by ID suffix (n1, n2, n10 not n1, n10, n2).
636
+ */
637
+ function serializeNotes(notes) {
638
+ if (notes.length === 0) return "";
639
+ const sorted = [...notes].sort((a, b) => {
640
+ return (Number.parseInt(a.id.replace(/^n/, ""), 10) || 0) - (Number.parseInt(b.id.replace(/^n/, ""), 10) || 0);
641
+ });
642
+ const lines = [];
643
+ for (const note of sorted) {
644
+ const attrStr = serializeAttrs({
645
+ id: note.id,
646
+ ref: note.ref,
647
+ role: note.role
648
+ });
649
+ lines.push(`{% note ${attrStr} %}\n${note.text}\n{% /note %}`);
650
+ }
651
+ return lines.join("\n\n");
652
+ }
653
+ /**
672
654
  * Serialize a field group.
673
655
  */
674
- function serializeFieldGroup(group, values, docs) {
656
+ function serializeFieldGroup(group, responses, docs) {
675
657
  const attrs = { id: group.id };
676
658
  if (group.title) attrs.title = group.title;
677
659
  if (group.validate) attrs.validate = group.validate;
660
+ if (group.report !== void 0) attrs.report = group.report;
678
661
  const lines = [`{% field-group ${serializeAttrs(attrs)} %}`];
679
662
  const docsByRef = /* @__PURE__ */ new Map();
680
663
  for (const doc of docs) {
@@ -684,7 +667,7 @@ function serializeFieldGroup(group, values, docs) {
684
667
  }
685
668
  for (const field of group.children) {
686
669
  lines.push("");
687
- lines.push(serializeField(field, values));
670
+ lines.push(serializeField(field, responses));
688
671
  const fieldDocs = docsByRef.get(field.id);
689
672
  if (fieldDocs) for (const doc of fieldDocs) {
690
673
  lines.push("");
@@ -698,7 +681,7 @@ function serializeFieldGroup(group, values, docs) {
698
681
  /**
699
682
  * Serialize a form schema.
700
683
  */
701
- function serializeFormSchema(schema, values, docs) {
684
+ function serializeFormSchema(schema, responses, docs, notes) {
702
685
  const attrs = { id: schema.id };
703
686
  if (schema.title) attrs.title = schema.title;
704
687
  const lines = [`{% form ${serializeAttrs(attrs)} %}`];
@@ -715,7 +698,12 @@ function serializeFormSchema(schema, values, docs) {
715
698
  }
716
699
  for (const group of schema.groups) {
717
700
  lines.push("");
718
- lines.push(serializeFieldGroup(group, values, docs));
701
+ lines.push(serializeFieldGroup(group, responses, docs));
702
+ }
703
+ const notesContent = serializeNotes(notes);
704
+ if (notesContent) {
705
+ lines.push("");
706
+ lines.push(notesContent);
719
707
  }
720
708
  lines.push("");
721
709
  lines.push("{% /form %}");
@@ -731,8 +719,8 @@ function serializeFormSchema(schema, values, docs) {
731
719
  function serialize(form, opts) {
732
720
  return `${`---
733
721
  markform:
734
- markform_version: "${opts?.markformVersion ?? "0.1.0"}"
735
- ---`}\n\n${serializeFormSchema(form.schema, form.valuesByFieldId, form.docs)}\n`;
722
+ spec: "${opts?.specVersion ?? MF_SPEC_VERSION}"
723
+ ---`}\n\n${serializeFormSchema(form.schema, form.responsesByFieldId, form.docs, form.notes)}\n`;
736
724
  }
737
725
  /** Map checkbox state to GFM marker for raw markdown output */
738
726
  const STATE_TO_GFM_MARKER = {
@@ -748,10 +736,11 @@ const STATE_TO_GFM_MARKER = {
748
736
  /**
749
737
  * Serialize a field value to raw markdown (human-readable).
750
738
  */
751
- function serializeFieldRaw(field, values) {
752
- const value = values[field.id];
739
+ function serializeFieldRaw(field, responses) {
740
+ const response = responses[field.id];
753
741
  const lines = [];
754
742
  lines.push(`**${field.label}:**`);
743
+ const value = response?.state === "answered" ? response.value : void 0;
755
744
  switch (field.kind) {
756
745
  case "string": {
757
746
  const strValue = value;
@@ -794,6 +783,30 @@ function serializeFieldRaw(field, values) {
794
783
  }
795
784
  break;
796
785
  }
786
+ case "url": {
787
+ const urlValue = value;
788
+ if (urlValue?.value) lines.push(urlValue.value);
789
+ else lines.push("_(empty)_");
790
+ break;
791
+ }
792
+ case "url_list": {
793
+ const urlListValue = value;
794
+ if (urlListValue?.items && urlListValue.items.length > 0) for (const item of urlListValue.items) lines.push(`- ${item}`);
795
+ else lines.push("_(empty)_");
796
+ break;
797
+ }
798
+ case "date": {
799
+ const dateValue = value;
800
+ if (dateValue?.value) lines.push(dateValue.value);
801
+ else lines.push("_(empty)_");
802
+ break;
803
+ }
804
+ case "year": {
805
+ const yearValue = value;
806
+ if (yearValue?.value !== null && yearValue?.value !== void 0) lines.push(String(yearValue.value));
807
+ else lines.push("_(empty)_");
808
+ break;
809
+ }
797
810
  }
798
811
  return lines.join("\n");
799
812
  }
@@ -834,7 +847,70 @@ function serializeRawMarkdown(form) {
834
847
  lines.push("");
835
848
  }
836
849
  for (const field of group.children) {
837
- lines.push(serializeFieldRaw(field, form.valuesByFieldId));
850
+ lines.push(serializeFieldRaw(field, form.responsesByFieldId));
851
+ lines.push("");
852
+ const fieldDocs = docsByRef.get(field.id);
853
+ if (fieldDocs) for (const doc of fieldDocs) {
854
+ lines.push(doc.bodyMarkdown.trim());
855
+ lines.push("");
856
+ }
857
+ }
858
+ }
859
+ return lines.join("\n").trim() + "\n";
860
+ }
861
+ /**
862
+ * Check if a documentation block should be included in reports.
863
+ * Default: instructions are excluded, everything else is included.
864
+ */
865
+ function shouldIncludeDoc(doc) {
866
+ if (doc.report !== void 0) return doc.report;
867
+ return doc.tag !== "instructions";
868
+ }
869
+ /**
870
+ * Serialize a form to filtered markdown for reports.
871
+ *
872
+ * Produces clean, readable markdown with filtered content based on `report` attribute:
873
+ * - Fields with report=false are excluded
874
+ * - Groups with report=false are excluded
875
+ * - Documentation blocks with report=false are excluded
876
+ * - Instructions blocks are excluded by default (unless report=true)
877
+ *
878
+ * @param form - The parsed form to serialize
879
+ * @returns Filtered plain markdown string suitable for sharing
880
+ */
881
+ function serializeReportMarkdown(form) {
882
+ const lines = [];
883
+ const docsByRef = /* @__PURE__ */ new Map();
884
+ for (const doc of form.docs) {
885
+ if (!shouldIncludeDoc(doc)) continue;
886
+ const list = docsByRef.get(doc.ref) ?? [];
887
+ list.push(doc);
888
+ docsByRef.set(doc.ref, list);
889
+ }
890
+ if (form.schema.title) {
891
+ lines.push(`# ${form.schema.title}`);
892
+ lines.push("");
893
+ }
894
+ const formDocs = docsByRef.get(form.schema.id);
895
+ if (formDocs) for (const doc of formDocs) {
896
+ lines.push(doc.bodyMarkdown.trim());
897
+ lines.push("");
898
+ }
899
+ for (const group of form.schema.groups) {
900
+ if (group.report === false) continue;
901
+ const visibleFields = group.children.filter((field) => field.report !== false);
902
+ if (visibleFields.length === 0 && !group.title) continue;
903
+ if (group.title) {
904
+ lines.push(`## ${group.title}`);
905
+ lines.push("");
906
+ }
907
+ const groupDocs = docsByRef.get(group.id);
908
+ if (groupDocs) for (const doc of groupDocs) {
909
+ lines.push(doc.bodyMarkdown.trim());
910
+ lines.push("");
911
+ }
912
+ for (const field of visibleFields) {
913
+ lines.push(serializeFieldRaw(field, form.responsesByFieldId));
838
914
  lines.push("");
839
915
  const fieldDocs = docsByRef.get(field.id);
840
916
  if (fieldDocs) for (const doc of fieldDocs) {
@@ -861,7 +937,11 @@ function computeStructureSummary(schema) {
861
937
  string_list: 0,
862
938
  checkboxes: 0,
863
939
  single_select: 0,
864
- multi_select: 0
940
+ multi_select: 0,
941
+ url: 0,
942
+ url_list: 0,
943
+ date: 0,
944
+ year: 0
865
945
  };
866
946
  const groupsById = {};
867
947
  const fieldsById = {};
@@ -922,6 +1002,16 @@ function isFieldSubmitted(field, value) {
922
1002
  }
923
1003
  return false;
924
1004
  }
1005
+ case "url": {
1006
+ const v = value;
1007
+ return v.value !== null && v.value.trim() !== "";
1008
+ }
1009
+ case "url_list": return value.items.length > 0;
1010
+ case "date": {
1011
+ const v = value;
1012
+ return v.value !== null && v.value.trim() !== "";
1013
+ }
1014
+ case "year": return value.value !== null;
925
1015
  }
926
1016
  }
927
1017
  /**
@@ -951,45 +1041,33 @@ function computeCheckboxProgress(field, value) {
951
1041
  return result;
952
1042
  }
953
1043
  /**
954
- * Check if a checkboxes field is complete based on its mode.
1044
+ * Compute whether a field is empty (has no value).
955
1045
  */
956
- function isCheckboxesComplete(field, value) {
957
- if (!value) return false;
958
- const mode = field.checkboxMode ?? "multi";
959
- for (const opt of field.options) {
960
- const state = value.values[opt.id];
961
- if (mode === "explicit") {
962
- if (state === "unfilled") return false;
963
- } else if (mode === "multi") {
964
- if (state === "todo" || state === "incomplete" || state === "active") return false;
965
- }
966
- }
967
- return true;
968
- }
969
- /**
970
- * Compute the progress state for a field.
971
- */
972
- function computeFieldState(field, value, issueCount) {
973
- if (!isFieldSubmitted(field, value)) return "empty";
974
- if (issueCount > 0) return "invalid";
975
- if (field.kind === "checkboxes" && value?.kind === "checkboxes") {
976
- if (!isCheckboxesComplete(field, value)) return "incomplete";
977
- }
978
- return "complete";
1046
+ function isFieldEmpty(field, value) {
1047
+ return !isFieldSubmitted(field, value);
979
1048
  }
980
1049
  /**
981
1050
  * Compute progress for a single field.
982
1051
  */
983
- function computeFieldProgress(field, value, issues) {
984
- const issueCount = issues.filter((i) => i.ref === field.id).length;
985
- const submitted = isFieldSubmitted(field, value);
986
- const valid = issueCount === 0;
987
- const state = computeFieldState(field, value, issueCount);
1052
+ function computeFieldProgress(field, response, notes, issues) {
1053
+ const fieldIssues = issues.filter((i) => i.ref === field.id);
1054
+ const issueCount = fieldIssues.length;
1055
+ const value = response.value;
1056
+ const empty = isFieldEmpty(field, value);
1057
+ let valid = true;
1058
+ if (response.state === "skipped" || response.state === "aborted") valid = issueCount === 0;
1059
+ else if (empty) valid = fieldIssues.filter((i) => i.reason !== "required_missing").length === 0;
1060
+ else valid = issueCount === 0;
1061
+ const fieldNotes = notes.filter((n) => n.ref === field.id);
1062
+ const hasNotes = fieldNotes.length > 0;
1063
+ const noteCount = fieldNotes.length;
988
1064
  const progress = {
989
1065
  kind: field.kind,
990
1066
  required: field.required,
991
- submitted,
992
- state,
1067
+ answerState: response.state,
1068
+ hasNotes,
1069
+ noteCount,
1070
+ empty,
993
1071
  valid,
994
1072
  issueCount
995
1073
  };
@@ -1000,34 +1078,42 @@ function computeFieldProgress(field, value, issues) {
1000
1078
  * Compute a progress summary for a form.
1001
1079
  *
1002
1080
  * @param schema - The form schema
1003
- * @param values - Current field values
1081
+ * @param responsesByFieldId - Current field responses (state + optional value)
1082
+ * @param notes - Notes attached to fields/groups/form
1004
1083
  * @param issues - Validation issues (from inspect)
1005
1084
  * @returns Progress summary with field states and counts
1006
1085
  */
1007
- function computeProgressSummary(schema, values, issues) {
1086
+ function computeProgressSummary(schema, responsesByFieldId, notes, issues) {
1008
1087
  const fields = {};
1009
1088
  const counts = {
1010
1089
  totalFields: 0,
1011
1090
  requiredFields: 0,
1012
- submittedFields: 0,
1013
- completeFields: 0,
1014
- incompleteFields: 0,
1091
+ unansweredFields: 0,
1092
+ answeredFields: 0,
1093
+ skippedFields: 0,
1094
+ abortedFields: 0,
1095
+ validFields: 0,
1015
1096
  invalidFields: 0,
1097
+ emptyFields: 0,
1098
+ filledFields: 0,
1016
1099
  emptyRequiredFields: 0,
1017
- emptyOptionalFields: 0
1100
+ totalNotes: notes.length
1018
1101
  };
1019
1102
  for (const group of schema.groups) for (const field of group.children) {
1020
- const value = values[field.id];
1021
- const progress = computeFieldProgress(field, value, issues);
1103
+ const progress = computeFieldProgress(field, responsesByFieldId[field.id] ?? { state: "unanswered" }, notes, issues);
1022
1104
  fields[field.id] = progress;
1023
1105
  counts.totalFields++;
1024
1106
  if (progress.required) counts.requiredFields++;
1025
- if (progress.submitted) counts.submittedFields++;
1026
- if (progress.state === "complete") counts.completeFields++;
1027
- if (progress.state === "incomplete") counts.incompleteFields++;
1028
- if (progress.state === "invalid") counts.invalidFields++;
1029
- if (progress.state === "empty") if (progress.required) counts.emptyRequiredFields++;
1030
- else counts.emptyOptionalFields++;
1107
+ if (progress.answerState === "answered") counts.answeredFields++;
1108
+ else if (progress.answerState === "skipped") counts.skippedFields++;
1109
+ else if (progress.answerState === "aborted") counts.abortedFields++;
1110
+ else if (progress.answerState === "unanswered") counts.unansweredFields++;
1111
+ if (progress.valid) counts.validFields++;
1112
+ else counts.invalidFields++;
1113
+ if (progress.empty) {
1114
+ counts.emptyFields++;
1115
+ if (progress.required) counts.emptyRequiredFields++;
1116
+ } else counts.filledFields++;
1031
1117
  }
1032
1118
  return {
1033
1119
  counts,
@@ -1041,32 +1127,47 @@ function computeProgressSummary(schema, values, issues) {
1041
1127
  * @returns The overall form state
1042
1128
  */
1043
1129
  function computeFormState(progress) {
1130
+ if (progress.counts.abortedFields > 0) return "invalid";
1044
1131
  if (progress.counts.invalidFields > 0) return "invalid";
1045
- if (progress.counts.incompleteFields > 0) return "incomplete";
1046
1132
  if (progress.counts.emptyRequiredFields === 0) return "complete";
1047
- if (progress.counts.submittedFields > 0) return "incomplete";
1133
+ if (progress.counts.answeredFields > 0) return "incomplete";
1048
1134
  return "empty";
1049
1135
  }
1050
1136
  /**
1051
1137
  * Determine if the form is complete (ready for submission).
1052
1138
  *
1139
+ * A form is complete when:
1140
+ * 1. No aborted fields (aborted fields block completion)
1141
+ * 2. No required fields are empty
1142
+ * 3. No fields have validation errors
1143
+ * 4. No fields are in incomplete state (e.g., partial checkbox completion)
1144
+ * 5. All fields must be addressed (answered + skipped == total)
1145
+ *
1146
+ * Every field must be explicitly addressed - either filled with a value or
1147
+ * skipped with a reason. This ensures agents fully process all fields.
1148
+ *
1053
1149
  * @param progress - The progress summary
1054
1150
  * @returns True if the form is complete
1055
1151
  */
1056
1152
  function isFormComplete(progress) {
1057
- return progress.counts.invalidFields === 0 && progress.counts.incompleteFields === 0 && progress.counts.emptyRequiredFields === 0;
1153
+ const { counts } = progress;
1154
+ if (counts.abortedFields > 0) return false;
1155
+ const baseComplete = counts.invalidFields === 0 && counts.emptyRequiredFields === 0;
1156
+ const allFieldsAccountedFor = counts.answeredFields + counts.skippedFields === counts.totalFields;
1157
+ return baseComplete && allFieldsAccountedFor;
1058
1158
  }
1059
1159
  /**
1060
1160
  * Compute all summaries for a parsed form.
1061
1161
  *
1062
1162
  * @param schema - The form schema
1063
- * @param values - Current field values
1163
+ * @param responsesByFieldId - Current field responses (state + optional value)
1164
+ * @param notes - Notes attached to fields/groups/form
1064
1165
  * @param issues - Validation issues
1065
1166
  * @returns All computed summaries
1066
1167
  */
1067
- function computeAllSummaries(schema, values, issues) {
1168
+ function computeAllSummaries(schema, responsesByFieldId, notes, issues) {
1068
1169
  const structureSummary = computeStructureSummary(schema);
1069
- const progressSummary = computeProgressSummary(schema, values, issues);
1170
+ const progressSummary = computeProgressSummary(schema, responsesByFieldId, notes, issues);
1070
1171
  return {
1071
1172
  structureSummary,
1072
1173
  progressSummary,
@@ -1332,10 +1433,195 @@ function validateCheckboxesField(field, value) {
1332
1433
  return issues;
1333
1434
  }
1334
1435
  /**
1436
+ * Check if a string is a valid URL.
1437
+ * Uses URL constructor for validation (RFC 3986 compliant).
1438
+ */
1439
+ function isValidUrl(str) {
1440
+ try {
1441
+ const url = new URL(str);
1442
+ return url.protocol === "http:" || url.protocol === "https:";
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ /**
1448
+ * Validate a URL field.
1449
+ */
1450
+ function validateUrlField(field, value) {
1451
+ const issues = [];
1452
+ const urlValue = value?.value ?? null;
1453
+ if (field.required && (urlValue === null || urlValue.trim() === "")) {
1454
+ issues.push({
1455
+ severity: "error",
1456
+ message: `Required field "${field.label}" is empty`,
1457
+ ref: field.id,
1458
+ source: "builtin"
1459
+ });
1460
+ return issues;
1461
+ }
1462
+ if (urlValue === null || urlValue === "") return issues;
1463
+ if (!isValidUrl(urlValue)) issues.push({
1464
+ severity: "error",
1465
+ message: `"${field.label}" is not a valid URL`,
1466
+ ref: field.id,
1467
+ source: "builtin"
1468
+ });
1469
+ return issues;
1470
+ }
1471
+ /**
1472
+ * Validate a URL list field.
1473
+ */
1474
+ function validateUrlListField(field, value) {
1475
+ const issues = [];
1476
+ const items = value?.items ?? [];
1477
+ if (field.required && items.length === 0) {
1478
+ issues.push({
1479
+ severity: "error",
1480
+ message: `Required field "${field.label}" is empty`,
1481
+ ref: field.id,
1482
+ source: "builtin"
1483
+ });
1484
+ return issues;
1485
+ }
1486
+ if (items.length === 0) return issues;
1487
+ if (field.minItems !== void 0 && items.length < field.minItems) issues.push({
1488
+ severity: "error",
1489
+ message: `"${field.label}" must have at least ${field.minItems} items (got ${items.length})`,
1490
+ ref: field.id,
1491
+ source: "builtin"
1492
+ });
1493
+ if (field.maxItems !== void 0 && items.length > field.maxItems) issues.push({
1494
+ severity: "error",
1495
+ message: `"${field.label}" must have at most ${field.maxItems} items (got ${items.length})`,
1496
+ ref: field.id,
1497
+ source: "builtin"
1498
+ });
1499
+ for (let i = 0; i < items.length; i++) {
1500
+ const item = items[i];
1501
+ if (item !== void 0 && !isValidUrl(item)) issues.push({
1502
+ severity: "error",
1503
+ message: `Item ${i + 1} in "${field.label}" is not a valid URL`,
1504
+ ref: field.id,
1505
+ source: "builtin"
1506
+ });
1507
+ }
1508
+ if (field.uniqueItems) {
1509
+ const seen = /* @__PURE__ */ new Set();
1510
+ for (const item of items) {
1511
+ if (seen.has(item)) {
1512
+ issues.push({
1513
+ severity: "error",
1514
+ message: `Duplicate URL "${item}" in "${field.label}"`,
1515
+ ref: field.id,
1516
+ source: "builtin"
1517
+ });
1518
+ break;
1519
+ }
1520
+ seen.add(item);
1521
+ }
1522
+ }
1523
+ return issues;
1524
+ }
1525
+ /**
1526
+ * Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
1527
+ */
1528
+ function isValidDate(str) {
1529
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false;
1530
+ const date = new Date(str);
1531
+ if (Number.isNaN(date.getTime())) return false;
1532
+ const [year, month, day] = str.split("-").map(Number);
1533
+ return date.getUTCFullYear() === year && date.getUTCMonth() === (month ?? 0) - 1 && date.getUTCDate() === day;
1534
+ }
1535
+ /**
1536
+ * Parse a date string to compare for min/max validation.
1537
+ * Returns date value or null if invalid.
1538
+ */
1539
+ function parseDateForComparison(str) {
1540
+ if (!isValidDate(str)) return null;
1541
+ return new Date(str).getTime();
1542
+ }
1543
+ /**
1544
+ * Validate a date field.
1545
+ */
1546
+ function validateDateField(field, value) {
1547
+ const issues = [];
1548
+ const dateValue = value?.value ?? null;
1549
+ if (field.required && (dateValue === null || dateValue.trim() === "")) {
1550
+ issues.push({
1551
+ severity: "error",
1552
+ message: `Required field "${field.label}" is empty`,
1553
+ ref: field.id,
1554
+ source: "builtin"
1555
+ });
1556
+ return issues;
1557
+ }
1558
+ if (dateValue === null || dateValue === "") return issues;
1559
+ if (!isValidDate(dateValue)) {
1560
+ issues.push({
1561
+ severity: "error",
1562
+ message: `"${field.label}" is not a valid date (expected YYYY-MM-DD)`,
1563
+ ref: field.id,
1564
+ source: "builtin"
1565
+ });
1566
+ return issues;
1567
+ }
1568
+ const dateTime = parseDateForComparison(dateValue);
1569
+ if (field.min !== void 0) {
1570
+ const minTime = parseDateForComparison(field.min);
1571
+ if (minTime !== null && dateTime !== null && dateTime < minTime) issues.push({
1572
+ severity: "error",
1573
+ message: `"${field.label}" must be on or after ${field.min} (got ${dateValue})`,
1574
+ ref: field.id,
1575
+ source: "builtin"
1576
+ });
1577
+ }
1578
+ if (field.max !== void 0) {
1579
+ const maxTime = parseDateForComparison(field.max);
1580
+ if (maxTime !== null && dateTime !== null && dateTime > maxTime) issues.push({
1581
+ severity: "error",
1582
+ message: `"${field.label}" must be on or before ${field.max} (got ${dateValue})`,
1583
+ ref: field.id,
1584
+ source: "builtin"
1585
+ });
1586
+ }
1587
+ return issues;
1588
+ }
1589
+ /**
1590
+ * Validate a year field.
1591
+ */
1592
+ function validateYearField(field, value) {
1593
+ const issues = [];
1594
+ const yearValue = value?.value ?? null;
1595
+ if (field.required && yearValue === null) {
1596
+ issues.push({
1597
+ severity: "error",
1598
+ message: `Required field "${field.label}" is empty`,
1599
+ ref: field.id,
1600
+ source: "builtin"
1601
+ });
1602
+ return issues;
1603
+ }
1604
+ if (yearValue === null) return issues;
1605
+ if (field.min !== void 0 && yearValue < field.min) issues.push({
1606
+ severity: "error",
1607
+ message: `"${field.label}" must be at least ${field.min} (got ${yearValue})`,
1608
+ ref: field.id,
1609
+ source: "builtin"
1610
+ });
1611
+ if (field.max !== void 0 && yearValue > field.max) issues.push({
1612
+ severity: "error",
1613
+ message: `"${field.label}" must be at most ${field.max} (got ${yearValue})`,
1614
+ ref: field.id,
1615
+ source: "builtin"
1616
+ });
1617
+ return issues;
1618
+ }
1619
+ /**
1335
1620
  * Validate a single field.
1336
1621
  */
1337
- function validateField(field, values) {
1338
- const value = values[field.id];
1622
+ function validateField(field, responses) {
1623
+ const response = responses[field.id];
1624
+ const value = response?.state === "answered" ? response.value : void 0;
1339
1625
  switch (field.kind) {
1340
1626
  case "string": return validateStringField(field, value);
1341
1627
  case "number": return validateNumberField(field, value);
@@ -1343,6 +1629,10 @@ function validateField(field, values) {
1343
1629
  case "single_select": return validateSingleSelectField(field, value);
1344
1630
  case "multi_select": return validateMultiSelectField(field, value);
1345
1631
  case "checkboxes": return validateCheckboxesField(field, value);
1632
+ case "url": return validateUrlField(field, value);
1633
+ case "url_list": return validateUrlListField(field, value);
1634
+ case "date": return validateDateField(field, value);
1635
+ case "year": return validateYearField(field, value);
1346
1636
  }
1347
1637
  }
1348
1638
  /**
@@ -1362,10 +1652,12 @@ function parseValidatorRef(ref) {
1362
1652
  /**
1363
1653
  * Run code validators for a field.
1364
1654
  */
1365
- function runCodeValidators(field, schema, values, registry) {
1655
+ function runCodeValidators(field, schema, responses, registry) {
1366
1656
  if (!field.validate) return [];
1367
1657
  const refs = Array.isArray(field.validate) ? field.validate : [field.validate];
1368
1658
  const issues = [];
1659
+ const values = {};
1660
+ for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
1369
1661
  for (const ref of refs) {
1370
1662
  const { id, params } = parseValidatorRef(ref);
1371
1663
  const validator = registry[id];
@@ -1404,10 +1696,12 @@ function runCodeValidators(field, schema, values, registry) {
1404
1696
  /**
1405
1697
  * Run code validators for a field group.
1406
1698
  */
1407
- function runGroupValidators(group, schema, values, registry) {
1699
+ function runGroupValidators(group, schema, responses, registry) {
1408
1700
  if (!group.validate) return [];
1409
1701
  const refs = Array.isArray(group.validate) ? group.validate : [group.validate];
1410
1702
  const issues = [];
1703
+ const values = {};
1704
+ for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
1411
1705
  for (const ref of refs) {
1412
1706
  const { id, params } = parseValidatorRef(ref);
1413
1707
  const validator = registry[id];
@@ -1455,10 +1749,10 @@ function validate(form, opts) {
1455
1749
  const registry = opts?.validatorRegistry ?? {};
1456
1750
  for (const group of form.schema.groups) {
1457
1751
  for (const field of group.children) {
1458
- issues.push(...validateField(field, form.valuesByFieldId));
1459
- if (!opts?.skipCodeValidators) issues.push(...runCodeValidators(field, form.schema, form.valuesByFieldId, registry));
1752
+ issues.push(...validateField(field, form.responsesByFieldId));
1753
+ if (!opts?.skipCodeValidators) issues.push(...runCodeValidators(field, form.schema, form.responsesByFieldId, registry));
1460
1754
  }
1461
- if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.valuesByFieldId, registry));
1755
+ if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.responsesByFieldId, registry));
1462
1756
  }
1463
1757
  return {
1464
1758
  issues,
@@ -1485,14 +1779,14 @@ function validate(form, opts) {
1485
1779
  function inspect(form, options = {}) {
1486
1780
  const validationInspectIssues = convertValidationIssues(validate(form, { skipCodeValidators: options.skipCodeValidators }).issues, form);
1487
1781
  const structureSummary = computeStructureSummary(form.schema);
1488
- const progressSummary = computeProgressSummary(form.schema, form.valuesByFieldId, validationInspectIssues);
1782
+ const progressSummary = computeProgressSummary(form.schema, form.responsesByFieldId, form.notes, validationInspectIssues);
1489
1783
  const formState = computeFormState(progressSummary);
1490
- const filteredIssues = filterIssuesByRole(sortAndAssignPriorities(addOptionalEmptyIssues(validationInspectIssues, form, progressSummary.fields), form), form, options.targetRoles);
1784
+ const issues = filterIssuesByRole(sortAndAssignPriorities(addOptionalEmptyIssues(validationInspectIssues, form, progressSummary.fields), form), form, options.targetRoles);
1491
1785
  return {
1492
1786
  structureSummary,
1493
1787
  progressSummary,
1494
- issues: filteredIssues,
1495
- isComplete: !filteredIssues.some((i) => i.severity === "required"),
1788
+ issues,
1789
+ isComplete: issues.length === 0,
1496
1790
  formState
1497
1791
  };
1498
1792
  }
@@ -1511,26 +1805,31 @@ function convertValidationIssues(validationIssues, form) {
1511
1805
  }
1512
1806
  /**
1513
1807
  * Add issues for empty optional fields that don't already have issues.
1808
+ * Fields that have been explicitly skipped do not get optional_empty issues.
1514
1809
  */
1515
1810
  function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
1516
1811
  const issues = [...existingIssues];
1517
1812
  const fieldsWithIssues = new Set(existingIssues.map((i) => i.ref));
1518
- for (const [fieldId, progress] of Object.entries(fieldProgress)) if (progress.state === "empty" && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
1519
- ref: fieldId,
1520
- scope: "field",
1521
- reason: "optional_empty",
1522
- message: "Optional field has no value",
1523
- severity: "recommended",
1524
- priority: 0
1525
- });
1813
+ for (const [fieldId, progress] of Object.entries(fieldProgress)) {
1814
+ if (progress.answerState === "skipped" || progress.answerState === "aborted") continue;
1815
+ if (progress.empty && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
1816
+ ref: fieldId,
1817
+ scope: "field",
1818
+ reason: "optional_empty",
1819
+ message: "Optional field has no value",
1820
+ severity: "recommended",
1821
+ priority: 0
1822
+ });
1823
+ }
1526
1824
  return issues;
1527
1825
  }
1528
1826
  /**
1529
1827
  * Map ValidationIssue to InspectIssue reason code.
1530
1828
  */
1531
1829
  function mapValidationToInspectReason(vi) {
1532
- if (vi.code === "REQUIRED_EMPTY" || vi.message.toLowerCase().includes("required") && vi.message.toLowerCase().includes("empty")) return "required_missing";
1533
- if (vi.code === "INVALID_CHECKBOX_STATE" || vi.code === "CHECKBOXES_INCOMPLETE" || vi.message.toLowerCase().includes("checkbox")) return "checkbox_incomplete";
1830
+ const msg = vi.message.toLowerCase();
1831
+ if (vi.code === "REQUIRED_EMPTY" || msg.includes("required") && msg.includes("empty") || msg.includes("required") && msg.includes("no selection") || msg.includes("required") && msg.includes("no selections") || msg.includes("must be answered") || msg.includes("must be completed") || msg.includes("must be checked")) return "required_missing";
1832
+ if (vi.code === "INVALID_CHECKBOX_STATE" || vi.code === "CHECKBOXES_INCOMPLETE" || msg.includes("checkbox")) return "checkbox_incomplete";
1534
1833
  if (vi.code === "MULTI_SELECT_TOO_FEW" || vi.code === "STRING_LIST_MIN_ITEMS" || vi.message.includes("at least")) return "min_items_not_met";
1535
1834
  return "validation_error";
1536
1835
  }
@@ -1672,7 +1971,9 @@ function isCheckboxComplete(form, fieldId) {
1672
1971
  const field = findFieldById(form, fieldId);
1673
1972
  if (field?.kind !== "checkboxes") return true;
1674
1973
  const checkboxField = field;
1675
- const value = form.valuesByFieldId[fieldId];
1974
+ const response = form.responsesByFieldId[fieldId];
1975
+ if (response?.state !== "answered") return false;
1976
+ const value = response.value;
1676
1977
  if (value?.kind !== "checkboxes") return false;
1677
1978
  const values = value.values;
1678
1979
  const optionIds = checkboxField.options.map((o) => o.id);
@@ -1748,10 +2049,25 @@ function findField(form, fieldId) {
1748
2049
  * Validate a single patch against the form schema.
1749
2050
  */
1750
2051
  function validatePatch(form, patch, index) {
1751
- const field = findField(form, patch.fieldId);
2052
+ if (patch.op === "add_note") {
2053
+ if (!form.idIndex.has(patch.ref)) return {
2054
+ patchIndex: index,
2055
+ message: `Reference "${patch.ref}" not found in form`
2056
+ };
2057
+ return null;
2058
+ }
2059
+ if (patch.op === "remove_note") {
2060
+ if (!form.notes.some((n) => n.id === patch.noteId)) return {
2061
+ patchIndex: index,
2062
+ message: `Note with id '${patch.noteId}' not found`
2063
+ };
2064
+ return null;
2065
+ }
2066
+ const fieldId = patch.fieldId;
2067
+ const field = findField(form, fieldId);
1752
2068
  if (!field) return {
1753
2069
  patchIndex: index,
1754
- message: `Field "${patch.fieldId}" not found`
2070
+ message: `Field "${fieldId}" not found`
1755
2071
  };
1756
2072
  switch (patch.op) {
1757
2073
  case "set_string":
@@ -1812,7 +2128,38 @@ function validatePatch(form, patch, index) {
1812
2128
  };
1813
2129
  break;
1814
2130
  }
2131
+ case "set_url":
2132
+ if (field.kind !== "url") return {
2133
+ patchIndex: index,
2134
+ message: `Cannot apply set_url to ${field.kind} field "${field.id}"`
2135
+ };
2136
+ break;
2137
+ case "set_url_list":
2138
+ if (field.kind !== "url_list") return {
2139
+ patchIndex: index,
2140
+ message: `Cannot apply set_url_list to ${field.kind} field "${field.id}"`
2141
+ };
2142
+ break;
2143
+ case "set_date":
2144
+ if (field.kind !== "date") return {
2145
+ patchIndex: index,
2146
+ message: `Cannot apply set_date to ${field.kind} field "${field.id}"`
2147
+ };
2148
+ break;
2149
+ case "set_year":
2150
+ if (field.kind !== "year") return {
2151
+ patchIndex: index,
2152
+ message: `Cannot apply set_year to ${field.kind} field "${field.id}"`
2153
+ };
2154
+ break;
1815
2155
  case "clear_field": break;
2156
+ case "skip_field":
2157
+ if (field.required) return {
2158
+ patchIndex: index,
2159
+ message: `Cannot skip required field "${field.id}"`
2160
+ };
2161
+ break;
2162
+ case "abort_field": break;
1816
2163
  }
1817
2164
  return null;
1818
2165
  }
@@ -1831,133 +2178,234 @@ function validatePatches(form, patches) {
1831
2178
  return errors;
1832
2179
  }
1833
2180
  /**
2181
+ * Generate a unique note ID for the form.
2182
+ */
2183
+ function generateNoteId(form) {
2184
+ const existingIds = new Set(form.notes.map((n) => n.id));
2185
+ let counter = 1;
2186
+ while (existingIds.has(`n${counter}`)) counter++;
2187
+ return `n${counter}`;
2188
+ }
2189
+ /**
1834
2190
  * Apply a set_string patch.
1835
2191
  */
1836
- function applySetString(values, patch) {
1837
- values[patch.fieldId] = {
1838
- kind: "string",
1839
- value: patch.value
2192
+ function applySetString(responses, patch) {
2193
+ responses[patch.fieldId] = {
2194
+ state: "answered",
2195
+ value: {
2196
+ kind: "string",
2197
+ value: patch.value
2198
+ }
1840
2199
  };
1841
2200
  }
1842
2201
  /**
1843
2202
  * Apply a set_number patch.
1844
2203
  */
1845
- function applySetNumber(values, patch) {
1846
- values[patch.fieldId] = {
1847
- kind: "number",
1848
- value: patch.value
2204
+ function applySetNumber(responses, patch) {
2205
+ responses[patch.fieldId] = {
2206
+ state: "answered",
2207
+ value: {
2208
+ kind: "number",
2209
+ value: patch.value
2210
+ }
1849
2211
  };
1850
2212
  }
1851
2213
  /**
1852
2214
  * Apply a set_string_list patch.
1853
2215
  */
1854
- function applySetStringList(values, patch) {
1855
- values[patch.fieldId] = {
1856
- kind: "string_list",
1857
- items: patch.items
2216
+ function applySetStringList(responses, patch) {
2217
+ responses[patch.fieldId] = {
2218
+ state: "answered",
2219
+ value: {
2220
+ kind: "string_list",
2221
+ items: patch.items
2222
+ }
1858
2223
  };
1859
2224
  }
1860
2225
  /**
1861
2226
  * Apply a set_single_select patch.
1862
2227
  */
1863
- function applySetSingleSelect(values, patch) {
1864
- values[patch.fieldId] = {
1865
- kind: "single_select",
1866
- selected: patch.selected
2228
+ function applySetSingleSelect(responses, patch) {
2229
+ responses[patch.fieldId] = {
2230
+ state: "answered",
2231
+ value: {
2232
+ kind: "single_select",
2233
+ selected: patch.selected
2234
+ }
1867
2235
  };
1868
2236
  }
1869
2237
  /**
1870
2238
  * Apply a set_multi_select patch.
1871
2239
  */
1872
- function applySetMultiSelect(values, patch) {
1873
- values[patch.fieldId] = {
1874
- kind: "multi_select",
1875
- selected: patch.selected
2240
+ function applySetMultiSelect(responses, patch) {
2241
+ responses[patch.fieldId] = {
2242
+ state: "answered",
2243
+ value: {
2244
+ kind: "multi_select",
2245
+ selected: patch.selected
2246
+ }
1876
2247
  };
1877
2248
  }
1878
2249
  /**
1879
2250
  * Apply a set_checkboxes patch (merges with existing values).
1880
2251
  */
1881
- function applySetCheckboxes(values, patch) {
2252
+ function applySetCheckboxes(responses, patch) {
1882
2253
  const merged = {
1883
- ...values[patch.fieldId]?.values ?? {},
2254
+ ...(responses[patch.fieldId]?.value)?.values ?? {},
1884
2255
  ...patch.values
1885
2256
  };
1886
- values[patch.fieldId] = {
1887
- kind: "checkboxes",
1888
- values: merged
2257
+ responses[patch.fieldId] = {
2258
+ state: "answered",
2259
+ value: {
2260
+ kind: "checkboxes",
2261
+ values: merged
2262
+ }
2263
+ };
2264
+ }
2265
+ /**
2266
+ * Apply a set_url patch.
2267
+ */
2268
+ function applySetUrl(responses, patch) {
2269
+ responses[patch.fieldId] = {
2270
+ state: "answered",
2271
+ value: {
2272
+ kind: "url",
2273
+ value: patch.value
2274
+ }
2275
+ };
2276
+ }
2277
+ /**
2278
+ * Apply a set_url_list patch.
2279
+ */
2280
+ function applySetUrlList(responses, patch) {
2281
+ responses[patch.fieldId] = {
2282
+ state: "answered",
2283
+ value: {
2284
+ kind: "url_list",
2285
+ items: patch.items
2286
+ }
2287
+ };
2288
+ }
2289
+ /**
2290
+ * Apply a set_date patch.
2291
+ */
2292
+ function applySetDate(responses, patch) {
2293
+ responses[patch.fieldId] = {
2294
+ state: "answered",
2295
+ value: {
2296
+ kind: "date",
2297
+ value: patch.value
2298
+ }
2299
+ };
2300
+ }
2301
+ /**
2302
+ * Apply a set_year patch.
2303
+ */
2304
+ function applySetYear(responses, patch) {
2305
+ responses[patch.fieldId] = {
2306
+ state: "answered",
2307
+ value: {
2308
+ kind: "year",
2309
+ value: patch.value
2310
+ }
1889
2311
  };
1890
2312
  }
1891
2313
  /**
1892
2314
  * Apply a clear_field patch.
1893
2315
  */
1894
- function applyClearField(form, values, patch) {
1895
- const field = findField(form, patch.fieldId);
1896
- if (!field) return;
1897
- switch (field.kind) {
1898
- case "string":
1899
- values[patch.fieldId] = {
1900
- kind: "string",
1901
- value: null
1902
- };
1903
- break;
1904
- case "number":
1905
- values[patch.fieldId] = {
1906
- kind: "number",
1907
- value: null
1908
- };
1909
- break;
1910
- case "string_list":
1911
- values[patch.fieldId] = {
1912
- kind: "string_list",
1913
- items: []
1914
- };
1915
- break;
1916
- case "single_select":
1917
- values[patch.fieldId] = {
1918
- kind: "single_select",
1919
- selected: null
1920
- };
1921
- break;
1922
- case "multi_select":
1923
- values[patch.fieldId] = {
1924
- kind: "multi_select",
1925
- selected: []
1926
- };
1927
- break;
1928
- case "checkboxes":
1929
- values[patch.fieldId] = {
1930
- kind: "checkboxes",
1931
- values: {}
1932
- };
1933
- break;
1934
- }
2316
+ function applyClearField(responses, patch) {
2317
+ responses[patch.fieldId] = { state: "unanswered" };
1935
2318
  }
1936
2319
  /**
1937
- * Apply a single patch to the values.
2320
+ * Apply a skip_field patch.
2321
+ * Marks the field as skipped and stores reason in FieldResponse.reason.
1938
2322
  */
1939
- function applyPatch(form, values, patch) {
2323
+ function applySkipField(responses, patch) {
2324
+ responses[patch.fieldId] = {
2325
+ state: "skipped",
2326
+ ...patch.reason && { reason: patch.reason }
2327
+ };
2328
+ }
2329
+ /**
2330
+ * Apply an abort_field patch.
2331
+ * Marks the field as aborted and stores reason in FieldResponse.reason.
2332
+ */
2333
+ function applyAbortField(responses, patch) {
2334
+ responses[patch.fieldId] = {
2335
+ state: "aborted",
2336
+ ...patch.reason && { reason: patch.reason }
2337
+ };
2338
+ }
2339
+ /**
2340
+ * Apply an add_note patch.
2341
+ * Adds a note to the form.
2342
+ */
2343
+ function applyAddNote(form, patch) {
2344
+ const noteId = generateNoteId(form);
2345
+ form.notes.push({
2346
+ id: noteId,
2347
+ ref: patch.ref,
2348
+ role: patch.role,
2349
+ text: patch.text
2350
+ });
2351
+ }
2352
+ /**
2353
+ * Apply a remove_note patch.
2354
+ * Removes a specific note by ID.
2355
+ */
2356
+ function applyRemoveNote(form, patch) {
2357
+ const index = form.notes.findIndex((n) => n.id === patch.noteId);
2358
+ if (index >= 0) form.notes.splice(index, 1);
2359
+ }
2360
+ /**
2361
+ * Apply a single patch to the form.
2362
+ */
2363
+ function applyPatch(form, responses, patch) {
1940
2364
  switch (patch.op) {
1941
2365
  case "set_string":
1942
- applySetString(values, patch);
2366
+ applySetString(responses, patch);
1943
2367
  break;
1944
2368
  case "set_number":
1945
- applySetNumber(values, patch);
2369
+ applySetNumber(responses, patch);
1946
2370
  break;
1947
2371
  case "set_string_list":
1948
- applySetStringList(values, patch);
2372
+ applySetStringList(responses, patch);
1949
2373
  break;
1950
2374
  case "set_single_select":
1951
- applySetSingleSelect(values, patch);
2375
+ applySetSingleSelect(responses, patch);
1952
2376
  break;
1953
2377
  case "set_multi_select":
1954
- applySetMultiSelect(values, patch);
2378
+ applySetMultiSelect(responses, patch);
1955
2379
  break;
1956
2380
  case "set_checkboxes":
1957
- applySetCheckboxes(values, patch);
2381
+ applySetCheckboxes(responses, patch);
2382
+ break;
2383
+ case "set_url":
2384
+ applySetUrl(responses, patch);
2385
+ break;
2386
+ case "set_url_list":
2387
+ applySetUrlList(responses, patch);
2388
+ break;
2389
+ case "set_date":
2390
+ applySetDate(responses, patch);
2391
+ break;
2392
+ case "set_year":
2393
+ applySetYear(responses, patch);
1958
2394
  break;
1959
2395
  case "clear_field":
1960
- applyClearField(form, values, patch);
2396
+ applyClearField(responses, patch);
2397
+ break;
2398
+ case "skip_field":
2399
+ applySkipField(responses, patch);
2400
+ break;
2401
+ case "abort_field":
2402
+ applyAbortField(responses, patch);
2403
+ break;
2404
+ case "add_note":
2405
+ applyAddNote(form, patch);
2406
+ break;
2407
+ case "remove_note":
2408
+ applyRemoveNote(form, patch);
1961
2409
  break;
1962
2410
  }
1963
2411
  }
@@ -1989,8 +2437,8 @@ function convertToInspectIssues(form) {
1989
2437
  */
1990
2438
  function applyPatches(form, patches) {
1991
2439
  if (validatePatches(form, patches).length > 0) {
1992
- const summaries$1 = computeAllSummaries(form.schema, form.valuesByFieldId, []);
1993
2440
  const issues$1 = convertToInspectIssues(form);
2441
+ const summaries$1 = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues$1);
1994
2442
  return {
1995
2443
  applyStatus: "rejected",
1996
2444
  structureSummary: summaries$1.structureSummary,
@@ -2000,11 +2448,14 @@ function applyPatches(form, patches) {
2000
2448
  formState: summaries$1.formState
2001
2449
  };
2002
2450
  }
2003
- const newValues = { ...form.valuesByFieldId };
2004
- for (const patch of patches) applyPatch(form, newValues, patch);
2005
- form.valuesByFieldId = newValues;
2451
+ const newResponses = { ...form.responsesByFieldId };
2452
+ const newNotes = [...form.notes];
2453
+ form.notes;
2454
+ form.notes = newNotes;
2455
+ for (const patch of patches) applyPatch(form, newResponses, patch);
2456
+ form.responsesByFieldId = newResponses;
2006
2457
  const issues = convertToInspectIssues(form);
2007
- const summaries = computeAllSummaries(form.schema, newValues, issues);
2458
+ const summaries = computeAllSummaries(form.schema, newResponses, newNotes, issues);
2008
2459
  return {
2009
2460
  applyStatus: "applied",
2010
2461
  structureSummary: summaries.structureSummary,
@@ -2016,4 +2467,4 @@ function applyPatches(form, patches) {
2016
2467
  }
2017
2468
 
2018
2469
  //#endregion
2019
- export { PatchSchema as $, DocumentationBlockSchema as A, IdSchema as B, ApplyResultSchema as C, StructureSummarySchema as Ct, CheckboxesFieldSchema as D, CheckboxValueSchema as E, FieldProgressSchema as F, MarkformFrontmatterSchema as G, InspectResultSchema as H, FieldSchema as I, MultiSelectValueSchema as J, MultiCheckboxStateSchema as K, FieldValueSchema as L, ExplicitCheckboxValueSchema as M, FieldGroupSchema as N, CheckboxesValueSchema as O, FieldKindSchema as P, OptionSchema as Q, FormSchemaSchema as R, parseRolesFlag as S, StringValueSchema as St, CheckboxProgressCountsSchema as T, ValidatorRefSchema as Tt, IssueReasonSchema as U, InspectIssueSchema as V, IssueScopeSchema as W, NumberValueSchema as X, NumberFieldSchema as Y, OptionIdSchema as Z, DEFAULT_PRIORITY as _, SourceRangeSchema as _t, computeAllSummaries as a, SessionTurnSchema as at, USER_ROLE as b, StringListFieldSchema as bt, computeStructureSummary as c, SetNumberPatchSchema as ct, serializeRawMarkdown as d, SetStringPatchSchema as dt, ProgressCountsSchema as et, AGENT_ROLE as f, SeveritySchema as ft, DEFAULT_PORT as g, SourcePositionSchema as gt, DEFAULT_MAX_TURNS as h, SingleSelectValueSchema as ht, validate as i, SessionTranscriptSchema as it, DocumentationTagSchema as j, ClearFieldPatchSchema as k, isFormComplete as l, SetSingleSelectPatchSchema as lt, DEFAULT_MAX_PATCHES_PER_TURN as m, SingleSelectFieldSchema as mt, getFieldsForRoles as n, ProgressSummarySchema as nt, computeFormState as o, SetCheckboxesPatchSchema as ot, DEFAULT_MAX_ISSUES as p, SimpleCheckboxStateSchema as pt, MultiSelectFieldSchema as q, inspect as r, SessionFinalSchema as rt, computeProgressSummary as s, SetMultiSelectPatchSchema as st, applyPatches as t, ProgressStateSchema as tt, serialize as u, SetStringListPatchSchema as ut, DEFAULT_ROLE_INSTRUCTIONS as v, StepResultSchema as vt, CheckboxModeSchema as w, ValidationIssueSchema as wt, formatSuggestedLlms as x, StringListValueSchema as xt, SUGGESTED_LLMS as y, StringFieldSchema as yt, HarnessConfigSchema as z };
2470
+ export { SUGGESTED_LLMS as A, DEFAULT_ROLE_INSTRUCTIONS as C, detectFileType as D, deriveExportPath as E, formatSuggestedLlms as M, getWebSearchConfig as N, getFormsDir as O, hasWebSearchSupport as P, DEFAULT_ROLES as S, USER_ROLE as T, DEFAULT_MAX_TURNS as _, computeAllSummaries as a, DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN as b, computeStructureSummary as c, serializeRawMarkdown as d, serializeReportMarkdown as f, DEFAULT_MAX_PATCHES_PER_TURN as g, DEFAULT_MAX_ISSUES_PER_TURN as h, validate as i, WEB_SEARCH_CONFIG as j, parseRolesFlag as k, isFormComplete as l, DEFAULT_FORMS_DIR as m, getFieldsForRoles as n, computeFormState as o, AGENT_ROLE as p, inspect as r, computeProgressSummary as s, applyPatches as t, serialize as u, DEFAULT_PORT as v, REPORT_EXTENSION as w, DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN as x, DEFAULT_PRIORITY as y };