lens-content-processor 0.21.3 → 0.24.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 +8 -0
- package/dist/content-schema.js +25 -1
- package/dist/content-schema.js.map +1 -1
- package/dist/index.d.ts +42 -0
- package/dist/index.js +139 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/course.js +45 -7
- package/dist/parser/course.js.map +1 -1
- package/dist/parser/learning-outcome.d.ts +25 -0
- package/dist/parser/learning-outcome.js +173 -1
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/sections.js +5 -0
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/survey.d.ts +12 -0
- package/dist/parser/survey.js +402 -0
- package/dist/parser/survey.js.map +1 -0
- package/dist/skill-tree.d.ts +83 -0
- package/dist/skill-tree.js +177 -0
- package/dist/skill-tree.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// src/parser/survey.ts
|
|
2
|
+
//
|
|
3
|
+
// Parses post-meeting survey definitions (surveys/ folder). A survey file is
|
|
4
|
+
// frontmatter (required id, optional title) plus flat #### segments, like a
|
|
5
|
+
// lens — but with its own segment vocabulary: Text (prose), Question (free
|
|
6
|
+
// text), Rating (1..N scale), Choice (pick one/many). Answerable segments
|
|
7
|
+
// carry a stable snake_case `key::` that names the answer in
|
|
8
|
+
// survey_responses.payload, so authors can reword a question without
|
|
9
|
+
// breaking response continuity (and the platform can special-case keys like
|
|
10
|
+
// `buddy_texted`).
|
|
11
|
+
//
|
|
12
|
+
// Surveys deliberately do NOT reuse the lens question machinery: there are no
|
|
13
|
+
// correct answers, no AI assessment, and no reveal semantics. Keeping the
|
|
14
|
+
// vocabulary separate also avoids colliding with the quiz `question-type::`
|
|
15
|
+
// syntax if that ever lands on lens questions.
|
|
16
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
17
|
+
import { parseSections } from "./sections.js";
|
|
18
|
+
import { validateFrontmatter } from "../validator/validate-frontmatter.js";
|
|
19
|
+
import { detectFieldTypos } from "../validator/field-typos.js";
|
|
20
|
+
import { stripAuthoringMarkup } from "./lens.js";
|
|
21
|
+
import { SURVEY_SEGMENT_SCHEMAS } from "../content-schema.js";
|
|
22
|
+
// Valid segment types for survey H4 headers
|
|
23
|
+
export const SURVEY_SEGMENT_TYPES = new Set([
|
|
24
|
+
"text",
|
|
25
|
+
"question",
|
|
26
|
+
"rating",
|
|
27
|
+
"choice",
|
|
28
|
+
]);
|
|
29
|
+
/** Keys are payload identifiers: snake_case, stable, short. */
|
|
30
|
+
const KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
31
|
+
const MAX_KEY_LENGTH = 64;
|
|
32
|
+
const DEFAULT_RATING_SCALE = 5;
|
|
33
|
+
const MIN_RATING_SCALE = 2;
|
|
34
|
+
const MAX_RATING_SCALE = 10;
|
|
35
|
+
function parseBoolField(raw, field, defaultValue) {
|
|
36
|
+
const value = raw.fields[field];
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return defaultValue;
|
|
39
|
+
return value.toLowerCase() === "true";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read and validate the required content:: prompt of an answerable segment.
|
|
43
|
+
* Unlike lens Text segments (where empty content degrades to a warning), an
|
|
44
|
+
* answerable survey segment without a prompt is unusable — hard error.
|
|
45
|
+
*/
|
|
46
|
+
function requirePrompt(raw, file, errors) {
|
|
47
|
+
const content = raw.fields.content;
|
|
48
|
+
if (content === undefined || content.trim() === "") {
|
|
49
|
+
const capitalized = raw.type[0].toUpperCase() + raw.type.slice(1);
|
|
50
|
+
errors.push({
|
|
51
|
+
file,
|
|
52
|
+
line: raw.line,
|
|
53
|
+
message: `${capitalized} segment is missing content:: (the question shown to the learner)`,
|
|
54
|
+
suggestion: `Add 'content:: Your question here' to the ${capitalized} segment`,
|
|
55
|
+
severity: "error",
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return content;
|
|
60
|
+
}
|
|
61
|
+
/** Read and validate the required key:: of an answerable segment. */
|
|
62
|
+
function requireKey(raw, file, errors) {
|
|
63
|
+
const key = raw.fields.key?.trim();
|
|
64
|
+
if (!key) {
|
|
65
|
+
const capitalized = raw.type[0].toUpperCase() + raw.type.slice(1);
|
|
66
|
+
errors.push({
|
|
67
|
+
file,
|
|
68
|
+
line: raw.line,
|
|
69
|
+
message: `${capitalized} segment is missing key:: (stable identifier for the answer)`,
|
|
70
|
+
suggestion: "Add a snake_case key, e.g. 'key:: worked_well'. Keys name answers in stored responses — never change one after responses exist",
|
|
71
|
+
severity: "error",
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (!KEY_PATTERN.test(key) || key.length > MAX_KEY_LENGTH) {
|
|
76
|
+
errors.push({
|
|
77
|
+
file,
|
|
78
|
+
line: raw.line,
|
|
79
|
+
message: `Invalid key '${key}' — keys must be snake_case (lowercase letters, digits, underscores; start with a letter; max ${MAX_KEY_LENGTH} chars)`,
|
|
80
|
+
suggestion: "Example: key:: buddy_texted",
|
|
81
|
+
severity: "error",
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return key;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse an options:: multiline value into option texts. Plain `- Item` list
|
|
89
|
+
* items only — survey choices have no correct answers, so checkbox syntax
|
|
90
|
+
* (`- [ ]` / `- [x]`) is rejected to keep the vocabulary unambiguous.
|
|
91
|
+
*/
|
|
92
|
+
function parseOptions(raw, file, errors) {
|
|
93
|
+
const value = raw.fields.options;
|
|
94
|
+
if (value === undefined || value.trim() === "") {
|
|
95
|
+
errors.push({
|
|
96
|
+
file,
|
|
97
|
+
line: raw.line,
|
|
98
|
+
message: "Choice segment is missing options::",
|
|
99
|
+
suggestion: "Add a list:\noptions::\n- First option\n- Second option",
|
|
100
|
+
severity: "error",
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const options = [];
|
|
105
|
+
for (const line of value.split("\n")) {
|
|
106
|
+
const trimmed = line.trim();
|
|
107
|
+
if (!trimmed)
|
|
108
|
+
continue;
|
|
109
|
+
const checkbox = trimmed.match(/^-\s*\[[ xX]?\]/);
|
|
110
|
+
if (checkbox) {
|
|
111
|
+
errors.push({
|
|
112
|
+
file,
|
|
113
|
+
line: raw.line,
|
|
114
|
+
message: `Choice options must be plain list items, got checkbox syntax: "${trimmed}"`,
|
|
115
|
+
suggestion: "Survey choices have no correct answers — use '- Option text' without brackets",
|
|
116
|
+
severity: "error",
|
|
117
|
+
});
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const item = trimmed.match(/^-\s+(.+)$/);
|
|
121
|
+
if (!item) {
|
|
122
|
+
errors.push({
|
|
123
|
+
file,
|
|
124
|
+
line: raw.line,
|
|
125
|
+
message: `Line in options:: is not a list item: "${trimmed}"`,
|
|
126
|
+
suggestion: "Every option must be on its own '- Option text' line",
|
|
127
|
+
severity: "error",
|
|
128
|
+
});
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
options.push(item[1].trim());
|
|
132
|
+
}
|
|
133
|
+
if (options.length < 2) {
|
|
134
|
+
errors.push({
|
|
135
|
+
file,
|
|
136
|
+
line: raw.line,
|
|
137
|
+
message: `Choice segment needs at least 2 options, got ${options.length}`,
|
|
138
|
+
suggestion: "Add more '- Option text' lines under options::",
|
|
139
|
+
severity: "error",
|
|
140
|
+
});
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return options;
|
|
144
|
+
}
|
|
145
|
+
/** Parse scale:: as an integer in [MIN_RATING_SCALE, MAX_RATING_SCALE]. */
|
|
146
|
+
function parseScale(raw, file, errors) {
|
|
147
|
+
const value = raw.fields.scale;
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
return DEFAULT_RATING_SCALE;
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
152
|
+
errors.push({
|
|
153
|
+
file,
|
|
154
|
+
line: raw.line,
|
|
155
|
+
message: `Field 'scale' must be an integer, got '${value}'`,
|
|
156
|
+
suggestion: `Use a number from ${MIN_RATING_SCALE} to ${MAX_RATING_SCALE} (default ${DEFAULT_RATING_SCALE}), or omit the field`,
|
|
157
|
+
severity: "error",
|
|
158
|
+
});
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const scale = parseInt(trimmed, 10);
|
|
162
|
+
if (scale < MIN_RATING_SCALE || scale > MAX_RATING_SCALE) {
|
|
163
|
+
errors.push({
|
|
164
|
+
file,
|
|
165
|
+
line: raw.line,
|
|
166
|
+
message: `Field 'scale' is ${scale}; ratings support ${MIN_RATING_SCALE}–${MAX_RATING_SCALE}`,
|
|
167
|
+
suggestion: `Use a value from ${MIN_RATING_SCALE} to ${MAX_RATING_SCALE}`,
|
|
168
|
+
severity: "error",
|
|
169
|
+
});
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return scale;
|
|
173
|
+
}
|
|
174
|
+
/** Parse max-chars:: as a positive integer (optional). */
|
|
175
|
+
function parseMaxChars(raw, file, errors) {
|
|
176
|
+
const value = raw.fields["max-chars"];
|
|
177
|
+
if (value === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
const trimmed = value.trim();
|
|
180
|
+
if (!/^\d+$/.test(trimmed) || parseInt(trimmed, 10) === 0) {
|
|
181
|
+
errors.push({
|
|
182
|
+
file,
|
|
183
|
+
line: raw.line,
|
|
184
|
+
message: `Field 'max-chars' must be a positive integer, got '${value}'`,
|
|
185
|
+
suggestion: "Use a number like 'max-chars:: 500', or remove the field",
|
|
186
|
+
severity: "error",
|
|
187
|
+
});
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
return parseInt(trimmed, 10);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Validate segment fields against the survey schema: unknown fields warn
|
|
194
|
+
* (with cross-vocabulary hints for lens-only fields), boolean fields must be
|
|
195
|
+
* true/false. Mirrors validateSegmentFields/validateFieldValues, which are
|
|
196
|
+
* bound to the lens SEGMENT_SCHEMAS and can't be reused here.
|
|
197
|
+
*/
|
|
198
|
+
function validateSurveySegmentFields(raw, file) {
|
|
199
|
+
const warnings = [];
|
|
200
|
+
const schema = SURVEY_SEGMENT_SCHEMAS[raw.type];
|
|
201
|
+
if (!schema)
|
|
202
|
+
return warnings;
|
|
203
|
+
const valid = new Set(schema.allFields);
|
|
204
|
+
for (const [name, value] of Object.entries(raw.fields)) {
|
|
205
|
+
if (!valid.has(name)) {
|
|
206
|
+
const lensOnly = name === "assessment-instructions" ||
|
|
207
|
+
name === "feedback" ||
|
|
208
|
+
name === "enforce-voice" ||
|
|
209
|
+
name === "max-time";
|
|
210
|
+
warnings.push({
|
|
211
|
+
file,
|
|
212
|
+
line: raw.line,
|
|
213
|
+
message: `Field '${name}' is not valid in a survey ${raw.type} segment`,
|
|
214
|
+
suggestion: lensOnly
|
|
215
|
+
? `'${name}' belongs to lens questions — survey answers are recorded without AI assessment`
|
|
216
|
+
: `Valid fields: ${schema.allFields.join(", ")}`,
|
|
217
|
+
severity: "warning",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (schema.booleanFields.includes(name) &&
|
|
221
|
+
value.toLowerCase() !== "true" &&
|
|
222
|
+
value.toLowerCase() !== "false") {
|
|
223
|
+
warnings.push({
|
|
224
|
+
file,
|
|
225
|
+
line: raw.line,
|
|
226
|
+
message: `Field '${name}' has non-boolean value '${value}'`,
|
|
227
|
+
suggestion: "Expected 'true' or 'false'",
|
|
228
|
+
severity: "warning",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return warnings;
|
|
233
|
+
}
|
|
234
|
+
function convertSurveySegment(raw, file) {
|
|
235
|
+
const errors = [];
|
|
236
|
+
if (raw.title) {
|
|
237
|
+
const capitalized = raw.type[0].toUpperCase() + raw.type.slice(1);
|
|
238
|
+
errors.push({
|
|
239
|
+
file,
|
|
240
|
+
line: raw.line,
|
|
241
|
+
message: `Titles are not supported for ${capitalized} segments — use just '#### ${capitalized}'`,
|
|
242
|
+
suggestion: `Remove the title after '${capitalized}:'`,
|
|
243
|
+
severity: "error",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
switch (raw.type) {
|
|
247
|
+
case "text": {
|
|
248
|
+
const content = raw.fields.content;
|
|
249
|
+
if (content === undefined) {
|
|
250
|
+
errors.push({
|
|
251
|
+
file,
|
|
252
|
+
line: raw.line,
|
|
253
|
+
message: "Text segment missing content:: field",
|
|
254
|
+
suggestion: "Add 'content:: Your text here' to the text segment",
|
|
255
|
+
severity: "error",
|
|
256
|
+
});
|
|
257
|
+
return { segment: null, errors };
|
|
258
|
+
}
|
|
259
|
+
const segment = { type: "text", content };
|
|
260
|
+
return { segment, errors };
|
|
261
|
+
}
|
|
262
|
+
case "question": {
|
|
263
|
+
const content = requirePrompt(raw, file, errors);
|
|
264
|
+
const key = requireKey(raw, file, errors);
|
|
265
|
+
const maxChars = parseMaxChars(raw, file, errors);
|
|
266
|
+
if (content === null || key === null)
|
|
267
|
+
return { segment: null, errors };
|
|
268
|
+
const segment = {
|
|
269
|
+
type: "question",
|
|
270
|
+
key,
|
|
271
|
+
content,
|
|
272
|
+
required: parseBoolField(raw, "required", false),
|
|
273
|
+
};
|
|
274
|
+
if (maxChars !== undefined)
|
|
275
|
+
segment.maxChars = maxChars;
|
|
276
|
+
const placeholder = raw.fields.placeholder?.trim();
|
|
277
|
+
if (placeholder)
|
|
278
|
+
segment.placeholder = placeholder;
|
|
279
|
+
return { segment, errors };
|
|
280
|
+
}
|
|
281
|
+
case "rating": {
|
|
282
|
+
const content = requirePrompt(raw, file, errors);
|
|
283
|
+
const key = requireKey(raw, file, errors);
|
|
284
|
+
const scale = parseScale(raw, file, errors);
|
|
285
|
+
if (content === null || key === null || scale === null) {
|
|
286
|
+
return { segment: null, errors };
|
|
287
|
+
}
|
|
288
|
+
const segment = {
|
|
289
|
+
type: "rating",
|
|
290
|
+
key,
|
|
291
|
+
content,
|
|
292
|
+
scale,
|
|
293
|
+
required: parseBoolField(raw, "required", false),
|
|
294
|
+
};
|
|
295
|
+
const lowLabel = raw.fields["low-label"]?.trim();
|
|
296
|
+
const highLabel = raw.fields["high-label"]?.trim();
|
|
297
|
+
if (lowLabel)
|
|
298
|
+
segment.lowLabel = lowLabel;
|
|
299
|
+
if (highLabel)
|
|
300
|
+
segment.highLabel = highLabel;
|
|
301
|
+
return { segment, errors };
|
|
302
|
+
}
|
|
303
|
+
case "choice": {
|
|
304
|
+
const content = requirePrompt(raw, file, errors);
|
|
305
|
+
const key = requireKey(raw, file, errors);
|
|
306
|
+
const options = parseOptions(raw, file, errors);
|
|
307
|
+
if (content === null || key === null || options === null) {
|
|
308
|
+
return { segment: null, errors };
|
|
309
|
+
}
|
|
310
|
+
const segment = {
|
|
311
|
+
type: "choice",
|
|
312
|
+
key,
|
|
313
|
+
content,
|
|
314
|
+
options,
|
|
315
|
+
multi: parseBoolField(raw, "multi", false),
|
|
316
|
+
required: parseBoolField(raw, "required", false),
|
|
317
|
+
};
|
|
318
|
+
return { segment, errors };
|
|
319
|
+
}
|
|
320
|
+
default:
|
|
321
|
+
// parseSections already errored on unknown types
|
|
322
|
+
return { segment: null, errors };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function parseSurvey(content, file) {
|
|
326
|
+
const errors = [];
|
|
327
|
+
// Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
|
|
328
|
+
content = stripAuthoringMarkup(content);
|
|
329
|
+
const frontmatterResult = parseFrontmatter(content, file);
|
|
330
|
+
if (frontmatterResult.error) {
|
|
331
|
+
errors.push(frontmatterResult.error);
|
|
332
|
+
return { survey: null, errors };
|
|
333
|
+
}
|
|
334
|
+
const { frontmatter, body, bodyStartLine } = frontmatterResult;
|
|
335
|
+
const frontmatterErrors = validateFrontmatter(frontmatter, "survey", file);
|
|
336
|
+
errors.push(...frontmatterErrors);
|
|
337
|
+
if (frontmatterErrors.some((e) => e.severity === "error")) {
|
|
338
|
+
return { survey: null, errors };
|
|
339
|
+
}
|
|
340
|
+
// id must be a string (YAML might parse UUID-ish values as numbers)
|
|
341
|
+
if (typeof frontmatter.id !== "string") {
|
|
342
|
+
errors.push({
|
|
343
|
+
file,
|
|
344
|
+
line: 2,
|
|
345
|
+
message: `Field 'id' must be a string, got ${typeof frontmatter.id}`,
|
|
346
|
+
suggestion: "Use quotes: id: '12345'",
|
|
347
|
+
severity: "error",
|
|
348
|
+
});
|
|
349
|
+
return { survey: null, errors };
|
|
350
|
+
}
|
|
351
|
+
const title = typeof frontmatter.title === "string" ? frontmatter.title : undefined;
|
|
352
|
+
// Flat H4 segments, same shape as lens bodies
|
|
353
|
+
const { sections: rawSegments, errors: segmentErrors } = parseSections(body, 3, SURVEY_SEGMENT_TYPES, file, true);
|
|
354
|
+
for (const error of segmentErrors) {
|
|
355
|
+
if (error.line)
|
|
356
|
+
error.line += bodyStartLine - 1;
|
|
357
|
+
}
|
|
358
|
+
errors.push(...segmentErrors);
|
|
359
|
+
const segments = [];
|
|
360
|
+
const seenKeys = new Map(); // key -> first line
|
|
361
|
+
for (const rawSeg of rawSegments) {
|
|
362
|
+
rawSeg.line += bodyStartLine - 1;
|
|
363
|
+
errors.push(...validateSurveySegmentFields(rawSeg, file));
|
|
364
|
+
errors.push(...detectFieldTypos(rawSeg.fields, file, rawSeg.line));
|
|
365
|
+
const { segment, errors: conversionErrors } = convertSurveySegment(rawSeg, file);
|
|
366
|
+
errors.push(...conversionErrors);
|
|
367
|
+
if (!segment)
|
|
368
|
+
continue;
|
|
369
|
+
if (segment.type !== "text") {
|
|
370
|
+
const firstLine = seenKeys.get(segment.key);
|
|
371
|
+
if (firstLine !== undefined) {
|
|
372
|
+
errors.push({
|
|
373
|
+
file,
|
|
374
|
+
line: rawSeg.line,
|
|
375
|
+
message: `Duplicate key '${segment.key}' (already used at line ${firstLine})`,
|
|
376
|
+
suggestion: "Every answerable segment needs a unique key",
|
|
377
|
+
severity: "error",
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
seenKeys.set(segment.key, rawSeg.line);
|
|
382
|
+
}
|
|
383
|
+
segments.push(segment);
|
|
384
|
+
}
|
|
385
|
+
if (seenKeys.size === 0) {
|
|
386
|
+
errors.push({
|
|
387
|
+
file,
|
|
388
|
+
line: bodyStartLine,
|
|
389
|
+
message: "Survey has no answerable segments",
|
|
390
|
+
suggestion: "Add at least one #### Question, #### Rating, or #### Choice segment",
|
|
391
|
+
severity: "error",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (errors.some((e) => e.severity === "error")) {
|
|
395
|
+
return { survey: null, errors };
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
survey: { id: frontmatter.id, title, segments },
|
|
399
|
+
errors,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=survey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"survey.js","sourceRoot":"","sources":["../../src/parser/survey.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ContentError } from "./index.js";
|
|
2
|
+
import type { ParsedLearningOutcome } from "./parser/learning-outcome.js";
|
|
3
|
+
export interface SkillTreeDomain {
|
|
4
|
+
slug: string;
|
|
5
|
+
title: string;
|
|
6
|
+
number: number;
|
|
7
|
+
/** Stage column the domain's row begins at. Skills staged earlier than
|
|
8
|
+
* this render inside the start column anyway. Absent = derived by the
|
|
9
|
+
* frontend from the domain's skills. */
|
|
10
|
+
startStage?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TaughtByCourse {
|
|
13
|
+
slug: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TaughtByModule {
|
|
17
|
+
moduleSlug: string;
|
|
18
|
+
moduleTitle: string;
|
|
19
|
+
/** Courses whose progression contains this module. A module is always
|
|
20
|
+
* doable standalone; course membership is context, not a constraint. */
|
|
21
|
+
courses: TaughtByCourse[];
|
|
22
|
+
}
|
|
23
|
+
export interface SkillTreeCard {
|
|
24
|
+
/** LO frontmatter id (uuid) — learner progress is keyed on this. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Filename stem; the LO file name is the card title by convention. */
|
|
27
|
+
title: string;
|
|
28
|
+
/** Slugs of every domain this skill belongs to — all equals; the first
|
|
29
|
+
* listed is where dependency edges attach (a drawing detail, not a rank). */
|
|
30
|
+
domains: string[];
|
|
31
|
+
stage: string;
|
|
32
|
+
/** Hard prerequisites, as LO ids. */
|
|
33
|
+
requires: string[];
|
|
34
|
+
/** The learner-facing outcome statement, when present. */
|
|
35
|
+
outcome?: string;
|
|
36
|
+
/**
|
|
37
|
+
* A stub that maps territory: positioned on the tree, but with no teaching
|
|
38
|
+
* content behind it yet. Rendered differently so a map that mixes real and
|
|
39
|
+
* placeholder material never overstates what exists.
|
|
40
|
+
*/
|
|
41
|
+
placeholder?: boolean;
|
|
42
|
+
taughtBy: TaughtByModule[];
|
|
43
|
+
}
|
|
44
|
+
export interface SkillTree {
|
|
45
|
+
/** Sorted by domain number. */
|
|
46
|
+
domains: SkillTreeDomain[];
|
|
47
|
+
cards: SkillTreeCard[];
|
|
48
|
+
/** LO file paths with no `domain:` field — not yet triaged. */
|
|
49
|
+
untriaged: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function slugifyTitle(title: string): string;
|
|
52
|
+
export interface ParsedDomainFile {
|
|
53
|
+
title: string;
|
|
54
|
+
number: number;
|
|
55
|
+
startStage?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Parse a Domains/ placeholder file: `domain-number:` frontmatter is the
|
|
58
|
+
* only content that matters; the title is the filename. */
|
|
59
|
+
export declare function parseDomainFile(content: string, file: string): {
|
|
60
|
+
domain: ParsedDomainFile | null;
|
|
61
|
+
errors: ContentError[];
|
|
62
|
+
};
|
|
63
|
+
export interface SkillTreeInput {
|
|
64
|
+
/** Domain file path → parsed domain (only successfully parsed ones). */
|
|
65
|
+
domainFiles: Map<string, ParsedDomainFile>;
|
|
66
|
+
/** LO file path → parsed LO (only successfully parsed ones). */
|
|
67
|
+
learningOutcomes: Map<string, ParsedLearningOutcome>;
|
|
68
|
+
/** LO file path → module file paths whose sections reference it. */
|
|
69
|
+
moduleLoRefs: Map<string, Set<string>>;
|
|
70
|
+
/** Module file path → its primary (parent) slug and title. */
|
|
71
|
+
moduleInfo: Map<string, {
|
|
72
|
+
slug: string;
|
|
73
|
+
title: string;
|
|
74
|
+
}>;
|
|
75
|
+
/** Module slug → courses containing it. */
|
|
76
|
+
moduleCourses: Map<string, TaughtByCourse[]>;
|
|
77
|
+
/** All vault files, for extension-tolerant link resolution. */
|
|
78
|
+
files: Map<string, string>;
|
|
79
|
+
}
|
|
80
|
+
export declare function buildSkillTree(input: SkillTreeInput): {
|
|
81
|
+
skillTree: SkillTree;
|
|
82
|
+
errors: ContentError[];
|
|
83
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/skill-tree.ts — assembles the skill-tree structure from parsed content.
|
|
2
|
+
//
|
|
3
|
+
// The processor emits relationships (LO ↔ module ↔ course); presentation
|
|
4
|
+
// decisions live in the frontend. Cards reference each other by LO id and
|
|
5
|
+
// reference domains by slug.
|
|
6
|
+
import { parseFrontmatter } from "./parser/frontmatter.js";
|
|
7
|
+
import { findFileWithExtension } from "./parser/wikilink.js";
|
|
8
|
+
export function slugifyTitle(title) {
|
|
9
|
+
return title
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12
|
+
.replace(/^-+|-+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
function fileStem(path) {
|
|
15
|
+
const base = path.split("/").pop() ?? path;
|
|
16
|
+
return base.replace(/\.md$/, "");
|
|
17
|
+
}
|
|
18
|
+
/** Parse a Domains/ placeholder file: `domain-number:` frontmatter is the
|
|
19
|
+
* only content that matters; the title is the filename. */
|
|
20
|
+
export function parseDomainFile(content, file) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
const frontmatterResult = parseFrontmatter(content, file);
|
|
23
|
+
if (frontmatterResult.error) {
|
|
24
|
+
errors.push(frontmatterResult.error);
|
|
25
|
+
return { domain: null, errors };
|
|
26
|
+
}
|
|
27
|
+
const rawNumber = frontmatterResult.frontmatter["domain-number"];
|
|
28
|
+
if (typeof rawNumber !== "number" || !Number.isInteger(rawNumber)) {
|
|
29
|
+
errors.push({
|
|
30
|
+
file,
|
|
31
|
+
line: 2,
|
|
32
|
+
message: `Domain file must have an integer 'domain-number' field, got: ${JSON.stringify(rawNumber)}`,
|
|
33
|
+
suggestion: "Add domain-number: <n> to the frontmatter",
|
|
34
|
+
severity: "error",
|
|
35
|
+
});
|
|
36
|
+
return { domain: null, errors };
|
|
37
|
+
}
|
|
38
|
+
const rawStart = frontmatterResult.frontmatter["start-stage"];
|
|
39
|
+
let startStage;
|
|
40
|
+
if (rawStart !== undefined && rawStart !== null) {
|
|
41
|
+
if (typeof rawStart === "string" &&
|
|
42
|
+
["beginner", "intermediate", "advanced"].includes(rawStart)) {
|
|
43
|
+
startStage = rawStart;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
errors.push({
|
|
47
|
+
file,
|
|
48
|
+
line: 2,
|
|
49
|
+
message: `Field 'start-stage' must be beginner, intermediate or advanced, got: ${JSON.stringify(rawStart)}`,
|
|
50
|
+
suggestion: "Use start-stage: beginner | intermediate | advanced",
|
|
51
|
+
severity: "error",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
domain: {
|
|
57
|
+
title: fileStem(file),
|
|
58
|
+
number: rawNumber,
|
|
59
|
+
...(startStage ? { startStage } : {}),
|
|
60
|
+
},
|
|
61
|
+
errors,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function buildSkillTree(input) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
// --- Domains ---
|
|
67
|
+
const numberToPath = new Map();
|
|
68
|
+
const pathToDomain = new Map();
|
|
69
|
+
for (const [path, parsed] of input.domainFiles) {
|
|
70
|
+
const existing = numberToPath.get(parsed.number);
|
|
71
|
+
if (existing) {
|
|
72
|
+
errors.push({
|
|
73
|
+
file: path,
|
|
74
|
+
line: 2,
|
|
75
|
+
message: `Duplicate domain-number ${parsed.number} — already used by ${existing}`,
|
|
76
|
+
suggestion: "Give each domain file a unique domain-number",
|
|
77
|
+
severity: "error",
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
numberToPath.set(parsed.number, path);
|
|
82
|
+
pathToDomain.set(path, {
|
|
83
|
+
slug: slugifyTitle(parsed.title),
|
|
84
|
+
title: parsed.title,
|
|
85
|
+
number: parsed.number,
|
|
86
|
+
...(parsed.startStage ? { startStage: parsed.startStage } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const domains = [...pathToDomain.values()].sort((a, b) => a.number - b.number);
|
|
90
|
+
// --- Cards ---
|
|
91
|
+
// First pass: which LO paths become cards (needed to validate requires
|
|
92
|
+
// edges — an edge must point at another card, not at an excluded or
|
|
93
|
+
// untriaged LO).
|
|
94
|
+
const cardPaths = new Map();
|
|
95
|
+
const untriaged = [];
|
|
96
|
+
for (const [path, lo] of input.learningOutcomes) {
|
|
97
|
+
if (lo.domains === undefined) {
|
|
98
|
+
// Not yet triaged. Reported via skillTree.untriaged (a TODO list for
|
|
99
|
+
// editors), not as a per-file warning — most LOs were written before
|
|
100
|
+
// the skill tree existed and blanket warnings would drown /validate.
|
|
101
|
+
untriaged.push(path);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (lo.domains === "none")
|
|
105
|
+
continue;
|
|
106
|
+
cardPaths.set(path, lo);
|
|
107
|
+
}
|
|
108
|
+
untriaged.sort();
|
|
109
|
+
const cards = [];
|
|
110
|
+
for (const [path, lo] of cardPaths) {
|
|
111
|
+
const domainRefs = lo.domains;
|
|
112
|
+
const domainSlugs = [];
|
|
113
|
+
for (const domainRef of domainRefs) {
|
|
114
|
+
const domainPath = findFileWithExtension(domainRef.resolvedPath, input.files);
|
|
115
|
+
const domain = domainPath ? pathToDomain.get(domainPath) : undefined;
|
|
116
|
+
if (!domain) {
|
|
117
|
+
errors.push({
|
|
118
|
+
file: path,
|
|
119
|
+
line: 2,
|
|
120
|
+
message: `Domain link does not resolve to a domain file: ${domainRef.resolvedPath}`,
|
|
121
|
+
suggestion: "Check the file name under Domains/",
|
|
122
|
+
severity: "error",
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
domainSlugs.push(domain.slug);
|
|
127
|
+
}
|
|
128
|
+
if (domainSlugs.length === 0)
|
|
129
|
+
continue;
|
|
130
|
+
if (lo.stage === undefined)
|
|
131
|
+
continue; // already reported by the parser
|
|
132
|
+
const requires = [];
|
|
133
|
+
for (const ref of lo.requires ?? []) {
|
|
134
|
+
const targetPath = findFileWithExtension(ref.resolvedPath, input.files);
|
|
135
|
+
const target = targetPath ? cardPaths.get(targetPath) : undefined;
|
|
136
|
+
if (!target) {
|
|
137
|
+
const reason = targetPath
|
|
138
|
+
? "resolves to a Learning Outcome that is not in the skill tree"
|
|
139
|
+
: "does not resolve to a Learning Outcome file";
|
|
140
|
+
errors.push({
|
|
141
|
+
file: path,
|
|
142
|
+
line: 2,
|
|
143
|
+
message: `'requires' link ${reason}: ${ref.resolvedPath}`,
|
|
144
|
+
suggestion: "Prerequisite edges must point at another skill-tree Learning Outcome",
|
|
145
|
+
severity: "error",
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
requires.push(target.id);
|
|
150
|
+
}
|
|
151
|
+
const taughtBy = [];
|
|
152
|
+
for (const modulePath of input.moduleLoRefs.get(path) ?? []) {
|
|
153
|
+
const info = input.moduleInfo.get(modulePath);
|
|
154
|
+
if (!info)
|
|
155
|
+
continue;
|
|
156
|
+
taughtBy.push({
|
|
157
|
+
moduleSlug: info.slug,
|
|
158
|
+
moduleTitle: info.title,
|
|
159
|
+
courses: input.moduleCourses.get(info.slug) ?? [],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
taughtBy.sort((a, b) => a.moduleSlug.localeCompare(b.moduleSlug));
|
|
163
|
+
cards.push({
|
|
164
|
+
id: lo.id,
|
|
165
|
+
title: fileStem(path),
|
|
166
|
+
domains: domainSlugs,
|
|
167
|
+
stage: lo.stage,
|
|
168
|
+
requires,
|
|
169
|
+
...(lo.outcomeStatement ? { outcome: lo.outcomeStatement } : {}),
|
|
170
|
+
...(lo.placeholder ? { placeholder: true } : {}),
|
|
171
|
+
taughtBy,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
cards.sort((a, b) => a.title.localeCompare(b.title));
|
|
175
|
+
return { skillTree: { domains, cards, untriaged }, errors };
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=skill-tree.js.map
|