@@ -21,7 +21,7 @@ import { parseCodebase } from "./parser/index.js";
2121import { buildGraph } from "./graph/index.js" ;
2222import { analyzeGraph } from "./analyzer/index.js" ;
2323import { startMcpServer } from "./mcp/index.js" ;
24- import { setIndexedHead } from "./server/graph-store.js" ;
24+ import { setIndexedHead , setRoot } from "./server/graph-store.js" ;
2525import { exportGraph , importGraph } from "./persistence/index.js" ;
2626import {
2727 computeOverview ,
@@ -47,7 +47,10 @@ import {
4747 ALL_AGENT_IDS ,
4848} from "./install/index.js" ;
4949import { promptSelection } from "./install/prompt.js" ;
50- import type { CodebaseGraph } from "./types/index.js" ;
50+ import { runCheck , exitCodeFor } from "./rules/check.js" ;
51+ import { formatResult , formatSummaryLine } from "./rules/format.js" ;
52+ import { ConfigError } from "./config/index.js" ;
53+ import type { CodebaseGraph , OutputFormat } from "./types/index.js" ;
5154
5255const INDEX_DIR_NAME = ".code-visualizer" ;
5356
@@ -63,6 +66,7 @@ function getHeadHash(targetPath: string): string {
6366 cwd : path . resolve ( targetPath ) ,
6467 encoding : "utf-8" ,
6568 timeout : 5000 ,
69+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
6670 } ) . trim ( ) ;
6771 } catch {
6872 return "unknown" ;
@@ -88,6 +92,7 @@ function loadGraph(targetPath: string, force = false): { graph: CodebaseGraph; h
8892 process . stderr . write ( `Error: Path does not exist: ${ targetPath } \n` ) ;
8993 process . exit ( 1 ) ;
9094 }
95+ setRoot ( resolved ) ;
9196
9297 const indexDir = getIndexDir ( targetPath ) ;
9398 const headHash = getHeadHash ( targetPath ) ;
@@ -1011,6 +1016,90 @@ program
10111016 output ( `Re-run anytime — writes are idempotent (managed blocks only).` ) ;
10121017 } ) ;
10131018
1019+ // ── Subcommand: check ──────────────────────────────────────
1020+
1021+ interface CheckOptions extends CliCommandOptions {
1022+ config ? : string ;
1023+ format ? : string ;
1024+ failOn ? : string ;
1025+ gate ? : string ;
1026+ base ? : string ;
1027+ quiet ? : boolean ;
1028+ summary ? : boolean ;
1029+ }
1030+
1031+ function resolveCheckFormat ( options : CheckOptions ) : OutputFormat | null {
1032+ if ( options . json ) return "json" ;
1033+ if ( ! options . format ) return "text" ;
1034+ if ( options . format === "json" || options . format === "sarif" || options . format === "text" ) {
1035+ return options . format ;
1036+ }
1037+ return null ;
1038+ }
1039+
1040+ function parseFailOn ( value : string | undefined ) : "error" | "warn" | "never" | undefined | false {
1041+ if ( value === undefined ) return undefined ;
1042+ if ( value === "error" || value === "warn" || value === "never" ) return value ;
1043+ return false ;
1044+ }
1045+
1046+ function parseGate ( value : string | undefined ) : "all" | "new-only" | undefined {
1047+ return value === "all" || value === "new-only" ? value : undefined ;
1048+ }
1049+
1050+ program
1051+ . command ( "check" )
1052+ . description ( "Run the rules engine and gate on findings (comments, circular deps, dead exports)" )
1053+ . argument ( "<path>" , "Path to TypeScript codebase" )
1054+ . option ( "--config <path>" , "Config file path (overrides discovery)" )
1055+ . option ( "--format <fmt>" , "Output: text, json, or sarif (default: text)" )
1056+ . option ( "--fail-on <severity>" , "Severity that fails the gate: error, warn, never" )
1057+ . option ( "--gate <mode>" , "Gate mode: all or new-only" )
1058+ . option ( "--base <ref>" , "Base git ref for new-only gating" )
1059+ . option ( "--quiet" , "Suppress output when the result passes" )
1060+ . option ( "--summary" , "Print summary counts only" )
1061+ . option ( "--json" , "Shortcut for --format json" )
1062+ . option ( "--force" , "Re-index even if HEAD unchanged" )
1063+ . action ( ( targetPath : string , options : CheckOptions ) => {
1064+ const format = resolveCheckFormat ( options ) ;
1065+ if ( ! format ) {
1066+ process . stderr . write ( "Error: --format must be one of: text, json, sarif\n" ) ;
1067+ process . exit ( 2 ) ;
1068+ }
1069+
1070+ const failOn = parseFailOn ( options . failOn ) ;
1071+ if ( failOn === false ) {
1072+ process . stderr . write ( "Error: --fail-on must be one of: error, warn, never\n" ) ;
1073+ process . exit ( 2 ) ;
1074+ }
1075+
1076+ try {
1077+ const { graph } = loadGraph ( targetPath , options . force ) ;
1078+ const result = runCheck ( graph , path . resolve ( targetPath ) , {
1079+ configPath : options . config ,
1080+ format,
1081+ failOn,
1082+ gate : parseGate ( options . gate ) ,
1083+ base : options . base ,
1084+ quiet : options . quiet ,
1085+ summary : options . summary ,
1086+ } ) ;
1087+
1088+ const silent = options . quiet === true && result . verdict === "pass" ;
1089+ if ( ! silent ) {
1090+ output ( options . summary ? formatSummaryLine ( result ) : formatResult ( result , format ) ) ;
1091+ }
1092+
1093+ process . exit ( exitCodeFor ( result ) ) ;
1094+ } catch ( err ) {
1095+ if ( err instanceof ConfigError ) {
1096+ process . stderr . write ( `Config error: ${ err . message } \n` ) ;
1097+ process . exit ( 2 ) ;
1098+ }
1099+ throw err ;
1100+ }
1101+ } ) ;
1102+
10141103// ── MCP fallback (backward compat) ──────────────────────────
10151104
10161105program
@@ -1025,6 +1114,7 @@ program
10251114
10261115async function runMcpMode ( targetPath : string , options : McpOptions ) : Promise < void > {
10271116 const indexDir = getIndexDir ( targetPath ) ;
1117+ setRoot ( path . resolve ( targetPath ) ) ;
10281118
10291119 if ( options . clean ) {
10301120 if ( fs . existsSync ( indexDir ) ) {
0 commit comments