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