@@ -18,6 +18,7 @@ import { readUsage, summarizeUsage } from "./usage.js";
1818import { findDeadEntries , findKeywordOverlaps , analyzeBudget } from "./analysis.js" ;
1919import { resolveLoadout , explainEntry } from "./resolve.js" ;
2020import type { ResolveOptions } from "./resolve.js" ;
21+ import { validateIndex } from "./validate.js" ;
2122
2223// ── Colors ────────────────────────────────────────────────────
2324const BOLD = "\x1b[1m" ;
@@ -73,7 +74,11 @@ function loadIndex(path: string): LoadoutIndex {
7374 if ( ! existsSync ( path ) ) {
7475 fail ( "FILE_NOT_FOUND" , `Index not found: ${ path } ` ) ;
7576 }
76- return JSON . parse ( readFileSync ( path , "utf-8" ) ) as LoadoutIndex ;
77+ try {
78+ return JSON . parse ( readFileSync ( path , "utf-8" ) ) as LoadoutIndex ;
79+ } catch ( e ) {
80+ fail ( "PARSE_ERROR" , `Failed to parse index: ${ path } ` , ( e as Error ) . message ) ;
81+ }
7782}
7883
7984function getVersion ( ) : string {
@@ -98,6 +103,7 @@ ${BOLD}Usage:${RESET}
98103 ai-loadout dead <index> <jsonl> Find entries never loaded
99104 ai-loadout overlaps <index> Find keyword routing ambiguities
100105 ai-loadout budget <index> [jsonl] Token budget breakdown
106+ ai-loadout validate <index> Validate index structure
101107
102108${ BOLD } Options:${ RESET }
103109 --json Output as JSON
@@ -115,6 +121,7 @@ ${BOLD}Examples:${RESET}
115121 ai-loadout usage .claude/loadout-usage.jsonl
116122 ai-loadout dead .claude/rules/index.json .claude/loadout-usage.jsonl
117123 ai-loadout overlaps .claude/rules/index.json
124+ ai-loadout validate .claude/rules/index.json
118125 ai-loadout budget .claude/rules/index.json .claude/loadout-usage.jsonl
119126` ) ;
120127}
@@ -266,6 +273,52 @@ function cmdBudget(args: string[]) {
266273 log ( "" ) ;
267274}
268275
276+ function cmdValidate ( args : string [ ] ) {
277+ const positional = positionalArgs ( args ) ;
278+ if ( positional . length < 1 ) {
279+ fail ( "MISSING_ARG" , "Usage: ai-loadout validate <index>" ) ;
280+ }
281+
282+ const index = loadIndex ( resolve ( positional [ 0 ] ) ) ;
283+ const issues = validateIndex ( index ) ;
284+ const json = hasFlag ( args , "json" ) ;
285+
286+ if ( json ) {
287+ log ( JSON . stringify ( {
288+ valid : issues . filter ( ( i ) => i . severity === "error" ) . length === 0 ,
289+ errors : issues . filter ( ( i ) => i . severity === "error" ) . length ,
290+ warnings : issues . filter ( ( i ) => i . severity === "warning" ) . length ,
291+ issues,
292+ } , null , 2 ) ) ;
293+ return ;
294+ }
295+
296+ const errors = issues . filter ( ( i ) => i . severity === "error" ) ;
297+ const warnings = issues . filter ( ( i ) => i . severity === "warning" ) ;
298+
299+ if ( issues . length === 0 ) {
300+ ok ( `Index is valid (${ index . entries . length } entries)` ) ;
301+ return ;
302+ }
303+
304+ log ( `\n${ BOLD } Validation Results${ RESET } \n` ) ;
305+
306+ for ( const issue of errors ) {
307+ log ( ` ${ RED } ✗ [${ issue . code } ]${ RESET } ${ issue . message } ` ) ;
308+ if ( issue . hint ) log ( ` ${ DIM } ${ issue . hint } ${ RESET } ` ) ;
309+ }
310+
311+ for ( const issue of warnings ) {
312+ warn ( `[${ issue . code } ] ${ issue . message } ` ) ;
313+ }
314+
315+ log ( `\n ${ errors . length } error(s), ${ warnings . length } warning(s)` ) ;
316+
317+ if ( errors . length > 0 ) {
318+ process . exit ( 1 ) ;
319+ }
320+ }
321+
269322function cmdResolve ( args : string [ ] ) {
270323 const opts = getResolveOpts ( args ) ;
271324 const json = hasFlag ( args , "json" ) ;
@@ -411,6 +464,9 @@ switch (cmd) {
411464 case "budget" :
412465 cmdBudget ( cmdArgs ) ;
413466 break ;
467+ case "validate" :
468+ cmdValidate ( cmdArgs ) ;
469+ break ;
414470 default :
415471 fail ( "UNKNOWN_COMMAND" , `Unknown command: ${ cmd } ` , "Run ai-loadout --help for usage" ) ;
416472}
0 commit comments