markform 0.1.1 → 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 (36) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +338 -71
  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-BQdd-fdx.mjs → apply-BfAGTHMh.mjs} +837 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-B3NVm6zL.mjs} +1349 -422
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +116 -19
  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 +17 -13
  26. package/examples/simple/simple-skipped-filled.form.md +32 -9
  27. package/examples/simple/simple-with-skips.session.yaml +102 -143
  28. package/examples/simple/simple.form.md +13 -13
  29. package/examples/simple/simple.session.yaml +80 -69
  30. package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
  31. package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
  32. package/examples/startup-research/startup-research.form.md +1 -1
  33. package/package.json +9 -5
  34. package/dist/src-Cs4_9lWP.mjs +0 -2151
  35. package/examples/political-research/political-research.form.md +0 -233
  36. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
@@ -1,444 +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 MockModeSchema = z.enum(["mock", "live"]);
34
- const ApprovalModeSchema = z.enum(["none", "blocking"]);
35
- const FieldKindSchema = z.enum([
36
- "string",
37
- "number",
38
- "string_list",
39
- "checkboxes",
40
- "single_select",
41
- "multi_select",
42
- "url",
43
- "url_list"
44
- ]);
45
- const FieldPriorityLevelSchema = z.enum([
46
- "high",
47
- "medium",
48
- "low"
49
- ]);
50
- const OptionSchema = z.object({
51
- id: IdSchema,
52
- label: z.string()
53
- });
54
- const FieldBaseSchemaPartial = {
55
- id: IdSchema,
56
- label: z.string(),
57
- required: z.boolean(),
58
- priority: FieldPriorityLevelSchema,
59
- role: z.string(),
60
- 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"]
35
+ };
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 }
61
75
  };
