lens-content-processor 0.2.2 → 0.4.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.
Files changed (63) hide show
  1. package/dist/bundler/article.d.ts +7 -2
  2. package/dist/bundler/article.js +23 -24
  3. package/dist/bundler/article.js.map +1 -1
  4. package/dist/cli.d.ts +1 -2
  5. package/dist/cli.js +6 -10
  6. package/dist/cli.js.map +1 -1
  7. package/dist/content-schema.js +8 -6
  8. package/dist/content-schema.js.map +1 -1
  9. package/dist/flattener/index.d.ts +41 -2
  10. package/dist/flattener/index.js +699 -48
  11. package/dist/flattener/index.js.map +1 -1
  12. package/dist/flattener/test-helpers.d.ts +23 -0
  13. package/dist/flattener/test-helpers.js +58 -0
  14. package/dist/flattener/test-helpers.js.map +1 -0
  15. package/dist/fs/read-vault.d.ts +1 -4
  16. package/dist/fs/read-vault.js +4 -8
  17. package/dist/fs/read-vault.js.map +1 -1
  18. package/dist/index.d.ts +34 -6
  19. package/dist/index.js +170 -33
  20. package/dist/index.js.map +1 -1
  21. package/dist/parser/article.d.ts +1 -1
  22. package/dist/parser/article.js +14 -4
  23. package/dist/parser/article.js.map +1 -1
  24. package/dist/parser/course.d.ts +2 -2
  25. package/dist/parser/course.js +16 -23
  26. package/dist/parser/course.js.map +1 -1
  27. package/dist/parser/learning-outcome.d.ts +11 -2
  28. package/dist/parser/learning-outcome.js +135 -62
  29. package/dist/parser/learning-outcome.js.map +1 -1
  30. package/dist/parser/lens.d.ts +63 -1
  31. package/dist/parser/lens.js +138 -11
  32. package/dist/parser/lens.js.map +1 -1
  33. package/dist/parser/module.d.ts +1 -1
  34. package/dist/parser/module.js +14 -9
  35. package/dist/parser/module.js.map +1 -1
  36. package/dist/parser/sections.d.ts +2 -0
  37. package/dist/parser/sections.js +57 -21
  38. package/dist/parser/sections.js.map +1 -1
  39. package/dist/parser/video-transcript.js +3 -0
  40. package/dist/parser/video-transcript.js.map +1 -1
  41. package/dist/parser/wikilink.js +6 -1
  42. package/dist/parser/wikilink.js.map +1 -1
  43. package/dist/utils/slug.d.ts +5 -0
  44. package/dist/utils/slug.js +16 -0
  45. package/dist/utils/slug.js.map +1 -0
  46. package/dist/validator/chat-precedence.d.ts +9 -0
  47. package/dist/validator/chat-precedence.js +32 -0
  48. package/dist/validator/chat-precedence.js.map +1 -0
  49. package/dist/validator/directives.d.ts +12 -0
  50. package/dist/validator/directives.js +428 -0
  51. package/dist/validator/directives.js.map +1 -0
  52. package/dist/validator/field-typos.js +0 -9
  53. package/dist/validator/field-typos.js.map +1 -1
  54. package/dist/validator/field-values.js +14 -0
  55. package/dist/validator/field-values.js.map +1 -1
  56. package/dist/validator/tier.d.ts +18 -0
  57. package/dist/validator/tier.js +74 -0
  58. package/dist/validator/tier.js.map +1 -0
  59. package/dist/validator/url-reachability.js +3 -1
  60. package/dist/validator/url-reachability.js.map +1 -1
  61. package/dist/validator/uuid.js +9 -6
  62. package/dist/validator/uuid.js.map +1 -1
  63. package/package.json +5 -5
@@ -3,8 +3,11 @@ import { parseSections, LO_SECTION_TYPES } from './sections.js';
3
3
  import { parseWikilink, resolveWikilinkPath, hasRelativePath } from './wikilink.js';
4
4
  import { detectFieldTypos } from '../validator/field-typos.js';
5
5
  import { validateFrontmatter } from '../validator/validate-frontmatter.js';
