markform 0.1.1 → 0.1.3

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 +340 -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-00UmzDKL.mjs} +849 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
  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-Dm8jZ5dl.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 +54 -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 +10 -14
  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,141 @@ 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];
235
+ }
236
+ /**
237
+ * Derive report path from any markform file path.
238
+ * Strips known extensions and appends .report.md.
239
+ */
240
+ function deriveReportPath(basePath) {
241
+ let base = basePath;
242
+ for (const ext of Object.values(ALL_EXTENSIONS)) if (base.endsWith(ext)) {
243
+ base = base.slice(0, -ext.length);
244
+ break;
245
+ }
246
+ return base + REPORT_EXTENSION;
562
247
  }
563
248
 
564
249
  //#endregion
565
250
  //#region src/engine/serialize.ts
566
251
  /**
252
+ * Find the maximum run of fence characters at line starts (indent ≤ 3 spaces).
253
+ * Lines with 4+ space indent are inside code blocks so don't break fences.
254
+ */
255
+ function maxRunAtLineStart(value, char) {
256
+ const escaped = char === "`" ? "`" : "~";
257
+ const pattern = new RegExp(`^( {0,3})${escaped}+`, "gm");
258
+ let maxRun = 0;
259
+ let match;
260
+ while ((match = pattern.exec(value)) !== null) {
261
+ const indent = match[1]?.length ?? 0;
262
+ const runLength = match[0].length - indent;
263
+ if (runLength > maxRun) maxRun = runLength;
264
+ }
265
+ return maxRun;
266
+ }
267
+ /**
268
+ * Pick the optimal fence character and length for a value.
269
+ * Also detects if process=false is needed for Markdoc tags.
270
+ */
271
+ function pickFence(value) {
272
+ const hasMarkdocTags = value.includes("{%");
273
+ const maxBackticks = maxRunAtLineStart(value, "`");
274
+ const maxTildes = maxRunAtLineStart(value, "~");
275
+ let char;
276
+ let maxRun;
277
+ if (maxBackticks <= maxTildes) {
278
+ char = "`";
279
+ maxRun = maxBackticks;
280
+ } else {
281
+ char = "~";
282
+ maxRun = maxTildes;
283
+ }
284
+ const len = Math.max(3, maxRun + 1);
285
+ return {
286
+ char,
287
+ len,
288
+ processFalse: hasMarkdocTags
289
+ };
290
+ }
291
+ /**
292
+ * Format a value fence block with the given content.
293
+ * Uses smart fence selection to avoid collision with code blocks in content.
294
+ */
295
+ function formatValueFence(content) {
296
+ const { char, len, processFalse } = pickFence(content);
297
+ const fence = char.repeat(len);
298
+ return `\n${fence}value${processFalse ? " {% process=false %}" : ""}\n${content}\n${fence}\n`;
299
+ }
300
+ /**
301
+ * Get sentinel value content for skipped/aborted fields with reason.
302
+ * Returns the fence block if there's a reason, empty string otherwise.
303
+ */
304
+ function getSentinelContent(response) {
305
+ if (response?.state === "skipped" && response.reason) return formatValueFence(`%SKIP% (${response.reason})`);
306
+ if (response?.state === "aborted" && response.reason) return formatValueFence(`%ABORT% (${response.reason})`);
307
+ return "";
308
+ }
309
+ /**
567
310
  * Serialize an attribute value to Markdoc format.
568
311
  */