62
- const StringFieldSchema = z.object({
63
- ...FieldBaseSchemaPartial,
64
- kind: z.literal("string"),
65
- multiline: z.boolean().optional(),
66
- pattern: z.string().optional(),
67
- minLength: z.number().int().nonnegative().optional(),
68
- maxLength: z.number().int().nonnegative().optional()
69
- });
70
- const NumberFieldSchema = z.object({
71
- ...FieldBaseSchemaPartial,
72
- kind: z.literal("number"),
73
- min: z.number().optional(),
74
- max: z.number().optional(),
75
- integer: z.boolean().optional()
76
- });
77
- const StringListFieldSchema = z.object({
78
- ...FieldBaseSchemaPartial,
79
- kind: z.literal("string_list"),
80
- minItems: z.number().int().nonnegative().optional(),
81
- maxItems: z.number().int().nonnegative().optional(),
82
- itemMinLength: z.number().int().nonnegative().optional(),
83
- itemMaxLength: z.number().int().nonnegative().optional(),
84
- uniqueItems: z.boolean().optional()
85
- });
86
- const CheckboxesFieldSchema = z.object({
87
- ...FieldBaseSchemaPartial,
88
- kind: z.literal("checkboxes"),
89
- checkboxMode: CheckboxModeSchema,
90
- minDone: z.number().int().optional(),
91
- options: z.array(OptionSchema),
92
- approvalMode: ApprovalModeSchema
93
- });
94
- const SingleSelectFieldSchema = z.object({
95
- ...FieldBaseSchemaPartial,
96
- kind: z.literal("single_select"),
97
- options: z.array(OptionSchema)
98
- });
99
- const MultiSelectFieldSchema = z.object({
100
- ...FieldBaseSchemaPartial,
101
- kind: z.literal("multi_select"),
102
- options: z.array(OptionSchema),
103
- minSelections: z.number().int().nonnegative().optional(),
104
- maxSelections: z.number().int().nonnegative().optional()
105
- });
106
- const UrlFieldSchema = z.object({
107
- ...FieldBaseSchemaPartial,
108
- kind: z.literal("url")
109
- });
110
- const UrlListFieldSchema = z.object({
111
- ...FieldBaseSchemaPartial,
112
- kind: z.literal("url_list"),
113
- minItems: z.number().int().nonnegative().optional(),
114
- maxItems: z.number().int().nonnegative().optional(),
115
- uniqueItems: z.boolean().optional()
116
- });
117
- const FieldSchema = z.discriminatedUnion("kind", [
118
- StringFieldSchema,
119
- NumberFieldSchema,
120
- StringListFieldSchema,
121
- CheckboxesFieldSchema,
122
- SingleSelectFieldSchema,
123
- MultiSelectFieldSchema,
124
- UrlFieldSchema,
125
- UrlListFieldSchema
126
- ]);
127
- const FieldGroupSchema = z.object({
128
- kind: z.literal("field_group"),
129
- id: IdSchema,
130
- title: z.string().optional(),
131
- validate: z.array(ValidatorRefSchema).optional(),
132
- children: z.array(FieldSchema)
133
- });
134
- const FormSchemaSchema = z.object({
135
- id: IdSchema,
136
- title: z.string().optional(),
137
- groups: z.array(FieldGroupSchema)
138
- });
139
- const StringValueSchema = z.object({
140
- kind: z.literal("string"),
141
- value: z.string().nullable()
142
- });
143
- const NumberValueSchema = z.object({
144
- kind: z.literal("number"),
145
- value: z.number().nullable()
146
- });
147
- const StringListValueSchema = z.object({
148
- kind: z.literal("string_list"),
149
- items: z.array(z.string())
150
- });
151
- const CheckboxesValueSchema = z.object({
152
- kind: z.literal("checkboxes"),
153
- values: z.record(OptionIdSchema, CheckboxValueSchema)
154
- });
155
- const SingleSelectValueSchema = z.object({
156
- kind: z.literal("single_select"),
157
- selected: OptionIdSchema.nullable()
158
- });
159
- const MultiSelectValueSchema = z.object({
160
- kind: z.literal("multi_select"),
161
- selected: z.array(OptionIdSchema)
162
- });
163
- const UrlValueSchema = z.object({
164
- kind: z.literal("url"),
165
- value: z.string().nullable()
166
- });
167
- const UrlListValueSchema = z.object({
168
- kind: z.literal("url_list"),
169
- items: z.array(z.string())
170
- });
171
- const FieldValueSchema = z.discriminatedUnion("kind", [
172
- StringValueSchema,
173
- NumberValueSchema,
174
- StringListValueSchema,
175
- CheckboxesValueSchema,
176
- SingleSelectValueSchema,
177
- MultiSelectValueSchema,
178
- UrlValueSchema,
179
- UrlListValueSchema
180
- ]);
181
- const DocumentationTagSchema = z.enum([
182
- "description",
183
- "instructions",
184
- "documentation"
185
- ]);
186
- const DocumentationBlockSchema = z.object({
187
- tag: DocumentationTagSchema,
188
- ref: z.string(),
189
- bodyMarkdown: z.string()
190
- });
191
- const FormMetadataSchema = z.object({
192
- markformVersion: z.string(),
193
- roles: z.array(z.string()).min(1),
194
- roleInstructions: z.record(z.string(), z.string())
195
- });
196
- const SeveritySchema = z.enum([
197
- "error",
198
- "warning",
199
- "info"
200
- ]);
201
- const SourcePositionSchema = z.object({
202
- line: z.number().int().positive(),
203
- col: z.number().int().positive()
204
- });
205
- const SourceRangeSchema = z.object({
206
- start: SourcePositionSchema,
207
- end: SourcePositionSchema
208
- });
209
- const ValidationIssueSchema = z.object({
210
- severity: SeveritySchema,
211
- message: z.string(),
212
- code: z.string().optional(),
213
- ref: IdSchema.optional(),
214
- path: z.string().optional(),
215
- range: SourceRangeSchema.optional(),
216
- validatorId: z.string().optional(),
217
- source: z.enum([
218
- "builtin",
219
- "code",
220
- "llm"
221
- ])
222
- });
223
- const IssueReasonSchema = z.enum([
224
- "validation_error",
225
- "required_missing",
226
- "checkbox_incomplete",
227
- "min_items_not_met",
228
- "optional_empty"
229
- ]);
230
- const IssueScopeSchema = z.enum([
231
- "form",
232
- "group",
233
- "field",
234
- "option"
235
- ]);
236
- const InspectIssueSchema = z.object({
237
- ref: z.union([IdSchema, z.string()]),
238
- scope: IssueScopeSchema,
239
- reason: IssueReasonSchema,
240
- message: z.string(),
241
- severity: z.enum(["required", "recommended"]),
242
- priority: z.number().int().positive(),
243
- blockedBy: IdSchema.optional()
244
- });
245
- const ProgressStateSchema = z.enum([
246
- "empty",
247
- "incomplete",
248
- "invalid",
249
- "complete"
250
- ]);
251
- const CheckboxProgressCountsSchema = z.object({
252
- total: z.number().int().nonnegative(),
253
- todo: z.number().int().nonnegative(),
254
- done: z.number().int().nonnegative(),
255
- incomplete: z.number().int().nonnegative(),
256
- active: z.number().int().nonnegative(),
257
- na: z.number().int().nonnegative(),
258
- unfilled: z.number().int().nonnegative(),
259
- yes: z.number().int().nonnegative(),
260
- no: z.number().int().nonnegative()
261
- });
262
- const FieldProgressSchema = z.object({
263
- kind: FieldKindSchema,
264
- required: z.boolean(),
265
- submitted: z.boolean(),
266
- state: ProgressStateSchema,
267
- valid: z.boolean(),
268
- issueCount: z.number().int().nonnegative(),
269
- checkboxProgress: CheckboxProgressCountsSchema.optional(),
270
- skipped: z.boolean(),
271
- skipReason: z.string().optional()
272
- });
273
- const ProgressCountsSchema = z.object({
274
- totalFields: z.number().int().nonnegative(),
275
- requiredFields: z.number().int().nonnegative(),
276
- submittedFields: z.number().int().nonnegative(),
277
- completeFields: z.number().int().nonnegative(),
278
- incompleteFields: z.number().int().nonnegative(),
279
- invalidFields: z.number().int().nonnegative(),
280
- emptyRequiredFields: z.number().int().nonnegative(),
281
- emptyOptionalFields: z.number().int().nonnegative(),
282
- answeredFields: z.number().int().nonnegative(),
283
- skippedFields: z.number().int().nonnegative()
284
- });
285
- const ProgressSummarySchema = z.object({
286
- counts: ProgressCountsSchema,
287
- fields: z.record(IdSchema, FieldProgressSchema)
288
- });
289
- const StructureSummarySchema = z.object({
290
- groupCount: z.number().int().nonnegative(),
291
- fieldCount: z.number().int().nonnegative(),
292
- optionCount: z.number().int().nonnegative(),
293
- fieldCountByKind: z.record(FieldKindSchema, z.number().int().nonnegative()),
294
- groupsById: z.record(IdSchema, z.literal("field_group")),
295
- fieldsById: z.record(IdSchema, FieldKindSchema),
296
- optionsById: z.record(z.string(), z.object({
297
- parentFieldId: IdSchema,
298
- parentFieldKind: FieldKindSchema
299
- }))
300
- });
301
- const InspectResultSchema = z.object({
302
- structureSummary: StructureSummarySchema,
303
- progressSummary: ProgressSummarySchema,
304
- issues: z.array(InspectIssueSchema),
305
- isComplete: z.boolean(),
306
- formState: ProgressStateSchema
307
- });
308
- const ApplyResultSchema = z.object({
309
- applyStatus: z.enum(["applied", "rejected"]),
310
- structureSummary: StructureSummarySchema,
311
- progressSummary: ProgressSummarySchema,
312
- issues: z.array(InspectIssueSchema),
313
- isComplete: z.boolean(),
314
- formState: ProgressStateSchema
315
- });
316
- const SetStringPatchSchema = z.object({
317
- op: z.literal("set_string"),
318
- fieldId: IdSchema,
319
- value: z.string().nullable()
320
- });
321
- const SetNumberPatchSchema = z.object({
322
- op: z.literal("set_number"),
323
- fieldId: IdSchema,
324
- value: z.number().nullable()
325
- });
326
- const SetStringListPatchSchema = z.object({
327
- op: z.literal("set_string_list"),
328
- fieldId: IdSchema,
329
- items: z.array(z.string())
330
- });
331
- const SetCheckboxesPatchSchema = z.object({
332
- op: z.literal("set_checkboxes"),
333
- fieldId: IdSchema,
334
- values: z.record(OptionIdSchema, CheckboxValueSchema)
335
- });
336
- const SetSingleSelectPatchSchema = z.object({
337
- op: z.literal("set_single_select"),
338
- fieldId: IdSchema,
339
- selected: OptionIdSchema.nullable()
340
- });
341
- const SetMultiSelectPatchSchema = z.object({
342
- op: z.literal("set_multi_select"),
343
- fieldId: IdSchema,
344
- selected: z.array(OptionIdSchema)
345
- });
346
- const SetUrlPatchSchema = z.object({
347
- op: z.literal("set_url"),
348
- fieldId: IdSchema,
349
- value: z.string().nullable()
350
- });
351
- const SetUrlListPatchSchema = z.object({
352
- op: z.literal("set_url_list"),
353
- fieldId: IdSchema,
354
- items: z.array(z.string())
355
- });
356
- const ClearFieldPatchSchema = z.object({
357
- op: z.literal("clear_field"),
358
- fieldId: IdSchema
359
- });
360
- const SkipFieldPatchSchema = z.object({
361
- op: z.literal("skip_field"),
362
- fieldId: IdSchema,
363
- reason: z.string().optional()
364
- });
365
- const PatchSchema = z.discriminatedUnion("op", [
366
- SetStringPatchSchema,
367
- SetNumberPatchSchema,
368
- SetStringListPatchSchema,
369
- SetCheckboxesPatchSchema,
370
- SetSingleSelectPatchSchema,
371
- SetMultiSelectPatchSchema,
372
- SetUrlPatchSchema,
373
- SetUrlListPatchSchema,
374
- ClearFieldPatchSchema,
375
- SkipFieldPatchSchema
376
- ]);
377
- const StepResultSchema = z.object({
378
- structureSummary: StructureSummarySchema,
379
- progressSummary: ProgressSummarySchema,
380
- issues: z.array(InspectIssueSchema),
381
- stepBudget: z.number().int().nonnegative(),
382
- isComplete: z.boolean(),
383
- turnNumber: z.number().int().positive()
384
- });
385
- const HarnessConfigSchema = z.object({
386
- maxIssues: z.number().int().positive(),
387
- maxPatchesPerTurn: z.number().int().positive(),
388
- maxTurns: z.number().int().positive(),
389
- maxFieldsPerTurn: z.number().int().positive().optional(),
390
- maxGroupsPerTurn: z.number().int().positive().optional(),
391
- targetRoles: z.array(z.string()).optional(),
392
- fillMode: FillModeSchema.optional()
393
- });
394
- const SessionTurnStatsSchema = z.object({
395
- inputTokens: z.number().int().nonnegative().optional(),
396
- outputTokens: z.number().int().nonnegative().optional(),
397
- toolCalls: z.array(z.object({
398
- name: z.string(),
399
- count: z.number().int().positive()
400
- })).optional()
401
- });
402
- const SessionTurnSchema = z.object({
403
- turn: z.number().int().positive(),
404
- inspect: z.object({ issues: z.array(InspectIssueSchema) }),
405
- apply: z.object({ patches: z.array(PatchSchema) }),
406
- after: z.object({
407
- requiredIssueCount: z.number().int().nonnegative(),
408
- markdownSha256: z.string(),
409
- answeredFieldCount: z.number().int().nonnegative(),
410
- skippedFieldCount: z.number().int().nonnegative()
411
- }),
412
- llm: SessionTurnStatsSchema.optional()
413
- });
414
- const SessionFinalSchema = z.object({
415
- expectComplete: z.boolean(),
416
- expectedCompletedForm: z.string()
417
- });
418
- const SessionTranscriptSchema = z.object({
419
- sessionVersion: z.string(),
420
- mode: MockModeSchema,
421
- form: z.object({ path: z.string() }),
422
- validators: z.object({ code: z.string().optional() }).optional(),
423
- mock: z.object({ completedMock: z.string() }).optional(),
424
- live: z.object({ modelId: z.string() }).optional(),
425
- harness: HarnessConfigSchema,
426
- turns: z.array(SessionTurnSchema),
427
- final: SessionFinalSchema
428
- });
429
- const MarkformFrontmatterSchema = z.object({
430
- markformVersion: z.string(),
431
- formSummary: StructureSummarySchema,
432
- formProgress: ProgressSummarySchema,
433
- formState: ProgressStateSchema
434
- });
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
+ }
435
90
 