6
+ import { parseSegments, convertSegment, stripAuthoringMarkup } from './lens.js';
6
7
  export function parseLearningOutcome(content, file) {
7
8
  const errors = [];
9
+ // Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
10
+ content = stripAuthoringMarkup(content);
8
11
  // Step 1: Parse frontmatter and validate id field
9
12
  const frontmatterResult = parseFrontmatter(content, file);
10
13
  if (frontmatterResult.error) {
@@ -28,8 +31,9 @@ export function parseLearningOutcome(content, file) {
28
31
  });
29
32
  return { learningOutcome: null, errors };
30
33
  }
31
- // Step 2: Parse sections with H2 level and LO_SECTION_TYPES ('lens', 'test')
32
- const sectionsResult = parseSections(body, 2, LO_SECTION_TYPES, file);
34
+ // Step 2: Parse sections with H2 level and LO_SECTION_TYPES + 'submodule'
35
+ const loSectionTypes = new Set([...LO_SECTION_TYPES, 'submodule']);
36
+ const sectionsResult = parseSections(body, 2, loSectionTypes, file);
33
37
  // Adjust line numbers to account for frontmatter
34
38
  for (const error of sectionsResult.errors) {
35
39
  if (error.line) {
@@ -40,85 +44,153 @@ export function parseLearningOutcome(content, file) {
40
44
  for (const section of sectionsResult.sections) {
41
45
  section.line += bodyStartLine - 1;
42
46
  }
43
- // Step 3: Extract lens refs with source field and optional flag
44
- const lenses = [];
45
- let testRef;
46
- for (const section of sectionsResult.sections) {
47
- // Detect likely typos in field names
47
+ // Helper: extract a lens ref from a parsed section
48
+ function extractLensRef(section) {
48
49
  const typoWarnings = detectFieldTypos(section.fields, file, section.line);
49
50
  errors.push(...typoWarnings);
50
- if (section.type === 'lens') {
51
- const source = section.fields.source;
52
- if (!source) {
53
- errors.push({
54
- file,
55
- line: section.line,
56
- message: 'Lens section missing source:: field',
57
- suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' to the lens section",
58
- severity: 'error',
59
- });
60
- continue;
61
- }
62
- // Parse wikilink and resolve path
51
+ const source = section.fields.source;
52
+ if (!source) {
53
+ errors.push({
54
+ file,
55
+ line: section.line,
56
+ message: 'Lens section missing source:: field',
57
+ suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' to the lens section",
58
+ severity: 'error',
59
+ });
60
+ return null;
61
+ }
62
+ const wikilink = parseWikilink(source);
63
+ if (!wikilink || wikilink.error) {
64
+ const suggestion = wikilink?.correctedPath
65
+ ? `Did you mean '[[${wikilink.correctedPath}]]'?`
66
+ : 'Use format [[../Lenses/filename.md|Display Text]]';
67
+ errors.push({
68
+ file,
69
+ line: section.line,
70
+ message: wikilink?.error
71
+ ? `${wikilink.error} in source:: field: ${source}`
72
+ : `Invalid wikilink format in source:: field: ${source}`,
73
+ suggestion,
74
+ severity: 'error',
75
+ });
76
+ return null;
77
+ }
78
+ if (!hasRelativePath(wikilink.path)) {
79
+ errors.push({
80
+ file,
81
+ line: section.line,
82
+ message: `source:: path must be relative (contain /): ${wikilink.path}`,
83
+ suggestion: 'Use format [[../Lenses/filename.md|Display Text]] with relative path',
84
+ severity: 'error',
85
+ });
86
+ return null;
87
+ }
88
+ const resolvedPath = resolveWikilinkPath(wikilink.path, file);
89
+ const optional = section.fields.optional?.toLowerCase() === 'true';
90
+ return { source, resolvedPath, optional };
91
+ }
92
+ // Helper: extract test ref from a parsed section
93
+ function extractTestRef(section) {
94
+ const source = section.fields.source;
95
+ let resolvedPath;
96
+ if (source) {
63
97
  const wikilink = parseWikilink(source);
64
98
  if (!wikilink || wikilink.error) {
65
99
  const suggestion = wikilink?.correctedPath
66
100
  ? `Did you mean '[[${wikilink.correctedPath}]]'?`
67
- : 'Use format [[../Lenses/filename.md|Display Text]]';
101
+ : 'Use format [[../Tests/filename.md|Display Text]]';
68
102
  errors.push({
69
103
  file,
70
104
  line: section.line,
71
- message: `Invalid wikilink format in source:: field: ${source}`,
105
+ message: wikilink?.error
106
+ ? `${wikilink.error} in source:: field: ${source}`
107
+ : `Invalid wikilink format in source:: field: ${source}`,
72
108
  suggestion,
73
109
  severity: 'error',
74
110
  });
75
- continue;
111
+ return undefined;
76
112
  }
77
- // Require relative path (must contain /)
78
- if (!hasRelativePath(wikilink.path)) {
79
- errors.push({
80
- file,
81
- line: section.line,
82
- message: `source:: path must be relative (contain /): ${wikilink.path}`,
83
- suggestion: 'Use format [[../Lenses/filename.md|Display Text]] with relative path',
84
- severity: 'error',
85
- });
86
- continue;
113
+ resolvedPath = resolveWikilinkPath(wikilink.path, file);
114
+ }
115
+ const { segments: rawSegments, errors: segmentErrors } = parseSegments(section.body, section.line + 1, file);
116
+ for (const err of segmentErrors) {
117
+ if (err.line) {
118
+ err.line += bodyStartLine - 1;
119
+ }
120
+ }
121
+ errors.push(...segmentErrors);
122
+ const testSegments = [];
123
+ for (const rawSeg of rawSegments) {
124
+ const { segment, errors: conversionErrors } = convertSegment(rawSeg, 'page', file);
125
+ for (const err of conversionErrors) {
126
+ if (err.line) {
127
+ err.line += bodyStartLine - 1;
128
+ }
129
+ }
130
+ errors.push(...conversionErrors);
131
+ if (segment) {
132
+ testSegments.push(segment);
87
133
  }
88
- const resolvedPath = resolveWikilinkPath(wikilink.path, file);
89
- const optional = section.fields.optional?.toLowerCase() === 'true';
90
- lenses.push({
91
- source,
92
- resolvedPath,
93
- optional,
134
+ }
135
+ return { source, resolvedPath, segments: testSegments };
136
+ }
137
+ // Step 3: Check for submodule sections
138
+ const hasSubmodules = sectionsResult.sections.some(s => s.type === 'submodule');
139
+ const lenses = [];
140
+ let testRef;
141
+ let submodules;
142
+ if (hasSubmodules) {
143
+ // All-or-nothing: reject top-level lens/test sections when submodules exist
144
+ const orphanedSections = sectionsResult.sections.filter(s => s.type !== 'submodule');
145
+ if (orphanedSections.length > 0) {
146
+ errors.push({
147
+ file,
148
+ line: orphanedSections[0].line,
149
+ message: 'Content found outside submodule boundaries — when using Submodule markers, all content must be inside a submodule',
150
+ suggestion: 'Move this content into a Submodule section',
151
+ severity: 'error',
94
152
  });
95
153
  }
96
- else if (section.type === 'test') {
97
- const source = section.fields.source;
98
- // source:: is optional for test sections (tests not fully implemented yet)
99
- if (!source) {
154
+ submodules = [];
155
+ for (const section of sectionsResult.sections) {
156
+ if (section.type !== 'submodule')
100
157
  continue;
158
+ const group = {
159
+ title: section.title,
160
+ customSlug: section.fields.slug,
161
+ lenses: [],
162
+ };
163
+ // Process children (lens/test sections at H3)
164
+ if (section.children) {
165
+ for (const child of section.children) {
166
+ if (child.type === 'lens') {
167
+ const ref = extractLensRef(child);
168
+ if (ref) {
169
+ group.lenses.push(ref);
170
+ lenses.push(ref); // Also add to flat list for validation
171
+ }
172
+ }
173
+ else if (child.type === 'test') {
174
+ group.test = extractTestRef(child);
175
+ }
176
+ }
101
177
  }
102
- // Parse wikilink and resolve path
103
- const wikilink = parseWikilink(source);
104
- if (!wikilink || wikilink.error) {
105
- const suggestion = wikilink?.correctedPath
106
- ? `Did you mean '[[${wikilink.correctedPath}]]'?`
107
- : 'Use format [[../Tests/filename.md|Display Text]]';
108
- errors.push({
109
- file,
110
- line: section.line,
111
- message: `Invalid wikilink format in source:: field: ${source}`,
112
- suggestion,
113
- severity: 'error',
114
- });
115
- continue;
178
+ submodules.push(group);
179
+ }
180
+ }
181
+ else {
182
+ // Normal path: extract lens refs from top-level sections
183
+ for (const section of sectionsResult.sections) {
184
+ const typoWarnings = detectFieldTypos(section.fields, file, section.line);
185
+ errors.push(...typoWarnings);
186
+ if (section.type === 'lens') {
187
+ const ref = extractLensRef(section);
188
+ if (ref)
189
+ lenses.push(ref);
190
+ }
191
+ else if (section.type === 'test') {
192
+ testRef = extractTestRef(section);
116
193
  }
117
- const resolvedPath = resolveWikilinkPath(wikilink.path, file);
118
- testRef = {
119
- source,
120
- resolvedPath,
121
- };
122
194
  }
123
195
  }
124
196
  // Step 4: Validate at least one lens exists
@@ -137,6 +209,7 @@ export function parseLearningOutcome(content, file) {
137
209
  lenses,
138
210
  test: testRef,
139
211
  discussion: frontmatter.discussion,
212
+ submodules,
140
213
  };
141
214
  return { learningOutcome, errors };
142
215
  }
@@ -1 +1 @@
1
- {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"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"}
@@ -24,7 +24,27 @@ export interface ParsedVideoExcerptSegment {
24
24
  toTimeStr: string;
25
25
  optional?: boolean;
26
26
  }
27
- export type ParsedLensSegment = ParsedTextSegment | ParsedChatSegment | ParsedArticleExcerptSegment | ParsedVideoExcerptSegment;
27
+ export interface ParsedQuestionSegment {
28
+ type: 'question';
29
+ content: string;
30
+ assessmentInstructions?: string;
31
+ maxTime?: string;
32
+ maxChars?: number;
33
+ enforceVoice?: boolean;
34
+ optional?: boolean;
35
+ feedback?: boolean;
36
+ }
37
+ export interface ParsedRoleplaySegment {
38
+ type: 'roleplay';
39
+ id: string;
40
+ content: string;
41
+ aiInstructions: string;
42
+ openingMessage?: string;
43
+ assessmentInstructions?: string;
44
+ optional?: boolean;
45
+ feedback?: boolean;
46
+ }
47
+ export type ParsedLensSegment = ParsedTextSegment | ParsedChatSegment | ParsedArticleExcerptSegment | ParsedVideoExcerptSegment | ParsedQuestionSegment | ParsedRoleplaySegment;
28
48
  export interface ParsedLensSection {
29
49
  type: string;
30
50
  title: string;
@@ -35,12 +55,53 @@ export interface ParsedLensSection {
35
55
  }
36
56
  export interface ParsedLens {
37
57
  id: string;
58
+ tldr?: string;
38
59
  sections: ParsedLensSection[];
39
60
  }
40
61
  export interface LensParseResult {
41
62
  lens: ParsedLens | null;
42
63
  errors: ContentError[];
43
64
  }
65
+ interface RawSegment {
66
+ type: string;
67
+ title?: string;
68
+ fields: Record<string, string>;
69
+ line: number;
70
+ }
71
+ /**
72
+ * Parse H4 segments from a section body.
73
+ * Segments are defined by `#### <type>` headers within a section.
74
+ */
75
+ export declare function parseSegments(sectionBody: string, bodyStartLine: number, file: string): {
76
+ segments: RawSegment[];
77
+ errors: ContentError[];
78
+ };
79
+ /**
80
+ * Convert a raw segment to a typed ParsedLensSegment.
81
+ */
82
+ export declare function convertSegment(raw: RawSegment, sectionType: string, file: string): {
83
+ segment: ParsedLensSegment | null;
84
+ errors: ContentError[];
85
+ };
86
+ /**
87
+ * Strip Obsidian %% comments %% from content.
88
+ * Handles both inline (%% ... %% on same line) and block (multiline) comments.
89
+ */
90
+ export declare function stripObsidianComments(content: string): string;
91
+ /**
92
+ * Strip CriticMarkup from content using reject-all-changes behavior:
93
+ * - {>>comments<<} → removed
94
+ * - {++additions++} → removed
95
+ * - {--deletions--} → inner content kept (original preserved)
96
+ * - {~~old~>new~~} → old text kept
97
+ * - {==highlights==} → inner content kept, markers removed
98
+ */
99
+ export declare function stripCriticMarkup(content: string): string;
100
+ /**
101
+ * Strip all authoring markup (CriticMarkup + Obsidian comments) from content.
102
+ * Call at the top of each parser before any processing.
103
+ */
104
+ export declare function stripAuthoringMarkup(content: string): string;
44
105
  /**
45
106
  * Parse a lens file into structured lens data.
46
107
  *
@@ -49,3 +110,4 @@ export interface LensParseResult {
49
110
  * - H4 (`####`) for segments: Text, Chat, Article-excerpt, Video-excerpt
50
111
  */
51
112
  export declare function parseLens(content: string, file: string): LensParseResult;
113
+ export {};
@@ -1,28 +1,31 @@
1
+ import { ALL_KNOWN_FIELDS } from '../content-schema.js';
1
2
  import { parseFrontmatter } from './frontmatter.js';
2
3
  import { parseSections, LENS_SECTION_TYPES, LENS_OUTPUT_TYPE } from './sections.js';
3
4
  import { validateSegmentFields } from '../validator/segment-fields.js';
4
5
  import { validateFieldValues } from '../validator/field-values.js';
5
6
  import { detectFieldTypos } from '../validator/field-typos.js';
6
7
  import { validateFrontmatter } from '../validator/validate-frontmatter.js';
8
+ import { validateChatPrecedence } from '../validator/chat-precedence.js';
9
+ import { detectDirectivesInNonArticle } from '../validator/directives.js';
7
10
  import { parseWikilink, hasRelativePath } from './wikilink.js';
8
11
  import { parseTimestamp } from '../bundler/video.js';
9
12
  // Valid segment types for lens H4 headers
10
- const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt']);
13
+ const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt', 'question', 'roleplay']);
11
14
  // Valid segment types per section output type
12
15
  const VALID_SEGMENTS_PER_SECTION = {
13
- 'page': new Set(['text', 'chat']),
14
- 'lens-article': new Set(['text', 'chat', 'article-excerpt']),
15
- 'lens-video': new Set(['text', 'chat', 'video-excerpt']),
16
+ 'page': new Set(['text', 'chat', 'question', 'roleplay']),
17
+ 'lens-article': new Set(['text', 'chat', 'article-excerpt', 'question', 'roleplay']),
18
+ 'lens-video': new Set(['text', 'chat', 'video-excerpt', 'question', 'roleplay']),
16
19
  };
17
20
  // H4 segment header pattern: #### <type> or #### <type>: <title>
18
- const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(.*))?$/i;
21
+ const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(.*?))?\s*$/i;
19
22
  // Field pattern: fieldname:: value
20
- const FIELD_PATTERN = /^(\w+)::\s*(.*)$/;
23
+ const FIELD_PATTERN = /^([\w-]+)::\s*(.*)$/;
21
24
  /**
22
25
  * Parse H4 segments from a section body.
23
26
  * Segments are defined by `#### <type>` headers within a section.
24
27
  */
25
- function parseSegments(sectionBody, bodyStartLine, file) {
28
+ export function parseSegments(sectionBody, bodyStartLine, file) {
26
29
  const lines = sectionBody.split('\n');
27
30
  const segments = [];
28
31
  const errors = [];
@@ -61,14 +64,14 @@ function parseSegments(sectionBody, bodyStartLine, file) {
61
64
  }
62
65
  else if (currentSegment) {
63
66
  // Check for single-colon field that should be double-colon
64
- const singleColonMatch = line.match(/^(\w+):\s+(.*)$/);
65
- if (singleColonMatch && !line.match(/^https?:/) && !FIELD_PATTERN.test(line)) {
67
+ const singleColonMatch = line.match(/^([\w-]+):\s+(.*)$/);
68
+ if (singleColonMatch && !line.match(/^https?:/) && !FIELD_PATTERN.test(line) && ALL_KNOWN_FIELDS.includes(singleColonMatch[1])) {
66
69
  errors.push({
67
70
  file,
68
71
  line: lineNum,
69
72
  message: `Found '${singleColonMatch[1]}:' with single colon — did you mean '${singleColonMatch[1]}::'?`,
70
73
  suggestion: `Change '${singleColonMatch[1]}:' to '${singleColonMatch[1]}::' (double colon)`,
71
- severity: 'warning',
74
+ severity: 'error',
72
75
  });
73
76
  }
74
77
  currentFieldLines.push(line);
@@ -125,7 +128,7 @@ function parseFieldsIntoSegment(segment, lines) {
125
128
  /**
126
129
  * Convert a raw segment to a typed ParsedLensSegment.
127
130
  */
128
- function convertSegment(raw, sectionType, file) {
131
+ export function convertSegment(raw, sectionType, file) {
129
132
  const errors = [];
130
133
  switch (raw.type) {
131
134
  case 'text': {
@@ -159,6 +162,9 @@ function convertSegment(raw, sectionType, file) {
159
162
  };
160
163
  return { segment, errors };
161
164
  }
165
+ // Warn if directives are used in lens text segments (they only render in articles)
166
+ const directiveWarnings = detectDirectivesInNonArticle(content, file, raw.line);
167
+ errors.push(...directiveWarnings);
162
168
  const segment = {
163
169
  type: 'text',
164
170
  content,
@@ -261,6 +267,76 @@ function convertSegment(raw, sectionType, file) {
261
267
  };
262
268
  return { segment, errors };
263
269
  }
270
+ case 'question': {
271
+ const content = raw.fields['content'];
272
+ if (!content || content.trim() === '') {
273
+ errors.push({
274
+ file,
275
+ line: raw.line,
276
+ message: 'Question segment missing content:: field',
277
+ suggestion: "Add 'content:: Your question here'",
278
+ severity: 'error',
279
+ });
280
+ return { segment: null, errors };
281
+ }
282
+ const segment = {
283
+ type: 'question',
284
+ content,
285
+ assessmentInstructions: raw.fields['assessment-instructions'] || undefined,
286
+ maxTime: raw.fields['max-time'] || undefined,
287
+ maxChars: raw.fields['max-chars'] ? parseInt(raw.fields['max-chars'], 10) : undefined,
288
+ enforceVoice: raw.fields['enforce-voice']?.toLowerCase() === 'true' ? true : undefined,
289
+ optional: raw.fields.optional?.toLowerCase() === 'true' ? true : undefined,
290
+ feedback: raw.fields['feedback']?.toLowerCase() === 'true' ? true : undefined,
291
+ };
292
+ return { segment, errors };
293
+ }
294
+ case 'roleplay': {
295
+ const id = raw.fields['id'];
296
+ if (!id || id.trim() === '') {
297
+ errors.push({
298
+ file,
299
+ line: raw.line,
300
+ message: 'Roleplay segment missing id:: field',
301
+ suggestion: "Add 'id:: <uuid>' to the roleplay segment",
302
+ severity: 'error',
303
+ });
304
+ }
305
+ const content = raw.fields['content'];
306
+ if (!content || content.trim() === '') {
307
+ errors.push({
308
+ file,
309
+ line: raw.line,
310
+ message: 'Roleplay segment missing content:: field',
311
+ suggestion: "Add 'content:: Your scenario briefing here'",
312
+ severity: 'error',
313
+ });
314
+ }
315
+ const aiInstructions = raw.fields['ai-instructions'];
316
+ if (!aiInstructions || aiInstructions.trim() === '') {
317
+ errors.push({
318
+ file,
319
+ line: raw.line,
320
+ message: 'Roleplay segment missing ai-instructions:: field',
321
+ suggestion: "Add 'ai-instructions:: Character behavior description'",
322
+ severity: 'error',
323
+ });
324
+ }
325
+ if (!id || id.trim() === '' || !content || content.trim() === '' || !aiInstructions || aiInstructions.trim() === '') {
326
+ return { segment: null, errors };
327
+ }
328
+ const segment = {
329
+ type: 'roleplay',
330
+ id,
331
+ content,
332
+ aiInstructions,
333
+ openingMessage: raw.fields['opening-message'] || undefined,
334
+ assessmentInstructions: raw.fields['assessment-instructions'] || undefined,
335
+ optional: raw.fields.optional?.toLowerCase() === 'true' ? true : undefined,
336
+ feedback: raw.fields['feedback']?.toLowerCase() === 'true' ? true : undefined,
337
+ };
338
+ return { segment, errors };
339
+ }
264
340
  default:
265
341
  // Unknown segment type - error already reported during parseSegments
266
342
  return { segment: null, errors };
@@ -299,6 +375,38 @@ function checkEmptySegment(raw, file) {
299
375
  }
300
376
  return null;
301
377
  }
378
+ /**
379
+ * Strip Obsidian %% comments %% from content.
380
+ * Handles both inline (%% ... %% on same line) and block (multiline) comments.
381
+ */
382
+ export function stripObsidianComments(content) {
383
+ return content.replace(/%%.*?%%/gs, '');
384
+ }
385
+ /**
386
+ * Strip CriticMarkup from content using reject-all-changes behavior:
387
+ * - {>>comments<<} → removed
388
+ * - {++additions++} → removed
389
+ * - {--deletions--} → inner content kept (original preserved)
390
+ * - {~~old~>new~~} → old text kept
391
+ * - {==highlights==} → inner content kept, markers removed
392
+ */
393
+ export function stripCriticMarkup(content) {
394
+ return content
395
+ .replace(/\{>>.*?<<\}/gs, '') // Comments → remove
396
+ .replace(/\{\+\+.*?\+\+\}/gs, '') // Additions → remove
397
+ .replace(/\{--(?:\{[^}]*\}@@)?(.*?)--\}/gs, '$1') // Deletions → keep inner (skip metadata)
398
+ .replace(/\{~~(?:\{[^}]*\}@@)?(.*?)~>.*?~~\}/gs, '$1') // Substitutions → keep old (skip metadata)
399
+ .replace(/\{==(?:\{[^}]*\}@@)?(.*?)==\}/gs, '$1'); // Highlights → keep inner (skip metadata)
400
+ }
401
+ /**
402
+ * Strip all authoring markup (CriticMarkup + Obsidian comments) from content.
403
+ * Call at the top of each parser before any processing.
404
+ */
405
+ export function stripAuthoringMarkup(content) {
406
+ const stripped = stripObsidianComments(stripCriticMarkup(content));
407
+ // Trim trailing whitespace left by inline markup removal
408
+ return stripped.split('\n').map(line => line.trimEnd()).join('\n');
409
+ }
302
410
  /**
303
411
  * Parse a lens file into structured lens data.
304
412
  *
@@ -308,6 +416,8 @@ function checkEmptySegment(raw, file) {
308
416
  */
309
417
  export function parseLens(content, file) {
310
418
  const errors = [];
419
+ // Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
420
+ content = stripAuthoringMarkup(content);
311
421
  // Step 1: Parse frontmatter and validate id field
312
422
  const frontmatterResult = parseFrontmatter(content, file);
313
423
  if (frontmatterResult.error) {
@@ -331,6 +441,19 @@ export function parseLens(content, file) {
331
441
  });
332
442
  return { lens: null, errors };
333
443
  }
444
+ const tldr = typeof frontmatter.tldr === 'string' ? frontmatter.tldr : undefined;
445
+ if (tldr) {
446
+ const wordCount = tldr.trim().split(/\s+/).filter(Boolean).length;
447
+ if (wordCount > 80) {
448
+ errors.push({
449
+ file,
450
+ line: 2,
451
+ message: `tldr exceeds 80 words (has ${wordCount})`,
452
+ suggestion: 'Shorten the tldr to 80 words or fewer',
453
+ severity: 'error',
454
+ });
455
+ }
456
+ }
334
457
  // Step 2: Parse H3 sections (Text, Article, Video)
335
458
  const sectionsResult = parseSections(body, 3, LENS_SECTION_TYPES, file);
336
459
  // Adjust line numbers to account for frontmatter
@@ -423,6 +546,9 @@ export function parseLens(content, file) {
423
546
  segments.push(segment);
424
547
  }
425
548
  }
549
+ // Validate Chat segments are preceded by Text segments
550
+ const chatErrors = validateChatPrecedence(rawSegments, file);
551
+ errors.push(...chatErrors);
426
552
  // Warn if section has no segments
427
553
  if (segments.length === 0) {
428
554
  errors.push({
@@ -456,6 +582,7 @@ export function parseLens(content, file) {
456
582
  }
457
583
  const lens = {
458
584
  id: frontmatter.id,
585
+ tldr,
459
586
  sections: parsedSections,
460
587
  };
461
588
  return { lens, errors };