@@ -13,19 +13,8 @@ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
1313import { join , dirname , relative , extname , basename , resolve } from "node:path" ;
1414import { estimateTokens } from "@mcptoolshop/ai-loadout" ;
1515import { parseMemoryMd } from "./parser.js" ;
16- import type { MemoryAnalysis , MemoryRef } from "./types.js" ;
17-
18- /**
19- * Try to resolve a reference path against multiple base directories.
20- * Returns the first path that exists, or null.
21- */
22- function resolveRefPath ( refPath : string , ...baseDirs : string [ ] ) : string | null {
23- for ( const base of baseDirs ) {
24- const full = join ( base , refPath ) ;
25- if ( existsSync ( full ) ) return full ;
26- }
27- return null ;
28- }
16+ import { resolveRefPath } from "./paths.js" ;
17+ import type { MemoryAnalysis , MemoryRef , Diagnostic } from "./types.js" ;
2918
3019/**
3120 * Analyze a MEMORY.md file and its referenced topic files.
@@ -47,6 +36,12 @@ export function analyzeMemoryMd(filePath: string): MemoryAnalysis {
4736
4837 // Check which referenced files exist
4938 // Try: relative to MEMORY.md, then relative to parent dir
39+ //
40+ // FT-MR3: every missing/orphan signal is recorded as a structured Diagnostic
41+ // here, at the point of detection — the flat string[] arrays below are derived
42+ // views kept for back-compat. Library consumers read `diagnostics`; the CLI
43+ // renders it uniformly. Nothing goes to stderr.
44+ const diagnostics : Diagnostic [ ] = [ ] ;
5045 const missingFiles : string [ ] = [ ] ;
5146 let topicTokens = 0 ;
5247
@@ -58,9 +53,25 @@ export function analyzeMemoryMd(filePath: string): MemoryAnalysis {
5853 topicTokens += estimateTokens ( topicContent ) ;
5954 } catch {
6055 missingFiles . push ( ref . path ) ;
56+ diagnostics . push ( {
57+ severity : "error" ,
58+ code : "MISSING_TOPIC_FILE" ,
59+ message : `Referenced topic file not found: ${ ref . path } ` ,
60+ refPath : ref . path ,
61+ line : ref . line ,
62+ hint : "Create the file or remove the reference from MEMORY.md" ,
63+ } ) ;
6164 }
6265 } else {
6366 missingFiles . push ( ref . path ) ;
67+ diagnostics . push ( {
68+ severity : "error" ,
69+ code : "MISSING_TOPIC_FILE" ,
70+ message : `Referenced topic file not found: ${ ref . path } ` ,
71+ refPath : ref . path ,
72+ line : ref . line ,
73+ hint : "Create the file or remove the reference from MEMORY.md" ,
74+ } ) ;
6475 }
6576 }
6677
@@ -85,11 +96,12 @@ export function analyzeMemoryMd(filePath: string): MemoryAnalysis {
8596 if ( stat . isFile ( ) && extname ( entry ) === ".md" && entry !== "MEMORY.md" ) {
8697 if ( ! referencedBasenames . has ( entry ) ) {
8798 orphanFiles . push ( entry ) ;
99+ diagnostics . push ( orphanDiagnostic ( entry ) ) ;
88100 }
89101 }
90102 // Also scan subdirectories
91103 if ( stat . isDirectory ( ) ) {
92- scanDir ( fullPath , fileDir , referencedBasenames , orphanFiles ) ;
104+ scanDir ( fullPath , fileDir , referencedBasenames , orphanFiles , diagnostics ) ;
93105 }
94106 } catch {
95107 // Skip entries we can't stat (permission denied, broken symlinks)
@@ -106,12 +118,24 @@ export function analyzeMemoryMd(filePath: string): MemoryAnalysis {
106118 refs,
107119 orphanFiles,
108120 missingFiles,
121+ diagnostics,
109122 totalTokens : inlineTokens + topicTokens ,
110123 inlineTokens,
111124 topicTokens,
112125 } ;
113126}
114127
128+ /** FT-MR3: build the structured Diagnostic for an orphan topic file. */
129+ function orphanDiagnostic ( path : string ) : Diagnostic {
130+ return {
131+ severity : "warning" ,
132+ code : "ORPHAN_TOPIC_FILE" ,
133+ message : `Topic file not referenced in MEMORY.md: ${ path } ` ,
134+ refPath : path ,
135+ hint : "Add a reference in MEMORY.md or delete the file" ,
136+ } ;
137+ }
138+
115139/**
116140 * Recursively scan a directory for .md files not in the referenced set.
117141 */
@@ -120,6 +144,7 @@ function scanDir(
120144 baseDir : string ,
121145 referenced : Set < string > ,
122146 orphans : string [ ] ,
147+ diagnostics : Diagnostic [ ] ,
123148) : void {
124149 let entries : string [ ] ;
125150 try {
@@ -134,11 +159,12 @@ function scanDir(
134159 const stat = statSync ( fullPath ) ;
135160
136161 if ( stat . isDirectory ( ) ) {
137- scanDir ( fullPath , baseDir , referenced , orphans ) ;
162+ scanDir ( fullPath , baseDir , referenced , orphans , diagnostics ) ;
138163 } else if ( extname ( entry ) === ".md" ) {
139164 const relPath = relative ( baseDir , fullPath ) . replace ( / \\ / g, "/" ) ;
140165 if ( ! referenced . has ( relPath ) && ! referenced . has ( basename ( relPath ) ) ) {
141166 orphans . push ( relPath ) ;
167+ diagnostics . push ( orphanDiagnostic ( relPath ) ) ;
142168 }
143169 }
144170 } catch {
0 commit comments