436
91
  //#endregion
437
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";
438
106
  /** Default role for fields without explicit role attribute */
439
107
  const AGENT_ROLE = "agent";
440
108
  /** Role for human-filled fields in interactive mode */
441
109
  const USER_ROLE = "user";
110
+ /** Default roles list for forms without explicit roles in frontmatter */
111
+ const DEFAULT_ROLES = [USER_ROLE, AGENT_ROLE];
442
112
  /** Default instructions per role (used when form doesn't specify role_instructions) */
443
113
  const DEFAULT_ROLE_INSTRUCTIONS = {
444
114
  [USER_ROLE]: "Fill in the fields you have direct knowledge of.",
@@ -477,6 +147,22 @@ const DEFAULT_PRIORITY = "medium";
477
147
  */
478
148
  const DEFAULT_PORT = 3344;
479
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
+ /**
480
166
  * Default maximum turns for the fill harness.
481
167
  * Prevents runaway loops during agent execution.
482
168
  */
@@ -486,84 +172,129 @@ const DEFAULT_MAX_TURNS = 100;
486
172
  */
487
173
  const DEFAULT_MAX_PATCHES_PER_TURN = 20;
488
174
  /**
489
- * 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.
490
177
  */
491
- const DEFAULT_MAX_ISSUES = 10;
178
+ const DEFAULT_MAX_ISSUES_PER_TURN = 10;
492
179
  /**
493
- * Suggested LLM models for the fill command, organized by provider.
494
- * These are shown in help/error messages. Only includes models from the
495
- * 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.
496
182
  */
497
- const SUGGESTED_LLMS = {
498
- openai: [
499
- "gpt-5-mini",
500
- "gpt-5-nano",
501
- "gpt-5.1",
502
- "gpt-5-pro",
503
- "gpt-5.2",
504
- "gpt-5.2-pro"
505
- ],
506
- anthropic: [
507
- "claude-opus-4-5",
508
- "claude-opus-4-1",
509
- "claude-sonnet-4-5",
510
- "claude-sonnet-4-0",
511
- "claude-haiku-4-5"
512
- ],
513
- google: [
514
- "gemini-2.5-pro",
515
- "gemini-2.5-flash",
516
- "gemini-2.0-flash",
517
- "gemini-2.0-flash-lite",
518
- "gemini-3-pro-preview"
519
- ],
520
- xai: ["grok-4", "grok-4-fast"],
521
- 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"
522
197
  };
523
198
  /**
524
- * 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.
525
201
  */
526
- function formatSuggestedLlms() {
527
- const lines = ["Available providers and example models:"];
528
- for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
529
- lines.push(` ${provider}/`);
530
- for (const model of models) lines.push(` - ${provider}/${model}`);
531
- }
532
- return lines.join("\n");
533
- }
202
+ const REPORT_EXTENSION = ".report.md";
534
203
  /**
535
- * Web search configuration per provider.
204
+ * All recognized markform file extensions.
205
+ * Combines export formats with report format.
536
206
  */
537
- const WEB_SEARCH_CONFIG = {
538
- openai: {
539
- supported: true,
540
- toolName: "web_search_preview",
541
- exportName: "openaiTools"
542
- },
543
- google: {
544
- supported: true,
545
- toolName: "googleSearch",
546
- exportName: "googleTools"
547
- },
548
- xai: {
549
- supported: true,
550
- toolName: "xai_search"
551
- },
552
- anthropic: { supported: false },
553
- deepseek: { supported: false }
207
+ const ALL_EXTENSIONS = {
208
+ ...EXPORT_EXTENSIONS,
209
+ report: REPORT_EXTENSION
554
210
  };
555
211
  /**
556
- * Get web search tool configuration for a provider.
557
- * Returns undefined if provider doesn't support web search.
212
+ * Detect file type from path based on extension.
213
+ * Used by serve command to dispatch to appropriate renderer.
558
214
  */
559
- function getWebSearchConfig(provider) {
560
- const config = WEB_SEARCH_CONFIG[provider];
561
- return config?.supported ? config : void 0;
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;
233
+ }
234
+ return base + EXPORT_EXTENSIONS[format];
562
235
  }
563
236
 
564
237
  //#endregion
565
238
  //#region src/engine/serialize.ts
566
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
+ /**
567
298
  * Serialize an attribute value to Markdoc format.
568
299
  */
