@@ -102,6 +102,12 @@ describe('Language Detection', () => {
102102 expect ( detectLanguage ( 'stdio.h' , '#ifndef STDIO_H\nvoid printf();\n#endif\n' ) ) . toBe ( 'c' ) ;
103103 } ) ;
104104
105+ it ( 'should detect Markdown files' , ( ) => {
106+ expect ( detectLanguage ( 'README.md' ) ) . toBe ( 'markdown' ) ;
107+ expect ( detectLanguage ( 'docs/guide.markdown' ) ) . toBe ( 'markdown' ) ;
108+ expect ( detectLanguage ( 'docs/page.mdx' ) ) . toBe ( 'markdown' ) ;
109+ } ) ;
110+
105111 it ( 'should detect Metal shader files as C++ (#1121)' , ( ) => {
106112 expect ( detectLanguage ( 'Shaders.metal' ) ) . toBe ( 'cpp' ) ;
107113 expect ( isSourceFile ( 'Renderer/Shaders.metal' ) ) . toBe ( true ) ;
@@ -250,11 +256,218 @@ describe('Language Support', () => {
250256 expect ( languages ) . toContain ( 'swift' ) ;
251257 expect ( languages ) . toContain ( 'kotlin' ) ;
252258 expect ( languages ) . toContain ( 'dart' ) ;
259+ expect ( languages ) . toContain ( 'markdown' ) ;
253260 expect ( languages ) . toContain ( 'solidity' ) ;
254261 expect ( languages ) . toContain ( 'nix' ) ;
255262 } ) ;
256263} ) ;
257264
265+ describe ( 'Markdown Extraction' , ( ) => {
266+ it ( 'should extract headings, links, and shell script references' , ( ) => {
267+ const markdown = `# Project Guide
268+
269+ See [Setup](docs/setup.md#install) and scripts/release.mjs.
270+
271+ ## Release
272+
273+ \`\`\`bash
274+ npm run build
275+ node scripts/release.mjs
276+ \`\`\`
277+ ` ;
278+
279+ const result = extractFromSource ( 'README.md' , markdown ) ;
280+
281+ const fileNode = result . nodes . find ( ( n ) => n . kind === 'file' ) ;
282+ expect ( fileNode ) . toMatchObject ( {
283+ name : 'README.md' ,
284+ language : 'markdown' ,
285+ } ) ;
286+
287+ const headings = result . nodes . filter ( ( n ) => n . kind === 'module' ) ;
288+ expect ( headings . map ( ( n ) => n . name ) ) . toContain ( 'Project Guide' ) ;
289+ expect ( headings . map ( ( n ) => n . name ) ) . toContain ( 'Release' ) ;
290+
291+ const commandNode = result . nodes . find ( ( n ) => n . kind === 'function' && n . signature === 'node scripts/release.mjs' ) ;
292+ expect ( commandNode ) . toBeDefined ( ) ;
293+
294+ expect ( result . unresolvedReferences ) . toEqual (
295+ expect . arrayContaining ( [
296+ expect . objectContaining ( {
297+ referenceName : 'docs/setup.md#install' ,
298+ referenceKind : 'imports' ,
299+ language : 'markdown' ,
300+ } ) ,
301+ expect . objectContaining ( {
302+ referenceName : 'scripts/release.mjs' ,
303+ referenceKind : 'calls' ,
304+ language : 'markdown' ,
305+ } ) ,
306+ ] )
307+ ) ;
308+ } ) ;
309+
310+ it ( 'should extract structured table rows and file-symbol references from Markdown' , ( ) => {
311+ const markdown = `# Maintenance Guide
312+
313+ ## Phase 4
314+
315+ | Template | CLI Entry | Dispatcher | Implementation |
316+ | --- | --- | --- | --- |
317+ | P4-S1 | \`python "{script_path}" p4 "{csv_file}" s1 "{conditions_or_-}" "{probe_cols}"\` | \`scripts/csv_search.py::run_p4\` | \`scripts/csv_search.py::_p4_stage1\` |
318+ | P4-S2 | \`python "{script_path}" p4 "{csv_file}" s2 "{stage1_rows}" "{condition_or_-}" "{detail_cols}"\` | \`scripts/csv_search.py::run_p4\` | \`scripts/csv_search.py::_p4_stage2\` |
319+
320+ - P4-FLOW changes must inspect \`scripts/csv_search.py::run_p4\`.
321+ ` ;
322+
323+ const result = extractFromSource ( 'phases/phase4.md' , markdown ) ;
324+
325+ const tableRows = result . nodes . filter ( ( n ) => n . kind === 'constant' && n . qualifiedName . includes ( 'table-row' ) ) ;
326+ expect ( tableRows . map ( ( n ) => n . name ) ) . toEqual ( expect . arrayContaining ( [ 'P4-S1' , 'P4-S2' ] ) ) ;
327+
328+ const p4s1 = tableRows . find ( ( n ) => n . name === 'P4-S1' ) ;
329+ expect ( p4s1 ?. signature ) . toContain ( 'Template: P4-S1' ) ;
330+ expect ( p4s1 ?. signature ) . toContain ( 'Dispatcher: scripts/csv_search.py::run_p4' ) ;
331+
332+ const commandNode = result . nodes . find ( ( n ) =>
333+ n . kind === 'function' &&
334+ n . language === 'markdown' &&
335+ n . signature ?. includes ( 'python "{script_path}" p4' )
336+ ) ;
337+ expect ( commandNode ) . toBeDefined ( ) ;
338+
339+ expect ( result . unresolvedReferences ) . toEqual (
340+ expect . arrayContaining ( [
341+ expect . objectContaining ( {
342+ referenceName : 'phases/scripts/csv_search.py::run_p4' ,
343+ referenceKind : 'references' ,
344+ language : 'markdown' ,
345+ } ) ,
346+ expect . objectContaining ( {
347+ referenceName : 'phases/scripts/csv_search.py::_p4_stage1' ,
348+ referenceKind : 'references' ,
349+ language : 'markdown' ,
350+ } ) ,
351+ ] )
352+ ) ;
353+ } ) ;
354+
355+ it ( 'should keep structured blocks after fences containing a different fence marker' , ( ) => {
356+ const markdown = `# Runbook
357+
358+ \`\`\`text
359+ ~~~~
360+ \`\`\`
361+
362+ - POST-FENCE references \`src/auth.ts::login\`.
363+
364+ | Key | Target |
365+ | --- | --- |
366+ | POST-TABLE | \`src/auth.ts::login\` |
367+ ` ;
368+
369+ const result = extractFromSource ( 'docs/runbook.md' , markdown ) ;
370+ const constants = result . nodes . filter ( ( n ) => n . kind === 'constant' ) ;
371+
372+ expect ( constants ) . toEqual ( expect . arrayContaining ( [
373+ expect . objectContaining ( { docstring : 'POST-FENCE references src/auth.ts::login.' } ) ,
374+ expect . objectContaining ( { name : 'POST-TABLE' } ) ,
375+ ] ) ) ;
376+ } ) ;
377+
378+ it ( 'indexes Setext (underline) headings and skips frontmatter / code fences' , ( ) => {
379+ const markdown = `---
380+ title: Config Doc
381+ ---
382+
383+ Architecture Overview
384+ =====================
385+
386+ Intro paragraph for the overview.
387+
388+ Routing Layer
389+ -------------
390+
391+ \`\`\`md
392+ Not A Heading
393+ =============
394+ \`\`\`
395+ ` ;
396+
397+ const result = extractFromSource ( 'docs/arch.md' , markdown ) ;
398+ const headings = result . nodes . filter ( ( n ) => n . kind === 'module' ) ;
399+ const byName = new Map ( headings . map ( ( h ) => [ h . name , h ] ) ) ;
400+
401+ // Setext H1 (===) and H2 (---) become module nodes.
402+ expect ( byName . get ( 'Architecture Overview' ) ?. signature ) . toBe ( '# Architecture Overview' ) ;
403+ expect ( byName . get ( 'Routing Layer' ) ?. signature ) . toBe ( '## Routing Layer' ) ;
404+ // Frontmatter `title:` (above the closing `---`) is NOT a heading, and a
405+ // setext-looking line inside a code fence is ignored.
406+ expect ( byName . has ( 'title: Config Doc' ) ) . toBe ( false ) ;
407+ expect ( byName . has ( 'Not A Heading' ) ) . toBe ( false ) ;
408+ } ) ;
409+
410+ it ( 'builds a deterministic, compact file digest (intro + key references)' , ( ) => {
411+ const markdown = `# Release Runbook
412+
413+ This runbook explains how to cut a release.
414+
415+ See [setup](docs/setup.md#install) and run \`scripts/release.mjs\`.
416+ It dispatches \`scripts/csv_search.py::run_p4\`.
417+ ` ;
418+
419+ const result = extractFromSource ( 'RUNBOOK.md' , markdown ) ;
420+ const fileNode = result . nodes . find ( ( n ) => n . kind === 'file' ) ;
421+
422+ expect ( fileNode ?. docstring ) . toBeDefined ( ) ;
423+ const digest = fileNode ! . docstring ! ;
424+ // Intro is the first prose line, not the heading or a link blob.
425+ expect ( digest ) . toContain ( 'This runbook explains how to cut a release.' ) ;
426+ // Key referenced files/symbols are surfaced, compacted to basenames.
427+ expect ( digest ) . toContain ( 'refs:' ) ;
428+ expect ( digest ) . toContain ( 'setup.md#install' ) ;
429+ expect ( digest ) . toContain ( 'release.mjs' ) ;
430+ expect ( digest ) . toContain ( 'csv_search.py::run_p4' ) ;
431+ // Short enough to show in node details (the < 200 char detail gate).
432+ expect ( digest . length ) . toBeLessThan ( 200 ) ;
433+ } ) ;
434+ } ) ;
435+
436+ describe ( 'Code to Markdown Reference Extraction' , ( ) => {
437+ it ( 'should extract Markdown path references from code string literals' , ( ) => {
438+ const code = `
439+ export const GUIDE = '../docs/guide.md';
440+
441+ export function loadDocs() {
442+ return fs.readFileSync('../docs/guide.md#install', 'utf8');
443+ }
444+ ` ;
445+
446+ const result = extractFromSource ( 'src/load-docs.ts' , code ) ;
447+ const loadDocs = result . nodes . find ( ( n ) => n . kind === 'function' && n . name === 'loadDocs' ) ;
448+ const guideConstant = result . nodes . find ( ( n ) => n . kind === 'constant' && n . name === 'GUIDE' ) ;
449+
450+ expect ( loadDocs ) . toBeDefined ( ) ;
451+ expect ( guideConstant ) . toBeDefined ( ) ;
452+ expect ( result . unresolvedReferences ) . toEqual (
453+ expect . arrayContaining ( [
454+ expect . objectContaining ( {
455+ fromNodeId : loadDocs ! . id ,
456+ referenceName : 'docs/guide.md#install' ,
457+ referenceKind : 'references' ,
458+ language : 'typescript' ,
459+ } ) ,
460+ expect . objectContaining ( {
461+ fromNodeId : guideConstant ! . id ,
462+ referenceName : 'docs/guide.md' ,
463+ referenceKind : 'references' ,
464+ language : 'typescript' ,
465+ } ) ,
466+ ] )
467+ ) ;
468+ } ) ;
469+ } ) ;
470+
258471describe ( 'Nix Extraction' , ( ) => {
259472 it ( 'should distinguish Nix variable and function bindings' , ( ) => {
260473 const code = `
0 commit comments