markform 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +546 -0
- package/README.md +338 -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-BfAGTHMh.mjs} +837 -730
- package/dist/bin.mjs +6 -3
- package/dist/{cli-pjOiHgCW.mjs → cli-B3NVm6zL.mjs} +1349 -422
- 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-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 +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 +9 -5
- 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,129 @@ const DEFAULT_MAX_TURNS = 100;
|
|
|
486
172
|
*/
|
|
487
173
|
const DEFAULT_MAX_PATCHES_PER_TURN = 20;
|
|
488
174
|
/**
|
|
489
|
-
* Default maximum issues to show per
|
|
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];
|
|
562
235
|
}
|
|
563
236
|
|
|
564
237
|
//#endregion
|
|
565
238
|
//#region src/engine/serialize.ts
|
|
566
239
|
/**
|
|
240
|
+
* Find the maximum run of fence characters at line starts (indent ≤ 3 spaces).
|
|
241
|
+
* Lines with 4+ space indent are inside code blocks so don't break fences.
|
|
242
|
+
*/
|
|
243
|
+
function maxRunAtLineStart(value, char) {
|
|
244
|
+
const escaped = char === "`" ? "`" : "~";
|
|
245
|
+
const pattern = new RegExp(`^( {0,3})${escaped}+`, "gm");
|
|
246
|
+
let maxRun = 0;
|
|
247
|
+
let match;
|
|
248
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
249
|
+
const indent = match[1]?.length ?? 0;
|
|
250
|
+
const runLength = match[0].length - indent;
|
|
251
|
+
if (runLength > maxRun) maxRun = runLength;
|
|
252
|
+
}
|
|
253
|
+
return maxRun;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Pick the optimal fence character and length for a value.
|
|
257
|
+
* Also detects if process=false is needed for Markdoc tags.
|
|
258
|
+
*/
|
|
259
|
+
function pickFence(value) {
|
|
260
|
+
const hasMarkdocTags = value.includes("{%");
|
|
261
|
+
const maxBackticks = maxRunAtLineStart(value, "`");
|
|
262
|
+
const maxTildes = maxRunAtLineStart(value, "~");
|
|
263
|
+
let char;
|
|
264
|
+
let maxRun;
|
|
265
|
+
if (maxBackticks <= maxTildes) {
|
|
266
|
+
char = "`";
|
|
267
|
+
maxRun = maxBackticks;
|
|
268
|
+
} else {
|
|
269
|
+
char = "~";
|
|
270
|
+
maxRun = maxTildes;
|
|
271
|
+
}
|
|
272
|
+
const len = Math.max(3, maxRun + 1);
|
|
273
|
+
return {
|
|
274
|
+
char,
|
|
275
|
+
len,
|
|
276
|
+
processFalse: hasMarkdocTags
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Format a value fence block with the given content.
|
|
281
|
+
* Uses smart fence selection to avoid collision with code blocks in content.
|
|
282
|
+
*/
|
|
283
|
+
function formatValueFence(content) {
|
|
284
|
+
const { char, len, processFalse } = pickFence(content);
|
|
285
|
+
const fence = char.repeat(len);
|
|
286
|
+
return `\n${fence}value${processFalse ? " {% process=false %}" : ""}\n${content}\n${fence}\n`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get sentinel value content for skipped/aborted fields with reason.
|
|
290
|
+
* Returns the fence block if there's a reason, empty string otherwise.
|
|
291
|
+
*/
|
|
292
|
+
function getSentinelContent(response) {
|
|
293
|
+
if (response?.state === "skipped" && response.reason) return formatValueFence(`%SKIP% (${response.reason})`);
|
|
294
|
+
if (response?.state === "aborted" && response.reason) return formatValueFence(`%ABORT% (${response.reason})`);
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
567
298
|
* Serialize an attribute value to Markdoc format.
|
|
568
299
|
*/
|
|
569
300
|
function serializeAttrValue(value) {
|
|
@@ -609,7 +340,7 @@ function getMarker(state) {
|
|
|
609
340
|
/**
|
|
610
341
|
* Serialize a string field.
|
|
611
342
|
*/
|
|
612
|
-
function serializeStringField(field,
|
|
343
|
+
function serializeStringField(field, response) {
|
|
613
344
|
const attrs = {
|
|
614
345
|
id: field.id,
|
|
615
346
|
label: field.label
|
|
@@ -622,15 +353,22 @@ function serializeStringField(field, value) {
|
|
|
622
353
|
if (field.minLength !== void 0) attrs.minLength = field.minLength;
|
|
623
354
|
if (field.maxLength !== void 0) attrs.maxLength = field.maxLength;
|
|
624
355
|
if (field.validate) attrs.validate = field.validate;
|
|
356
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
357
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
625
358
|
const attrStr = serializeAttrs(attrs);
|
|
626
359
|
let content = "";
|
|
627
|
-
if (
|
|
360
|
+
if (response?.state === "answered" && response.value) {
|
|
361
|
+
const value = response.value;
|
|
362
|
+
if (value.value) content = formatValueFence(value.value);
|
|
363
|
+
}
|
|
364
|
+
const sentinelContent = getSentinelContent(response);
|
|
365
|
+
if (sentinelContent) content = sentinelContent;
|
|
628
366
|
return `{% string-field ${attrStr} %}${content}{% /string-field %}`;
|
|
629
367
|
}
|
|
630
368
|
/**
|
|
631
369
|
* Serialize a number field.
|
|
632
370
|
*/
|
|
633
|
-
function serializeNumberField(field,
|
|
371
|
+
function serializeNumberField(field, response) {
|
|
634
372
|
const attrs = {
|
|
635
373
|
id: field.id,
|
|
636
374
|
label: field.label
|
|
@@ -642,15 +380,22 @@ function serializeNumberField(field, value) {
|
|
|
642
380
|
if (field.max !== void 0) attrs.max = field.max;
|
|
643
381
|
if (field.integer) attrs.integer = field.integer;
|
|
644
382
|
if (field.validate) attrs.validate = field.validate;
|
|
383
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
384
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
645
385
|
const attrStr = serializeAttrs(attrs);
|
|
646
386
|
let content = "";
|
|
647
|
-
if (
|
|
387
|
+
if (response?.state === "answered" && response.value) {
|
|
388
|
+
const value = response.value;
|
|
389
|
+
if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
|
|
390
|
+
}
|
|
391
|
+
const sentinelContent = getSentinelContent(response);
|
|
392
|
+
if (sentinelContent) content = sentinelContent;
|
|
648
393
|
return `{% number-field ${attrStr} %}${content}{% /number-field %}`;
|
|
649
394
|
}
|
|
650
395
|
/**
|
|
651
396
|
* Serialize a string-list field.
|
|
652
397
|
*/
|
|
653
|
-
function serializeStringListField(field,
|
|
398
|
+
function serializeStringListField(field, response) {
|
|
654
399
|
const attrs = {
|
|
655
400
|
id: field.id,
|
|
656
401
|
label: field.label
|
|
@@ -664,9 +409,16 @@ function serializeStringListField(field, value) {
|
|
|
664
409
|
if (field.itemMaxLength !== void 0) attrs.itemMaxLength = field.itemMaxLength;
|
|
665
410
|
if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
|
|
666
411
|
if (field.validate) attrs.validate = field.validate;
|
|
412
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
413
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
667
414
|
const attrStr = serializeAttrs(attrs);
|
|
668
415
|
let content = "";
|
|
669
|
-
if (
|
|
416
|
+
if (response?.state === "answered" && response.value) {
|
|
417
|
+
const value = response.value;
|
|
418
|
+
if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
|
|
419
|
+
}
|
|
420
|
+
const sentinelContent = getSentinelContent(response);
|
|
421
|
+
if (sentinelContent) content = sentinelContent;
|
|
670
422
|
return `{% string-list ${attrStr} %}${content}{% /string-list %}`;
|
|
671
423
|
}
|
|
672
424
|
/**
|
|
@@ -683,7 +435,7 @@ function serializeOptions(options, selected) {
|
|
|
683
435
|
/**
|
|
684
436
|
* Serialize a single-select field.
|
|
685
437
|
*/
|
|
686
|
-
function serializeSingleSelectField(field,
|
|
438
|
+
function serializeSingleSelectField(field, response) {
|
|
687
439
|
const attrs = {
|
|
688
440
|
id: field.id,
|
|
689
441
|
label: field.label
|
|
@@ -692,7 +444,11 @@ function serializeSingleSelectField(field, value) {
|
|
|
692
444
|
if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
|
|
693
445
|
if (field.role !== AGENT_ROLE) attrs.role = field.role;
|
|
694
446
|
if (field.validate) attrs.validate = field.validate;
|
|
447
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
448
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
695
449
|
const attrStr = serializeAttrs(attrs);
|
|
450
|
+
let value;
|
|
451
|
+
if (response?.state === "answered" && response.value) value = response.value;
|
|
696
452
|
const selected = {};
|
|
697
453
|
for (const opt of field.options) selected[opt.id] = opt.id === value?.selected ? "done" : "todo";
|
|
698
454
|
return `{% single-select ${attrStr} %}\n${serializeOptions(field.options, selected)}\n{% /single-select %}`;
|
|
@@ -700,7 +456,7 @@ function serializeSingleSelectField(field, value) {
|
|
|
700
456
|
/**
|
|
701
457
|
* Serialize a multi-select field.
|
|
702
458
|
*/
|
|
703
|
-
function serializeMultiSelectField(field,
|
|
459
|
+
function serializeMultiSelectField(field, response) {
|
|
704
460
|
const attrs = {
|
|
705
461
|
id: field.id,
|
|
706
462
|
label: field.label
|
|
@@ -711,7 +467,11 @@ function serializeMultiSelectField(field, value) {
|
|
|
711
467
|
if (field.minSelections !== void 0) attrs.minSelections = field.minSelections;
|
|
712
468
|
if (field.maxSelections !== void 0) attrs.maxSelections = field.maxSelections;
|
|
713
469
|
if (field.validate) attrs.validate = field.validate;
|
|
470
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
471
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
714
472
|
const attrStr = serializeAttrs(attrs);
|
|
473
|
+
let value;
|
|
474
|
+
if (response?.state === "answered" && response.value) value = response.value;
|
|
715
475
|
const selected = {};
|
|
716
476
|
const selectedSet = new Set(value?.selected ?? []);
|
|
717
477
|
for (const opt of field.options) selected[opt.id] = selectedSet.has(opt.id) ? "done" : "todo";
|
|
@@ -720,7 +480,7 @@ function serializeMultiSelectField(field, value) {
|
|
|
720
480
|
/**
|
|
721
481
|
* Serialize a checkboxes field.
|
|
722
482
|
*/
|
|
723
|
-
function serializeCheckboxesField(field,
|
|
483
|
+
function serializeCheckboxesField(field, response) {
|
|
724
484
|
const attrs = {
|
|
725
485
|
id: field.id,
|
|
726
486
|
label: field.label
|
|
@@ -732,12 +492,17 @@ function serializeCheckboxesField(field, value) {
|
|
|
732
492
|
if (field.minDone !== void 0) attrs.minDone = field.minDone;
|
|
733
493
|
if (field.approvalMode !== "none") attrs.approvalMode = field.approvalMode;
|
|
734
494
|
if (field.validate) attrs.validate = field.validate;
|
|
735
|
-
|
|
495
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
496
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
497
|
+
const attrStr = serializeAttrs(attrs);
|
|
498
|
+
let value;
|
|
499
|
+
if (response?.state === "answered" && response.value) value = response.value;
|
|
500
|
+
return `{% checkboxes ${attrStr} %}\n${serializeOptions(field.options, value?.values ?? {})}\n{% /checkboxes %}`;
|
|
736
501
|
}
|
|
737
502
|
/**
|
|
738
503
|
* Serialize a url-field.
|
|
739
504
|
*/
|
|
740
|
-
function serializeUrlField(field,
|
|
505
|
+
function serializeUrlField(field, response) {
|
|
741
506
|
const attrs = {
|
|
742
507
|
id: field.id,
|
|
743
508
|
label: field.label
|
|
@@ -746,15 +511,22 @@ function serializeUrlField(field, value) {
|
|
|
746
511
|
if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
|
|
747
512
|
if (field.role !== AGENT_ROLE) attrs.role = field.role;
|
|
748
513
|
if (field.validate) attrs.validate = field.validate;
|
|
514
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
515
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
749
516
|
const attrStr = serializeAttrs(attrs);
|
|
750
517
|
let content = "";
|
|
751
|
-
if (
|
|
518
|
+
if (response?.state === "answered" && response.value) {
|
|
519
|
+
const value = response.value;
|
|
520
|
+
if (value.value) content = formatValueFence(value.value);
|
|
521
|
+
}
|
|
522
|
+
const sentinelContent = getSentinelContent(response);
|
|
523
|
+
if (sentinelContent) content = sentinelContent;
|
|
752
524
|
return `{% url-field ${attrStr} %}${content}{% /url-field %}`;
|
|
753
525
|
}
|
|
754
526
|
/**
|
|
755
527
|
* Serialize a url-list field.
|
|
756
528
|
*/
|
|
757
|
-
function serializeUrlListField(field,
|
|
529
|
+
function serializeUrlListField(field, response) {
|
|
758
530
|
const attrs = {
|
|
759
531
|
id: field.id,
|
|
760
532
|
label: field.label
|
|
@@ -766,25 +538,86 @@ function serializeUrlListField(field, value) {
|
|
|
766
538
|
if (field.maxItems !== void 0) attrs.maxItems = field.maxItems;
|
|
767
539
|
if (field.uniqueItems) attrs.uniqueItems = field.uniqueItems;
|
|
768
540
|
if (field.validate) attrs.validate = field.validate;
|
|
541
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
542
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
769
543
|
const attrStr = serializeAttrs(attrs);
|
|
770
544
|
let content = "";
|
|
771
|
-
if (
|
|
545
|
+
if (response?.state === "answered" && response.value) {
|
|
546
|
+
const value = response.value;
|
|
547
|
+
if (value.items && value.items.length > 0) content = formatValueFence(value.items.join("\n"));
|
|
548
|
+
}
|
|
549
|
+
const sentinelContent = getSentinelContent(response);
|
|
550
|
+
if (sentinelContent) content = sentinelContent;
|
|
772
551
|
return `{% url-list ${attrStr} %}${content}{% /url-list %}`;
|
|
773
552
|
}
|
|
774
553
|
/**
|
|
554
|
+
* Serialize a date-field.
|
|
555
|
+
*/
|
|
556
|
+
function serializeDateField(field, response) {
|
|
557
|
+
const attrs = {
|
|
558
|
+
id: field.id,
|
|
559
|
+
label: field.label
|
|
560
|
+
};
|
|
561
|
+
if (field.required) attrs.required = field.required;
|
|
562
|
+
if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
|
|
563
|
+
if (field.role !== AGENT_ROLE) attrs.role = field.role;
|
|
564
|
+
if (field.min !== void 0) attrs.min = field.min;
|
|
565
|
+
if (field.max !== void 0) attrs.max = field.max;
|
|
566
|
+
if (field.validate) attrs.validate = field.validate;
|
|
567
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
568
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
569
|
+
const attrStr = serializeAttrs(attrs);
|
|
570
|
+
let content = "";
|
|
571
|
+
if (response?.state === "answered" && response.value) {
|
|
572
|
+
const value = response.value;
|
|
573
|
+
if (value.value) content = formatValueFence(value.value);
|
|
574
|
+
}
|
|
575
|
+
const sentinelContent = getSentinelContent(response);
|
|
576
|
+
if (sentinelContent) content = sentinelContent;
|
|
577
|
+
return `{% date-field ${attrStr} %}${content}{% /date-field %}`;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Serialize a year-field.
|
|
581
|
+
*/
|
|
582
|
+
function serializeYearField(field, response) {
|
|
583
|
+
const attrs = {
|
|
584
|
+
id: field.id,
|
|
585
|
+
label: field.label
|
|
586
|
+
};
|
|
587
|
+
if (field.required) attrs.required = field.required;
|
|
588
|
+
if (field.priority !== DEFAULT_PRIORITY) attrs.priority = field.priority;
|
|
589
|
+
if (field.role !== AGENT_ROLE) attrs.role = field.role;
|
|
590
|
+
if (field.min !== void 0) attrs.min = field.min;
|
|
591
|
+
if (field.max !== void 0) attrs.max = field.max;
|
|
592
|
+
if (field.validate) attrs.validate = field.validate;
|
|
593
|
+
if (field.report !== void 0) attrs.report = field.report;
|
|
594
|
+
if (response?.state === "skipped" || response?.state === "aborted") attrs.state = response.state;
|
|
595
|
+
const attrStr = serializeAttrs(attrs);
|
|
596
|
+
let content = "";
|
|
597
|
+
if (response?.state === "answered" && response.value) {
|
|
598
|
+
const value = response.value;
|
|
599
|
+
if (value.value !== null && value.value !== void 0) content = formatValueFence(String(value.value));
|
|
600
|
+
}
|
|
601
|
+
const sentinelContent = getSentinelContent(response);
|
|
602
|
+
if (sentinelContent) content = sentinelContent;
|
|
603
|
+
return `{% year-field ${attrStr} %}${content}{% /year-field %}`;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
775
606
|
* Serialize a field to Markdoc format.
|
|
776
607
|
*/
|
|
777
|
-
function serializeField(field,
|
|
778
|
-
const
|
|
608
|
+
function serializeField(field, responses) {
|
|
609
|
+
const response = responses[field.id];
|
|
779
610
|
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,
|
|
611
|
+
case "string": return serializeStringField(field, response);
|
|
612
|
+
case "number": return serializeNumberField(field, response);
|
|
613
|
+
case "string_list": return serializeStringListField(field, response);
|
|
614
|
+
case "single_select": return serializeSingleSelectField(field, response);
|
|
615
|
+
case "multi_select": return serializeMultiSelectField(field, response);
|
|
616
|
+
case "checkboxes": return serializeCheckboxesField(field, response);
|
|
617
|
+
case "url": return serializeUrlField(field, response);
|
|
618
|
+
case "url_list": return serializeUrlListField(field, response);
|
|
619
|
+
case "date": return serializeDateField(field, response);
|
|
620
|
+
case "year": return serializeYearField(field, response);
|
|
788
621
|
}
|
|
789
622
|
}
|
|
790
623
|
/**
|
|
@@ -792,16 +625,39 @@ function serializeField(field, values) {
|
|
|
792
625
|
* Uses the semantic tag name (description, instructions, documentation).
|
|
793
626
|
*/
|
|
794
627
|
function serializeDocBlock(doc) {
|
|
795
|
-
const
|
|
628
|
+
const attrs = { ref: doc.ref };
|
|
629
|
+
if (doc.report !== void 0) attrs.report = doc.report;
|
|
630
|
+
const attrStr = serializeAttrs(attrs);
|
|
796
631
|
return `{% ${doc.tag} ${attrStr} %}\n${doc.bodyMarkdown}\n{% /${doc.tag} %}`;
|
|
797
632
|
}
|
|
798
633
|
/**
|
|
634
|
+
* Serialize notes in sorted order.
|
|
635
|
+
* Notes are sorted numerically by ID suffix (n1, n2, n10 not n1, n10, n2).
|
|
636
|
+
*/
|
|
637
|
+
function serializeNotes(notes) {
|
|
638
|
+
if (notes.length === 0) return "";
|
|
639
|
+
const sorted = [...notes].sort((a, b) => {
|
|
640
|
+
return (Number.parseInt(a.id.replace(/^n/, ""), 10) || 0) - (Number.parseInt(b.id.replace(/^n/, ""), 10) || 0);
|
|
641
|
+
});
|
|
642
|
+
const lines = [];
|
|
643
|
+
for (const note of sorted) {
|
|
644
|
+
const attrStr = serializeAttrs({
|
|
645
|
+
id: note.id,
|
|
646
|
+
ref: note.ref,
|
|
647
|
+
role: note.role
|
|
648
|
+
});
|
|
649
|
+
lines.push(`{% note ${attrStr} %}\n${note.text}\n{% /note %}`);
|
|
650
|
+
}
|
|
651
|
+
return lines.join("\n\n");
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
799
654
|
* Serialize a field group.
|
|
800
655
|
*/
|
|
801
|
-
function serializeFieldGroup(group,
|
|
656
|
+
function serializeFieldGroup(group, responses, docs) {
|
|
802
657
|
const attrs = { id: group.id };
|
|
803
658
|
if (group.title) attrs.title = group.title;
|
|
804
659
|
if (group.validate) attrs.validate = group.validate;
|
|
660
|
+
if (group.report !== void 0) attrs.report = group.report;
|
|
805
661
|
const lines = [`{% field-group ${serializeAttrs(attrs)} %}`];
|
|
806
662
|
const docsByRef = /* @__PURE__ */ new Map();
|
|
807
663
|
for (const doc of docs) {
|
|
@@ -811,7 +667,7 @@ function serializeFieldGroup(group, values, docs) {
|
|
|
811
667
|
}
|
|
812
668
|
for (const field of group.children) {
|
|
813
669
|
lines.push("");
|
|
814
|
-
lines.push(serializeField(field,
|
|
670
|
+
lines.push(serializeField(field, responses));
|
|
815
671
|
const fieldDocs = docsByRef.get(field.id);
|
|
816
672
|
if (fieldDocs) for (const doc of fieldDocs) {
|
|
817
673
|
lines.push("");
|
|
@@ -825,7 +681,7 @@ function serializeFieldGroup(group, values, docs) {
|
|
|
825
681
|
/**
|
|
826
682
|
* Serialize a form schema.
|
|
827
683
|
*/
|
|
828
|
-
function serializeFormSchema(schema,
|
|
684
|
+
function serializeFormSchema(schema, responses, docs, notes) {
|
|
829
685
|
const attrs = { id: schema.id };
|
|
830
686
|
if (schema.title) attrs.title = schema.title;
|
|
831
687
|
const lines = [`{% form ${serializeAttrs(attrs)} %}`];
|
|
@@ -842,7 +698,12 @@ function serializeFormSchema(schema, values, docs) {
|
|
|
842
698
|
}
|
|
843
699
|
for (const group of schema.groups) {
|
|
844
700
|
lines.push("");
|
|
845
|
-
lines.push(serializeFieldGroup(group,
|
|
701
|
+
lines.push(serializeFieldGroup(group, responses, docs));
|
|
702
|
+
}
|
|
703
|
+
const notesContent = serializeNotes(notes);
|
|
704
|
+
if (notesContent) {
|
|
705
|
+
lines.push("");
|
|
706
|
+
lines.push(notesContent);
|
|
846
707
|
}
|
|
847
708
|
lines.push("");
|
|
848
709
|
lines.push("{% /form %}");
|
|
@@ -858,8 +719,8 @@ function serializeFormSchema(schema, values, docs) {
|
|
|
858
719
|
function serialize(form, opts) {
|
|
859
720
|
return `${`---
|
|
860
721
|
markform:
|
|
861
|
-
|
|
862
|
-
---`}\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`;
|
|
863
724
|
}
|
|
864
725
|
/** Map checkbox state to GFM marker for raw markdown output */
|
|
865
726
|
const STATE_TO_GFM_MARKER = {
|
|
@@ -875,10 +736,11 @@ const STATE_TO_GFM_MARKER = {
|
|
|
875
736
|
/**
|
|
876
737
|
* Serialize a field value to raw markdown (human-readable).
|
|
877
738
|
*/
|
|
878
|
-
function serializeFieldRaw(field,
|
|
879
|
-
const
|
|
739
|
+
function serializeFieldRaw(field, responses) {
|
|
740
|
+
const response = responses[field.id];
|
|
880
741
|
const lines = [];
|
|
881
742
|
lines.push(`**${field.label}:**`);
|
|
743
|
+
const value = response?.state === "answered" ? response.value : void 0;
|
|
882
744
|
switch (field.kind) {
|
|
883
745
|
case "string": {
|
|
884
746
|
const strValue = value;
|
|
@@ -933,6 +795,18 @@ function serializeFieldRaw(field, values) {
|
|
|
933
795
|
else lines.push("_(empty)_");
|
|
934
796
|
break;
|
|
935
797
|
}
|
|
798
|
+
case "date": {
|
|
799
|
+
const dateValue = value;
|
|
800
|
+
if (dateValue?.value) lines.push(dateValue.value);
|
|
801
|
+
else lines.push("_(empty)_");
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case "year": {
|
|
805
|
+
const yearValue = value;
|
|
806
|
+
if (yearValue?.value !== null && yearValue?.value !== void 0) lines.push(String(yearValue.value));
|
|
807
|
+
else lines.push("_(empty)_");
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
936
810
|
}
|
|
937
811
|
return lines.join("\n");
|
|
938
812
|
}
|
|
@@ -973,7 +847,70 @@ function serializeRawMarkdown(form) {
|
|
|
973
847
|
lines.push("");
|
|
974
848
|
}
|
|
975
849
|
for (const field of group.children) {
|
|
976
|
-
lines.push(serializeFieldRaw(field, form.
|
|
850
|
+
lines.push(serializeFieldRaw(field, form.responsesByFieldId));
|
|
851
|
+
lines.push("");
|
|
852
|
+
const fieldDocs = docsByRef.get(field.id);
|
|
853
|
+
if (fieldDocs) for (const doc of fieldDocs) {
|
|
854
|
+
lines.push(doc.bodyMarkdown.trim());
|
|
855
|
+
lines.push("");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return lines.join("\n").trim() + "\n";
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Check if a documentation block should be included in reports.
|
|
863
|
+
* Default: instructions are excluded, everything else is included.
|
|
864
|
+
*/
|
|
865
|
+
function shouldIncludeDoc(doc) {
|
|
866
|
+
if (doc.report !== void 0) return doc.report;
|
|
867
|
+
return doc.tag !== "instructions";
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Serialize a form to filtered markdown for reports.
|
|
871
|
+
*
|
|
872
|
+
* Produces clean, readable markdown with filtered content based on `report` attribute:
|
|
873
|
+
* - Fields with report=false are excluded
|
|
874
|
+
* - Groups with report=false are excluded
|
|
875
|
+
* - Documentation blocks with report=false are excluded
|
|
876
|
+
* - Instructions blocks are excluded by default (unless report=true)
|
|
877
|
+
*
|
|
878
|
+
* @param form - The parsed form to serialize
|
|
879
|
+
* @returns Filtered plain markdown string suitable for sharing
|
|
880
|
+
*/
|
|
881
|
+
function serializeReportMarkdown(form) {
|
|
882
|
+
const lines = [];
|
|
883
|
+
const docsByRef = /* @__PURE__ */ new Map();
|
|
884
|
+
for (const doc of form.docs) {
|
|
885
|
+
if (!shouldIncludeDoc(doc)) continue;
|
|
886
|
+
const list = docsByRef.get(doc.ref) ?? [];
|
|
887
|
+
list.push(doc);
|
|
888
|
+
docsByRef.set(doc.ref, list);
|
|
889
|
+
}
|
|
890
|
+
if (form.schema.title) {
|
|
891
|
+
lines.push(`# ${form.schema.title}`);
|
|
892
|
+
lines.push("");
|
|
893
|
+
}
|
|
894
|
+
const formDocs = docsByRef.get(form.schema.id);
|
|
895
|
+
if (formDocs) for (const doc of formDocs) {
|
|
896
|
+
lines.push(doc.bodyMarkdown.trim());
|
|
897
|
+
lines.push("");
|
|
898
|
+
}
|
|
899
|
+
for (const group of form.schema.groups) {
|
|
900
|
+
if (group.report === false) continue;
|
|
901
|
+
const visibleFields = group.children.filter((field) => field.report !== false);
|
|
902
|
+
if (visibleFields.length === 0 && !group.title) continue;
|
|
903
|
+
if (group.title) {
|
|
904
|
+
lines.push(`## ${group.title}`);
|
|
905
|
+
lines.push("");
|
|
906
|
+
}
|
|
907
|
+
const groupDocs = docsByRef.get(group.id);
|
|
908
|
+
if (groupDocs) for (const doc of groupDocs) {
|
|
909
|
+
lines.push(doc.bodyMarkdown.trim());
|
|
910
|
+
lines.push("");
|
|
911
|
+
}
|
|
912
|
+
for (const field of visibleFields) {
|
|
913
|
+
lines.push(serializeFieldRaw(field, form.responsesByFieldId));
|
|
977
914
|
lines.push("");
|
|
978
915
|
const fieldDocs = docsByRef.get(field.id);
|
|
979
916
|
if (fieldDocs) for (const doc of fieldDocs) {
|
|
@@ -1002,7 +939,9 @@ function computeStructureSummary(schema) {
|
|
|
1002
939
|
single_select: 0,
|
|
1003
940
|
multi_select: 0,
|
|
1004
941
|
url: 0,
|
|
1005
|
-
url_list: 0
|
|
942
|
+
url_list: 0,
|
|
943
|
+
date: 0,
|
|
944
|
+
year: 0
|
|
1006
945
|
};
|
|
1007
946
|
const groupsById = {};
|
|
1008
947
|
const fieldsById = {};
|
|
@@ -1068,6 +1007,11 @@ function isFieldSubmitted(field, value) {
|
|
|
1068
1007
|
return v.value !== null && v.value.trim() !== "";
|
|
1069
1008
|
}
|
|
1070
1009
|
case "url_list": return value.items.length > 0;
|
|
1010
|
+
case "date": {
|
|
1011
|
+
const v = value;
|
|
1012
|
+
return v.value !== null && v.value.trim() !== "";
|
|
1013
|
+
}
|
|
1014
|
+
case "year": return value.value !== null;
|
|
1071
1015
|
}
|
|
1072
1016
|
}
|
|
1073
1017
|
/**
|
|
@@ -1097,50 +1041,35 @@ function computeCheckboxProgress(field, value) {
|
|
|
1097
1041
|
return result;
|
|
1098
1042
|
}
|
|
1099
1043
|
/**
|
|
1100
|
-
*
|
|
1101
|
-
*/
|
|
1102
|
-
function isCheckboxesComplete(field, value) {
|
|
1103
|
-
if (!value) return false;
|
|
1104
|
-
const mode = field.checkboxMode ?? "multi";
|
|
1105
|
-
for (const opt of field.options) {
|
|
1106
|
-
const state = value.values[opt.id];
|
|
1107
|
-
if (mode === "explicit") {
|
|
1108
|
-
if (state === "unfilled") return false;
|
|
1109
|
-
} else if (mode === "multi") {
|
|
1110
|
-
if (state === "todo" || state === "incomplete" || state === "active") return false;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
return true;
|
|
1114
|
-
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Compute the progress state for a field.
|
|
1044
|
+
* Compute whether a field is empty (has no value).
|
|
1117
1045
|
*/
|
|
1118
|
-
function
|
|
1119
|
-
|
|
1120
|
-
if (issueCount > 0) return "invalid";
|
|
1121
|
-
if (field.kind === "checkboxes" && value?.kind === "checkboxes") {
|
|
1122
|
-
if (!isCheckboxesComplete(field, value)) return "incomplete";
|
|
1123
|
-
}
|
|
1124
|
-
return "complete";
|
|
1046
|
+
function isFieldEmpty(field, value) {
|
|
1047
|
+
return !isFieldSubmitted(field, value);
|
|
1125
1048
|
}
|
|
1126
1049
|
/**
|
|
1127
1050
|
* Compute progress for a single field.
|
|
1128
1051
|
*/
|
|
1129
|
-
function computeFieldProgress(field,
|
|
1130
|
-
const
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1052
|
+
function computeFieldProgress(field, response, notes, issues) {
|
|
1053
|
+
const fieldIssues = issues.filter((i) => i.ref === field.id);
|
|
1054
|
+
const issueCount = fieldIssues.length;
|
|
1055
|
+
const value = response.value;
|
|
1056
|
+
const empty = isFieldEmpty(field, value);
|
|
1057
|
+
let valid = true;
|
|
1058
|
+
if (response.state === "skipped" || response.state === "aborted") valid = issueCount === 0;
|
|
1059
|
+
else if (empty) valid = fieldIssues.filter((i) => i.reason !== "required_missing").length === 0;
|
|
1060
|
+
else valid = issueCount === 0;
|
|
1061
|
+
const fieldNotes = notes.filter((n) => n.ref === field.id);
|
|
1062
|
+
const hasNotes = fieldNotes.length > 0;
|
|
1063
|
+
const noteCount = fieldNotes.length;
|
|
1135
1064
|
const progress = {
|
|
1136
1065
|
kind: field.kind,
|
|
1137
1066
|
required: field.required,
|
|
1138
|
-
|
|
1139
|
-
|
|
1067
|
+
answerState: response.state,
|
|
1068
|
+
hasNotes,
|
|
1069
|
+
noteCount,
|
|
1070
|
+
empty,
|
|
1140
1071
|
valid,
|
|
1141
|
-
issueCount
|
|
1142
|
-
skipped,
|
|
1143
|
-
skipReason: skipInfo?.reason
|
|
1072
|
+
issueCount
|
|
1144
1073
|
};
|
|
1145
1074
|
if (field.kind === "checkboxes") progress.checkboxProgress = computeCheckboxProgress(field, value);
|
|
1146
1075
|
return progress;
|
|
@@ -1149,42 +1078,42 @@ function computeFieldProgress(field, value, issues, skipInfo) {
|
|
|
1149
1078
|
* Compute a progress summary for a form.
|
|
1150
1079
|
*
|
|
1151
1080
|
* @param schema - The form schema
|
|
1152
|
-
* @param
|
|
1081
|
+
* @param responsesByFieldId - Current field responses (state + optional value)
|
|
1082
|
+
* @param notes - Notes attached to fields/groups/form
|
|
1153
1083
|
* @param issues - Validation issues (from inspect)
|
|
1154
|
-
* @param skips - Skip state per field (from skip_field patches)
|
|
1155
1084
|
* @returns Progress summary with field states and counts
|
|
1156
1085
|
*/
|
|
1157
|
-
function computeProgressSummary(schema,
|
|
1086
|
+
function computeProgressSummary(schema, responsesByFieldId, notes, issues) {
|
|
1158
1087
|
const fields = {};
|
|
1159
1088
|
const counts = {
|
|
1160
1089
|
totalFields: 0,
|
|
1161
1090
|
requiredFields: 0,
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1091
|
+
unansweredFields: 0,
|
|
1092
|
+
answeredFields: 0,
|
|
1093
|
+
skippedFields: 0,
|
|
1094
|
+
abortedFields: 0,
|
|
1095
|
+
validFields: 0,
|
|
1165
1096
|
invalidFields: 0,
|
|
1097
|
+
emptyFields: 0,
|
|
1098
|
+
filledFields: 0,
|
|
1166
1099
|
emptyRequiredFields: 0,
|
|
1167
|
-
|
|
1168
|
-
answeredFields: 0,
|
|
1169
|
-
skippedFields: 0
|
|
1100
|
+
totalNotes: notes.length
|
|
1170
1101
|
};
|
|
1171
1102
|
for (const group of schema.groups) for (const field of group.children) {
|
|
1172
|
-
const
|
|
1173
|
-
const skipInfo = skips[field.id];
|
|
1174
|
-
const progress = computeFieldProgress(field, value, issues, skipInfo);
|
|
1103
|
+
const progress = computeFieldProgress(field, responsesByFieldId[field.id] ?? { state: "unanswered" }, notes, issues);
|
|
1175
1104
|
fields[field.id] = progress;
|
|
1176
1105
|
counts.totalFields++;
|
|
1177
1106
|
if (progress.required) counts.requiredFields++;
|
|
1178
|
-
if (progress.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (progress.
|
|
1183
|
-
|
|
1184
|
-
if (progress.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
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++;
|
|
1188
1117
|
}
|
|
1189
1118
|
return {
|
|
1190
1119
|
counts,
|
|
@@ -1198,20 +1127,21 @@ function computeProgressSummary(schema, values, issues, skips = {}) {
|
|
|
1198
1127
|
* @returns The overall form state
|
|
1199
1128
|
*/
|
|
1200
1129
|
function computeFormState(progress) {
|
|
1130
|
+
if (progress.counts.abortedFields > 0) return "invalid";
|
|
1201
1131
|
if (progress.counts.invalidFields > 0) return "invalid";
|
|
1202
|
-
if (progress.counts.incompleteFields > 0) return "incomplete";
|
|
1203
1132
|
if (progress.counts.emptyRequiredFields === 0) return "complete";
|
|
1204
|
-
if (progress.counts.
|
|
1133
|
+
if (progress.counts.answeredFields > 0) return "incomplete";
|
|
1205
1134
|
return "empty";
|
|
1206
1135
|
}
|
|
1207
1136
|
/**
|
|
1208
1137
|
* Determine if the form is complete (ready for submission).
|
|
1209
1138
|
*
|
|
1210
1139
|
* A form is complete when:
|
|
1211
|
-
* 1. No
|
|
1212
|
-
* 2. No fields
|
|
1213
|
-
* 3. No fields
|
|
1214
|
-
* 4.
|
|
1140
|
+
* 1. No aborted fields (aborted fields block completion)
|
|
1141
|
+
* 2. No required fields are empty
|
|
1142
|
+
* 3. No fields have validation errors
|
|
1143
|
+
* 4. No fields are in incomplete state (e.g., partial checkbox completion)
|
|
1144
|
+
* 5. All fields must be addressed (answered + skipped == total)
|
|
1215
1145
|
*
|
|
1216
1146
|
* Every field must be explicitly addressed - either filled with a value or
|
|
1217
1147
|
* skipped with a reason. This ensures agents fully process all fields.
|
|
@@ -1221,7 +1151,8 @@ function computeFormState(progress) {
|
|
|
1221
1151
|
*/
|
|
1222
1152
|
function isFormComplete(progress) {
|
|
1223
1153
|
const { counts } = progress;
|
|
1224
|
-
|
|
1154
|
+
if (counts.abortedFields > 0) return false;
|
|
1155
|
+
const baseComplete = counts.invalidFields === 0 && counts.emptyRequiredFields === 0;
|
|
1225
1156
|
const allFieldsAccountedFor = counts.answeredFields + counts.skippedFields === counts.totalFields;
|
|
1226
1157
|
return baseComplete && allFieldsAccountedFor;
|
|
1227
1158
|
}
|
|
@@ -1229,14 +1160,14 @@ function isFormComplete(progress) {
|
|
|
1229
1160
|
* Compute all summaries for a parsed form.
|
|
1230
1161
|
*
|
|
1231
1162
|
* @param schema - The form schema
|
|
1232
|
-
* @param
|
|
1163
|
+
* @param responsesByFieldId - Current field responses (state + optional value)
|
|
1164
|
+
* @param notes - Notes attached to fields/groups/form
|
|
1233
1165
|
* @param issues - Validation issues
|
|
1234
|
-
* @param skips - Skip state per field (from skip_field patches)
|
|
1235
1166
|
* @returns All computed summaries
|
|
1236
1167
|
*/
|
|
1237
|
-
function computeAllSummaries(schema,
|
|
1168
|
+
function computeAllSummaries(schema, responsesByFieldId, notes, issues) {
|
|
1238
1169
|
const structureSummary = computeStructureSummary(schema);
|
|
1239
|
-
const progressSummary = computeProgressSummary(schema,
|
|
1170
|
+
const progressSummary = computeProgressSummary(schema, responsesByFieldId, notes, issues);
|
|
1240
1171
|
return {
|
|
1241
1172
|
structureSummary,
|
|
1242
1173
|
progressSummary,
|
|
@@ -1592,10 +1523,105 @@ function validateUrlListField(field, value) {
|
|
|
1592
1523
|
return issues;
|
|
1593
1524
|
}
|
|
1594
1525
|
/**
|
|
1526
|
+
* Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
|
|
1527
|
+
*/
|
|
1528
|
+
function isValidDate(str) {
|
|
1529
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false;
|
|
1530
|
+
const date = new Date(str);
|
|
1531
|
+
if (Number.isNaN(date.getTime())) return false;
|
|
1532
|
+
const [year, month, day] = str.split("-").map(Number);
|
|
1533
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === (month ?? 0) - 1 && date.getUTCDate() === day;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Parse a date string to compare for min/max validation.
|
|
1537
|
+
* Returns date value or null if invalid.
|
|
1538
|
+
*/
|
|
1539
|
+
function parseDateForComparison(str) {
|
|
1540
|
+
if (!isValidDate(str)) return null;
|
|
1541
|
+
return new Date(str).getTime();
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Validate a date field.
|
|
1545
|
+
*/
|
|
1546
|
+
function validateDateField(field, value) {
|
|
1547
|
+
const issues = [];
|
|
1548
|
+
const dateValue = value?.value ?? null;
|
|
1549
|
+
if (field.required && (dateValue === null || dateValue.trim() === "")) {
|
|
1550
|
+
issues.push({
|
|
1551
|
+
severity: "error",
|
|
1552
|
+
message: `Required field "${field.label}" is empty`,
|
|
1553
|
+
ref: field.id,
|
|
1554
|
+
source: "builtin"
|
|
1555
|
+
});
|
|
1556
|
+
return issues;
|
|
1557
|
+
}
|
|
1558
|
+
if (dateValue === null || dateValue === "") return issues;
|
|
1559
|
+
if (!isValidDate(dateValue)) {
|
|
1560
|
+
issues.push({
|
|
1561
|
+
severity: "error",
|
|
1562
|
+
message: `"${field.label}" is not a valid date (expected YYYY-MM-DD)`,
|
|
1563
|
+
ref: field.id,
|
|
1564
|
+
source: "builtin"
|
|
1565
|
+
});
|
|
1566
|
+
return issues;
|
|
1567
|
+
}
|
|
1568
|
+
const dateTime = parseDateForComparison(dateValue);
|
|
1569
|
+
if (field.min !== void 0) {
|
|
1570
|
+
const minTime = parseDateForComparison(field.min);
|
|
1571
|
+
if (minTime !== null && dateTime !== null && dateTime < minTime) issues.push({
|
|
1572
|
+
severity: "error",
|
|
1573
|
+
message: `"${field.label}" must be on or after ${field.min} (got ${dateValue})`,
|
|
1574
|
+
ref: field.id,
|
|
1575
|
+
source: "builtin"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (field.max !== void 0) {
|
|
1579
|
+
const maxTime = parseDateForComparison(field.max);
|
|
1580
|
+
if (maxTime !== null && dateTime !== null && dateTime > maxTime) issues.push({
|
|
1581
|
+
severity: "error",
|
|
1582
|
+
message: `"${field.label}" must be on or before ${field.max} (got ${dateValue})`,
|
|
1583
|
+
ref: field.id,
|
|
1584
|
+
source: "builtin"
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
return issues;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Validate a year field.
|
|
1591
|
+
*/
|
|
1592
|
+
function validateYearField(field, value) {
|
|
1593
|
+
const issues = [];
|
|
1594
|
+
const yearValue = value?.value ?? null;
|
|
1595
|
+
if (field.required && yearValue === null) {
|
|
1596
|
+
issues.push({
|
|
1597
|
+
severity: "error",
|
|
1598
|
+
message: `Required field "${field.label}" is empty`,
|
|
1599
|
+
ref: field.id,
|
|
1600
|
+
source: "builtin"
|
|
1601
|
+
});
|
|
1602
|
+
return issues;
|
|
1603
|
+
}
|
|
1604
|
+
if (yearValue === null) return issues;
|
|
1605
|
+
if (field.min !== void 0 && yearValue < field.min) issues.push({
|
|
1606
|
+
severity: "error",
|
|
1607
|
+
message: `"${field.label}" must be at least ${field.min} (got ${yearValue})`,
|
|
1608
|
+
ref: field.id,
|
|
1609
|
+
source: "builtin"
|
|
1610
|
+
});
|
|
1611
|
+
if (field.max !== void 0 && yearValue > field.max) issues.push({
|
|
1612
|
+
severity: "error",
|
|
1613
|
+
message: `"${field.label}" must be at most ${field.max} (got ${yearValue})`,
|
|
1614
|
+
ref: field.id,
|
|
1615
|
+
source: "builtin"
|
|
1616
|
+
});
|
|
1617
|
+
return issues;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1595
1620
|
* Validate a single field.
|
|
1596
1621
|
*/
|
|
1597
|
-
function validateField(field,
|
|
1598
|
-
const
|
|
1622
|
+
function validateField(field, responses) {
|
|
1623
|
+
const response = responses[field.id];
|
|
1624
|
+
const value = response?.state === "answered" ? response.value : void 0;
|
|
1599
1625
|
switch (field.kind) {
|
|
1600
1626
|
case "string": return validateStringField(field, value);
|
|
1601
1627
|
case "number": return validateNumberField(field, value);
|
|
@@ -1605,6 +1631,8 @@ function validateField(field, values) {
|
|
|
1605
1631
|
case "checkboxes": return validateCheckboxesField(field, value);
|
|
1606
1632
|
case "url": return validateUrlField(field, value);
|
|
1607
1633
|
case "url_list": return validateUrlListField(field, value);
|
|
1634
|
+
case "date": return validateDateField(field, value);
|
|
1635
|
+
case "year": return validateYearField(field, value);
|
|
1608
1636
|
}
|
|
1609
1637
|
}
|
|
1610
1638
|
/**
|
|
@@ -1624,10 +1652,12 @@ function parseValidatorRef(ref) {
|
|
|
1624
1652
|
/**
|
|
1625
1653
|
* Run code validators for a field.
|
|
1626
1654
|
*/
|
|
1627
|
-
function runCodeValidators(field, schema,
|
|
1655
|
+
function runCodeValidators(field, schema, responses, registry) {
|
|
1628
1656
|
if (!field.validate) return [];
|
|
1629
1657
|
const refs = Array.isArray(field.validate) ? field.validate : [field.validate];
|
|
1630
1658
|
const issues = [];
|
|
1659
|
+
const values = {};
|
|
1660
|
+
for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
|
|
1631
1661
|
for (const ref of refs) {
|
|
1632
1662
|
const { id, params } = parseValidatorRef(ref);
|
|
1633
1663
|
const validator = registry[id];
|
|
@@ -1666,10 +1696,12 @@ function runCodeValidators(field, schema, values, registry) {
|
|
|
1666
1696
|
/**
|
|
1667
1697
|
* Run code validators for a field group.
|
|
1668
1698
|
*/
|
|
1669
|
-
function runGroupValidators(group, schema,
|
|
1699
|
+
function runGroupValidators(group, schema, responses, registry) {
|
|
1670
1700
|
if (!group.validate) return [];
|
|
1671
1701
|
const refs = Array.isArray(group.validate) ? group.validate : [group.validate];
|
|
1672
1702
|
const issues = [];
|
|
1703
|
+
const values = {};
|
|
1704
|
+
for (const [id, response] of Object.entries(responses)) if (response.state === "answered" && response.value !== void 0) values[id] = response.value;
|
|
1673
1705
|
for (const ref of refs) {
|
|
1674
1706
|
const { id, params } = parseValidatorRef(ref);
|
|
1675
1707
|
const validator = registry[id];
|
|
@@ -1717,10 +1749,10 @@ function validate(form, opts) {
|
|
|
1717
1749
|
const registry = opts?.validatorRegistry ?? {};
|
|
1718
1750
|
for (const group of form.schema.groups) {
|
|
1719
1751
|
for (const field of group.children) {
|
|
1720
|
-
issues.push(...validateField(field, form.
|
|
1721
|
-
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));
|
|
1722
1754
|
}
|
|
1723
|
-
if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.
|
|
1755
|
+
if (!opts?.skipCodeValidators) issues.push(...runGroupValidators(group, form.schema, form.responsesByFieldId, registry));
|
|
1724
1756
|
}
|
|
1725
1757
|
return {
|
|
1726
1758
|
issues,
|
|
@@ -1747,7 +1779,7 @@ function validate(form, opts) {
|
|
|
1747
1779
|
function inspect(form, options = {}) {
|
|
1748
1780
|
const validationInspectIssues = convertValidationIssues(validate(form, { skipCodeValidators: options.skipCodeValidators }).issues, form);
|
|
1749
1781
|
const structureSummary = computeStructureSummary(form.schema);
|
|
1750
|
-
const progressSummary = computeProgressSummary(form.schema, form.
|
|
1782
|
+
const progressSummary = computeProgressSummary(form.schema, form.responsesByFieldId, form.notes, validationInspectIssues);
|
|
1751
1783
|
const formState = computeFormState(progressSummary);
|
|
1752
1784
|
const issues = filterIssuesByRole(sortAndAssignPriorities(addOptionalEmptyIssues(validationInspectIssues, form, progressSummary.fields), form), form, options.targetRoles);
|
|
1753
1785
|
return {
|
|
@@ -1779,8 +1811,8 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
|
|
|
1779
1811
|
const issues = [...existingIssues];
|
|
1780
1812
|
const fieldsWithIssues = new Set(existingIssues.map((i) => i.ref));
|
|
1781
1813
|
for (const [fieldId, progress] of Object.entries(fieldProgress)) {
|
|
1782
|
-
if (progress.skipped) continue;
|
|
1783
|
-
if (progress.
|
|
1814
|
+
if (progress.answerState === "skipped" || progress.answerState === "aborted") continue;
|
|
1815
|
+
if (progress.empty && !fieldsWithIssues.has(fieldId) && !isRequiredField(fieldId, form)) issues.push({
|
|
1784
1816
|
ref: fieldId,
|
|
1785
1817
|
scope: "field",
|
|
1786
1818
|
reason: "optional_empty",
|
|
@@ -1795,8 +1827,9 @@ function addOptionalEmptyIssues(existingIssues, form, fieldProgress) {
|
|
|
1795
1827
|
* Map ValidationIssue to InspectIssue reason code.
|
|
1796
1828
|
*/
|
|
1797
1829
|
function mapValidationToInspectReason(vi) {
|
|
1798
|
-
|
|
1799
|
-
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";
|
|
1800
1833
|
if (vi.code === "MULTI_SELECT_TOO_FEW" || vi.code === "STRING_LIST_MIN_ITEMS" || vi.message.includes("at least")) return "min_items_not_met";
|
|
1801
1834
|
return "validation_error";
|
|
1802
1835
|
}
|
|
@@ -1938,7 +1971,9 @@ function isCheckboxComplete(form, fieldId) {
|
|
|
1938
1971
|
const field = findFieldById(form, fieldId);
|
|
1939
1972
|
if (field?.kind !== "checkboxes") return true;
|
|
1940
1973
|
const checkboxField = field;
|
|
1941
|
-
const
|
|
1974
|
+
const response = form.responsesByFieldId[fieldId];
|
|
1975
|
+
if (response?.state !== "answered") return false;
|
|
1976
|
+
const value = response.value;
|
|
1942
1977
|
if (value?.kind !== "checkboxes") return false;
|
|
1943
1978
|
const values = value.values;
|
|
1944
1979
|
const optionIds = checkboxField.options.map((o) => o.id);
|
|
@@ -2014,10 +2049,25 @@ function findField(form, fieldId) {
|
|
|
2014
2049
|
* Validate a single patch against the form schema.
|
|
2015
2050
|
*/
|
|
2016
2051
|
function validatePatch(form, patch, index) {
|
|
2017
|
-
|
|
2052
|
+
if (patch.op === "add_note") {
|
|
2053
|
+
if (!form.idIndex.has(patch.ref)) return {
|
|
2054
|
+
patchIndex: index,
|
|
2055
|
+
message: `Reference "${patch.ref}" not found in form`
|
|
2056
|
+
};
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
if (patch.op === "remove_note") {
|
|
2060
|
+
if (!form.notes.some((n) => n.id === patch.noteId)) return {
|
|
2061
|
+
patchIndex: index,
|
|
2062
|
+
message: `Note with id '${patch.noteId}' not found`
|
|
2063
|
+
};
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
const fieldId = patch.fieldId;
|
|
2067
|
+
const field = findField(form, fieldId);
|
|
2018
2068
|
if (!field) return {
|
|
2019
2069
|
patchIndex: index,
|
|
2020
|
-
message: `Field "${
|
|
2070
|
+
message: `Field "${fieldId}" not found`
|
|
2021
2071
|
};
|
|
2022
2072
|
switch (patch.op) {
|
|
2023
2073
|
case "set_string":
|
|
@@ -2090,6 +2140,18 @@ function validatePatch(form, patch, index) {
|
|
|
2090
2140
|
message: `Cannot apply set_url_list to ${field.kind} field "${field.id}"`
|
|
2091
2141
|
};
|
|
2092
2142
|
break;
|
|
2143
|
+
case "set_date":
|
|
2144
|
+
if (field.kind !== "date") return {
|
|
2145
|
+
patchIndex: index,
|
|
2146
|
+
message: `Cannot apply set_date to ${field.kind} field "${field.id}"`
|
|
2147
|
+
};
|
|
2148
|
+
break;
|
|
2149
|
+
case "set_year":
|
|
2150
|
+
if (field.kind !== "year") return {
|
|
2151
|
+
patchIndex: index,
|
|
2152
|
+
message: `Cannot apply set_year to ${field.kind} field "${field.id}"`
|
|
2153
|
+
};
|
|
2154
|
+
break;
|
|
2093
2155
|
case "clear_field": break;
|
|
2094
2156
|
case "skip_field":
|
|
2095
2157
|
if (field.required) return {
|
|
@@ -2097,6 +2159,7 @@ function validatePatch(form, patch, index) {
|
|
|
2097
2159
|
message: `Cannot skip required field "${field.id}"`
|
|
2098
2160
|
};
|
|
2099
2161
|
break;
|
|
2162
|
+
case "abort_field": break;
|
|
2100
2163
|
}
|
|
2101
2164
|
return null;
|
|
2102
2165
|
}
|
|
@@ -2115,191 +2178,234 @@ function validatePatches(form, patches) {
|
|
|
2115
2178
|
return errors;
|
|
2116
2179
|
}
|
|
2117
2180
|
/**
|
|
2181
|
+
* Generate a unique note ID for the form.
|
|
2182
|
+
*/
|
|
2183
|
+
function generateNoteId(form) {
|
|
2184
|
+
const existingIds = new Set(form.notes.map((n) => n.id));
|
|
2185
|
+
let counter = 1;
|
|
2186
|
+
while (existingIds.has(`n${counter}`)) counter++;
|
|
2187
|
+
return `n${counter}`;
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2118
2190
|
* Apply a set_string patch.
|
|
2119
2191
|
*/
|
|
2120
|
-
function applySetString(
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
value:
|
|
2192
|
+
function applySetString(responses, patch) {
|
|
2193
|
+
responses[patch.fieldId] = {
|
|
2194
|
+
state: "answered",
|
|
2195
|
+
value: {
|
|
2196
|
+
kind: "string",
|
|
2197
|
+
value: patch.value
|
|
2198
|
+
}
|
|
2124
2199
|
};
|
|
2125
2200
|
}
|
|
2126
2201
|
/**
|
|
2127
2202
|
* Apply a set_number patch.
|
|
2128
2203
|
*/
|
|
2129
|
-
function applySetNumber(
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
value:
|
|
2204
|
+
function applySetNumber(responses, patch) {
|
|
2205
|
+
responses[patch.fieldId] = {
|
|
2206
|
+
state: "answered",
|
|
2207
|
+
value: {
|
|
2208
|
+
kind: "number",
|
|
2209
|
+
value: patch.value
|
|
2210
|
+
}
|
|
2133
2211
|
};
|
|
2134
2212
|
}
|
|
2135
2213
|
/**
|
|
2136
2214
|
* Apply a set_string_list patch.
|
|
2137
2215
|
*/
|
|
2138
|
-
function applySetStringList(
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2216
|
+
function applySetStringList(responses, patch) {
|
|
2217
|
+
responses[patch.fieldId] = {
|
|
2218
|
+
state: "answered",
|
|
2219
|
+
value: {
|
|
2220
|
+
kind: "string_list",
|
|
2221
|
+
items: patch.items
|
|
2222
|
+
}
|
|
2142
2223
|
};
|
|
2143
2224
|
}
|
|
2144
2225
|
/**
|
|
2145
2226
|
* Apply a set_single_select patch.
|
|
2146
2227
|
*/
|
|
2147
|
-
function applySetSingleSelect(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2228
|
+
function applySetSingleSelect(responses, patch) {
|
|
2229
|
+
responses[patch.fieldId] = {
|
|
2230
|
+
state: "answered",
|
|
2231
|
+
value: {
|
|
2232
|
+
kind: "single_select",
|
|
2233
|
+
selected: patch.selected
|
|
2234
|
+
}
|
|
2151
2235
|
};
|
|
2152
2236
|
}
|
|
2153
2237
|
/**
|
|
2154
2238
|
* Apply a set_multi_select patch.
|
|
2155
2239
|
*/
|
|
2156
|
-
function applySetMultiSelect(
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2240
|
+
function applySetMultiSelect(responses, patch) {
|
|
2241
|
+
responses[patch.fieldId] = {
|
|
2242
|
+
state: "answered",
|
|
2243
|
+
value: {
|
|
2244
|
+
kind: "multi_select",
|
|
2245
|
+
selected: patch.selected
|
|
2246
|
+
}
|
|
2160
2247
|
};
|
|
2161
2248
|
}
|
|
2162
2249
|
/**
|
|
2163
2250
|
* Apply a set_checkboxes patch (merges with existing values).
|
|
2164
2251
|
*/
|
|
2165
|
-
function applySetCheckboxes(
|
|
2252
|
+
function applySetCheckboxes(responses, patch) {
|
|
2166
2253
|
const merged = {
|
|
2167
|
-
...
|
|
2254
|
+
...(responses[patch.fieldId]?.value)?.values ?? {},
|
|
2168
2255
|
...patch.values
|
|
2169
2256
|
};
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2257
|
+
responses[patch.fieldId] = {
|
|
2258
|
+
state: "answered",
|
|
2259
|
+
value: {
|
|
2260
|
+
kind: "checkboxes",
|
|
2261
|
+
values: merged
|
|
2262
|
+
}
|
|
2173
2263
|
};
|
|
2174
2264
|
}
|
|
2175
2265
|
/**
|
|
2176
2266
|
* Apply a set_url patch.
|
|
2177
2267
|
*/
|
|
2178
|
-
function applySetUrl(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
value:
|
|
2268
|
+
function applySetUrl(responses, patch) {
|
|
2269
|
+
responses[patch.fieldId] = {
|
|
2270
|
+
state: "answered",
|
|
2271
|
+
value: {
|
|
2272
|
+
kind: "url",
|
|
2273
|
+
value: patch.value
|
|
2274
|
+
}
|
|
2182
2275
|
};
|
|
2183
2276
|
}
|
|
2184
2277
|
/**
|
|
2185
2278
|
* Apply a set_url_list patch.
|
|
2186
2279
|
*/
|
|
2187
|
-
function applySetUrlList(
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2280
|
+
function applySetUrlList(responses, patch) {
|
|
2281
|
+
responses[patch.fieldId] = {
|
|
2282
|
+
state: "answered",
|
|
2283
|
+
value: {
|
|
2284
|
+
kind: "url_list",
|
|
2285
|
+
items: patch.items
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Apply a set_date patch.
|
|
2291
|
+
*/
|
|
2292
|
+
function applySetDate(responses, patch) {
|
|
2293
|
+
responses[patch.fieldId] = {
|
|
2294
|
+
state: "answered",
|
|
2295
|
+
value: {
|
|
2296
|
+
kind: "date",
|
|
2297
|
+
value: patch.value
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Apply a set_year patch.
|
|
2303
|
+
*/
|
|
2304
|
+
function applySetYear(responses, patch) {
|
|
2305
|
+
responses[patch.fieldId] = {
|
|
2306
|
+
state: "answered",
|
|
2307
|
+
value: {
|
|
2308
|
+
kind: "year",
|
|
2309
|
+
value: patch.value
|
|
2310
|
+
}
|
|
2191
2311
|
};
|
|
2192
2312
|
}
|
|
2193
2313
|
/**
|
|
2194
2314
|
* Apply a clear_field patch.
|
|
2195
2315
|
*/
|
|
2196
|
-
function applyClearField(
|
|
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
|
-
}
|
|
2316
|
+
function applyClearField(responses, patch) {
|
|
2317
|
+
responses[patch.fieldId] = { state: "unanswered" };
|
|
2249
2318
|
}
|
|
2250
2319
|
/**
|
|
2251
2320
|
* Apply a skip_field patch.
|
|
2252
|
-
* Marks the field as skipped and
|
|
2321
|
+
* Marks the field as skipped and stores reason in FieldResponse.reason.
|
|
2253
2322
|
*/
|
|
2254
|
-
function applySkipField(
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
reason: patch.reason
|
|
2323
|
+
function applySkipField(responses, patch) {
|
|
2324
|
+
responses[patch.fieldId] = {
|
|
2325
|
+
state: "skipped",
|
|
2326
|
+
...patch.reason && { reason: patch.reason }
|
|
2259
2327
|
};
|
|
2260
|
-
delete values[patch.fieldId];
|
|
2261
2328
|
}
|
|
2262
2329
|
/**
|
|
2263
|
-
* Apply
|
|
2330
|
+
* Apply an abort_field patch.
|
|
2331
|
+
* Marks the field as aborted and stores reason in FieldResponse.reason.
|
|
2332
|
+
*/
|
|
2333
|
+
function applyAbortField(responses, patch) {
|
|
2334
|
+
responses[patch.fieldId] = {
|
|
2335
|
+
state: "aborted",
|
|
2336
|
+
...patch.reason && { reason: patch.reason }
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Apply an add_note patch.
|
|
2341
|
+
* Adds a note to the form.
|
|
2342
|
+
*/
|
|
2343
|
+
function applyAddNote(form, patch) {
|
|
2344
|
+
const noteId = generateNoteId(form);
|
|
2345
|
+
form.notes.push({
|
|
2346
|
+
id: noteId,
|
|
2347
|
+
ref: patch.ref,
|
|
2348
|
+
role: patch.role,
|
|
2349
|
+
text: patch.text
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Apply a remove_note patch.
|
|
2354
|
+
* Removes a specific note by ID.
|
|
2355
|
+
*/
|
|
2356
|
+
function applyRemoveNote(form, patch) {
|
|
2357
|
+
const index = form.notes.findIndex((n) => n.id === patch.noteId);
|
|
2358
|
+
if (index >= 0) form.notes.splice(index, 1);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Apply a single patch to the form.
|
|
2264
2362
|
*/
|
|
2265
|
-
function applyPatch(form,
|
|
2363
|
+
function applyPatch(form, responses, patch) {
|
|
2266
2364
|
switch (patch.op) {
|
|
2267
2365
|
case "set_string":
|
|
2268
|
-
applySetString(
|
|
2269
|
-
delete skips[patch.fieldId];
|
|
2366
|
+
applySetString(responses, patch);
|
|
2270
2367
|
break;
|
|
2271
2368
|
case "set_number":
|
|
2272
|
-
applySetNumber(
|
|
2273
|
-
delete skips[patch.fieldId];
|
|
2369
|
+
applySetNumber(responses, patch);
|
|
2274
2370
|
break;
|
|
2275
2371
|
case "set_string_list":
|
|
2276
|
-
applySetStringList(
|
|
2277
|
-
delete skips[patch.fieldId];
|
|
2372
|
+
applySetStringList(responses, patch);
|
|
2278
2373
|
break;
|
|
2279
2374
|
case "set_single_select":
|
|
2280
|
-
applySetSingleSelect(
|
|
2281
|
-
delete skips[patch.fieldId];
|
|
2375
|
+
applySetSingleSelect(responses, patch);
|
|
2282
2376
|
break;
|
|
2283
2377
|
case "set_multi_select":
|
|
2284
|
-
applySetMultiSelect(
|
|
2285
|
-
delete skips[patch.fieldId];
|
|
2378
|
+
applySetMultiSelect(responses, patch);
|
|
2286
2379
|
break;
|
|
2287
2380
|
case "set_checkboxes":
|
|
2288
|
-
applySetCheckboxes(
|
|
2289
|
-
delete skips[patch.fieldId];
|
|
2381
|
+
applySetCheckboxes(responses, patch);
|
|
2290
2382
|
break;
|
|
2291
2383
|
case "set_url":
|
|
2292
|
-
applySetUrl(
|
|
2384
|
+
applySetUrl(responses, patch);
|
|
2293
2385
|
break;
|
|
2294
2386
|
case "set_url_list":
|
|
2295
|
-
applySetUrlList(
|
|
2387
|
+
applySetUrlList(responses, patch);
|
|
2388
|
+
break;
|
|
2389
|
+
case "set_date":
|
|
2390
|
+
applySetDate(responses, patch);
|
|
2391
|
+
break;
|
|
2392
|
+
case "set_year":
|
|
2393
|
+
applySetYear(responses, patch);
|
|
2296
2394
|
break;
|
|
2297
2395
|
case "clear_field":
|
|
2298
|
-
applyClearField(
|
|
2299
|
-
delete skips[patch.fieldId];
|
|
2396
|
+
applyClearField(responses, patch);
|
|
2300
2397
|
break;
|
|
2301
2398
|
case "skip_field":
|
|
2302
|
-
applySkipField(
|
|
2399
|
+
applySkipField(responses, patch);
|
|
2400
|
+
break;
|
|
2401
|
+
case "abort_field":
|
|
2402
|
+
applyAbortField(responses, patch);
|
|
2403
|
+
break;
|
|
2404
|
+
case "add_note":
|
|
2405
|
+
applyAddNote(form, patch);
|
|
2406
|
+
break;
|
|
2407
|
+
case "remove_note":
|
|
2408
|
+
applyRemoveNote(form, patch);
|
|
2303
2409
|
break;
|
|
2304
2410
|
}
|
|
2305
2411
|
}
|
|
@@ -2331,8 +2437,8 @@ function convertToInspectIssues(form) {
|
|
|
2331
2437
|
*/
|
|
2332
2438
|
function applyPatches(form, patches) {
|
|
2333
2439
|
if (validatePatches(form, patches).length > 0) {
|
|
2334
|
-
const summaries$1 = computeAllSummaries(form.schema, form.valuesByFieldId, [], form.skipsByFieldId);
|
|
2335
2440
|
const issues$1 = convertToInspectIssues(form);
|
|
2441
|
+
const summaries$1 = computeAllSummaries(form.schema, form.responsesByFieldId, form.notes, issues$1);
|
|
2336
2442
|
return {
|
|
2337
2443
|
applyStatus: "rejected",
|
|
2338
2444
|
structureSummary: summaries$1.structureSummary,
|
|
@@ -2342,13 +2448,14 @@ function applyPatches(form, patches) {
|
|
|
2342
2448
|
formState: summaries$1.formState
|
|
2343
2449
|
};
|
|
2344
2450
|
}
|
|
2345
|
-
const
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
form.
|
|
2349
|
-
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;
|
|
2350
2457
|
const issues = convertToInspectIssues(form);
|
|
2351
|
-
const summaries = computeAllSummaries(form.schema,
|
|
2458
|
+
const summaries = computeAllSummaries(form.schema, newResponses, newNotes, issues);
|
|
2352
2459
|
return {
|
|
2353
2460
|
applyStatus: "applied",
|
|
2354
2461
|
structureSummary: summaries.structureSummary,
|
|
@@ -2360,4 +2467,4 @@ function applyPatches(form, patches) {
|
|
|
2360
2467
|
}
|
|
2361
2468
|
|
|
2362
2469
|
//#endregion
|
|
2363
|
-
export {
|
|
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 };
|