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.
- package/DOCS.md +546 -0
- package/README.md +340 -71
- package/SPEC.md +2779 -0
- package/dist/ai-sdk.d.mts +2 -2
- package/dist/ai-sdk.mjs +5 -3
- package/dist/{apply-BQdd-fdx.mjs → apply-00UmzDKL.mjs} +849 -730
- package/dist/bin.mjs +6 -3
- package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
- package/dist/cli.mjs +6 -3
- package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
- package/dist/coreTypes-Dful87E0.mjs +537 -0
- package/dist/index.d.mts +116 -19
- 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-Dm8jZ5dl.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 +54 -0
- package/examples/simple/simple-mock-filled.form.md +17 -13
- package/examples/simple/simple-skipped-filled.form.md +32 -9
- package/examples/simple/simple-with-skips.session.yaml +102 -143
- package/examples/simple/simple.form.md +13 -13
- package/examples/simple/simple.session.yaml +80 -69
- package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
- package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
- package/examples/startup-research/startup-research.form.md +1 -1
- package/package.json +10 -14
- package/dist/src-Cs4_9lWP.mjs +0 -2151
- package/examples/political-research/political-research.form.md +0 -233
- package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
|
@@ -1,444 +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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
178
|
+
const DEFAULT_MAX_ISSUES_PER_TURN = 10;
|
|
492
179
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
*
|
|
199
|
+
* Report extension - generated by the report command.
|
|
200
|
+
* Separate from exports as it's a filtered human-readable output.
|
|
525
201
|
*/
|
|
526
|
-
|
|
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
|
-
*
|
|
204
|
+
* All recognized markform file extensions.
|
|
205
|
+
* Combines export formats with report format.
|
|
536
206
|
*/
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
*
|
|
557
|
-
*
|
|
212
|
+
* Detect file type from path based on extension.
|
|
213
|
+
* Used by serve command to dispatch to appropriate renderer.
|
|
558
214
|
*/
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
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,
|
|
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 (
|
|
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,
|
|
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 (
|
|
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,
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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,
|
|
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 (
|
|
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,
|
|
778
|
-
const
|
|
620
|
+
function serializeField(field, responses) {
|
|
621
|
+
const response = responses[field.id];
|
|
779
622
|
switch (field.kind) {
|
|
780
|
-
case "string": return serializeStringField(field,
|
|
781
|
-
case "number": return serializeNumberField(field,
|
|
782
|
-
case "string_list": return serializeStringListField(field,
|
|
783
|
-
case "single_select": return serializeSingleSelectField(field,
|
|
784
|
-
case "multi_select": return serializeMultiSelectField(field,
|
|
785
|
-
case "checkboxes": return serializeCheckboxesField(field,
|
|
786
|
-
case "url": return serializeUrlField(field,
|
|
787
|
-
case "url_list": return serializeUrlListField(field,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
862
|
-
---`}\n\n${serializeFormSchema(form.schema, form.
|
|
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,
|
|
879
|
-
const
|
|
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.
|
|
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
|
-
*
|
|
1056
|
+
* Compute whether a field is empty (has no value).
|
|
1101
1057
|
*/
|
|
1102
|
-
function
|
|
1103
|
-
|
|
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,
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
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
|
|
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,
|
|
1098
|
+
function computeProgressSummary(schema, responsesByFieldId, notes, issues) {
|
|
1158
1099
|
const fields = {};
|
|
1159
1100
|
const counts = {
|
|
1160
1101
|
totalFields: 0,
|
|
1161
1102
|
requiredFields: 0,
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (progress.
|
|
1183
|
-
|
|
1184
|
-
if (progress.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
else counts.
|
|
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.
|
|
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
|
|
1212
|
-
* 2. No fields
|
|
1213
|
-
* 3. No fields
|
|
1214
|
-
* 4.
|
|
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
|
-
|
|
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
|
|
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,
|
|
1180
|
+
function computeAllSummaries(schema, responsesByFieldId, notes, issues) {
|
|
1238
1181
|
const structureSummary = computeStructureSummary(schema);
|
|
1239
|
-
const progressSummary = computeProgressSummary(schema,
|
|
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,
|
|
1598
|
-
const
|
|
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,
|
|
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,
|
|
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.
|
|
1721
|
-
if (!opts?.skipCodeValidators) issues.push(...runCodeValidators(field, form.schema, form.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1799
|
-
if (vi.code === "
|
|
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
|
|
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
|
-
|
|
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 "${
|
|
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(
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
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(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
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(
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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(
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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(
|
|
2264
|
+
function applySetCheckboxes(responses, patch) {
|
|
2166
2265
|
const merged = {
|
|
2167
|
-
...
|
|
2266
|
+
...(responses[patch.fieldId]?.value)?.values ?? {},
|
|
2168
2267
|
...patch.values
|
|
2169
2268
|
};
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
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(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
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(
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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(
|
|
2197
|
-
|
|
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
|
|
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
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
|
2352
|
+
* Apply an add_note patch.
|
|
2353
|
+
* Adds a note to the form.
|
|
2264
2354
|
*/
|
|
2265
|
-
function
|
|
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(
|
|
2269
|
-
delete skips[patch.fieldId];
|
|
2378
|
+
applySetString(responses, patch);
|
|
2270
2379
|
break;
|
|
2271
2380
|
case "set_number":
|
|
2272
|
-
applySetNumber(
|
|
2273
|
-
delete skips[patch.fieldId];
|
|
2381
|
+
applySetNumber(responses, patch);
|
|
2274
2382
|
break;
|
|
2275
2383
|
case "set_string_list":
|
|
2276
|
-
applySetStringList(
|
|
2277
|
-
delete skips[patch.fieldId];
|
|
2384
|
+
applySetStringList(responses, patch);
|
|
2278
2385
|
break;
|
|
2279
2386
|
case "set_single_select":
|
|
2280
|
-
applySetSingleSelect(
|
|
2281
|
-
delete skips[patch.fieldId];
|
|
2387
|
+
applySetSingleSelect(responses, patch);
|
|
2282
2388
|
break;
|
|
2283
2389
|
case "set_multi_select":
|
|
2284
|
-
applySetMultiSelect(
|
|
2285
|
-
delete skips[patch.fieldId];
|
|
2390
|
+
applySetMultiSelect(responses, patch);
|
|
2286
2391
|
break;
|
|
2287
2392
|
case "set_checkboxes":
|
|
2288
|
-
applySetCheckboxes(
|
|
2289
|
-
delete skips[patch.fieldId];
|
|
2393
|
+
applySetCheckboxes(responses, patch);
|
|
2290
2394
|
break;
|
|
2291
2395
|
case "set_url":
|
|
2292
|
-
applySetUrl(
|
|
2396
|
+
applySetUrl(responses, patch);
|
|
2293
2397
|
break;
|
|
2294
2398
|
case "set_url_list":
|
|
2295
|
-
applySetUrlList(
|
|
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(
|
|
2299
|
-
delete skips[patch.fieldId];
|
|
2408
|
+
applyClearField(responses, patch);
|
|
2300
2409
|
break;
|
|
2301
2410
|
case "skip_field":
|
|
2302
|
-
applySkipField(
|
|
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
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
form.
|
|
2349
|
-
form
|
|
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,
|
|
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 {
|
|
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 };
|