569
312
  function serializeAttrValue(value) {
@@ -609,7 +352,7 @@ function getMarker(state) {
609
352
  /**
610
353
  * Serialize a string field.
611
354
  */
612
- function serializeStringField(field, value) {
355
+ function serializeStringField(field, response) {
613
356
  const attrs = {
614
357
  id: field.id,
615
358
  label: field.label
@@ -622,15 +365,22 @@ function serializeStringField(field, value) {
622
365
  if (field.minLength !== void 0) attrs.minLength = field.minLength;
623
366
  if (field.maxLength !== void 0) attrs.maxLength = field.maxLength;
624
367
  if (field.validate) attrs.validate = field.validate;
368
+ if (field.report !== void 0) attrs.report = field.report;
369
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
625
370
  const attrStr = serializeAttrs(attrs);
626
371
  let content = "";
627
- if (value?.value) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
372
+ if (response?.state === "answered" && response.value) {
373
+ const value = response.value;
374
+ if (value.value) content = formatValueFence(value.value);
375
+ }
376
+ const sentinelContent = getSentinelContent(response);
377
+ if (sentinelContent) content = sentinelContent;
628
378
  return `{% string-field ${attrStr} %}${content}{% /string-field %}`;
629
379
  }
630
380
  /**
631
381
  * Serialize a number field.
632
382
  */
633
- function serializeNumberField(field, value) {
383
+ function serializeNumberField(field, response) {
634
384
  const attrs = {
635
385
  id: field.id,
636
386
  label: field.label
@@ -642,15 +392,22 @@ function serializeNumberField(field, value) {
642
392
  if (field.max !== void 0) attrs.max = field.max;
643
393
  if (field.integer) attrs.integer = field.integer;
644
394
  if (field.validate) attrs.validate = field.validate;
395
+ if (field.report !== void 0) attrs.report = field.report;
396
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
645
397
  const attrStr = serializeAttrs(attrs);
646
398
  let content = "";
647
- if (value?.value !== null && value?.value !== void 0) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
399
+ if (response?.state === "answered" && response.value) {
400
+ const value = response.value;
401
+ if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
402
+ }
403
+ const sentinelContent = getSentinelContent(response);
404
+ if (sentinelContent) content = sentinelContent;
648
405
  return `{% number-field ${attrStr} %}${content}{% /number-field %}`;
649
406
  }
650
407
  /**
651
408
  * Serialize a string-list field.
652
409
  */
653
- function serializeStringListField(field, value) {
410
+ function serializeStringListField(field, response) {
654
411
  const attrs = {
655
412
  id: field.id,
656
413
  label: field.label
@@ -664,9 +421,16 @@ function serializeStringListField(field, value) {
664
421
  if (field.itemMaxLength !== void 0) attrs.itemMaxLength = field.itemMaxLength;
665
422
  if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
666
423
  if (field.validate) attrs.validate = field.validate;
424
+ if (field.report !== void 0) attrs.report = field.report;
425
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
667
426
  const attrStr = serializeAttrs(attrs);
668
427
  let content = "";
669
- if (value?.items && value.items.length > 0) content = `\n\`\`\`value\n${value.items.join("\n")}\n\`\`\`\n`;
428
+ if (response?.state === "answered" && response.value) {
429
+ const value = response.value;
430
+ if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
431
+ }
432
+ const sentinelContent = getSentinelContent(response);
433
+ if (sentinelContent) content = sentinelContent;
670
434
  return `{% string-list ${attrStr} %}${content}{% /string-list %}`;
671
435
  }
672
436
  /**
@@ -683,7 +447,7 @@ function serializeOptions(options, selected) {
683
447
  /**
684
448
  * Serialize a single-select field.
685
449
  */
686
- function serializeSingleSelectField(field, value) {
450
+ function serializeSingleSelectField(field, response) {
687
451
  const attrs = {
688
452
  id: field.id,
689
453
  label: field.label
@@ -692,7 +456,11 @@ function serializeSingleSelectField(field, value) {
692
456
  if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
693
457
  if (field.role !== AGENT_ROLE) attrs.role = field.role;
694
458
  if (field.validate) attrs.validate = field.validate;
459
+ if (field.report !== void 0) attrs.report = field.report;
460
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
695
461
  const attrStr = serializeAttrs(attrs);
462
+ let value;
463
+ if (response?.state === "answered" && response.value) value = response.value;
696
464
  const selected = {};
697
465
  for (const opt of field.options) selected[opt.id] = opt.id === value?.selected ? "done" : "todo";
698
466
  return `{% single-select ${attrStr} %}\n${serializeOptions(field.options, selected)}\n{% /single-select %}`;
@@ -700,7 +468,7 @@ function serializeSingleSelectField(field, value) {
700
468
  /**
701
469
  * Serialize a multi-select field.
702
470
  */
703
- function serializeMultiSelectField(field, value) {
471
+ function serializeMultiSelectField(field, response) {
704
472
  const attrs = {
705
473
  id: field.id,
706
474
  label: field.label
@@ -711,7 +479,11 @@ function serializeMultiSelectField(field, value) {
711
479
  if (field.minSelections !== void 0) attrs.minSelections = field.minSelections;
712
480
  if (field.maxSelections !== void 0) attrs.maxSelections = field.maxSelections;
713
481
  if (field.validate) attrs.validate = field.validate;
482
+ if (field.report !== void 0) attrs.report = field.report;
483
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
714
484
  const attrStr = serializeAttrs(attrs);
485
+ let value;
486
+ if (response?.state === "answered" && response.value) value = response.value;
715
487
  const selected = {};
716
488
  const selectedSet = new Set(value?.selected ?? []);
717
489
  for (const opt of field.options) selected[opt.id] = selectedSet.has(opt.id) ? "done" : "todo";
@@ -720,7 +492,7 @@ function serializeMultiSelectField(field, value) {
720
492
  /**
721
493
  * Serialize a checkboxes field.
722
494
  */
723
- function serializeCheckboxesField(field, value) {
495
+ function serializeCheckboxesField(field, response) {
724
496
  const attrs = {
725
497
  id: field.id,
726
498
  label: field.label
@@ -732,12 +504,17 @@ function serializeCheckboxesField(field, value) {
732
504
  if (field.minDone !== void 0) attrs.minDone = field.minDone;
733
505
  if (field.approvalMode !== "none") attrs.approvalMode = field.approvalMode;
734
506
  if (field.validate) attrs.validate = field.validate;
735
- return `{% checkboxes ${serializeAttrs(attrs)} %}\n${serializeOptions(field.options, value?.values ?? {})}\n{% /checkboxes %}`;
507
+ if (field.report !== void 0) attrs.report = field.report;
508
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
509
+ const attrStr = serializeAttrs(attrs);
510
+ let value;
511
+ if (response?.state === "answered" && response.value) value = response.value;
512
+ return `{% checkboxes ${attrStr} %}\n${serializeOptions(field.options, value?.values ?? {})}\n{% /checkboxes %}`;
736
513
  }
737
514
  /**
738
515
  * Serialize a url-field.
739
516
  */
740
- function serializeUrlField(field, value) {
517
+ function serializeUrlField(field, response) {
741
518
  const attrs = {
742
519
  id: field.id,
743
520
  label: field.label
@@ -746,15 +523,22 @@ function serializeUrlField(field, value) {
746
523
  if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
747
524
  if (field.role !== AGENT_ROLE) attrs.role = field.role;
748
525
  if (field.validate) attrs.validate = field.validate;
526
+ if (field.report !== void 0) attrs.report = field.report;
527
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
749
528
  const attrStr = serializeAttrs(attrs);
750
529
  let content = "";
751
- if (value?.value) content = `\n\`\`\`value\n${value.value}\n\`\`\`\n`;
530
+ if (response?.state === "answered" && response.value) {
531
+ const value = response.value;
532
+ if (value.value) content = formatValueFence(value.value);
533
+ }
534
+ const sentinelContent = getSentinelContent(response);
535
+ if (sentinelContent) content = sentinelContent;
752
536
  return `{% url-field ${attrStr} %}${content}{% /url-field %}`;
753
537
  }
754
538
  /**
755
539
  * Serialize a url-list field.
756
540
  */
757
- function serializeUrlListField(field, value) {
541
+ function serializeUrlListField(field, response) {
758
542
  const attrs = {
759
543
  id: field.id,
760
544
  label: field.label
@@ -766,25 +550,86 @@ function serializeUrlListField(field, value) {
766
550
  if (field.maxItems !== void 0) attrs.maxItems = field.maxItems;
767
551
  if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
768
552
  if (field.validate) attrs.validate = field.validate;
553
+ if (field.report !== void 0) attrs.report = field.report;
554
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
769
555
  const attrStr = serializeAttrs(attrs);
770
556
  let content = "";
771
- if (value?.items && value.items.length > 0) content = `\n\`\`\`value\n${value.items.join("\n")}\n\`\`\`\n`;
557
+ if (response?.state === "answered" && response.value) {
558
+ const value = response.value;
559
+ if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
560
+ }
561
+ const sentinelContent = getSentinelContent(response);
562
+ if (sentinelContent) content = sentinelContent;
772
563
  return `{% url-list ${attrStr} %}${content}{% /url-list %}`;
773
564
  }
774
565
  /**
566
+ * Serialize a date-field.
567
+ */
568
+ function serializeDateField(field, response) {
569
+ const attrs = {
570
+ id: field.id,
571
+ label: field.label
572
+ };
573
+ if (field.required) attrs.required = field.required;
574
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
575
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
576
+ if (field.min !== void 0) attrs.min = field.min;
577
+ if (field.max !== void 0) attrs.max = field.max;
578
+ if (field.validate) attrs.validate = field.validate;
579
+ if (field.report !== void 0) attrs.report = field.report;
580
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
581
+ const attrStr = serializeAttrs(attrs);
582
+ let content = "";
583
+ if (response?.state === "answered" && response.value) {
584
+ const value = response.value;
585
+ if (value.value) content = formatValueFence(value.value);
586
+ }
587
+ const sentinelContent = getSentinelContent(response);
588
+ if (sentinelContent) content = sentinelContent;
589
+ return `{% date-field ${attrStr} %}${content}{% /date-field %}`;
590
+ }
591
+ /**
592
+ * Serialize a year-field.
593
+ */
594
+ function serializeYearField(field, response) {
595
+ const attrs = {
596
+ id: field.id,
597
+ label: field.label
598
+ };
599
+ if (field.required) attrs.required = field.required;
600
+ if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
601
+ if (field.role !== AGENT_ROLE) attrs.role = field.role;
602
+ if (field.min !== void 0) attrs.min = field.min;
603
+ if (field.max !== void 0) attrs.max = field.max;
604
+ if (field.validate) attrs.validate = field.validate;
605
+ if (field.report !== void 0) attrs.report = field.report;
606
+ if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
607
+ const attrStr = serializeAttrs(attrs);
608
+ let content = "";
609
+ if (response?.state === "answered" && response.value) {
610
+ const value = response.value;
611
+ if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
612
+ }
613
+ const sentinelContent = getSentinelContent(response);
614
+ if (sentinelContent) content = sentinelContent;
615
+ return `{% year-field ${attrStr} %}${content}{% /year-field %}`;
616
+ }
617
+ /**
775
618
  * Serialize a field to Markdoc format.
776
619
  */
777
- function serializeField(field, values) {
778
- const value = values[field.id];
620
+ function serializeField(field, responses) {
621
+ const response = responses[field.id];
779
622
  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);
623
+ case "string": return serializeStringField(field, response);
624
+ case "number": return serializeNumberField(field, response);
625
+ case "string_list": return serializeStringListField(field, response);
626
+ case "single_select": return serializeSingleSelectField(field, response);
627
+ case "multi_select": return serializeMultiSelectField(field, response);
628
+ case "checkboxes": return serializeCheckboxesField(field, response);
629
+ case "url": return serializeUrlField(field, response);
630
+ case "url_list": return serializeUrlListField(field, response);
631
+ case "date": return serializeDateField(field, response);
632
+ case "year": return serializeYearField(field, response);
788
633
  }
789
634
  }
790
635
  /**
@@ -792,16 +637,39 @@ function serializeField(field, values) {
792
637
  * Uses the semantic tag name (description, instructions, documentation).
793
638
  */
794
639
  function serializeDocBlock(doc) {
795
- const attrStr = serializeAttrs({ ref: doc.ref });
640
+ const attrs = { ref: doc.ref };
641
+ if (doc.report !== void 0) attrs.report = doc.report;
642
+ const attrStr = serializeAttrs(attrs);
796
643
  return `{% ${doc.tag} ${attrStr} %}\n${doc.bodyMarkdown}\n{% /${doc.tag} %}`;
797
644
  }
798
645
  /**
646
+ * Serialize notes in sorted order.
647
+ * Notes are sorted numerically by ID suffix (n1, n2, n10 not n1, n10, n2).
648
+ */
649
+ function serializeNotes(notes) {
650
+ if (notes.length === 0) return "";
651
+ const sorted = [...notes].sort((a, b) => {
652
+ return (Number.parseInt(a.id.replace(/^n/, ""), 10) || 0) - (Number.parseInt(b.id.replace(/^n/, ""), 10) || 0);
653
+ });
654
+ const lines = [];
655
+ for (const note of sorted) {
656
+ const attrStr = serializeAttrs({
657
+ id: note.id,
658
+ ref: note.ref,
659
+ role: note.role
660
+ });
661
+ lines.push(`{% note ${attrStr} %}\n${note.text}\n{% /note %}`);
662
+ }
663
+ return lines.join("\n\n");
664
+ }
665
+ /**
799
666
  * Serialize a field group.
800
667
  */
801
- function serializeFieldGroup(group, values, docs) {
668
+ function serializeFieldGroup(group, responses, docs) {
802
669
  const attrs = { id: group.id };
803
670
  if (group.title) attrs.title = group.title;
804
671
  if (group.validate) attrs.validate = group.validate;
672
+ if (group.report !== void 0) attrs.report = group.report;
805
673
  const lines = [`{% field-group ${serializeAttrs(attrs)} %}`];
806
674
  const docsByRef = /* @__PURE__ */ new Map();
807
675
  for (const doc of docs) {
@@ -811,7 +679,7 @@ function serializeFieldGroup(group, values, docs) {
811
679
  }
812
680
  for (const field of group.children) {
813
681
  lines.push("");
814
- lines.push(serializeField(field, values));
682
+ lines.push(serializeField(field, responses));
815
683
  const fieldDocs = docsByRef.get(field.id);
816
684
  if (fieldDocs) for (const doc of fieldDocs) {
817
685
  lines.push("");
@@ -825,7 +693,7 @@ function serializeFieldGroup(group, values, docs) {
825
693
  /**
826
694
  * Serialize a form schema.
827
695
  */
828
- function serializeFormSchema(schema, values, docs) {
696
+ function serializeFormSchema(schema, responses, docs, notes) {
829
697
  const attrs = { id: schema.id };
830
698
  if (schema.title) attrs.title = schema.title;
831
699
  const lines = [`{% form ${serializeAttrs(attrs)} %}`];
@@ -842,7 +710,12 @@ function serializeFormSchema(schema, values, docs) {
842
710
  }
843
711
  for (const group of schema.groups) {
844
712
  lines.push("");
845
- lines.push(serializeFieldGroup(group, values, docs));
713
+ lines.push(serializeFieldGroup(group, responses, docs));
714
+ }
715
+ const notesContent = serializeNotes(notes);
716
+ if (notesContent) {
717
+ lines.push("");
718
+ lines.push(notesContent);
846
719
  }
847
720
  lines.push("");
848
721
  lines.push("{% /form %}");
@@ -858,8 +731,8 @@ function serializeFormSchema(schema, values, docs) {
858
731
  function serialize(form, opts) {
859
732
  return `${`---
860
733
  markform:
861
- markform_version: "${opts?.markformVersion ?? "0.1.0"}"
862
- ---`}\n\n${serializeFormSchema(form.schema, form.valuesByFieldId, form.docs)}\n`;
734
+ spec: "${opts?.specVersion ?? MF_SPEC_VERSION}"
735
+ ---`}\n\n${serializeFormSchema(form.schema, form.responsesByFieldId, form.docs, form.notes)}\n`;
863
736
  }
864
737
  /** Map checkbox state to GFM marker for raw markdown output */
865
738
  const STATE_TO_GFM_MARKER = {
@@ -875,10 +748,11 @@ const STATE_TO_GFM_MARKER = {
875
748
  /**
876
749
  * Serialize a field value to raw markdown (human-readable).
877
750
  */
878
- function serializeFieldRaw(field, values) {
879
- const value = values[field.id];
751
+ function serializeFieldRaw(field, responses) {
752
+ const response = responses[field.id];
880
753
  const lines = [];
881
754
  lines.push(`**${field.label}:**`);
755
+ const value = response?.state === "answered" ? response.value : void 0;
882
756
  switch (field.kind) {
883
757
  case "string": {
884
758
  const strValue = value;
@@ -933,6 +807,18 @@ function serializeFieldRaw(field, values) {
933
807
  else lines.push("_(empty)_");
934
808
  break;
935
809
  }
810
+ case "date": {
811
+ const dateValue = value;
812
+ if (dateValue?.value) lines.push(dateValue.value);
813
+ else lines.push("_(empty)_");
814
+ break;
815
+ }
816
+ case "year": {
817
+ const yearValue = value;
818
+ if (yearValue?.value !== null && yearValue?.value !== void 0) lines.push(String(yearValue.value));
819
+ else lines.push("_(empty)_");
820
+ break;
821
+ }
936
822
  }
937
823
  return lines.join("\n");
938
824
  }
@@ -973,7 +859,70 @@ function serializeRawMarkdown(form) {
973
859
  lines.push("");
974
860
  }
975
861
  for (const field of group.children) {
976
- lines.push(serializeFieldRaw(field, form.valuesByFieldId));
862
+ lines.push(serializeFieldRaw(field, form.responsesByFieldId));
863
+ lines.push("");
864
+ const fieldDocs = docsByRef.get(field.id);
865
+ if (fieldDocs) for (const doc of fieldDocs) {
866
+ lines.push(doc.bodyMarkdown.trim());
867
+ lines.push("");
868
+ }
869
+ }
870
+ }
871
+ return lines.join("\n").trim() + "\n";
872
+ }
873
+ /**
874
+ * Check if a documentation block should be included in reports.
875
+ * Default: instructions are excluded, everything else is included.
876
+ */
877
+ function shouldIncludeDoc(doc) {
878
+ if (doc.report !== void 0) return doc.report;
879
+ return doc.tag !== "instructions";
880
+ }
881
+ /**
882
+ * Serialize a form to filtered markdown for reports.
883
+ *
884
+ * Produces clean, readable markdown with filtered content based on `report` attribute:
885
+ * - Fields with report=false are excluded
886
+ * - Groups with report=false are excluded
887
+ * - Documentation blocks with report=false are excluded
888
+ * - Instructions blocks are excluded by default (unless report=true)
889
+ *
890
+ * @param form - The parsed form to serialize
891
+ * @returns Filtered plain markdown string suitable for sharing
892
+ */
893
+ function serializeReportMarkdown(form) {
894
+ const lines = [];
895
+ const docsByRef = /* @__PURE__ */ new Map();
896
+ for (const doc of form.docs) {
897
+ if (!shouldIncludeDoc(doc)) continue;
898
+ const list = docsByRef.get(doc.ref) ?? [];
899
+ list.push(doc);
900
+ docsByRef.set(doc.ref, list);
901
+ }
902
+ if (form.schema.title) {
903
+ lines.push(`# ${form.schema.title}`);
904
+ lines.push("");
905
+ }
906
+ const formDocs = docsByRef.get(form.schema.id);
907
+ if (formDocs) for (const doc of formDocs) {
908
+ lines.push(doc.bodyMarkdown.trim());
909
+ lines.push("");
910
+ }
911
+ for (const group of form.schema.groups) {
912
+ if (group.report === false) continue;
913
+ const visibleFields = group.children.filter((field) => field.report !== false);
914
+ if (visibleFields.length === 0 && !group.title) continue;
915
+ if (group.title) {
916
+ lines.push(`## ${group.title}`);
917
+ lines.push("");
918
+ }
919
+ const groupDocs = docsByRef.get(group.id);
920
+ if (groupDocs) for (const doc of groupDocs) {
921
+ lines.push(doc.bodyMarkdown.trim());
922
+ lines.push("");
923
+ }
924
+ for (const field of visibleFields) {
925
+ lines.push(serializeFieldRaw(field, form.responsesByFieldId));
977
926
  lines.push("");
978
927
  const fieldDocs = docsByRef.get(field.id);
979
928
  if (fieldDocs) for (const doc of fieldDocs) {
@@ -1002,7 +951,9 @@ function computeStructureSummary(schema) {
1002
951
  single_select: 0,
1003
952
  multi_select: 0,
1004
953
  url: 0,
1005
- url_list: 0
954
+ url_list: 0,
955
+ date: 0,
956
+ year: 0
1006
957
  };
1007
958
  const groupsById = {};
1008
959
  const fieldsById = {};
@@ -1068,6 +1019,11 @@ function isFieldSubmitted(field, value) {
1068
1019
  return v.value !== null && v.value.trim() !== "";
1069
1020
  }
1070
1021
  case "url_list": return value.items.length > 0;
1022
+ case "date": {
1023
+ const v = value;
1024
+ return v.value !== null && v.value.trim() !== "";
1025
+ }
1026
+ case "year": return value.value !== null;
1071
1027
  }
1072
1028
  }
1073
1029
  /**
@@ -1097,50 +1053,35 @@ function computeCheckboxProgress(field, value) {
1097
1053
  return result;
1098
1054
  }
1099
1055
  /**
1100
- * Check if a checkboxes field is complete based on its mode.
1056
+ * Compute whether a field is empty (has no value).
1101
1057
  */
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.
1117
- */
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";
1058
+ function isFieldEmpty(field, value) {
1059
+ return !isFieldSubmitted(field, value);
1125
1060
  }
1126
1061
  /**
1127
1062
  * Compute progress for a single field.
1128
1063
  */
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;
1064
+ function computeFieldProgress(field, response, notes, issues) {
1065
+ const fieldIssues = issues.filter((i) => i.ref === field.id);
1066
+ const issueCount = fieldIssues.length;
1067
+ const value = response.value;
1068
+ const empty = isFieldEmpty(field, value);
1069
+ let valid = true;
1070
+ if (response.state === "skipped" || response.state === "aborted") valid = issueCount === 0;
1071
+ else if (empty) valid = fieldIssues.filter((i) => i.reason !== "required_missing").length === 0;
1072
+ else valid = issueCount === 0;
1073
+ const fieldNotes = notes.filter((n) => n.ref === field.id);
1074
+ const hasNotes = fieldNotes.length > 0;
1075
+ const noteCount = fieldNotes.length;
1135
1076
  const progress = {
1136
1077
  kind: field.kind,
1137
1078
  required: field.required,
1138
- submitted,
1139
- state,
1079
+ answerState: response.state,
1080
+ hasNotes,
1081
+ noteCount,
1082
+ empty,
1140
1083
  valid,
1141
- issueCount,
1142
- skipped,
1143
- skipReason: skipInfo?.reason
1084
+ issueCount
1144
1085
  };
1145
1086
  if (field.kind === "checkboxes") progress.checkboxProgress = computeCheckboxProgress(field, value);
1146
1087
  return progress;
@@ -1149,42 +1090,42 @@ function computeFieldProgress(field, value, issues, skipInfo) {
1149
1090
  * Compute a progress summary for a form.
1150
1091
  *
1151
1092
  * @param schema - The form schema
1152
- * @param values - Current field values
1093
+ * @param responsesByFieldId - Current field responses (state + optional value)
1094
+ * @param notes - Notes attached to fields/groups/form
1153
1095
  * @param issues - Validation issues (from inspect)
1154
- * @param skips - Skip state per field (from skip_field patches)
1155
1096
  * @returns Progress summary with field states and counts
1156
1097
  */
1157
- function computeProgressSummary(schema, values, issues, skips = {}) {
1098
+ function computeProgressSummary(schema, responsesByFieldId, notes, issues) {
1158
1099
  const fields = {};
1159
1100
  const counts = {
1160
1101
  totalFields: 0,
1161
1102
  requiredFields: 0,
1162
- submittedFields: 0,
1163
- completeFields: 0,
1164
- incompleteFields: 0,
1103
+ unansweredFields: 0,
1104
+ answeredFields: 0,
1105
+ skippedFields: 0,
1106
+ abortedFields: 0,
1107
+ validFields: 0,
1165
1108
  invalidFields: 0,
1109
+ emptyFields: 0,
1110
+ filledFields: 0,
1166
1111
  emptyRequiredFields: 0,
1167
- emptyOptionalFields: 0,
1168
- answeredFields: 0,
1169
- skippedFields: 0
1112
+ totalNotes: notes.length
1170
1113
  };
1171
1114
  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);
1115
+ const progress = computeFieldProgress(field, responsesByFieldId[field.id] ?? { state: "unanswered" }, notes, issues);
1175
1116
  fields[field.id] = progress;
1176
1117
  counts.totalFields++;
1177
1118
  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++;
1119
+ if (progress.answerState === "answered") counts.answeredFields++;
1120
+ else if (progress.answerState === "skipped") counts.skippedFields++;
1121
+ else if (progress.answerState === "aborted") counts.abortedFields++;
1122
+ else if (progress.answerState === "unanswered") counts.unansweredFields++;
1123
+ if (progress.valid) counts.validFields++;
1124
+ else counts.invalidFields++;
1125
+ if (progress.empty) {
1126
+ counts.emptyFields++;
1127
+ if (progress.required) counts.emptyRequiredFields++;
1128
+ } else counts.filledFields++;
1188
1129
  }
1189
1130
  return {
1190
1131
  counts,
@@ -1198,20 +1139,21 @@ function computeProgressSummary(schema, values, issues, skips = {}) {
1198
1139
  * @returns The overall form state
1199
1140
  */
1200
1141
  function computeFormState(progress) {
1142
+ if (progress.counts.abortedFields > 0) return "invalid";
1201
1143
  if (progress.counts.invalidFields > 0) return "invalid";
1202
- if (progress.counts.incompleteFields > 0) return "incomplete";
1203
1144
  if (progress.counts.emptyRequiredFields === 0) return "complete";
1204
- if (progress.counts.submittedFields > 0) return "incomplete";
1145
+ if (progress.counts.answeredFields > 0) return "incomplete";
1205
1146
  return "empty";
1206
1147
  }
1207
1148
  /**
1208
1149
  * Determine if the form is complete (ready for submission).
1209
1150
  *
1210
1151
  * 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)
1152
+ * 1. No aborted fields (aborted fields block completion)
1153
+ * 2. No required fields are empty
1154
+ * 3. No fields have validation errors
1155
+ * 4. No fields are in incomplete state (e.g., partial checkbox completion)
1156
+ * 5. All fields must be addressed (answered + skipped == total)
1215
1157
  *
1216
1158
  * Every field must be explicitly addressed - either filled with a value or
1217
1159
  * skipped with a reason. This ensures agents fully process all fields.
@@ -1221,7 +1163,8 @@ function computeFormState(progress) {
1221
1163
  */
1222
1164
  function isFormComplete(progress) {
1223
1165
  const { counts } = progress;
1224
- const baseComplete = counts.invalidFields === 0 && counts.incompleteFields === 0 && counts.emptyRequiredFields === 0;
1166
+ if (counts.abortedFields > 0) return false;
1167
+ const baseComplete = counts.invalidFields === 0 && counts.emptyRequiredFields === 0;
1225
1168
  const allFieldsAccountedFor = counts.answeredFields + counts.skippedFields === counts.totalFields;
1226
1169
  return baseComplete && allFieldsAccountedFor;
1227
1170
  }
@@ -1229,14 +1172,14 @@ function isFormComplete(progress) {
1229
1172
  * Compute all summaries for a parsed form.
1230
1173
  *
1231
1174
  * @param schema - The form schema
1232
- * @param values - Current field values
1175
+ * @param responsesByFieldId - Current field responses (state + optional value)
1176
+ * @param notes - Notes attached to fields/groups/form
1233
1177
  * @param issues - Validation issues
1234
- * @param skips - Skip state per field (from skip_field patches)
1235
1178
  * @returns All computed summaries
1236
1179
  */
1237
- function computeAllSummaries(schema, values, issues, skips = {}) {
1180
+ function computeAllSummaries(schema, responsesByFieldId, notes, issues) {
1238
1181
  const structureSummary = computeStructureSummary(schema);
1239
- const progressSummary = computeProgressSummary(schema, values, issues, skips);
1182
+ const progressSummary = computeProgressSummary(schema, responsesByFieldId, notes, issues);
1240
1183
  return {
1241
1184
  structureSummary,
1242
1185
  progressSummary,
@@ -1592,10 +1535,105 @@ function validateUrlListField(field, value) {
1592
1535
  return issues;
1593
1536
  }
1594
1537
  /**
1538
+ * Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
1539
+ */
1540
+ function isValidDate(str) {
1541
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false;
1542
+ const date = new Date(str);
1543
+ if (Number.isNaN(date.getTime())) return false;
1544
+ const [year, month, day] = str.split("-").map(Number);
1545
+ return date.getUTCFullYear() === year && date.getUTCMonth() === (month ?? 0) - 1 && date.getUTCDate() === day;
1546
+ }
1547
+ /**
1548
+ * Parse a date string to compare for min/max validation.
1549
+ * Returns date value or null if invalid.
1550
+ */
1551
+ function parseDateForComparison(str) {
1552
+ if (!isValidDate(str)) return null;
1553
+ return new Date(str).getTime();
1554
+ }
1555
+ /**
1556
+ * Validate a date field.
1557
+ */
1558
+ function validateDateField(field, value) {
1559
+ const issues = [];
1560
+ const dateValue = value?.value ?? null;
1561
+ if (field.required && (dateValue === null || dateValue.trim() === "")) {
1562
+ issues.push({
1563
+ severity: "error",
1564
+ message: `Required field "${field.label}" is empty`,
1565
+ ref: field.id,
1566
+ source: "builtin"
1567
+ });
1568
+ return issues;
1569
+ }
1570
+ if (dateValue === null || dateValue === "") return issues;
1571
+ if (!isValidDate(dateValue)) {
1572
+ issues.push({
1573
+ severity: "error",
1574
+ message: `"${field.label}" is not a valid date (expected YYYY-MM-DD)`,
1575
+ ref: field.id,
1576
+ source: "builtin"
1577
+ });
1578
+ return issues;
1579
+ }
1580
+ const dateTime = parseDateForComparison(dateValue);
1581
+ if (field.min !== void 0) {
1582
+ const minTime = parseDateForComparison(field.min);
1583
+ if (minTime !== null && dateTime !== null && dateTime < minTime) issues.push({
1584
+ severity: "error",
1585
+ message: `"${field.label}" must be on or after ${field.min} (got ${dateValue})`,
1586
+ ref: field.id,
1587
+ source: "builtin"
1588
+ });
1589
+ }
1590
+ if (field.max !== void 0) {
1591
+ const maxTime = parseDateForComparison(field.max);
1592
+ if (maxTime !== null && dateTime !== null && dateTime > maxTime) issues.push({
1593
+ severity: "error",
1594
+ message: `"${field.label}" must be on or before ${field.max} (got ${dateValue})`,
1595
+ ref: field.id,
1596
+ source: "builtin"
1597
+ });
1598
+ }
1599
+ return issues;
1600
+ }
1601
+ /**
1602
+ * Validate a year field.
1603
+ */
1604
+ function validateYearField(field, value) {
1605
+ const issues = [];
1606
+ const yearValue = value?.value ?? null;
1607
+ if (field.required && yearValue === null) {
1608
+ issues.push({
1609
+ severity: "error",
1610
+ message: `Required field "${field.label}" is empty`,
1611
+ ref: field.id,
1612
+ source: "builtin"
1613
+ });
1614
+ return issues;
1615
+ }
1616
+ if (yearValue === null) return issues;
1617
+ if (field.min !== void 0 && yearValue < field.min) issues.push({
1618
+ severity: "error",
1619
+ message: `"${field.label}" must be at least ${field.min} (got ${yearValue})`,
1620
+ ref: field.id,
1621
+ source: "builtin"
1622
+ });
1623
+ if (field.max !== void 0 && yearValue > field.max) issues.push({
1624
+ severity: "error",
1625
+ message: `"${field.label}" must be at most ${field.max} (got ${yearValue})`,
1626
+ ref: field.id,
1627
+ source: "builtin"
1628
+ });
1629
+ return issues;
1630
+ }
1631
+ /**
1595
1632
  * Validate a single field.
1596
1633
  */
1597
- function validateField(field, values) {
1598
- const value = values[field.id];
1634
+ function validateField(field, responses) {
1635
+ const response = responses[field.id];
1636
+ const value = response?.state === "answered" ? response.value : void 0;
1599
1637
  switch (field.kind) {
1600
1638
  case "string": return validateStringField(field, value);
1601
1639
  case "number": return validateNumberField(field, value);
@@ -1605,6 +1643,8 @@ function validateField(field, values) {
1605
1643
  case "checkboxes": return validateCheckboxesField(field, value);
1606
1644
  case "url": return validateUrlField(field, value);
1607
1645
  case "url_list": return validateUrlListField(field, value);
1646
+ case "date": return validateDateField(field, value);
1647
+ case "year": return validateYearField(field, value);
1608
1648
  }
1609
1649
  }
1610
1650
  /**
@@ -1624,10 +1664,12 @@ function parseValidatorRef(ref) {
1624
1664
  /**
1625
1665
  * Run code validators for a field.
1626
1666
  */
1627
- function runCodeValidators(field, schema, values, registry) {
1667
+ function runCodeValidators(field, schema, responses, registry) {
1628
1668
  if (!field.validate) return [];
1629
1669
  const refs = Array.isArray(field.validate) ? field.validate : [field.validate];
1630
1670
  const issues = [];
1671
+ const values = {};
1672
+ for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
1631
1673
  for (const ref of refs) {
1632
1674
  const { id, params } = parseValidatorRef(ref);
1633
1675
  const validator = registry[id];
@@ -1666,10 +1708,12 @@ function runCodeValidators(field, schema, values, registry) {
1666
1708
  /**
1667
1709
  * Run code validators for a field group.
1668
1710
  */
1669
- function runGroupValidators(group, schema, values, registry) {
1711
+ function runGroupValidators(group, schema, responses, registry) {
1670
1712
  if (!group.validate) return [];
1671
1713
  const refs = Array.isArray(group.validate) ? group.validate : [group.validate];
1672
1714
  const issues = [];
1715
+ const values = {};
1716
+ for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
1673
1717
  for (const ref of refs) {
1674
1718
  const { id, params } = parseValidatorRef(ref);
1675
1719
  const validator = registry[id];
@@ -1717,10 +1761,10 @@ function validate(form, opts) {
1717
1761
  const registry = opts?.validatorRegistry ?? {};
1718
1762
  for (const group of form.schema.groups) {
1719
1763
  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));
1764
+ issues.push(...validateField(field, form.responsesByFieldId));
1765
+ if (!opts?.skipCodeValidators) issues.push(...runCodeValidators(field, form.schema, form.responsesByFieldId, registry));
1722
1766
  }
1723
- if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.valuesByFieldId, registry));
1767
+ if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.responsesByFieldId, registry));
1724
1768
  }
1725
1769
  return {
1726
1770
  issues,
@@ -1747,7 +1791,7 @@ function validate(form, opts) {
1747
1791
  function inspect(form, options = {}) {
1748
1792
  const validationInspectIssues = convertValidationIssues(validate(form, { skipCodeValidators: options.skipCodeValidators }).issues, form);
1749
1793
  const structureSummary = computeStructureSummary(form.schema);
1750
- const progressSummary = computeProgressSummary(form.schema, form.valuesByFieldId, validationInspectIssues, form.skipsByFieldId);
1794
+ const progressSummary = computeProgressSummary(form.schema, form.responsesByFieldId, form.notes, validationInspectIssues);
1751
1795
  const formState = computeFormState(progressSummary);
1752
1796
  const issues = filterIssuesByRole(sortAndAssignPriorities(addOptionalEmptyIssues(validationInspectIssues, form, progressSummary.fields), form), form, options.targetRoles);
1753
1797
  return {
@@ -1779,8 +1823,8 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
1779
1823
  const issues = [...existingIssues];
1780
1824
  const fieldsWithIssues = new Set(existingIssues.map((i) => i.ref));
1781
1825
  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({
1826
+ if (progress.answerState === "skipped" || progress.answerState === "aborted") continue;
1827
+ if (progress.empty && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
1784
1828
  ref: fieldId,
1785
1829
  scope: "field",
1786
1830
  reason: "optional_empty",
@@ -1795,8 +1839,9 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
1795
1839
  * Map ValidationIssue to InspectIssue reason code.
1796
1840
  */
1797
1841
  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";
1842
+ const msg = vi.message.toLowerCase();
1843
+ 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";
1844
+ if (vi.code === "INVALID_CHECKBOX_STATE" || vi.code === "CHECKBOXES_INCOMPLETE" || msg.includes("checkbox")) return "checkbox_incomplete";
1800
1845
  if (vi.code === "MULTI_SELECT_TOO_FEW" || vi.code === "STRING_LIST_MIN_ITEMS" || vi.message.includes("at least")) return "min_items_not_met";
1801
1846
  return "validation_error";
1802
1847
  }
@@ -1938,7 +1983,9 @@ function isCheckboxComplete(form, fieldId) {
1938
1983
  const field = findFieldById(form, fieldId);
1939
1984
  if (field?.kind !== "checkboxes") return true;
1940
1985
  const checkboxField = field;
1941
- const value = form.valuesByFieldId[fieldId];
1986
+ const response = form.responsesByFieldId[fieldId];
1987
+ if (response?.state !== "answered") return false;
1988
+ const value = response.value;
1942
1989
  if (value?.kind !== "checkboxes") return false;
1943
1990
  const values = value.values;
1944
1991
  const optionIds = checkboxField.options.map((o) => o.id);
@@ -2014,10 +2061,25 @@ function findField(form, fieldId) {
2014
2061
  * Validate a single patch against the form schema.
2015
2062
  */
2016
2063
  function validatePatch(form, patch, index) {
2017
- const field = findField(form, patch.fieldId);
2064
+ if (patch.op === "add_note") {
2065
+ if (!form.idIndex.has(patch.ref)) return {
2066
+ patchIndex: index,
2067
+ message: `Reference "${patch.ref}" not found in form`
2068
+ };
2069
+ return null;
2070
+ }
2071
+ if (patch.op === "remove_note") {
2072
+ if (!form.notes.some((n) => n.id === patch.noteId)) return {
2073
+ patchIndex: index,
2074
+ message: `Note with id '${patch.noteId}' not found`
2075
+ };
2076
+ return null;
2077
+ }
2078
+ const fieldId = patch.fieldId;
2079
+ const field = findField(form, fieldId);
2018
2080
  if (!field) return {
2019
2081
  patchIndex: index,
2020
- message: `Field "${patch.fieldId}" not found`
2082
+ message: `Field "${fieldId}" not found`
2021
2083
  };
2022
2084
  switch (patch.op) {
2023
2085
  case "set_string":
@@ -2090,6 +2152,18 @@ function validatePatch(form, patch, index) {
2090
2152
  message: `Cannot apply set_url_list to ${field.kind} field "${field.id}"`
2091
2153
  };
2092
2154
  break;
2155
+ case "set_date":
2156
+ if (field.kind !== "date") return {
2157
+ patchIndex: index,
2158
+ message: `Cannot apply set_date to ${field.kind} field "${field.id}"`
2159
+ };
2160
+ break;
2161
+ case "set_year":
2162
+ if (field.kind !== "year") return {
2163
+ patchIndex: index,
2164
+ message: `Cannot apply set_year to ${field.kind} field "${field.id}"`
2165
+ };
2166
+ break;
2093
2167
  case "clear_field": break;
2094
2168
  case "skip_field":
2095
2169
  if (field.required) return {
@@ -2097,6 +2171,7 @@ function validatePatch(form, patch, index) {
2097
2171
  message: `Cannot skip required field "${field.id}"`
2098
2172
  };
2099
2173
  break;
2174
+ case "abort_field": break;
2100
2175
  }
2101
2176
  return null;
2102
2177
  }
@@ -2115,191 +2190,234 @@ function validatePatches(form, patches) {
2115
2190
  return errors;
2116
2191
  }
2117
2192
  /**
2193
+ * Generate a unique note ID for the form.
2194
+ */
2195
+ function generateNoteId(form) {
2196
+ const existingIds = new Set(form.notes.map((n) => n.id));
2197
+ let counter = 1;
2198
+ while (existingIds.has(`n${counter}`)) counter++;
2199
+ return `n${counter}`;
2200
+ }
2201
+ /**
2118
2202
  * Apply a set_string patch.
2119
2203
  */
2120
- function applySetString(values, patch) {
2121
- values[patch.fieldId] = {
2122
- kind: "string",
2123
- value: patch.value
2204
+ function applySetString(responses, patch) {
2205
+ responses[patch.fieldId] = {
2206
+ state: "answered",
2207
+ value: {
2208
+ kind: "string",
2209
+ value: patch.value
2210
+ }
2124
2211
  };
2125
2212
  }
2126
2213
  /**
2127
2214
  * Apply a set_number patch.
2128
2215
  */
2129
- function applySetNumber(values, patch) {
2130
- values[patch.fieldId] = {
2131
- kind: "number",
2132
- value: patch.value
2216
+ function applySetNumber(responses, patch) {
2217
+ responses[patch.fieldId] = {
2218
+ state: "answered",
2219
+ value: {
2220
+ kind: "number",
2221
+ value: patch.value
2222
+ }
2133
2223
  };
2134
2224
  }
2135
2225
  /**
2136
2226
  * Apply a set_string_list patch.
2137
2227
  */
2138
- function applySetStringList(values, patch) {
2139
- values[patch.fieldId] = {
2140
- kind: "string_list",
2141
- items: patch.items
2228
+ function applySetStringList(responses, patch) {
2229
+ responses[patch.fieldId] = {
2230
+ state: "answered",
2231
+ value: {
2232
+ kind: "string_list",
2233
+ items: patch.items
2234
+ }
2142
2235
  };
2143
2236
  }
2144
2237
  /**
2145
2238
  * Apply a set_single_select patch.
2146
2239
  */
2147
- function applySetSingleSelect(values, patch) {
2148
- values[patch.fieldId] = {
2149
- kind: "single_select",
2150
- selected: patch.selected
2240
+ function applySetSingleSelect(responses, patch) {
2241
+ responses[patch.fieldId] = {
2242
+ state: "answered",
2243
+ value: {
2244
+ kind: "single_select",
2245
+ selected: patch.selected
2246
+ }
2151
2247
  };
2152
2248
  }
2153
2249
  /**
2154
2250
  * Apply a set_multi_select patch.
2155
2251
  */
2156
- function applySetMultiSelect(values, patch) {
2157
- values[patch.fieldId] = {
2158
- kind: "multi_select",
2159
- selected: patch.selected
2252
+ function applySetMultiSelect(responses, patch) {
2253
+ responses[patch.fieldId] = {
2254
+ state: "answered",
2255
+ value: {
2256
+ kind: "multi_select",
2257
+ selected: patch.selected
2258
+ }
2160
2259
  };
2161
2260
  }
2162
2261
  /**
2163
2262
  * Apply a set_checkboxes patch (merges with existing values).
2164
2263
  */
2165
- function applySetCheckboxes(values, patch) {
2264
+ function applySetCheckboxes(responses, patch) {
2166
2265
  const merged = {
2167
- ...values[patch.fieldId]?.values ?? {},
2266
+ ...(responses[patch.fieldId]?.value)?.values ?? {},
2168
2267
  ...patch.values
2169
2268
  };
2170
- values[patch.fieldId] = {
2171
- kind: "checkboxes",
2172
- values: merged
2269
+ responses[patch.fieldId] = {
2270
+ state: "answered",
2271
+ value: {
2272
+ kind: "checkboxes",
2273
+ values: merged
2274
+ }
2173
2275
  };
2174
2276
  }
2175
2277
  /**
2176
2278
  * Apply a set_url patch.
2177
2279
  */
2178
- function applySetUrl(values, patch) {
2179
- values[patch.fieldId] = {
2180
- kind: "url",
2181
- value: patch.value
2280
+ function applySetUrl(responses, patch) {
2281
+ responses[patch.fieldId] = {
2282
+ state: "answered",
2283
+ value: {
2284
+ kind: "url",
2285
+ value: patch.value
2286
+ }
2182
2287
  };
2183
2288
  }
2184
2289
  /**
2185
2290
  * Apply a set_url_list patch.
2186
2291
  */
2187
- function applySetUrlList(values, patch) {
2188
- values[patch.fieldId] = {
2189
- kind: "url_list",
2190
- items: patch.items
2292
+ function applySetUrlList(responses, patch) {
2293
+ responses[patch.fieldId] = {
2294
+ state: "answered",
2295
+ value: {
2296
+ kind: "url_list",
2297
+ items: patch.items
2298
+ }
2299
+ };
2300
+ }
2301
+ /**
2302
+ * Apply a set_date patch.
2303
+ */
2304
+ function applySetDate(responses, patch) {
2305
+ responses[patch.fieldId] = {
2306
+ state: "answered",
2307
+ value: {
2308
+ kind: "date",
2309
+ value: patch.value
2310
+ }
2311
+ };
2312
+ }
2313
+ /**
2314
+ * Apply a set_year patch.
2315
+ */
2316
+ function applySetYear(responses, patch) {
2317
+ responses[patch.fieldId] = {
2318
+ state: "answered",
2319
+ value: {
2320
+ kind: "year",
2321
+ value: patch.value
2322
+ }
2191
2323
  };
2192
2324
  }
2193
2325
  /**
2194
2326
  * Apply a clear_field patch.
2195
2327
  */
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
- }
2328
+ function applyClearField(responses, patch) {
2329
+ responses[patch.fieldId] = { state: "unanswered" };
2249
2330
  }
2250
2331
  /**
2251
2332
  * Apply a skip_field patch.
2252
- * Marks the field as skipped and clears any existing value.
2333
+ * Marks the field as skipped and stores reason in FieldResponse.reason.
2334
+ */
2335
+ function applySkipField(responses, patch) {
2336
+ responses[patch.fieldId] = {
2337
+ state: "skipped",
2338
+ ...patch.reason && { reason: patch.reason }
2339
+ };
2340
+ }
2341
+ /**
2342
+ * Apply an abort_field patch.
2343
+ * Marks the field as aborted and stores reason in FieldResponse.reason.
2253
2344
  */
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
2345
+ function applyAbortField(responses, patch) {
2346
+ responses[patch.fieldId] = {
2347
+ state: "aborted",
2348
+ ...patch.reason && { reason: patch.reason }
2259
2349
  };
2260
- delete values[patch.fieldId];
2261
2350
  }
2262
2351
  /**
2263
- * Apply a single patch to the values and skips.
2352
+ * Apply an add_note patch.
2353
+ * Adds a note to the form.
2264
2354
  */
2265
- function applyPatch(form, values, skips, patch) {
2355
+ function applyAddNote(form, patch) {
2356
+ const noteId = generateNoteId(form);
2357
+ form.notes.push({
2358
+ id: noteId,
2359
+ ref: patch.ref,
2360
+ role: patch.role,
2361
+ text: patch.text
2362
+ });
2363
+ }
2364
+ /**
2365
+ * Apply a remove_note patch.
2366
+ * Removes a specific note by ID.
2367
+ */
2368
+ function applyRemoveNote(form, patch) {
2369
+ const index = form.notes.findIndex((n) => n.id === patch.noteId);
2370
+ if (index >= 0) form.notes.splice(index, 1);
2371
+ }
2372
+ /**
2373
+ * Apply a single patch to the form.
2374
+ */
2375
+ function applyPatch(form, responses, patch) {
2266
2376
  switch (patch.op) {
2267
2377
  case "set_string":
2268
- applySetString(values, patch);
2269
- delete skips[patch.fieldId];
2378
+ applySetString(responses, patch);
2270
2379
  break;
2271
2380
  case "set_number":
2272
- applySetNumber(values, patch);
2273
- delete skips[patch.fieldId];
2381
+ applySetNumber(responses, patch);
2274
2382
  break;
2275
2383
  case "set_string_list":
2276
- applySetStringList(values, patch);
2277
- delete skips[patch.fieldId];
2384
+ applySetStringList(responses, patch);
2278
2385
  break;
2279
2386
  case "set_single_select":
2280
- applySetSingleSelect(values, patch);
2281
- delete skips[patch.fieldId];
2387
+ applySetSingleSelect(responses, patch);
2282
2388
  break;
2283
2389
  case "set_multi_select":
2284
- applySetMultiSelect(values, patch);
2285
- delete skips[patch.fieldId];
2390
+ applySetMultiSelect(responses, patch);
2286
2391
  break;
2287
2392
  case "set_checkboxes":
2288
- applySetCheckboxes(values, patch);
2289
- delete skips[patch.fieldId];
2393
+ applySetCheckboxes(responses, patch);
2290
2394
  break;
2291
2395
  case "set_url":
2292
- applySetUrl(values, patch);
2396
+ applySetUrl(responses, patch);
2293
2397
  break;
2294
2398
  case "set_url_list":
2295
- applySetUrlList(values, patch);
2399
+ applySetUrlList(responses, patch);
2400
+ break;
2401
+ case "set_date":
2402
+ applySetDate(responses, patch);
2403
+ break;
2404
+ case "set_year":
2405
+ applySetYear(responses, patch);
2296
2406
  break;
2297
2407
  case "clear_field":
2298
- applyClearField(form, values, patch);
2299
- delete skips[patch.fieldId];
2408
+ applyClearField(responses, patch);
2300
2409
  break;
2301
2410
  case "skip_field":
2302
- applySkipField(form, values, skips, patch);
2411
+ applySkipField(responses, patch);
2412
+ break;
2413
+ case "abort_field":
2414
+ applyAbortField(responses, patch);
2415
+ break;
2416
+ case "add_note":
2417
+ applyAddNote(form, patch);
2418
+ break;
2419
+ case "remove_note":
2420
+ applyRemoveNote(form, patch);
2303
2421
  break;
2304
2422
  }
2305
2423
  }
@@ -2331,8 +2449,8 @@ function convertToInspectIssues(form) {
2331
2449
  */
2332
2450
  function applyPatches(form, patches) {
2333
2451
  if (validatePatches(form, patches).length > 0) {
2334
- const summaries$1 = computeAllSummaries(form.schema, form.valuesByFieldId, [], form.skipsByFieldId);
2335
2452
  const issues$1 = convertToInspectIssues(form);
2453
+ const summaries$1 = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues$1);
2336
2454
  return {
2337
2455
  applyStatus: "rejected",
2338
2456
  structureSummary: summaries$1.structureSummary,
@@ -2342,13 +2460,14 @@ function applyPatches(form, patches) {
2342
2460
  formState: summaries$1.formState
2343
2461
  };
2344
2462
  }
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;
2463
+ const newResponses = { ...form.responsesByFieldId };
2464
+ const newNotes = [...form.notes];
2465
+ form.notes;
2466
+ form.notes = newNotes;
2467
+ for (const patch of patches) applyPatch(form, newResponses, patch);
2468
+ form.responsesByFieldId = newResponses;
2350
2469
  const issues = convertToInspectIssues(form);
2351
- const summaries = computeAllSummaries(form.schema, newValues, issues, newSkips);
2470
+ const summaries = computeAllSummaries(form.schema, newResponses, newNotes, issues);
2352
2471
  return {
2353
2472
  applyStatus: "applied",
2354
2473
  structureSummary: summaries.structureSummary,
@@ -2360,4 +2479,4 @@ function applyPatches(form, patches) {
2360
2479
  }
2361
2480
 
2362
2481
  //#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 };
2482
+ export { parseRolesFlag as A, DEFAULT_ROLE_INSTRUCTIONS as C, deriveReportPath as D, deriveExportPath as E, hasWebSearchSupport as F, WEB_SEARCH_CONFIG as M, formatSuggestedLlms as N, detectFileType as O, getWebSearchConfig 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, SUGGESTED_LLMS as j, getFormsDir 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 };