lens-content-processor 0.28.0 → 0.34.0
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/dist/content-schema.d.ts +3 -1
- package/dist/content-schema.js +46 -5
- package/dist/content-schema.js.map +1 -1
- package/dist/flattener/index.d.ts +23 -0
- package/dist/flattener/index.js +23 -1
- package/dist/flattener/index.js.map +1 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +84 -14
- package/dist/index.js.map +1 -1
- package/dist/parser/learning-outcome.js +12 -1
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +6 -1
- package/dist/parser/lens.js +10 -3
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/response-segments.d.ts +54 -0
- package/dist/parser/response-segments.js +499 -0
- package/dist/parser/response-segments.js.map +1 -0
- package/dist/parser/survey.js +41 -9
- package/dist/parser/survey.js.map +1 -1
- package/dist/validator/emphasis.js.map +1 -1
- package/dist/validator/html-tags.js +109 -16
- package/dist/validator/html-tags.js.map +1 -1
- package/dist/validator/output-integrity.d.ts +1 -1
- package/dist/validator/output-integrity.js +7 -7
- package/dist/validator/segment-fields.d.ts +4 -2
- package/dist/validator/segment-fields.js +27 -5
- package/dist/validator/segment-fields.js.map +1 -1
- package/dist/validator/test-segments.d.ts +11 -0
- package/dist/validator/test-segments.js +45 -0
- package/dist/validator/test-segments.js.map +1 -1
- package/dist/validator/uuid.d.ts +1 -1
- package/dist/validator/uuid.js +4 -4
- package/dist/validator/uuid.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// src/parser/response-segments.ts
|
|
2
|
+
//
|
|
3
|
+
// Response (subtyped question) segments: `#### Question: Open|Rating|Choice|
|
|
4
|
+
// FillBlank|Ranking`. One segment family collects one learner response per
|
|
5
|
+
// segment, with the same syntax in lenses, Learning Outcome tests, and
|
|
6
|
+
// surveys. Context determines grading: surveys never grade. A bare
|
|
7
|
+
// `#### Question` (no title) stays the legacy segment everywhere and never
|
|
8
|
+
// reaches this module.
|
|
9
|
+
import { isValidUuid } from "../validator/uuid.js";
|
|
10
|
+
import { stripQuotes, validatePromptImportSyntax, } from "./lens.js";
|
|
11
|
+
export const RESPONSE_QUESTION_SUBTYPES = [
|
|
12
|
+
"open",
|
|
13
|
+
"rating",
|
|
14
|
+
"choice",
|
|
15
|
+
"fillblank",
|
|
16
|
+
"ranking",
|
|
17
|
+
];
|
|
18
|
+
/** Author-facing capitalization, for error messages. */
|
|
19
|
+
const SUBTYPE_DISPLAY = {
|
|
20
|
+
open: "Open",
|
|
21
|
+
rating: "Rating",
|
|
22
|
+
choice: "Choice",
|
|
23
|
+
fillblank: "FillBlank",
|
|
24
|
+
ranking: "Ranking",
|
|
25
|
+
};
|
|
26
|
+
const VALID_SUBTYPES_HINT = RESPONSE_QUESTION_SUBTYPES.map((s) => SUBTYPE_DISPLAY[s]).join(", ");
|
|
27
|
+
const DEFAULT_RATING_SCALE = 5;
|
|
28
|
+
const MIN_RATING_SCALE = 2;
|
|
29
|
+
const MAX_RATING_SCALE = 10;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a raw segment to its "question:<subtype>" schema key, for field
|
|
32
|
+
* validation against QUESTION_SUBTYPE_SCHEMAS. Returns null for anything
|
|
33
|
+
* that is not a titled question segment with a known subtype (legacy
|
|
34
|
+
* questions fall back to the plain "question" schema).
|
|
35
|
+
*/
|
|
36
|
+
export function responseSegmentSchemaKey(raw) {
|
|
37
|
+
if (raw.type !== "question" || !raw.title)
|
|
38
|
+
return null;
|
|
39
|
+
const subtype = raw.title.trim().toLowerCase();
|
|
40
|
+
return RESPONSE_QUESTION_SUBTYPES.includes(subtype)
|
|
41
|
+
? `question:${subtype}`
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
44
|
+
function parseBool(raw, field) {
|
|
45
|
+
return raw.fields[field]?.toLowerCase() === "true" ? true : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Parse a positive-integer field; error and drop on anything else. */
|
|
48
|
+
function parsePositiveInt(raw, field, file, errors) {
|
|
49
|
+
const value = raw.fields[field];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (!/^\d+$/.test(trimmed) || parseInt(trimmed, 10) === 0) {
|
|
54
|
+
errors.push({
|
|
55
|
+
file,
|
|
56
|
+
line: raw.line,
|
|
57
|
+
message: `Field '${field}' must be a positive integer, got '${value}'`,
|
|
58
|
+
suggestion: `Use a number like '${field}:: 500', or remove the field`,
|
|
59
|
+
severity: "error",
|
|
60
|
+
});
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return parseInt(trimmed, 10);
|
|
64
|
+
}
|
|
65
|
+
/** Parse scale:: as an integer in [2, 10] (default 5). Null = hard error. */
|
|
66
|
+
function parseScale(raw, file, errors) {
|
|
67
|
+
const value = raw.fields.scale;
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return DEFAULT_RATING_SCALE;
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
72
|
+
errors.push({
|
|
73
|
+
file,
|
|
74
|
+
line: raw.line,
|
|
75
|
+
message: `Field 'scale' must be an integer, got '${value}'`,
|
|
76
|
+
suggestion: `Use a number from ${MIN_RATING_SCALE} to ${MAX_RATING_SCALE} (default ${DEFAULT_RATING_SCALE}), or omit the field`,
|
|
77
|
+
severity: "error",
|
|
78
|
+
});
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const scale = parseInt(trimmed, 10);
|
|
82
|
+
if (scale < MIN_RATING_SCALE || scale > MAX_RATING_SCALE) {
|
|
83
|
+
errors.push({
|
|
84
|
+
file,
|
|
85
|
+
line: raw.line,
|
|
86
|
+
message: `Field 'scale' is ${scale}; ratings support ${MIN_RATING_SCALE}–${MAX_RATING_SCALE}`,
|
|
87
|
+
suggestion: `Use a value from ${MIN_RATING_SCALE} to ${MAX_RATING_SCALE}`,
|
|
88
|
+
severity: "error",
|
|
89
|
+
});
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return scale;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse a multiline `- Item` list field shared by choice options:: and
|
|
96
|
+
* ranking items::. `allowCorrectMarks` (choice in lens/LO context) accepts
|
|
97
|
+
* `- [x] Item` and records its index; explicit-unchecked `- [ ]` is always
|
|
98
|
+
* an error, as is checkbox syntax where none is allowed.
|
|
99
|
+
*/
|
|
100
|
+
function parseListField(raw, field, allowCorrectMarks, file, errors) {
|
|
101
|
+
const value = raw.fields[field];
|
|
102
|
+
const capitalized = field === "options" ? "Choice" : "Ranking";
|
|
103
|
+
if (value === undefined || value.trim() === "") {
|
|
104
|
+
errors.push({
|
|
105
|
+
file,
|
|
106
|
+
line: raw.line,
|
|
107
|
+
message: `${capitalized} segment is missing ${field}::`,
|
|
108
|
+
suggestion: `Add a list:\n${field}::\n- First ${field === "options" ? "option" : "item"}\n- Second ${field === "options" ? "option" : "item"}`,
|
|
109
|
+
severity: "error",
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const items = [];
|
|
114
|
+
const correct = [];
|
|
115
|
+
for (const line of value.split("\n")) {
|
|
116
|
+
const trimmed = line.trim();
|
|
117
|
+
if (!trimmed)
|
|
118
|
+
continue;
|
|
119
|
+
const uncheckedBox = trimmed.match(/^-\s*\[\s?\]\s*(.*)$/);
|
|
120
|
+
if (uncheckedBox) {
|
|
121
|
+
errors.push({
|
|
122
|
+
file,
|
|
123
|
+
line: raw.line,
|
|
124
|
+
message: `Explicit-unchecked checkbox syntax is not supported in ${field}::: "${trimmed}"`,
|
|
125
|
+
suggestion: allowCorrectMarks
|
|
126
|
+
? "Use plain '- Option text' items; mark correct options with '- [x] Option text'"
|
|
127
|
+
: "Use plain '- Item text' items without brackets",
|
|
128
|
+
severity: "error",
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const checkedBox = trimmed.match(/^-\s*\[[xX]\]\s*(.*)$/);
|
|
133
|
+
if (checkedBox) {
|
|
134
|
+
if (!allowCorrectMarks) {
|
|
135
|
+
errors.push({
|
|
136
|
+
file,
|
|
137
|
+
line: raw.line,
|
|
138
|
+
message: field === "options"
|
|
139
|
+
? `Survey choices never have correct answers, got checkbox syntax: "${trimmed}"`
|
|
140
|
+
: `Ranking items must be plain list items, got checkbox syntax: "${trimmed}"`,
|
|
141
|
+
suggestion: field === "options"
|
|
142
|
+
? "Use plain '- Option text' items without brackets"
|
|
143
|
+
: "Use plain '- Item text' items in the intended order — the intended order is the answer",
|
|
144
|
+
severity: "error",
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const text = checkedBox[1].trim();
|
|
149
|
+
if (!text) {
|
|
150
|
+
errors.push({
|
|
151
|
+
file,
|
|
152
|
+
line: raw.line,
|
|
153
|
+
message: `Empty ${field}:: list item: "${trimmed}"`,
|
|
154
|
+
suggestion: "Every list item needs text after the marker",
|
|
155
|
+
severity: "error",
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
correct.push(items.length);
|
|
160
|
+
items.push(text);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const item = trimmed.match(/^-\s+(.+)$/);
|
|
164
|
+
if (!item) {
|
|
165
|
+
errors.push({
|
|
166
|
+
file,
|
|
167
|
+
line: raw.line,
|
|
168
|
+
message: `Line in ${field}:: is not a list item: "${trimmed}"`,
|
|
169
|
+
suggestion: `Every entry must be on its own '- ${field === "options" ? "Option" : "Item"} text' line`,
|
|
170
|
+
severity: "error",
|
|
171
|
+
});
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
items.push(item[1].trim());
|
|
175
|
+
}
|
|
176
|
+
if (items.length < 2) {
|
|
177
|
+
errors.push({
|
|
178
|
+
file,
|
|
179
|
+
line: raw.line,
|
|
180
|
+
message: `${capitalized} segment needs at least 2 ${field}, got ${items.length}`,
|
|
181
|
+
suggestion: `Add more '- …' lines under ${field}::`,
|
|
182
|
+
severity: "error",
|
|
183
|
+
});
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return { items, correct };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A number reference for a graded `{{number <ref>}}` blank: optional minus,
|
|
190
|
+
* digits either plain or with comma thousands separators in exact 3-digit
|
|
191
|
+
* groups, optional decimal part.
|
|
192
|
+
*/
|
|
193
|
+
const REFERENCE_NUMBER = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/;
|
|
194
|
+
/**
|
|
195
|
+
* Parse `{{…}}` blanks out of a fillblank content:: value:
|
|
196
|
+
* - `{{blank}}` — ungraded text blank
|
|
197
|
+
* - `{{number}}` — ungraded number blank
|
|
198
|
+
* - `{{number <ref>}}` — graded number blank (reference validated)
|
|
199
|
+
* - anything else — graded text blank, `|`-separated alternatives
|
|
200
|
+
*/
|
|
201
|
+
function parseFillBlanks(content, raw, file, errors) {
|
|
202
|
+
const blanks = [];
|
|
203
|
+
let failed = false;
|
|
204
|
+
const rewritten = content.replace(/\{\{(.*?)\}\}/gs, (match, inner) => {
|
|
205
|
+
const index = blanks.length;
|
|
206
|
+
const trimmed = String(inner).trim();
|
|
207
|
+
const keyword = trimmed.toLowerCase();
|
|
208
|
+
if (trimmed === "") {
|
|
209
|
+
errors.push({
|
|
210
|
+
file,
|
|
211
|
+
line: raw.line,
|
|
212
|
+
message: "Empty blank {{}} in fillblank content::",
|
|
213
|
+
suggestion: "Use {{blank}} for an ungraded blank, or {{expected answer}} for a graded one",
|
|
214
|
+
severity: "error",
|
|
215
|
+
});
|
|
216
|
+
failed = true;
|
|
217
|
+
return match;
|
|
218
|
+
}
|
|
219
|
+
if (keyword === "blank") {
|
|
220
|
+
blanks.push({ kind: "text", graded: false });
|
|
221
|
+
return `{{${index}}}`;
|
|
222
|
+
}
|
|
223
|
+
if (keyword === "number") {
|
|
224
|
+
blanks.push({ kind: "number", graded: false });
|
|
225
|
+
return `{{${index}}}`;
|
|
226
|
+
}
|
|
227
|
+
const numberRef = trimmed.match(/^number\s+(.+)$/i);
|
|
228
|
+
if (numberRef) {
|
|
229
|
+
const ref = numberRef[1].trim();
|
|
230
|
+
if (!REFERENCE_NUMBER.test(ref)) {
|
|
231
|
+
errors.push({
|
|
232
|
+
file,
|
|
233
|
+
line: raw.line,
|
|
234
|
+
message: `Malformed reference number in blank {{${trimmed}}}: '${ref}'`,
|
|
235
|
+
suggestion: "Use digits with optional comma thousands separators and decimal part, e.g. {{number 149,600,000}} or {{number 3.14}}",
|
|
236
|
+
severity: "error",
|
|
237
|
+
});
|
|
238
|
+
failed = true;
|
|
239
|
+
return match;
|
|
240
|
+
}
|
|
241
|
+
blanks.push({ kind: "number", graded: true, referenceNumber: ref });
|
|
242
|
+
return `{{${index}}}`;
|
|
243
|
+
}
|
|
244
|
+
const alternatives = trimmed.split("|").map((alt) => alt.trim());
|
|
245
|
+
if (alternatives.some((alt) => alt === "")) {
|
|
246
|
+
errors.push({
|
|
247
|
+
file,
|
|
248
|
+
line: raw.line,
|
|
249
|
+
message: `Empty alternative in blank {{${trimmed}}}`,
|
|
250
|
+
suggestion: "Separate accepted answers with '|', each non-empty: {{Paris|paris}}",
|
|
251
|
+
severity: "error",
|
|
252
|
+
});
|
|
253
|
+
failed = true;
|
|
254
|
+
return match;
|
|
255
|
+
}
|
|
256
|
+
blanks.push({ kind: "text", graded: true, expected: alternatives });
|
|
257
|
+
return `{{${index}}}`;
|
|
258
|
+
});
|
|
259
|
+
if (failed)
|
|
260
|
+
return null;
|
|
261
|
+
if (blanks.length === 0) {
|
|
262
|
+
errors.push({
|
|
263
|
+
file,
|
|
264
|
+
line: raw.line,
|
|
265
|
+
message: "FillBlank segment has no {{…}} blanks in content::",
|
|
266
|
+
suggestion: "Add at least one blank, e.g. 'The capital of France is {{Paris}}.'",
|
|
267
|
+
severity: "error",
|
|
268
|
+
});
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return { content: rewritten, blanks };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Parse a `#### Question: <Subtype>` segment. Shared by lens parsing
|
|
275
|
+
* (context "lens", also used for LO tests via convertSegment) and survey
|
|
276
|
+
* parsing (context "survey": never graded, `[x]` forbidden,
|
|
277
|
+
* assessment-instructions warned but passed through).
|
|
278
|
+
*/
|
|
279
|
+
export function parseResponseSegment(raw, context, file) {
|
|
280
|
+
const errors = [];
|
|
281
|
+
const subtypeRaw = (raw.title ?? "").trim();
|
|
282
|
+
const subtype = subtypeRaw.toLowerCase();
|
|
283
|
+
if (!RESPONSE_QUESTION_SUBTYPES.includes(subtype)) {
|
|
284
|
+
errors.push({
|
|
285
|
+
file,
|
|
286
|
+
line: raw.line,
|
|
287
|
+
message: `Unknown question subtype '${subtypeRaw}'`,
|
|
288
|
+
suggestion: `Valid subtypes: ${VALID_SUBTYPES_HINT} (e.g. '#### Question: Open'), or use plain '#### Question' for a legacy question`,
|
|
289
|
+
severity: "error",
|
|
290
|
+
});
|
|
291
|
+
return { segment: null, errors };
|
|
292
|
+
}
|
|
293
|
+
const questionType = subtype;
|
|
294
|
+
const display = `Question: ${SUBTYPE_DISPLAY[questionType]}`;
|
|
295
|
+
// id:: — plain UUID, quotes stripped like roleplay ids (a quoted id would
|
|
296
|
+
// reach the frontend verbatim and fail server-side UUID parsing).
|
|
297
|
+
const rawId = raw.fields.id?.trim();
|
|
298
|
+
const responseId = rawId ? stripQuotes(rawId).trim() : rawId;
|
|
299
|
+
if (!responseId) {
|
|
300
|
+
errors.push({
|
|
301
|
+
file,
|
|
302
|
+
line: raw.line,
|
|
303
|
+
message: `${display} segment missing id:: field`,
|
|
304
|
+
suggestion: "Add 'id:: <uuid>' — the id is the stable response key and must never change",
|
|
305
|
+
severity: "error",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else if (!isValidUuid(responseId)) {
|
|
309
|
+
errors.push({
|
|
310
|
+
file,
|
|
311
|
+
line: raw.line,
|
|
312
|
+
message: `${display} id:: is not a valid UUID: ${responseId}`,
|
|
313
|
+
suggestion: "Use a plain UUID, e.g. 'id:: 17a55209-deab-48c8-afb0-fc522722fe8f'",
|
|
314
|
+
severity: "error",
|
|
315
|
+
});
|
|
316
|
+
return { segment: null, errors };
|
|
317
|
+
}
|
|
318
|
+
const content = raw.fields.content;
|
|
319
|
+
if (!content || content.trim() === "") {
|
|
320
|
+
errors.push({
|
|
321
|
+
file,
|
|
322
|
+
line: raw.line,
|
|
323
|
+
message: `${display} segment missing content:: field`,
|
|
324
|
+
suggestion: "Add 'content:: Your question here'",
|
|
325
|
+
severity: "error",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (!responseId || !content || content.trim() === "") {
|
|
329
|
+
return { segment: null, errors };
|
|
330
|
+
}
|
|
331
|
+
// assessment-instructions:: — grading rubric. Ratings are never graded;
|
|
332
|
+
// surveys never grade (warn, but pass the field through).
|
|
333
|
+
let assessmentInstructions = raw.fields["assessment-instructions"] || undefined;
|
|
334
|
+
if (assessmentInstructions && questionType === "rating") {
|
|
335
|
+
errors.push({
|
|
336
|
+
file,
|
|
337
|
+
line: raw.line,
|
|
338
|
+
message: "assessment-instructions:: on a Question: Rating segment — ratings are never graded",
|
|
339
|
+
suggestion: "Remove the assessment-instructions:: field",
|
|
340
|
+
severity: "error",
|
|
341
|
+
});
|
|
342
|
+
assessmentInstructions = undefined;
|
|
343
|
+
}
|
|
344
|
+
if (assessmentInstructions) {
|
|
345
|
+
errors.push(...validatePromptImportSyntax(assessmentInstructions, "assessment-instructions", file, raw.line));
|
|
346
|
+
if (context === "survey") {
|
|
347
|
+
errors.push({
|
|
348
|
+
file,
|
|
349
|
+
line: raw.line,
|
|
350
|
+
message: "assessment-instructions:: in a survey segment — surveys never grade, so this field will be ignored",
|
|
351
|
+
suggestion: "Remove the field, or move the segment into a lens",
|
|
352
|
+
severity: "warning",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const feedbackInstructions = raw.fields["feedback-instructions"] || undefined;
|
|
357
|
+
if (feedbackInstructions) {
|
|
358
|
+
errors.push(...validatePromptImportSyntax(feedbackInstructions, "feedback-instructions", file, raw.line));
|
|
359
|
+
}
|
|
360
|
+
const segment = {
|
|
361
|
+
type: "question",
|
|
362
|
+
questionType,
|
|
363
|
+
// Canonical lowercase: the backend matches segment ids against Postgres
|
|
364
|
+
// uuid strings (always lowercase), so the cache must never carry an
|
|
365
|
+
// uppercase-authored id.
|
|
366
|
+
responseId: responseId.toLowerCase(),
|
|
367
|
+
content,
|
|
368
|
+
graded: false, // derived per subtype below; always false in surveys
|
|
369
|
+
assessmentInstructions,
|
|
370
|
+
feedbackInstructions,
|
|
371
|
+
optional: parseBool(raw, "optional"),
|
|
372
|
+
};
|
|
373
|
+
switch (questionType) {
|
|
374
|
+
case "open": {
|
|
375
|
+
segment.maxChars = parsePositiveInt(raw, "max-chars", file, errors);
|
|
376
|
+
segment.maxTime = raw.fields["max-time"] || undefined;
|
|
377
|
+
const placeholder = raw.fields.placeholder?.trim();
|
|
378
|
+
if (placeholder)
|
|
379
|
+
segment.placeholder = placeholder;
|
|
380
|
+
segment.enforceVoice = parseBool(raw, "enforce-voice");
|
|
381
|
+
segment.graded = Boolean(assessmentInstructions);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case "rating": {
|
|
385
|
+
const scale = parseScale(raw, file, errors);
|
|
386
|
+
if (scale === null)
|
|
387
|
+
return { segment: null, errors };
|
|
388
|
+
segment.scale = scale;
|
|
389
|
+
const lowLabel = raw.fields["low-label"]?.trim();
|
|
390
|
+
const highLabel = raw.fields["high-label"]?.trim();
|
|
391
|
+
if (lowLabel)
|
|
392
|
+
segment.lowLabel = lowLabel;
|
|
393
|
+
if (highLabel)
|
|
394
|
+
segment.highLabel = highLabel;
|
|
395
|
+
segment.graded = false;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "choice": {
|
|
399
|
+
const parsed = parseListField(raw, "options", context === "lens", file, errors);
|
|
400
|
+
if (parsed === null)
|
|
401
|
+
return { segment: null, errors };
|
|
402
|
+
segment.multi = parseBool(raw, "multi");
|
|
403
|
+
if (parsed.correct.length > 1 && !segment.multi) {
|
|
404
|
+
// A single-choice submission carries exactly one selection, so a
|
|
405
|
+
// multi-option correct set could never be matched — every learner
|
|
406
|
+
// would score 0.
|
|
407
|
+
errors.push({
|
|
408
|
+
file,
|
|
409
|
+
line: raw.line,
|
|
410
|
+
message: `Choice has ${parsed.correct.length} correct options but is single-choice — add 'multi:: true' or mark exactly one option`,
|
|
411
|
+
suggestion: "Add 'multi:: true' to allow multiple selections, or keep exactly one '- [x]' option",
|
|
412
|
+
severity: "error",
|
|
413
|
+
});
|
|
414
|
+
return { segment: null, errors };
|
|
415
|
+
}
|
|
416
|
+
segment.options = parsed.items;
|
|
417
|
+
if (parsed.correct.length > 0)
|
|
418
|
+
segment.correctOptions = parsed.correct;
|
|
419
|
+
segment.shuffle = parseBool(raw, "shuffle");
|
|
420
|
+
segment.graded = parsed.correct.length > 0;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "fillblank": {
|
|
424
|
+
const parsed = parseFillBlanks(content, raw, file, errors);
|
|
425
|
+
if (parsed === null)
|
|
426
|
+
return { segment: null, errors };
|
|
427
|
+
segment.content = parsed.content;
|
|
428
|
+
segment.blanks = parsed.blanks;
|
|
429
|
+
segment.graded =
|
|
430
|
+
parsed.blanks.some((b) => b.graded) || Boolean(assessmentInstructions);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "ranking": {
|
|
434
|
+
const parsed = parseListField(raw, "items", false, file, errors);
|
|
435
|
+
if (parsed === null)
|
|
436
|
+
return { segment: null, errors };
|
|
437
|
+
segment.items = parsed.items;
|
|
438
|
+
segment.graded = Boolean(assessmentInstructions);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (context === "survey")
|
|
443
|
+
segment.graded = false;
|
|
444
|
+
return { segment, errors };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Map a parsed response segment to its flattened QuestionSegment shape.
|
|
448
|
+
* Used directly by survey parsing (surveys have no flatten pass) and by the
|
|
449
|
+
* lens flattener, which then replaces assessmentInstructions /
|
|
450
|
+
* feedbackInstructions with their prompt-import-expanded values.
|
|
451
|
+
*/
|
|
452
|
+
export function toFlattenedResponseSegment(parsed) {
|
|
453
|
+
const segment = {
|
|
454
|
+
type: "question",
|
|
455
|
+
questionType: parsed.questionType,
|
|
456
|
+
responseId: parsed.responseId,
|
|
457
|
+
content: parsed.content,
|
|
458
|
+
graded: parsed.graded,
|
|
459
|
+
};
|
|
460
|
+
if (parsed.assessmentInstructions)
|
|
461
|
+
segment.assessmentInstructions = parsed.assessmentInstructions;
|
|
462
|
+
if (parsed.feedbackInstructions)
|
|
463
|
+
segment.feedbackInstructions = parsed.feedbackInstructions;
|
|
464
|
+
if (parsed.optional)
|
|
465
|
+
segment.optional = true;
|
|
466
|
+
// open:
|
|
467
|
+
if (parsed.maxTime)
|
|
468
|
+
segment.maxTime = parsed.maxTime;
|
|
469
|
+
if (parsed.maxChars !== undefined)
|
|
470
|
+
segment.maxChars = parsed.maxChars;
|
|
471
|
+
if (parsed.enforceVoice)
|
|
472
|
+
segment.enforceVoice = true;
|
|
473
|
+
if (parsed.placeholder)
|
|
474
|
+
segment.placeholder = parsed.placeholder;
|
|
475
|
+
// rating:
|
|
476
|
+
if (parsed.scale !== undefined)
|
|
477
|
+
segment.scale = parsed.scale;
|
|
478
|
+
if (parsed.lowLabel)
|
|
479
|
+
segment.lowLabel = parsed.lowLabel;
|
|
480
|
+
if (parsed.highLabel)
|
|
481
|
+
segment.highLabel = parsed.highLabel;
|
|
482
|
+
// choice:
|
|
483
|
+
if (parsed.options)
|
|
484
|
+
segment.options = parsed.options;
|
|
485
|
+
if (parsed.correctOptions)
|
|
486
|
+
segment.correctOptions = parsed.correctOptions;
|
|
487
|
+
if (parsed.multi)
|
|
488
|
+
segment.multi = true;
|
|
489
|
+
if (parsed.shuffle)
|
|
490
|
+
segment.shuffle = true;
|
|
491
|
+
// fillblank:
|
|
492
|
+
if (parsed.blanks)
|
|
493
|
+
segment.blanks = parsed.blanks;
|
|
494
|
+
// ranking:
|
|
495
|
+
if (parsed.items)
|
|
496
|
+
segment.items = parsed.items;
|
|
497
|
+
return segment;
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=response-segments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-segments.js","sourceRoot":"","sources":["../../src/parser/response-segments.ts"],"names":[],"mappings":"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"}
|
package/dist/parser/survey.js
CHANGED
|
@@ -9,16 +9,21 @@
|
|
|
9
9
|
// breaking response continuity (and the platform can special-case keys like
|
|
10
10
|
// `buddy_texted`).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// correct answers, no AI assessment, and no reveal
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// Legacy survey segments deliberately do NOT reuse the lens question
|
|
13
|
+
// machinery: there are no correct answers, no AI assessment, and no reveal
|
|
14
|
+
// semantics. New-style response segments (`#### Question: <Subtype>` with a
|
|
15
|
+
// stable id:: instead of key::) DO share the lens parser
|
|
16
|
+
// (parser/response-segments.ts), but in survey context: never graded, and
|
|
17
|
+
// correct-answer marks are forbidden.
|
|
16
18
|
import { parseFrontmatter } from "./frontmatter.js";
|
|
17
19
|
import { parseSections } from "./sections.js";
|
|
18
20
|
import { validateFrontmatter } from "../validator/validate-frontmatter.js";
|
|
19
21
|
import { detectFieldTypos } from "../validator/field-typos.js";
|
|
22
|
+
import { validateSegmentFields } from "../validator/segment-fields.js";
|
|
23
|
+
import { validateFieldValues } from "../validator/field-values.js";
|
|
20
24
|
import { stripAuthoringMarkup } from "./lens.js";
|
|
21
25
|
import { SURVEY_SEGMENT_SCHEMAS } from "../content-schema.js";
|
|
26
|
+
import { parseResponseSegment, responseSegmentSchemaKey, toFlattenedResponseSegment, } from "./response-segments.js";
|
|
22
27
|
// Valid segment types for survey H4 headers
|
|
23
28
|
export const SURVEY_SEGMENT_TYPES = new Set([
|
|
24
29
|
"text",
|
|
@@ -233,6 +238,18 @@ function validateSurveySegmentFields(raw, file) {
|
|
|
233
238
|
}
|
|
234
239
|
function convertSurveySegment(raw, file) {
|
|
235
240
|
const errors = [];
|
|
241
|
+
// `#### Question: <Subtype>` — new-style response segment, shared with
|
|
242
|
+
// lens parsing but in survey context: keyed by id:: (no key::), never
|
|
243
|
+
// graded, correct-answer marks forbidden. Legacy survey segments
|
|
244
|
+
// (untitled Question, and Rating/Choice) are untouched below.
|
|
245
|
+
if (raw.type === "question" && raw.title) {
|
|
246
|
+
const { segment, errors: responseErrors } = parseResponseSegment(raw, "survey", file);
|
|
247
|
+
errors.push(...responseErrors);
|
|
248
|
+
return {
|
|
249
|
+
segment: segment ? toFlattenedResponseSegment(segment) : null,
|
|
250
|
+
errors,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
236
253
|
if (raw.title) {
|
|
237
254
|
const capitalized = raw.type[0].toUpperCase() + raw.type.slice(1);
|
|
238
255
|
errors.push({
|
|
@@ -360,25 +377,40 @@ export function parseSurvey(content, file) {
|
|
|
360
377
|
const seenKeys = new Map(); // key -> first line
|
|
361
378
|
for (const rawSeg of rawSegments) {
|
|
362
379
|
rawSeg.line += bodyStartLine - 1;
|
|
363
|
-
|
|
380
|
+
// New-style response segments validate against their
|
|
381
|
+
// "question:<subtype>" schema; everything else against the survey
|
|
382
|
+
// vocabulary.
|
|
383
|
+
const responseSchemaKey = responseSegmentSchemaKey(rawSeg);
|
|
384
|
+
if (responseSchemaKey) {
|
|
385
|
+
errors.push(...validateSegmentFields(responseSchemaKey, rawSeg.fields, file, rawSeg.line));
|
|
386
|
+
errors.push(...validateFieldValues(rawSeg.fields, file, rawSeg.line));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
errors.push(...validateSurveySegmentFields(rawSeg, file));
|
|
390
|
+
}
|
|
364
391
|
errors.push(...detectFieldTypos(rawSeg.fields, file, rawSeg.line));
|
|
365
392
|
const { segment, errors: conversionErrors } = convertSurveySegment(rawSeg, file);
|
|
366
393
|
errors.push(...conversionErrors);
|
|
367
394
|
if (!segment)
|
|
368
395
|
continue;
|
|
369
396
|
if (segment.type !== "text") {
|
|
370
|
-
|
|
397
|
+
// Answerable segments need a unique identifier: legacy segments their
|
|
398
|
+
// key::, new-style response segments their id:: (responseId).
|
|
399
|
+
const isLegacy = "key" in segment;
|
|
400
|
+
const dedupKey = isLegacy ? segment.key : segment.responseId;
|
|
401
|
+
const label = isLegacy ? "key" : "id";
|
|
402
|
+
const firstLine = seenKeys.get(dedupKey);
|
|
371
403
|
if (firstLine !== undefined) {
|
|
372
404
|
errors.push({
|
|
373
405
|
file,
|
|
374
406
|
line: rawSeg.line,
|
|
375
|
-
message: `Duplicate
|
|
376
|
-
suggestion:
|
|
407
|
+
message: `Duplicate ${label} '${dedupKey}' (already used at line ${firstLine})`,
|
|
408
|
+
suggestion: `Every answerable segment needs a unique ${label}`,
|
|
377
409
|
severity: "error",
|
|
378
410
|
});
|
|
379
411
|
continue;
|
|
380
412
|
}
|
|
381
|
-
seenKeys.set(
|
|
413
|
+
seenKeys.set(dedupKey, rawSeg.line);
|
|
382
414
|
}
|
|
383
415
|
segments.push(segment);
|
|
384
416
|
}
|