569
300
  function serializeAttrValue(value) {
@@ -609,7 +340,7 @@ function getMarker(state) {
609
340
  /**
610
341
  * Serialize a string field.
611
342
  */
612
- function serializeStringField(field, value) {
343
+ function serializeStringField(field, response) {
613
344
  const attrs = {
614
345
  id: field.id,
615
346
  label: field.label
@@ -622,15 +353,22 @@ function serializeStringField(field, value) {
622
353
  if (field.minLength !== void 0) attrs.minLength = field.minLength;
623
354
  if (field.maxLength !== void 0) attrs.maxLength = field.maxLength;
624
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;
625
358
  const attrStr = serializeAttrs(attrs);
626
359
  let content = "";
627
- 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;
628
366
  return `{% string-field ${attrStr} %}${content}{% /string-field %}`;
629
367
  }
630
368
  /**
631
369
  * Serialize a number field.
632
370
  */
633
- function serializeNumberField(field, value) {
371
+ function serializeNumberField(field, response) {
634
372
  const attrs = {
635
373
  id: field.id,
636
374
  label: field.label
@@ -642,15 +380,22 @@ function serializeNumberField(field, value) {
642
380
  if (field.max !== void 0) attrs.max = field.max;
643
381
  if (field.integer) attrs.integer = field.integer;
644
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;
645
385
  const attrStr = serializeAttrs(attrs);
646
386
  let content = "";
647
- 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;
648
393
  return `{% number-field ${attrStr} %}${content}{% /number-field %}`;
649
394
  }
650
395
  /**
651
396
  * Serialize a string-list field.
652
397
  */
653
- function serializeStringListField(field, value) {
398
+ function serializeStringListField(field, response) {
654
399
  const attrs = {
655
400
  id: field.id,
656
401
  label: field.label
@@ -664,9 +409,16 @@ function serializeStringListField(field, value) {
664
409
  if (field.itemMaxLength !== void 0) attrs.itemMaxLength = field.itemMaxLength;
665
410
  if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
666
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;
667
414
  const attrStr = serializeAttrs(attrs);
668
415
  let content = "";
669
- 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;
670
422
  return `{% string-list ${attrStr} %}${content}{% /string-list %}`;
671
423
  }
672
424
  /**
@@ -683,7 +435,7 @@ function serializeOptions(options, selected) {
683
435
  /**
684
436
  * Serialize a single-select field.
685
437
  */
686
- function serializeSingleSelectField(field, value) {
438
+ function serializeSingleSelectField(field, response) {
687
439
  const attrs = {
688
440
  id: field.id,
689
441
  label: field.label
@@ -692,7 +444,11 @@ function serializeSingleSelectField(field, value) {
692
444
  if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
693
445
  if (field.role !== AGENT_ROLE) attrs.role = field.role;
694
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;
695
449
  const attrStr = serializeAttrs(attrs);
450
+ let value;
451
+ if (response?.state === "answered" && response.value) value = response.value;
696
452
  const selected = {};
697
453
  for (const opt of field.options) selected[opt.id] = opt.id === value?.selected ? "done" : "todo";
698
454
  return `{% single-select ${attrStr} %}\n${serializeOptions(field.options, selected)}\n{% /single-select %}`;
@@ -700,7 +456,7 @@ function serializeSingleSelectField(field, value) {
700
456
  /**
701
457
  * Serialize a multi-select field.
702
458
  */
703
- function serializeMultiSelectField(field, value) {
459
+ function serializeMultiSelectField(field, response) {
704
460
  const attrs = {
705
461
  id: field.id,
706
462
  label: field.label
@@ -711,7 +467,11 @@ function serializeMultiSelectField(field, value) {
711
467
  if (field.minSelections !== void 0) attrs.minSelections = field.minSelections;
712
468
  if (field.maxSelections !== void 0) attrs.maxSelections = field.maxSelections;
713
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;
714
472
  const attrStr = serializeAttrs(attrs);
473
+ let value;
474
+ if (response?.state === "answered" && response.value) value = response.value;
715
475
  const selected = {};
716
476
  const selectedSet = new Set(value?.selected ?? []);
717
477
  for (const opt of field.options) selected[opt.id] = selectedSet.has(opt.id) ? "done" : "todo";
@@ -720,7 +480,7 @@ function serializeMultiSelectField(field, value) {
720
480
  /**
721
481
  * Serialize a checkboxes field.
722
482
  */
723
- function serializeCheckboxesField(field, value) {
483
+ function serializeCheckboxesField(field, response) {
724
484
  const attrs = {
725
485
  id: field.id,
726
486
  label: field.label
@@ -732,12 +492,17 @@ function serializeCheckboxesField(field, value) {
732
492
  if (field.minDone !== void 0) attrs.minDone = field.minDone;
733
493
  if (field.approvalMode !== "none") attrs.approvalMode = field.approvalMode;
734
494
  if (field.validate) attrs.validate = field.validate;
735
- 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 %}`;
736
501
  }
737
502
  /**
738
503
  * Serialize a url-field.
739
504
  */
740
- function serializeUrlField(field, value) {
505
+ function serializeUrlField(field, response) {
741
506
  const attrs = {
742
507
  id: field.id,
743
508
  label: field.label
@@ -746,15 +511,22 @@ function serializeUrlField(field, value) {
746
511
  if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
747
512
  if (field.role !== AGENT_ROLE) attrs.role = field.role;
748
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;
749
516
  const attrStr = serializeAttrs(attrs);
750
517
  let content = "";
751
- if (value?.value) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
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;
752
524
  return `{% url-field ${attrStr} %}${content}{% /url-field %}`;
753
525
  }
754
526
  /**
755
527
  * Serialize a url-list field.
756
528
  */
757
- function serializeUrlListField(field, value) {
529
+ function serializeUrlListField(field, response) {
758
530
  const attrs = {
759
531
  id: field.id,
760
532
  label: field.label
@@ -766,25 +538,86 @@ function serializeUrlListField(field, value) {
766
538
  if (field.maxItems !== void 0) attrs.maxItems = field.maxItems;
767
539
  if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
768
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;
769
543
  const attrStr = serializeAttrs(attrs);
770
544
  let content = "";
771
- if (value?.items && value.items.length > 0) content = `\n\`\`\`value\n${value.items.join("\n")}\n\`\`\`\n`;
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;
772
551
  return `{% url-list ${attrStr} %}${content}{% /url-list %}`;
773
552
  }
774
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 %}`;
604
+ }
605
+ /**
775
606
  * Serialize a field to Markdoc format.
776
607
  */
777
- function serializeField(field, values) {
778
- const value = values[field.id];
608
+ function serializeField(field, responses) {
609
+ const response = responses[field.id];
779
610
  switch (field.kind) {
780
- case "string": return serializeStringField(field, value);
781
- case "number": return serializeNumberField(field, value);
782
- case "string_list": return serializeStringListField(field, value);
783
- case "single_select": return serializeSingleSelectField(field, value);
784
- case "multi_select": return serializeMultiSelectField(field, value);
785
- case "checkboxes": return serializeCheckboxesField(field, value);
786
- case "url": return serializeUrlField(field, value);
787
- case "url_list": return serializeUrlListField(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);
788
621
  }
789
622
  }
790
623
  /**
@@ -792,16 +625,39 @@ function serializeField(field, values) {
792
625
  * Uses the semantic tag name (description, instructions, documentation).
793
626
  */
794
627
  function serializeDocBlock(doc) {
795
- 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);
796
631
  return `{% ${doc.tag} ${attrStr} %}\n${doc.bodyMarkdown}\n{% /${doc.tag} %}`;
797
632
  }
798
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
+ /**
799
654
  * Serialize a field group.
800
655
  */
801
- function serializeFieldGroup(group, values, docs) {
656
+ function serializeFieldGroup(group, responses, docs) {
802
657
  const attrs = { id: group.id };
803
658
  if (group.title) attrs.title = group.title;
804
659
  if (group.validate) attrs.validate = group.validate;
660
+ if (group.report !== void 0) attrs.report = group.report;
805
661
  const lines = [`{% field-group ${serializeAttrs(attrs)} %}`];
806
662
  const docsByRef = /* @__PURE__ */ new Map();
807
663
  for (const doc of docs) {
@@ -811,7 +667,7 @@ function serializeFieldGroup(group, values, docs) {
811
667
  }
812
668
  for (const field of group.children) {
813
669
  lines.push("");
814
- lines.push(serializeField(field, values));
670
+ lines.push(serializeField(field, responses));
815
671
  const fieldDocs = docsByRef.get(field.id);
816
672
  if (fieldDocs) for (const doc of fieldDocs) {
817
673
  lines.push("");
@@ -825,7 +681,7 @@ function serializeFieldGroup(group, values, docs) {
825
681
  /**
826
682
  * Serialize a form schema.
827
683
  */
828
- function serializeFormSchema(schema, values, docs) {
684
+ function serializeFormSchema(schema, responses, docs, notes) {
829
685
  const attrs = { id: schema.id };
830
686
  if (schema.title) attrs.title = schema.title;
831
687
  const lines = [`{% form ${serializeAttrs(attrs)} %}`];
@@ -842,7 +698,12 @@ function serializeFormSchema(schema, values, docs) {
842
698
  }
843
699
  for (const group of schema.groups) {
844
700
  lines.push("");
845
- 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);
846
707
  }
847
708
  lines.push("");
848
709
  lines.push("{% /form %}");
@@ -858,8 +719,8 @@ function serializeFormSchema(schema, values, docs) {
858
719
  function serialize(form, opts) {
859
720
  return `${`---
860
721
  markform:
861
- markform_version: "${opts?.markformVersion ?? "0.1.0"}"
862
- ---`}\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`;
863
724
  }
864
725
  /** Map checkbox state to GFM marker for raw markdown output */
865
726
  const STATE_TO_GFM_MARKER = {
@@ -875,10 +736,11 @@ const STATE_TO_GFM_MARKER = {
875
736
  /**
876
737
  * Serialize a field value to raw markdown (human-readable).
877
738
  */
878
- function serializeFieldRaw(field, values) {
879
- const value = values[field.id];
739
+ function serializeFieldRaw(field, responses) {
740
+ const response = responses[field.id];
880
741
  const lines = [];
881
742
  lines.push(`**${field.label}:**`);
743
+ const value = response?.state === "answered" ? response.value : void 0;
882
744
  switch (field.kind) {
883
745
  case "string": {
884
746
  const strValue = value;
@@ -933,6 +795,18 @@ function serializeFieldRaw(field, values) {
933
795
  else lines.push("_(empty)_");
934
796
  break;
935
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
+ }
936
810
  }
937
811
  return lines.join("\n");
938
812
  }
@@ -973,7 +847,70 @@ function serializeRawMarkdown(form) {
973
847
  lines.push("");
974
848
  }
975
849
  for (const field of group.children) {
976
- 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));
977
914
  lines.push("");
978
915
  const fieldDocs = docsByRef.get(field.id);
979
916
  if (fieldDocs) for (const doc of fieldDocs) {
@@ -1002,7 +939,9 @@ function computeStructureSummary(schema) {
1002
939
  single_select: 0,
1003
940
  multi_select: 0,
1004
941
  url: 0,
1005
- url_list: 0
942
+ url_list: 0,
943
+ date: 0,
944
+ year: 0
1006
945
  };
1007
946
  const groupsById = {};
1008
947
  const fieldsById = {};
@@ -1068,6 +1007,11 @@ function isFieldSubmitted(field, value) {
1068
1007
  return v.value !== null && v.value.trim() !== "";
1069
1008
  }
1070
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;
1071
1015
  }
1072
1016
  }
1073
1017
  /**
@@ -1097,50 +1041,35 @@ function computeCheckboxProgress(field, value) {
1097
1041
  return result;
1098
1042
  }
1099
1043
  /**
1100
- * Check if a checkboxes field is complete based on its mode.
1101
- */
1102
- function isCheckboxesComplete(field, value) {
1103
- if (!value) return false;
1104
- const mode = field.checkboxMode ?? "multi";
1105
- for (const opt of field.options) {
1106
- const state = value.values[opt.id];
1107
- if (mode === "explicit") {
1108
- if (state === "unfilled") return false;
1109
- } else if (mode === "multi") {
1110
- if (state === "todo" || state === "incomplete" || state === "active") return false;
1111
- }
1112
- }
1113
- return true;
1114
- }
1115
- /**
1116
- * Compute the progress state for a field.
1044
+ * Compute whether a field is empty (has no value).
1117
1045
  */
1118
- function computeFieldState(field, value, issueCount) {
1119
- if (!isFieldSubmitted(field, value)) return "empty";
1120
- if (issueCount > 0) return "invalid";
1121
- if (field.kind === "checkboxes" && value?.kind === "checkboxes") {
1122
- if (!isCheckboxesComplete(field, value)) return "incomplete";
1123
- }
1124
- return "complete";
1046
+ function isFieldEmpty(field, value) {
1047
+ return !isFieldSubmitted(field, value);
1125
1048
  }
1126
1049
  /**
1127
1050
  * Compute progress for a single field.
1128
1051
  */
1129
- function computeFieldProgress(field, value, issues, skipInfo) {
1130
- const issueCount = issues.filter((i) => i.ref === field.id).length;
1131
- const submitted = isFieldSubmitted(field, value);
1132
- const valid = issueCount === 0;
1133
- const state = computeFieldState(field, value, issueCount);
1134
- const skipped = skipInfo?.skipped ?? false;
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;
1135
1064
  const progress = {
1136
1065
  kind: field.kind,
1137
1066
  required: field.required,
1138
- submitted,
1139
- state,
1067
+ answerState: response.state,
1068
+ hasNotes,
1069
+ noteCount,
1070
+ empty,
1140
1071
  valid,
1141
- issueCount,
1142
- skipped,
1143
- skipReason: skipInfo?.reason
1072
+ issueCount
1144
1073
  };
1145
1074
  if (field.kind === "checkboxes") progress.checkboxProgress = computeCheckboxProgress(field, value);
1146
1075
  return progress;
@@ -1149,42 +1078,42 @@ function computeFieldProgress(field, value, issues, skipInfo) {
1149
1078
  * Compute a progress summary for a form.
1150
1079
  *
1151
1080
  * @param schema - The form schema
1152
- * @param values - Current field values
1081
+ * @param responsesByFieldId - Current field responses (state + optional value)
1082
+ * @param notes - Notes attached to fields/groups/form
1153
1083
  * @param issues - Validation issues (from inspect)
1154
- * @param skips - Skip state per field (from skip_field patches)
1155
1084
  * @returns Progress summary with field states and counts
1156
1085
  */
1157
- function computeProgressSummary(schema, values, issues, skips = {}) {
1086
+ function computeProgressSummary(schema, responsesByFieldId, notes, issues) {
1158
1087
  const fields = {};
1159
1088
  const counts = {
1160
1089
  totalFields: 0,
1161
1090
  requiredFields: 0,
1162
- submittedFields: 0,
1163
- completeFields: 0,
1164
- incompleteFields: 0,
1091
+ unansweredFields: 0,
1092
+ answeredFields: 0,
1093
+ skippedFields: 0,
1094
+ abortedFields: 0,
1095
+ validFields: 0,
1165
1096
  invalidFields: 0,
1097
+ emptyFields: 0,
1098
+ filledFields: 0,
1166
1099
  emptyRequiredFields: 0,
1167
- emptyOptionalFields: 0,
1168
- answeredFields: 0,
1169
- skippedFields: 0
1100
+ totalNotes: notes.length
1170
1101
  };
1171
1102
  for (const group of schema.groups) for (const field of group.children) {
1172
- const value = values[field.id];
1173
- const skipInfo = skips[field.id];
1174
- const progress = computeFieldProgress(field, value, issues, skipInfo);
1103
+ const progress = computeFieldProgress(field, responsesByFieldId[field.id] ?? { state: "unanswered" }, notes, issues);
1175
1104
  fields[field.id] = progress;
1176
1105
  counts.totalFields++;
1177
1106
  if (progress.required) counts.requiredFields++;
1178
- if (progress.submitted) {
1179
- counts.submittedFields++;
1180
- counts.answeredFields++;
1181
- }
1182
- if (progress.skipped) counts.skippedFields++;
1183
- if (progress.state === "complete") counts.completeFields++;
1184
- if (progress.state === "incomplete") counts.incompleteFields++;
1185
- if (progress.state === "invalid") counts.invalidFields++;
1186
- if (progress.state === "empty") if (progress.required) counts.emptyRequiredFields++;
1187
- 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++;
1188
1117
  }
1189
1118
  return {
1190
1119
  counts,
@@ -1198,20 +1127,21 @@ function computeProgressSummary(schema, values, issues, skips = {}) {
1198
1127
  * @returns The overall form state
1199
1128
  */
1200
1129
  function computeFormState(progress) {
1130
+ if (progress.counts.abortedFields > 0) return "invalid";
1201
1131
  if (progress.counts.invalidFields > 0) return "invalid";
1202
- if (progress.counts.incompleteFields > 0) return "incomplete";
1203
1132
  if (progress.counts.emptyRequiredFields === 0) return "complete";
1204
- if (progress.counts.submittedFields > 0) return "incomplete";
1133
+ if (progress.counts.answeredFields > 0) return "incomplete";
1205
1134
  return "empty";
1206
1135
  }
1207
1136
  /**
1208
1137
  * Determine if the form is complete (ready for submission).
1209
1138
  *
1210
1139
  * A form is complete when:
1211
- * 1. No required fields are empty
1212
- * 2. No fields have validation errors
1213
- * 3. No fields are in incomplete state (e.g., partial checkbox completion)
1214
- * 4. All fields must be addressed (answered + skipped == total)
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)
1215
1145
  *
1216
1146
  * Every field must be explicitly addressed - either filled with a value or
1217
1147
  * skipped with a reason. This ensures agents fully process all fields.
@@ -1221,7 +1151,8 @@ function computeFormState(progress) {
1221
1151
  */
1222
1152
  function isFormComplete(progress) {
1223
1153
  const { counts } = progress;
1224
- const baseComplete = counts.invalidFields === 0 && counts.incompleteFields === 0 && counts.emptyRequiredFields === 0;
1154
+ if (counts.abortedFields > 0) return false;
1155
+ const baseComplete = counts.invalidFields === 0 && counts.emptyRequiredFields === 0;
1225
1156
  const allFieldsAccountedFor = counts.answeredFields + counts.skippedFields === counts.totalFields;
1226
1157
  return baseComplete && allFieldsAccountedFor;
1227
1158
  }
@@ -1229,14 +1160,14 @@ function isFormComplete(progress) {
1229
1160
  * Compute all summaries for a parsed form.
1230
1161
  *
1231
1162
  * @param schema - The form schema
1232
- * @param values - Current field values
1163
+ * @param responsesByFieldId - Current field responses (state + optional value)
1164
+ * @param notes - Notes attached to fields/groups/form
1233
1165
  * @param issues - Validation issues
1234
- * @param skips - Skip state per field (from skip_field patches)
1235
1166
  * @returns All computed summaries
1236
1167
  */
1237
- function computeAllSummaries(schema, values, issues, skips = {}) {
1168
+ function computeAllSummaries(schema, responsesByFieldId, notes, issues) {
1238
1169
  const structureSummary = computeStructureSummary(schema);
1239
- const progressSummary = computeProgressSummary(schema, values, issues, skips);
1170
+ const progressSummary = computeProgressSummary(schema, responsesByFieldId, notes, issues);
1240
1171
  return {
1241
1172
  structureSummary,
1242
1173
  progressSummary,
@@ -1592,10 +1523,105 @@ function validateUrlListField(field, value) {
1592
1523
  return issues;
1593
1524
  }
1594
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
+ /**
1595
1620
  * Validate a single field.
1596
1621
  */
1597
- function validateField(field, values) {
1598
- 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;
1599
1625
  switch (field.kind) {
1600
1626
  case "string": return validateStringField(field, value);
1601
1627
  case "number": return validateNumberField(field, value);
@@ -1605,6 +1631,8 @@ function validateField(field, values) {
1605
1631
  case "checkboxes": return validateCheckboxesField(field, value);
1606
1632
  case "url": return validateUrlField(field, value);
1607
1633
  case "url_list": return validateUrlListField(field, value);
1634
+ case "date": return validateDateField(field, value);
1635
+ case "year": return validateYearField(field, value);
1608
1636
  }
1609
1637
  }
1610
1638
  /**
@@ -1624,10 +1652,12 @@ function parseValidatorRef(ref) {
1624
1652
  /**
1625
1653
  * Run code validators for a field.
1626
1654
  */
1627
- function runCodeValidators(field, schema, values, registry) {
1655
+ function runCodeValidators(field, schema, responses, registry) {
1628
1656
  if (!field.validate) return [];
1629
1657
  const refs = Array.isArray(field.validate) ? field.validate : [field.validate];
1630
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;
1631
1661
  for (const ref of refs) {
1632
1662
  const { id, params } = parseValidatorRef(ref);
1633
1663
  const validator = registry[id];
@@ -1666,10 +1696,12 @@ function runCodeValidators(field, schema, values, registry) {
1666
1696
  /**
1667
1697
  * Run code validators for a field group.
1668
1698
  */
1669
- function runGroupValidators(group, schema, values, registry) {
1699
+ function runGroupValidators(group, schema, responses, registry) {
1670
1700
  if (!group.validate) return [];
1671
1701
  const refs = Array.isArray(group.validate) ? group.validate : [group.validate];
1672
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;
1673
1705
  for (const ref of refs) {
1674
1706
  const { id, params } = parseValidatorRef(ref);
1675
1707
  const validator = registry[id];
@@ -1717,10 +1749,10 @@ function validate(form, opts) {
1717
1749
  const registry = opts?.validatorRegistry ?? {};
1718
1750
  for (const group of form.schema.groups) {
1719
1751
  for (const field of group.children) {
1720
- issues.push(...validateField(field, form.valuesByFieldId));
1721
- 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));
1722
1754
  }
1723
- 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));
1724
1756
  }
1725
1757
  return {
1726
1758
  issues,
@@ -1747,7 +1779,7 @@ function validate(form, opts) {
1747
1779
  function inspect(form, options = {}) {
1748
1780
  const validationInspectIssues = convertValidationIssues(validate(form, { skipCodeValidators: options.skipCodeValidators }).issues, form);
1749
1781
  const structureSummary = computeStructureSummary(form.schema);
1750
- const progressSummary = computeProgressSummary(form.schema, form.valuesByFieldId, validationInspectIssues, form.skipsByFieldId);
1782
+ const progressSummary = computeProgressSummary(form.schema, form.responsesByFieldId, form.notes, validationInspectIssues);
1751
1783
  const formState = computeFormState(progressSummary);
1752
1784
  const issues = filterIssuesByRole(sortAndAssignPriorities(addOptionalEmptyIssues(validationInspectIssues, form, progressSummary.fields), form), form, options.targetRoles);
1753
1785
  return {
@@ -1779,8 +1811,8 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
1779
1811
  const issues = [...existingIssues];
1780
1812
  const fieldsWithIssues = new Set(existingIssues.map((i) => i.ref));
1781
1813
  for (const [fieldId, progress] of Object.entries(fieldProgress)) {
1782
- if (progress.skipped) continue;
1783
- if (progress.state === "empty" && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
1814
+ if (progress.answerState === "skipped" || progress.answerState === "aborted") continue;
1815
+ if (progress.empty && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
1784
1816
  ref: fieldId,
1785
1817
  scope: "field",
1786
1818
  reason: "optional_empty",
@@ -1795,8 +1827,9 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
1795
1827
  * Map ValidationIssue to InspectIssue reason code.
1796
1828
  */
1797
1829
  function mapValidationToInspectReason(vi) {
1798
- if (vi.code === "REQUIRED_EMPTY" || vi.message.toLowerCase().includes("required") && vi.message.toLowerCase().includes("empty")) return "required_missing";
1799
- 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";
1800
1833
  if (vi.code === "MULTI_SELECT_TOO_FEW" || vi.code === "STRING_LIST_MIN_ITEMS" || vi.message.includes("at least")) return "min_items_not_met";
1801
1834
  return "validation_error";
1802
1835
  }
@@ -1938,7 +1971,9 @@ function isCheckboxComplete(form, fieldId) {
1938
1971
  const field = findFieldById(form, fieldId);
1939
1972
  if (field?.kind !== "checkboxes") return true;
1940
1973
  const checkboxField = field;
1941
- const value = form.valuesByFieldId[fieldId];
1974
+ const response = form.responsesByFieldId[fieldId];
1975
+ if (response?.state !== "answered") return false;
1976
+ const value = response.value;
1942
1977
  if (value?.kind !== "checkboxes") return false;
1943
1978
  const values = value.values;
1944
1979
  const optionIds = checkboxField.options.map((o) => o.id);
@@ -2014,10 +2049,25 @@ function findField(form, fieldId) {
2014
2049
  * Validate a single patch against the form schema.
2015
2050
  */
2016
2051
  function validatePatch(form, patch, index) {
2017
- 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);
2018
2068
  if (!field) return {
2019
2069
  patchIndex: index,
2020
- message: `Field "${patch.fieldId}" not found`
2070
+ message: `Field "${fieldId}" not found`
2021
2071
  };
2022
2072
  switch (patch.op) {
2023
2073
  case "set_string":
@@ -2090,6 +2140,18 @@ function validatePatch(form, patch, index) {
2090
2140
  message: `Cannot apply set_url_list to ${field.kind} field "${field.id}"`
2091
2141
  };
2092
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;
2093
2155
  case "clear_field": break;
2094
2156
  case "skip_field":
2095
2157
  if (field.required) return {
@@ -2097,6 +2159,7 @@ function validatePatch(form, patch, index) {
2097
2159
  message: `Cannot skip required field "${field.id}"`
2098
2160
  };
2099
2161
  break;
2162
+ case "abort_field": break;
2100
2163
  }
2101
2164
  return null;
2102
2165
  }
@@ -2115,191 +2178,234 @@ function validatePatches(form, patches) {
2115
2178
  return errors;
2116
2179
  }
2117
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
+ /**
2118
2190
  * Apply a set_string patch.
2119
2191
  */
2120
- function applySetString(values, patch) {
2121
- values[patch.fieldId] = {
2122
- kind: "string",
2123
- 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
+ }
2124
2199
  };
2125
2200
  }
2126
2201
  /**
2127
2202
  * Apply a set_number patch.
2128
2203
  */
2129
- function applySetNumber(values, patch) {
2130
- values[patch.fieldId] = {
2131
- kind: "number",
2132
- 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
+ }
2133
2211
  };
2134
2212
  }
2135
2213
  /**
2136
2214
  * Apply a set_string_list patch.
2137
2215
  */
2138
- function applySetStringList(values, patch) {
2139
- values[patch.fieldId] = {
2140
- kind: "string_list",
2141
- 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
+ }
2142
2223
  };
2143
2224
  }
2144
2225
  /**
2145
2226
  * Apply a set_single_select patch.
2146
2227
  */
2147
- function applySetSingleSelect(values, patch) {
2148
- values[patch.fieldId] = {
2149
- kind: "single_select",
2150
- 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
+ }
2151
2235
  };
2152
2236
  }
2153
2237
  /**
2154
2238
  * Apply a set_multi_select patch.
2155
2239
  */
2156
- function applySetMultiSelect(values, patch) {
2157
- values[patch.fieldId] = {
2158
- kind: "multi_select",
2159
- 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
+ }
2160
2247
  };
2161
2248
  }
2162
2249
  /**
2163
2250
  * Apply a set_checkboxes patch (merges with existing values).
2164
2251
  */
2165
- function applySetCheckboxes(values, patch) {
2252
+ function applySetCheckboxes(responses, patch) {
2166
2253
  const merged = {
2167
- ...values[patch.fieldId]?.values ?? {},
2254
+ ...(responses[patch.fieldId]?.value)?.values ?? {},
2168
2255
  ...patch.values
2169
2256
  };
2170
- values[patch.fieldId] = {
2171
- kind: "checkboxes",
2172
- values: merged
2257
+ responses[patch.fieldId] = {
2258
+ state: "answered",
2259
+ value: {
2260
+ kind: "checkboxes",
2261
+ values: merged
2262
+ }
2173
2263
  };
2174
2264
  }
2175
2265
  /**
2176
2266
  * Apply a set_url patch.
2177
2267
  */
2178
- function applySetUrl(values, patch) {
2179
- values[patch.fieldId] = {
2180
- kind: "url",
2181
- value: patch.value
2268
+ function applySetUrl(responses, patch) {
2269
+ responses[patch.fieldId] = {
2270
+ state: "answered",
2271
+ value: {
2272
+ kind: "url",
2273
+ value: patch.value
2274
+ }
2182
2275
  };
2183
2276
  }
2184
2277
  /**
2185
2278
  * Apply a set_url_list patch.
2186
2279
  */
2187
- function applySetUrlList(values, patch) {
2188
- values[patch.fieldId] = {
2189
- kind: "url_list",
2190
- items: patch.items
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
+ }
2191
2311
  };
2192
2312
  }
2193
2313
  /**
2194
2314
  * Apply a clear_field patch.
2195
2315
  */
2196
- function applyClearField(form, values, patch) {
2197
- const field = findField(form, patch.fieldId);
2198
- if (!field) return;
2199
- switch (field.kind) {
2200
- case "string":
2201
- values[patch.fieldId] = {
2202
- kind: "string",
2203
- value: null
2204
- };
2205
- break;
2206
- case "number":
2207
- values[patch.fieldId] = {
2208
- kind: "number",
2209
- value: null
2210
- };
2211
- break;
2212
- case "string_list":
2213
- values[patch.fieldId] = {
2214
- kind: "string_list",
2215
- items: []
2216
- };
2217
- break;
2218
- case "single_select":
2219
- values[patch.fieldId] = {
2220
- kind: "single_select",
2221
- selected: null
2222
- };
2223
- break;
2224
- case "multi_select":
2225
- values[patch.fieldId] = {
2226
- kind: "multi_select",
2227
- selected: []
2228
- };
2229
- break;
2230
- case "checkboxes":
2231
- values[patch.fieldId] = {
2232
- kind: "checkboxes",
2233
- values: {}
2234
- };
2235
- break;
2236
- case "url":
2237
- values[patch.fieldId] = {
2238
- kind: "url",
2239
- value: null
2240
- };
2241
- break;
2242
- case "url_list":
2243
- values[patch.fieldId] = {
2244
- kind: "url_list",
2245
- items: []
2246
- };
2247
- break;
2248
- }
2316
+ function applyClearField(responses, patch) {
2317
+ responses[patch.fieldId] = { state: "unanswered" };
2249
2318
  }
2250
2319
  /**
2251
2320
  * Apply a skip_field patch.
2252
- * Marks the field as skipped and clears any existing value.
2321
+ * Marks the field as skipped and stores reason in FieldResponse.reason.
2253
2322
  */
2254
- function applySkipField(form, values, skips, patch) {
2255
- if (!findField(form, patch.fieldId)) return;
2256
- skips[patch.fieldId] = {
2257
- skipped: true,
2258
- reason: patch.reason
2323
+ function applySkipField(responses, patch) {
2324
+ responses[patch.fieldId] = {
2325
+ state: "skipped",
2326
+ ...patch.reason && { reason: patch.reason }
2259
2327
  };
2260
- delete values[patch.fieldId];
2261
2328
  }
2262
2329
  /**
2263
- * Apply a single patch to the values and skips.
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.
2264
2362
  */
2265
- function applyPatch(form, values, skips, patch) {
2363
+ function applyPatch(form, responses, patch) {
2266
2364
  switch (patch.op) {
2267
2365
  case "set_string":
2268
- applySetString(values, patch);
2269
- delete skips[patch.fieldId];
2366
+ applySetString(responses, patch);
2270
2367
  break;
2271
2368
  case "set_number":
2272
- applySetNumber(values, patch);
2273
- delete skips[patch.fieldId];
2369
+ applySetNumber(responses, patch);
2274
2370
  break;
2275
2371
  case "set_string_list":
2276
- applySetStringList(values, patch);
2277
- delete skips[patch.fieldId];
2372
+ applySetStringList(responses, patch);
2278
2373
  break;
2279
2374
  case "set_single_select":
2280
- applySetSingleSelect(values, patch);
2281
- delete skips[patch.fieldId];
2375
+ applySetSingleSelect(responses, patch);
2282
2376
  break;
2283
2377
  case "set_multi_select":
2284
- applySetMultiSelect(values, patch);
2285
- delete skips[patch.fieldId];
2378
+ applySetMultiSelect(responses, patch);
2286
2379
  break;
2287
2380
  case "set_checkboxes":
2288
- applySetCheckboxes(values, patch);
2289
- delete skips[patch.fieldId];
2381
+ applySetCheckboxes(responses, patch);
2290
2382
  break;
2291
2383
  case "set_url":
2292
- applySetUrl(values, patch);
2384
+ applySetUrl(responses, patch);
2293
2385
  break;
2294
2386
  case "set_url_list":
2295
- applySetUrlList(values, patch);
2387
+ applySetUrlList(responses, patch);
2388
+ break;
2389
+ case "set_date":
2390
+ applySetDate(responses, patch);
2391
+ break;
2392
+ case "set_year":
2393
+ applySetYear(responses, patch);
2296
2394
  break;
2297
2395
  case "clear_field":
2298
- applyClearField(form, values, patch);
2299
- delete skips[patch.fieldId];
2396
+ applyClearField(responses, patch);
2300
2397
  break;
2301
2398
  case "skip_field":
2302
- applySkipField(form, values, skips, patch);
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);
2303
2409
  break;
2304
2410
  }
2305
2411
  }
@@ -2331,8 +2437,8 @@ function convertToInspectIssues(form) {
2331
2437
  */
2332
2438
  function applyPatches(form, patches) {
2333
2439
  if (validatePatches(form, patches).length > 0) {
2334
- const summaries$1 = computeAllSummaries(form.schema, form.valuesByFieldId, [], form.skipsByFieldId);
2335
2440
  const issues$1 = convertToInspectIssues(form);
2441
+ const summaries$1 = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues$1);
2336
2442
  return {
2337
2443
  applyStatus: "rejected",
2338
2444
  structureSummary: summaries$1.structureSummary,
@@ -2342,13 +2448,14 @@ function applyPatches(form, patches) {
2342
2448
  formState: summaries$1.formState
2343
2449
  };
2344
2450
  }
2345
- const newValues = { ...form.valuesByFieldId };
2346
- const newSkips = { ...form.skipsByFieldId };
2347
- for (const patch of patches) applyPatch(form, newValues, newSkips, patch);
2348
- form.valuesByFieldId = newValues;
2349
- form.skipsByFieldId = newSkips;
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;
2350
2457
  const issues = convertToInspectIssues(form);
2351
- const summaries = computeAllSummaries(form.schema, newValues, issues, newSkips);
2458
+ const summaries = computeAllSummaries(form.schema, newResponses, newNotes, issues);
2352
2459
  return {
2353
2460
  applyStatus: "applied",
2354
2461
  structureSummary: summaries.structureSummary,
@@ -2360,4 +2467,4 @@ function applyPatches(form, patches) {
2360
2467
  }
2361
2468
 
2362
2469
  //#endregion
2363
- export { OptionSchema as $, ClearFieldPatchSchema as A, HarnessConfigSchema as B, parseRolesFlag as C, StringValueSchema as Ct, CheckboxValueSchema as D, CheckboxProgressCountsSchema as E, ValidatorRefSchema as Et, FieldKindSchema as F, IssueScopeSchema as G, InspectIssueSchema as H, FieldProgressSchema as I, MultiSelectFieldSchema as J, MarkformFrontmatterSchema as K, FieldSchema as L, DocumentationTagSchema as M, ExplicitCheckboxValueSchema as N, CheckboxesFieldSchema as O, FieldGroupSchema as P, OptionIdSchema as Q, FieldValueSchema as R, getWebSearchConfig as S, StringListValueSchema as St, CheckboxModeSchema as T, ValidationIssueSchema as Tt, InspectResultSchema as U, IdSchema as V, IssueReasonSchema as W, NumberFieldSchema as X, MultiSelectValueSchema as Y, NumberValueSchema as Z, DEFAULT_PRIORITY as _, SourcePositionSchema as _t, computeAllSummaries as a, SessionTranscriptSchema as at, USER_ROLE as b, StringFieldSchema as bt, computeStructureSummary as c, SetMultiSelectPatchSchema as ct, serializeRawMarkdown as d, SetStringListPatchSchema as dt, PatchSchema as et, AGENT_ROLE as f, SetStringPatchSchema as ft, DEFAULT_PORT as g, SingleSelectValueSchema as gt, DEFAULT_MAX_TURNS as h, SingleSelectFieldSchema as ht, validate as i, SessionFinalSchema as it, DocumentationBlockSchema as j, CheckboxesValueSchema as k, isFormComplete as l, SetNumberPatchSchema as lt, DEFAULT_MAX_PATCHES_PER_TURN as m, SimpleCheckboxStateSchema as mt, getFieldsForRoles as n, ProgressStateSchema as nt, computeFormState as o, SessionTurnSchema as ot, DEFAULT_MAX_ISSUES as p, SeveritySchema as pt, MultiCheckboxStateSchema as q, inspect as r, ProgressSummarySchema as rt, computeProgressSummary as s, SetCheckboxesPatchSchema as st, applyPatches as t, ProgressCountsSchema as tt, serialize as u, SetSingleSelectPatchSchema as ut, DEFAULT_ROLE_INSTRUCTIONS as v, SourceRangeSchema as vt, ApplyResultSchema as w, StructureSummarySchema as wt, formatSuggestedLlms as x, StringListFieldSchema as xt, SUGGESTED_LLMS as y, StepResultSchema as yt, FormSchemaSchema 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 };