lens-content-processor 0.2.1 → 0.3.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/bundler/video.js +4 -4
- package/dist/bundler/video.js.map +1 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +11 -9
- package/dist/cli.js.map +1 -1
- package/dist/content-schema.d.ts +34 -0
- package/dist/content-schema.js +62 -0
- package/dist/content-schema.js.map +1 -0
- package/dist/flattener/index.d.ts +21 -1
- package/dist/flattener/index.js +316 -33
- package/dist/flattener/index.js.map +1 -1
- package/dist/fs/read-vault.d.ts +1 -4
- package/dist/fs/read-vault.js +4 -8
- package/dist/fs/read-vault.js.map +1 -1
- package/dist/index.d.ts +25 -2
- package/dist/index.js +231 -6
- package/dist/index.js.map +1 -1
- package/dist/parser/article.d.ts +16 -0
- package/dist/parser/article.js +70 -0
- package/dist/parser/article.js.map +1 -0
- package/dist/parser/course.d.ts +2 -2
- package/dist/parser/course.js +18 -35
- package/dist/parser/course.js.map +1 -1
- package/dist/parser/frontmatter.js +2 -0
- package/dist/parser/frontmatter.js.map +1 -1
- package/dist/parser/learning-outcome.d.ts +4 -2
- package/dist/parser/learning-outcome.js +65 -23
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +38 -1
- package/dist/parser/lens.js +173 -18
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/module.d.ts +11 -5
- package/dist/parser/module.js +172 -76
- package/dist/parser/module.js.map +1 -1
- package/dist/parser/sections.js +116 -13
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/video-transcript.d.ts +11 -0
- package/dist/parser/video-transcript.js +39 -0
- package/dist/parser/video-transcript.js.map +1 -0
- package/dist/parser/wikilink.d.ts +2 -0
- package/dist/parser/wikilink.js +88 -24
- package/dist/parser/wikilink.js.map +1 -1
- package/dist/utils/slug.d.ts +5 -0
- package/dist/utils/slug.js +16 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/validator/chat-precedence.d.ts +9 -0
- package/dist/validator/chat-precedence.js +32 -0
- package/dist/validator/chat-precedence.js.map +1 -0
- package/dist/validator/duplicates.d.ts +13 -0
- package/dist/validator/duplicates.js +27 -0
- package/dist/validator/duplicates.js.map +1 -0
- package/dist/validator/field-typos.d.ts +11 -0
- package/dist/validator/field-typos.js +36 -28
- package/dist/validator/field-typos.js.map +1 -1
- package/dist/validator/field-values.d.ts +14 -0
- package/dist/validator/field-values.js +61 -7
- package/dist/validator/field-values.js.map +1 -1
- package/dist/validator/output-integrity.d.ts +7 -0
- package/dist/validator/output-integrity.js +57 -0
- package/dist/validator/output-integrity.js.map +1 -0
- package/dist/validator/segment-fields.js +1 -15
- package/dist/validator/segment-fields.js.map +1 -1
- package/dist/validator/tier.d.ts +18 -0
- package/dist/validator/tier.js +74 -0
- package/dist/validator/tier.js.map +1 -0
- package/dist/validator/timestamps.d.ts +2 -0
- package/dist/validator/timestamps.js +91 -0
- package/dist/validator/timestamps.js.map +1 -0
- package/dist/validator/url-reachability.d.ts +8 -0
- package/dist/validator/url-reachability.js +60 -0
- package/dist/validator/url-reachability.js.map +1 -0
- package/dist/validator/validate-frontmatter.d.ts +9 -0
- package/dist/validator/validate-frontmatter.js +53 -0
- package/dist/validator/validate-frontmatter.js.map +1 -0
- package/package.json +5 -5
package/dist/flattener/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkTierViolation } from '../validator/tier.js';
|
|
2
|
+
import { parseModule, parsePageSegments } from '../parser/module.js';
|
|
2
3
|
import { parseLearningOutcome } from '../parser/learning-outcome.js';
|
|
3
4
|
import { parseLens } from '../parser/lens.js';
|
|
4
5
|
import { parseWikilink, resolveWikilinkPath, findFileWithExtension, findSimilarFiles, formatSuggestion } from '../parser/wikilink.js';
|
|
5
6
|
import { parseFrontmatter } from '../parser/frontmatter.js';
|
|
7
|
+
import { fileNameToSlug } from '../utils/slug.js';
|
|
6
8
|
import { extractArticleExcerpt } from '../bundler/article.js';
|
|
7
9
|
import { extractVideoExcerpt } from '../bundler/video.js';
|
|
8
10
|
/**
|
|
@@ -34,7 +36,7 @@ function extractVideoIdFromUrl(url) {
|
|
|
34
36
|
* @param visitedPaths - Optional set of already-visited paths for cycle detection
|
|
35
37
|
* @returns Flattened module with resolved sections and segments, plus any errors
|
|
36
38
|
*/
|
|
37
|
-
export function flattenModule(modulePath, files, visitedPaths = new Set()) {
|
|
39
|
+
export function flattenModule(modulePath, files, visitedPaths = new Set(), tierMap) {
|
|
38
40
|
// Check for circular reference
|
|
39
41
|
if (visitedPaths.has(modulePath)) {
|
|
40
42
|
return {
|
|
@@ -75,21 +77,23 @@ export function flattenModule(modulePath, files, visitedPaths = new Set()) {
|
|
|
75
77
|
// This allows the same file to be referenced in different sections
|
|
76
78
|
// while still detecting cycles within a single chain
|
|
77
79
|
const sectionVisitedPaths = new Set(visitedPaths);
|
|
78
|
-
const result = flattenLearningOutcomeSection(section, modulePath, files, sectionVisitedPaths);
|
|
80
|
+
const result = flattenLearningOutcomeSection(section, modulePath, files, sectionVisitedPaths, tierMap);
|
|
79
81
|
errors.push(...result.errors);
|
|
80
82
|
flattenedSections.push(...result.sections);
|
|
81
83
|
}
|
|
82
84
|
else if (section.type === 'page') {
|
|
83
85
|
// Page sections don't have LO references, they have inline content
|
|
84
86
|
// Parse the section body for ## Text subsections
|
|
85
|
-
const
|
|
87
|
+
const textResult = parsePageSegments(section.body, modulePath, section.line);
|
|
88
|
+
errors.push(...textResult.errors);
|
|
86
89
|
const pageSection = {
|
|
87
90
|
type: 'page',
|
|
88
91
|
meta: { title: section.title },
|
|
89
|
-
segments:
|
|
90
|
-
optional: section.fields.optional === 'true',
|
|
92
|
+
segments: textResult.segments,
|
|
93
|
+
optional: section.fields.optional?.toLowerCase() === 'true',
|
|
91
94
|
contentId: section.fields.id ?? null,
|
|
92
95
|
learningOutcomeId: null,
|
|
96
|
+
learningOutcomeName: null,
|
|
93
97
|
videoId: null,
|
|
94
98
|
};
|
|
95
99
|
flattenedSections.push(pageSection);
|
|
@@ -98,7 +102,7 @@ export function flattenModule(modulePath, files, visitedPaths = new Set()) {
|
|
|
98
102
|
// Uncategorized sections can contain ## Lens: references
|
|
99
103
|
// Each lens becomes its own section (lens-video, lens-article, or page)
|
|
100
104
|
const sectionVisitedPaths = new Set(visitedPaths);
|
|
101
|
-
const result = flattenUncategorizedSection(section, modulePath, files, sectionVisitedPaths);
|
|
105
|
+
const result = flattenUncategorizedSection(section, modulePath, files, sectionVisitedPaths, tierMap);
|
|
102
106
|
errors.push(...result.errors);
|
|
103
107
|
flattenedSections.push(...result.sections);
|
|
104
108
|
}
|
|
@@ -118,7 +122,7 @@ export function flattenModule(modulePath, files, visitedPaths = new Set()) {
|
|
|
118
122
|
* Flatten a Learning Outcome section by resolving its LO file and all referenced lenses.
|
|
119
123
|
* Each lens in the LO becomes its own section.
|
|
120
124
|
*/
|
|
121
|
-
function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths) {
|
|
125
|
+
function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths, tierMap) {
|
|
122
126
|
const errors = [];
|
|
123
127
|
const sections = [];
|
|
124
128
|
// Get the source wikilink
|
|
@@ -135,12 +139,17 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
135
139
|
}
|
|
136
140
|
// Parse and resolve the wikilink
|
|
137
141
|
const wikilink = parseWikilink(source);
|
|
138
|
-
if (!wikilink) {
|
|
142
|
+
if (!wikilink || wikilink.error) {
|
|
143
|
+
const suggestion = wikilink?.correctedPath
|
|
144
|
+
? `Did you mean '[[${wikilink.correctedPath}]]'?`
|
|
145
|
+
: 'Use format [[../Learning Outcomes/filename.md|Display Text]]';
|
|
139
146
|
errors.push({
|
|
140
147
|
file: modulePath,
|
|
141
148
|
line: section.line,
|
|
142
|
-
message:
|
|
143
|
-
|
|
149
|
+
message: wikilink?.error
|
|
150
|
+
? `${wikilink.error}: ${source}`
|
|
151
|
+
: `Invalid wikilink format: ${source}`,
|
|
152
|
+
suggestion,
|
|
144
153
|
severity: 'error',
|
|
145
154
|
});
|
|
146
155
|
return { sections: [], errors };
|
|
@@ -161,6 +170,19 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
161
170
|
});
|
|
162
171
|
return { sections: [], errors };
|
|
163
172
|
}
|
|
173
|
+
// Check tier violation (module → LO)
|
|
174
|
+
if (tierMap) {
|
|
175
|
+
const parentTier = tierMap.get(modulePath) ?? 'production';
|
|
176
|
+
const childTier = tierMap.get(loPath) ?? 'production';
|
|
177
|
+
const violation = checkTierViolation(modulePath, parentTier, loPath, childTier, 'learning outcome', section.line);
|
|
178
|
+
if (violation) {
|
|
179
|
+
errors.push(violation);
|
|
180
|
+
}
|
|
181
|
+
// Skip ignored children silently (they're not processed)
|
|
182
|
+
if (childTier === 'ignored') {
|
|
183
|
+
return { sections: [], errors };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
164
186
|
// Check for circular reference
|
|
165
187
|
if (visitedPaths.has(loPath)) {
|
|
166
188
|
errors.push({
|
|
@@ -195,6 +217,18 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
195
217
|
});
|
|
196
218
|
continue;
|
|
197
219
|
}
|
|
220
|
+
// Check tier violation (LO → Lens)
|
|
221
|
+
if (tierMap) {
|
|
222
|
+
const parentTier = tierMap.get(loPath) ?? 'production';
|
|
223
|
+
const childTier = tierMap.get(lensPath) ?? 'production';
|
|
224
|
+
const violation = checkTierViolation(loPath, parentTier, lensPath, childTier, 'lens');
|
|
225
|
+
if (violation) {
|
|
226
|
+
errors.push(violation);
|
|
227
|
+
}
|
|
228
|
+
if (childTier === 'ignored') {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
198
232
|
// Check for circular reference
|
|
199
233
|
if (visitedPaths.has(lensPath)) {
|
|
200
234
|
errors.push({
|
|
@@ -226,7 +260,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
226
260
|
// Extract article metadata from the article file's frontmatter
|
|
227
261
|
if (lensSection.source) {
|
|
228
262
|
const articleWikilink = parseWikilink(lensSection.source);
|
|
229
|
-
if (articleWikilink) {
|
|
263
|
+
if (articleWikilink && !articleWikilink.error) {
|
|
230
264
|
const articlePathResolved = resolveWikilinkPath(articleWikilink.path, lensPath);
|
|
231
265
|
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
232
266
|
if (articlePath) {
|
|
@@ -237,21 +271,29 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
237
271
|
meta.title = articleFrontmatter.frontmatter.title;
|
|
238
272
|
}
|
|
239
273
|
if (articleFrontmatter.frontmatter.author) {
|
|
240
|
-
|
|
274
|
+
const raw = articleFrontmatter.frontmatter.author;
|
|
275
|
+
meta.author = Array.isArray(raw) ? raw.join(', ') : String(raw);
|
|
241
276
|
}
|
|
242
|
-
if (articleFrontmatter.frontmatter.
|
|
243
|
-
meta.sourceUrl = articleFrontmatter.frontmatter.
|
|
277
|
+
if (articleFrontmatter.frontmatter.source_url) {
|
|
278
|
+
meta.sourceUrl = articleFrontmatter.frontmatter.source_url;
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
283
|
}
|
|
284
|
+
else if (lensSection.type === 'page') {
|
|
285
|
+
sectionType = 'page';
|
|
286
|
+
// For page sections, use the title from the ### Page: header
|
|
287
|
+
if (lensSection.title) {
|
|
288
|
+
meta.title = lensSection.title;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
249
291
|
else if (lensSection.type === 'lens-video') {
|
|
250
292
|
sectionType = 'lens-video';
|
|
251
293
|
// Extract video metadata from the video transcript file's frontmatter
|
|
252
294
|
if (lensSection.source) {
|
|
253
295
|
const videoWikilink = parseWikilink(lensSection.source);
|
|
254
|
-
if (videoWikilink) {
|
|
296
|
+
if (videoWikilink && !videoWikilink.error) {
|
|
255
297
|
const videoPathResolved = resolveWikilinkPath(videoWikilink.path, lensPath);
|
|
256
298
|
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
257
299
|
if (videoPath) {
|
|
@@ -277,7 +319,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
277
319
|
}
|
|
278
320
|
// Process segments
|
|
279
321
|
for (const parsedSegment of lensSection.segments) {
|
|
280
|
-
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths);
|
|
322
|
+
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths, tierMap);
|
|
281
323
|
errors.push(...segmentResult.errors);
|
|
282
324
|
if (segmentResult.segment) {
|
|
283
325
|
segments.push(segmentResult.segment);
|
|
@@ -292,26 +334,67 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths)
|
|
|
292
334
|
type: sectionType,
|
|
293
335
|
meta,
|
|
294
336
|
segments,
|
|
295
|
-
optional: section.fields.optional === 'true' || lensRef.optional,
|
|
337
|
+
optional: section.fields.optional?.toLowerCase() === 'true' || lensRef.optional,
|
|
296
338
|
learningOutcomeId: lo.id ?? null,
|
|
339
|
+
learningOutcomeName: loPath.split('/').pop()?.replace(/\.md$/i, '') ?? null,
|
|
297
340
|
contentId: lens.id ?? null,
|
|
298
341
|
videoId: videoId ?? null,
|
|
299
342
|
};
|
|
300
343
|
sections.push(resultSection);
|
|
301
344
|
}
|
|
345
|
+
// After lens processing, add test section if present with inline segments
|
|
346
|
+
if (lo.test && lo.test.segments.length > 0) {
|
|
347
|
+
const testSegments = [];
|
|
348
|
+
for (const parsedSegment of lo.test.segments) {
|
|
349
|
+
// For test sections with question/chat/text segments, no source file resolution needed
|
|
350
|
+
// Create a minimal stub lensSection since these segment types don't need it
|
|
351
|
+
const stubLensSection = {
|
|
352
|
+
type: 'page',
|
|
353
|
+
title: 'Test',
|
|
354
|
+
segments: [],
|
|
355
|
+
line: 0,
|
|
356
|
+
};
|
|
357
|
+
const segmentResult = convertSegment(parsedSegment, stubLensSection, loPath, files, visitedPaths, tierMap);
|
|
358
|
+
errors.push(...segmentResult.errors);
|
|
359
|
+
if (segmentResult.segment) {
|
|
360
|
+
testSegments.push(segmentResult.segment);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (testSegments.length > 0) {
|
|
364
|
+
const hasFeedback = testSegments.some((s) => s.type === 'question' && s.feedback);
|
|
365
|
+
sections.push({
|
|
366
|
+
type: 'test',
|
|
367
|
+
meta: { title: 'Test' },
|
|
368
|
+
segments: testSegments,
|
|
369
|
+
optional: false,
|
|
370
|
+
...(hasFeedback && { feedback: true }),
|
|
371
|
+
contentId: null,
|
|
372
|
+
learningOutcomeId: lo.id ?? null,
|
|
373
|
+
learningOutcomeName: loPath.split('/').pop()?.replace(/\.md$/i, '') ?? null,
|
|
374
|
+
videoId: null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
302
378
|
return { sections, errors };
|
|
303
379
|
}
|
|
304
380
|
/**
|
|
305
381
|
* Flatten an Uncategorized section by parsing its ## Lens: references.
|
|
306
382
|
* Each lens becomes its own section with the appropriate type (lens-video, lens-article, or page).
|
|
307
383
|
*/
|
|
308
|
-
function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
384
|
+
function flattenUncategorizedSection(section, modulePath, files, visitedPaths, tierMap) {
|
|
309
385
|
const errors = [];
|
|
310
386
|
const sections = [];
|
|
311
387
|
// Parse the section body for ## Lens: subsections
|
|
312
388
|
const lensRefs = parseUncategorizedLensRefs(section.body, modulePath);
|
|
313
|
-
// If no lens refs found, return empty array
|
|
389
|
+
// If no lens refs found, warn and return empty array
|
|
314
390
|
if (lensRefs.length === 0) {
|
|
391
|
+
errors.push({
|
|
392
|
+
file: modulePath,
|
|
393
|
+
line: section.line,
|
|
394
|
+
message: 'Uncategorized section has no ## Lens: references — this section will produce no output',
|
|
395
|
+
suggestion: "Add '## Lens: [[../Lenses/lens-name.md|Display]]' references",
|
|
396
|
+
severity: 'warning',
|
|
397
|
+
});
|
|
315
398
|
return { sections: [], errors };
|
|
316
399
|
}
|
|
317
400
|
// Process each lens reference as a separate section
|
|
@@ -329,6 +412,18 @@ function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
|
329
412
|
});
|
|
330
413
|
continue;
|
|
331
414
|
}
|
|
415
|
+
// Check tier violation (Uncategorized/Module → Lens)
|
|
416
|
+
if (tierMap) {
|
|
417
|
+
const parentTier = tierMap.get(modulePath) ?? 'production';
|
|
418
|
+
const childTier = tierMap.get(lensPath) ?? 'production';
|
|
419
|
+
const violation = checkTierViolation(modulePath, parentTier, lensPath, childTier, 'lens');
|
|
420
|
+
if (violation) {
|
|
421
|
+
errors.push(violation);
|
|
422
|
+
}
|
|
423
|
+
if (childTier === 'ignored') {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
332
427
|
// Check for circular reference
|
|
333
428
|
if (visitedPaths.has(lensPath)) {
|
|
334
429
|
errors.push({
|
|
@@ -360,7 +455,7 @@ function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
|
360
455
|
// Extract article metadata from the article file's frontmatter
|
|
361
456
|
if (lensSection.source) {
|
|
362
457
|
const articleWikilink = parseWikilink(lensSection.source);
|
|
363
|
-
if (articleWikilink) {
|
|
458
|
+
if (articleWikilink && !articleWikilink.error) {
|
|
364
459
|
const articlePathResolved = resolveWikilinkPath(articleWikilink.path, lensPath);
|
|
365
460
|
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
366
461
|
if (articlePath) {
|
|
@@ -371,21 +466,29 @@ function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
|
371
466
|
meta.title = articleFrontmatter.frontmatter.title;
|
|
372
467
|
}
|
|
373
468
|
if (articleFrontmatter.frontmatter.author) {
|
|
374
|
-
|
|
469
|
+
const raw = articleFrontmatter.frontmatter.author;
|
|
470
|
+
meta.author = Array.isArray(raw) ? raw.join(', ') : String(raw);
|
|
375
471
|
}
|
|
376
|
-
if (articleFrontmatter.frontmatter.
|
|
377
|
-
meta.sourceUrl = articleFrontmatter.frontmatter.
|
|
472
|
+
if (articleFrontmatter.frontmatter.source_url) {
|
|
473
|
+
meta.sourceUrl = articleFrontmatter.frontmatter.source_url;
|
|
378
474
|
}
|
|
379
475
|
}
|
|
380
476
|
}
|
|
381
477
|
}
|
|
382
478
|
}
|
|
479
|
+
else if (lensSection.type === 'page') {
|
|
480
|
+
sectionType = 'page';
|
|
481
|
+
// For page sections, use the title from the ### Page: header
|
|
482
|
+
if (lensSection.title) {
|
|
483
|
+
meta.title = lensSection.title;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
383
486
|
else if (lensSection.type === 'lens-video') {
|
|
384
487
|
sectionType = 'lens-video';
|
|
385
488
|
// Extract video metadata from the video transcript file's frontmatter
|
|
386
489
|
if (lensSection.source) {
|
|
387
490
|
const videoWikilink = parseWikilink(lensSection.source);
|
|
388
|
-
if (videoWikilink) {
|
|
491
|
+
if (videoWikilink && !videoWikilink.error) {
|
|
389
492
|
const videoPathResolved = resolveWikilinkPath(videoWikilink.path, lensPath);
|
|
390
493
|
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
391
494
|
if (videoPath) {
|
|
@@ -411,7 +514,7 @@ function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
|
411
514
|
}
|
|
412
515
|
// Process segments
|
|
413
516
|
for (const parsedSegment of lensSection.segments) {
|
|
414
|
-
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths);
|
|
517
|
+
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths, tierMap);
|
|
415
518
|
errors.push(...segmentResult.errors);
|
|
416
519
|
if (segmentResult.segment) {
|
|
417
520
|
segments.push(segmentResult.segment);
|
|
@@ -425,6 +528,7 @@ function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
|
425
528
|
segments,
|
|
426
529
|
optional: lensRef.optional,
|
|
427
530
|
learningOutcomeId: null,
|
|
531
|
+
learningOutcomeName: null,
|
|
428
532
|
contentId: lens.id ?? null,
|
|
429
533
|
videoId: videoId ?? null,
|
|
430
534
|
};
|
|
@@ -464,7 +568,7 @@ function parseUncategorizedLensRefs(body, parentPath) {
|
|
|
464
568
|
lensRefs.push({
|
|
465
569
|
source: currentFields.source,
|
|
466
570
|
resolvedPath,
|
|
467
|
-
optional: currentFields.optional === 'true',
|
|
571
|
+
optional: currentFields.optional?.toLowerCase() === 'true',
|
|
468
572
|
});
|
|
469
573
|
}
|
|
470
574
|
}
|
|
@@ -483,12 +587,12 @@ function parseUncategorizedLensRefs(body, parentPath) {
|
|
|
483
587
|
}
|
|
484
588
|
if (currentFields.source) {
|
|
485
589
|
const wikilink = parseWikilink(currentFields.source);
|
|
486
|
-
if (wikilink) {
|
|
590
|
+
if (wikilink && !wikilink.error) {
|
|
487
591
|
const resolvedPath = resolveWikilinkPath(wikilink.path, parentPath);
|
|
488
592
|
lensRefs.push({
|
|
489
593
|
source: currentFields.source,
|
|
490
594
|
resolvedPath,
|
|
491
|
-
optional: currentFields.optional === 'true',
|
|
595
|
+
optional: currentFields.optional?.toLowerCase() === 'true',
|
|
492
596
|
});
|
|
493
597
|
}
|
|
494
598
|
}
|
|
@@ -535,7 +639,7 @@ function parseUncategorizedLensRefs(body, parentPath) {
|
|
|
535
639
|
lensRefs.push({
|
|
536
640
|
source: currentFields.source,
|
|
537
641
|
resolvedPath,
|
|
538
|
-
optional: currentFields.optional === 'true',
|
|
642
|
+
optional: currentFields.optional?.toLowerCase() === 'true',
|
|
539
643
|
});
|
|
540
644
|
}
|
|
541
645
|
}
|
|
@@ -546,7 +650,7 @@ function parseUncategorizedLensRefs(body, parentPath) {
|
|
|
546
650
|
* Convert a parsed lens segment into a final flattened segment.
|
|
547
651
|
* For article-excerpt and video-excerpt, this involves extracting content from source files.
|
|
548
652
|
*/
|
|
549
|
-
function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths) {
|
|
653
|
+
function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths, tierMap) {
|
|
550
654
|
const errors = [];
|
|
551
655
|
switch (parsedSegment.type) {
|
|
552
656
|
case 'text': {
|
|
@@ -588,10 +692,14 @@ function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPath
|
|
|
588
692
|
return { segment: null, errors };
|
|
589
693
|
}
|
|
590
694
|
const wikilink = parseWikilink(lensSection.source);
|
|
591
|
-
if (!wikilink) {
|
|
695
|
+
if (!wikilink || wikilink.error) {
|
|
696
|
+
const suggestion = wikilink?.correctedPath
|
|
697
|
+
? `Did you mean '[[${wikilink.correctedPath}]]'?`
|
|
698
|
+
: undefined;
|
|
592
699
|
errors.push({
|
|
593
700
|
file: lensPath,
|
|
594
701
|
message: `Invalid wikilink in article source: ${lensSection.source}`,
|
|
702
|
+
suggestion,
|
|
595
703
|
severity: 'error',
|
|
596
704
|
});
|
|
597
705
|
return { segment: null, errors };
|
|
@@ -610,6 +718,18 @@ function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPath
|
|
|
610
718
|
});
|
|
611
719
|
return { segment: null, errors };
|
|
612
720
|
}
|
|
721
|
+
// Check tier violation (Lens → Article)
|
|
722
|
+
if (tierMap) {
|
|
723
|
+
const parentTier = tierMap.get(lensPath) ?? 'production';
|
|
724
|
+
const childTier = tierMap.get(articlePath) ?? 'production';
|
|
725
|
+
const violation = checkTierViolation(lensPath, parentTier, articlePath, childTier, 'article');
|
|
726
|
+
if (violation) {
|
|
727
|
+
errors.push(violation);
|
|
728
|
+
}
|
|
729
|
+
if (childTier === 'ignored') {
|
|
730
|
+
return { segment: null, errors };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
613
733
|
// Check if the article path points back to an already-visited structural file
|
|
614
734
|
// This would indicate a circular reference (e.g., a lens source pointing back to an LO)
|
|
615
735
|
// Note: We only check, we don't add article paths to visitedPaths since
|
|
@@ -649,10 +769,14 @@ function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPath
|
|
|
649
769
|
return { segment: null, errors };
|
|
650
770
|
}
|
|
651
771
|
const wikilink = parseWikilink(lensSection.source);
|
|
652
|
-
if (!wikilink) {
|
|
772
|
+
if (!wikilink || wikilink.error) {
|
|
773
|
+
const suggestion = wikilink?.correctedPath
|
|
774
|
+
? `Did you mean '[[${wikilink.correctedPath}]]'?`
|
|
775
|
+
: undefined;
|
|
653
776
|
errors.push({
|
|
654
777
|
file: lensPath,
|
|
655
778
|
message: `Invalid wikilink in video source: ${lensSection.source}`,
|
|
779
|
+
suggestion,
|
|
656
780
|
severity: 'error',
|
|
657
781
|
});
|
|
658
782
|
return { segment: null, errors };
|
|
@@ -671,6 +795,18 @@ function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPath
|
|
|
671
795
|
});
|
|
672
796
|
return { segment: null, errors };
|
|
673
797
|
}
|
|
798
|
+
// Check tier violation (Lens → Video)
|
|
799
|
+
if (tierMap) {
|
|
800
|
+
const parentTier = tierMap.get(lensPath) ?? 'production';
|
|
801
|
+
const childTier = tierMap.get(videoPath) ?? 'production';
|
|
802
|
+
const violation = checkTierViolation(lensPath, parentTier, videoPath, childTier, 'video transcript');
|
|
803
|
+
if (violation) {
|
|
804
|
+
errors.push(violation);
|
|
805
|
+
}
|
|
806
|
+
if (childTier === 'ignored') {
|
|
807
|
+
return { segment: null, errors };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
674
810
|
// Check if the video path points back to an already-visited structural file
|
|
675
811
|
// This would indicate a circular reference
|
|
676
812
|
// Note: We only check, we don't add video paths to visitedPaths since
|
|
@@ -718,8 +854,155 @@ function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPath
|
|
|
718
854
|
}
|
|
719
855
|
return { segment, errors };
|
|
720
856
|
}
|
|
857
|
+
case 'question': {
|
|
858
|
+
const segment = {
|
|
859
|
+
type: 'question',
|
|
860
|
+
content: parsedSegment.content,
|
|
861
|
+
};
|
|
862
|
+
if (parsedSegment.assessmentInstructions)
|
|
863
|
+
segment.assessmentInstructions = parsedSegment.assessmentInstructions;
|
|
864
|
+
if (parsedSegment.maxTime)
|
|
865
|
+
segment.maxTime = parsedSegment.maxTime;
|
|
866
|
+
if (parsedSegment.maxChars !== undefined)
|
|
867
|
+
segment.maxChars = parsedSegment.maxChars;
|
|
868
|
+
if (parsedSegment.enforceVoice)
|
|
869
|
+
segment.enforceVoice = true;
|
|
870
|
+
if (parsedSegment.optional)
|
|
871
|
+
segment.optional = true;
|
|
872
|
+
if (parsedSegment.feedback)
|
|
873
|
+
segment.feedback = true;
|
|
874
|
+
return { segment, errors };
|
|
875
|
+
}
|
|
721
876
|
default:
|
|
722
877
|
return { segment: null, errors };
|
|
723
878
|
}
|
|
724
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Flatten a single Lens file into a FlattenedModule.
|
|
882
|
+
*
|
|
883
|
+
* This wraps a standalone Lens as a single-section module so it can be
|
|
884
|
+
* rendered by the frontend using the existing Module.tsx component.
|
|
885
|
+
*
|
|
886
|
+
* The resulting module has:
|
|
887
|
+
* - slug: 'lens/' + fileNameToSlug(lensPath)
|
|
888
|
+
* - title: extracted from source metadata or lens header
|
|
889
|
+
* - sections: exactly one section (lens-article, lens-video, or page)
|
|
890
|
+
*
|
|
891
|
+
* @param lensPath - Path to the lens file within the files Map
|
|
892
|
+
* @param files - Map of all file paths to their content
|
|
893
|
+
* @param tierMap - Optional tier map for filtering ignored content
|
|
894
|
+
* @param preParsedLens - Optional pre-parsed lens to avoid re-parsing
|
|
895
|
+
* @returns Flattened module with a single section, plus any errors
|
|
896
|
+
*/
|
|
897
|
+
export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
898
|
+
const errors = [];
|
|
899
|
+
// Skip ignored lenses
|
|
900
|
+
if (tierMap?.get(lensPath) === 'ignored') {
|
|
901
|
+
return { module: null, errors };
|
|
902
|
+
}
|
|
903
|
+
const lensContent = files.get(lensPath);
|
|
904
|
+
if (!lensContent) {
|
|
905
|
+
errors.push({
|
|
906
|
+
file: lensPath,
|
|
907
|
+
message: `Lens file not found: ${lensPath}`,
|
|
908
|
+
severity: 'error',
|
|
909
|
+
});
|
|
910
|
+
return { module: null, errors };
|
|
911
|
+
}
|
|
912
|
+
// Use pre-parsed lens if provided, otherwise parse
|
|
913
|
+
const lens = preParsedLens ?? (() => {
|
|
914
|
+
const lensResult = parseLens(lensContent, lensPath);
|
|
915
|
+
errors.push(...lensResult.errors);
|
|
916
|
+
return lensResult.lens;
|
|
917
|
+
})();
|
|
918
|
+
if (!lens) {
|
|
919
|
+
return { module: null, errors };
|
|
920
|
+
}
|
|
921
|
+
const visitedPaths = new Set([lensPath]);
|
|
922
|
+
// Process lens sections into a single flattened Section
|
|
923
|
+
let sectionType = 'page';
|
|
924
|
+
const meta = {};
|
|
925
|
+
const segments = [];
|
|
926
|
+
let videoId;
|
|
927
|
+
for (const lensSection of lens.sections) {
|
|
928
|
+
if (lensSection.type === 'lens-article') {
|
|
929
|
+
sectionType = 'lens-article';
|
|
930
|
+
// Extract article metadata from the article file's frontmatter
|
|
931
|
+
if (lensSection.source) {
|
|
932
|
+
const articleWikilink = parseWikilink(lensSection.source);
|
|
933
|
+
if (articleWikilink && !articleWikilink.error) {
|
|
934
|
+
const articlePathResolved = resolveWikilinkPath(articleWikilink.path, lensPath);
|
|
935
|
+
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
936
|
+
if (articlePath) {
|
|
937
|
+
const articleContent = files.get(articlePath);
|
|
938
|
+
const articleFrontmatter = parseFrontmatter(articleContent, articlePath);
|
|
939
|
+
if (articleFrontmatter.frontmatter.title)
|
|
940
|
+
meta.title = articleFrontmatter.frontmatter.title;
|
|
941
|
+
if (articleFrontmatter.frontmatter.author)
|
|
942
|
+
meta.author = articleFrontmatter.frontmatter.author;
|
|
943
|
+
if (articleFrontmatter.frontmatter.source_url)
|
|
944
|
+
meta.sourceUrl = articleFrontmatter.frontmatter.source_url;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
else if (lensSection.type === 'page') {
|
|
950
|
+
sectionType = 'page';
|
|
951
|
+
if (lensSection.title)
|
|
952
|
+
meta.title = lensSection.title;
|
|
953
|
+
}
|
|
954
|
+
else if (lensSection.type === 'lens-video') {
|
|
955
|
+
sectionType = 'lens-video';
|
|
956
|
+
// Extract video metadata from the video transcript file's frontmatter
|
|
957
|
+
if (lensSection.source) {
|
|
958
|
+
const videoWikilink = parseWikilink(lensSection.source);
|
|
959
|
+
if (videoWikilink && !videoWikilink.error) {
|
|
960
|
+
const videoPathResolved = resolveWikilinkPath(videoWikilink.path, lensPath);
|
|
961
|
+
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
962
|
+
if (videoPath) {
|
|
963
|
+
const videoContent = files.get(videoPath);
|
|
964
|
+
const videoFrontmatter = parseFrontmatter(videoContent, videoPath);
|
|
965
|
+
if (videoFrontmatter.frontmatter.title)
|
|
966
|
+
meta.title = videoFrontmatter.frontmatter.title;
|
|
967
|
+
if (videoFrontmatter.frontmatter.channel)
|
|
968
|
+
meta.channel = videoFrontmatter.frontmatter.channel;
|
|
969
|
+
if (videoFrontmatter.frontmatter.url) {
|
|
970
|
+
const extractedId = extractVideoIdFromUrl(videoFrontmatter.frontmatter.url);
|
|
971
|
+
if (extractedId)
|
|
972
|
+
videoId = extractedId;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Process segments
|
|
979
|
+
for (const parsedSegment of lensSection.segments) {
|
|
980
|
+
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths, tierMap);
|
|
981
|
+
errors.push(...segmentResult.errors);
|
|
982
|
+
if (segmentResult.segment)
|
|
983
|
+
segments.push(segmentResult.segment);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// Fallback title from filename if not extracted from source metadata
|
|
987
|
+
if (!meta.title) {
|
|
988
|
+
meta.title = fileNameToSlug(lensPath).replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
989
|
+
}
|
|
990
|
+
const section = {
|
|
991
|
+
type: sectionType,
|
|
992
|
+
meta,
|
|
993
|
+
segments,
|
|
994
|
+
optional: false,
|
|
995
|
+
learningOutcomeId: null,
|
|
996
|
+
learningOutcomeName: null,
|
|
997
|
+
contentId: lens.id ?? null,
|
|
998
|
+
videoId: videoId ?? null,
|
|
999
|
+
};
|
|
1000
|
+
const flattenedModule = {
|
|
1001
|
+
slug: 'lens/' + fileNameToSlug(lensPath),
|
|
1002
|
+
title: meta.title ?? fileNameToSlug(lensPath),
|
|
1003
|
+
contentId: lens.id ?? null,
|
|
1004
|
+
sections: [section],
|
|
1005
|
+
};
|
|
1006
|
+
return { module: flattenedModule, errors };
|
|
1007
|
+
}
|
|
725
1008
|
//# sourceMappingURL=index.js.map
|