@ctxo/cli 0.9.3 → 0.11.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.
package/README.md CHANGED
@@ -58,6 +58,63 @@ Full-stack analytics UI with eight views: File Tree, Heatmap, Co-Changes, Timeli
58
58
 
59
59
  [Open Dependency Graph](https://alperhankendi.github.io/Ctxo/visualize.html)
60
60
 
61
+ ## Safe-Edit Guard
62
+
63
+ AI agents don't fail because they can't code. They fail because they code blind. Ctxo gives them the full picture before they write a single line.
64
+
65
+ The problem: an agent opens a file, makes a targeted edit, and walks away. It never checked what else depends on that symbol. Downstream callers silently break. Tests pass because the agent only ran the ones in the same file.
66
+
67
+ The safe-edit guard closes this loop deterministically. When you run `ctxo init`, it:
68
+
69
+ 1. Registers a **PreToolUse hook** in `.claude/settings.json` (Claude Code) that intercepts every Edit call.
70
+ 2. Installs three **model-invoked skills** (`ctxo-understand`, `ctxo-safe-edit`, `ctxo-review-pr`) so the agent knows when to check blast radius, history, and PR risk.
71
+
72
+ If an agent tries to edit a high-impact symbol without first calling `get_blast_radius`, the hook blocks the edit and tells the agent what to check. Once the agent runs the tool, the edit goes through normally. Fires once per symbol per session, fail-open on any uncertainty.
73
+
74
+ **Tune it:**
75
+
76
+ ```shell
77
+ # Preview which symbols the guard would flag at current sensitivity
78
+ ctxo gate --preview
79
+
80
+ # Check blast radius for a specific symbol (pipe to jq, CI scripts, etc.)
81
+ ctxo blast-radius "src/core/graph.ts::buildGraph::function" --json
82
+ ```
83
+
84
+ Configure sensitivity in `.ctxo/config.yaml`:
85
+
86
+ ```yaml
87
+ gate:
88
+ enabled: true
89
+ sensitivity: balanced # strict | balanced (DEFAULT) | lenient
90
+ ```
91
+
92
+ **Platform matrix:**
93
+
94
+ | Platform | Hook enforcement | Skills | Rules |
95
+ |---|---|---|---|
96
+ | Claude Code | yes (PreToolUse blocks) | yes | yes |
97
+ | Cursor | no (no blocking pre-edit hook) | yes | yes |
98
+ | Other | no | where supported | yes |
99
+
100
+ ## Supported Languages
101
+
102
+ | Language | Plugin | Tier | Notes |
103
+ |---|---|---|---|
104
+ | TypeScript / JavaScript | `@ctxo/lang-typescript` | full | ts-morph; type-aware cross-file resolution |
105
+ | Go | `@ctxo/lang-go` | syntax | tree-sitter; exported-symbol analysis |
106
+ | C# | `@ctxo/lang-csharp` | full | Roslyn; .NET SDK 8+ for full tier, tree-sitter fallback |
107
+ | Java | `@ctxo/lang-java` | syntax / full | tree-sitter syntax tier built-in; full tier via `@ctxo/lang-java-analyzer` companion (requires JRE 17+) |
108
+
109
+ Install via the CLI shortcut:
110
+
111
+ ```bash
112
+ ctxo install typescript go csharp # syntax or full tier auto-detected
113
+ ctxo install java # installs syntax tier; adds full-tier analyzer when JRE 17+ detected
114
+ ctxo install java --full-tier # force full-tier analyzer install
115
+ ctxo install java --syntax-only # skip analyzer, syntax tier only
116
+ ```
117
+
61
118
  ## Links
62
119
 
63
120
  * [Docs](https://alperhankendi.github.io/Ctxo/docs/) quick start, MCP tools, CLI reference, integrations
@@ -2,9 +2,10 @@
2
2
  import {
3
3
  atomicWrite,
4
4
  sweepStaleTmpFiles
5
- } from "./chunk-6EKERTJ7.js";
5
+ } from "./chunk-RFEXMBTG.js";
6
+ import "./chunk-5DSKZJNN.js";
6
7
  export {
7
8
  atomicWrite,
8
9
  sweepStaleTmpFiles
9
10
  };
10
- //# sourceMappingURL=atomic-write-AQNYJFAG.js.map
11
+ //# sourceMappingURL=atomic-write-BAFH464Q.js.map
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/logger.ts
4
+ function isDebugEnabled(namespace) {
5
+ const debugEnv = process.env["DEBUG"] ?? "";
6
+ if (!debugEnv) return false;
7
+ const patterns = debugEnv.split(",").map((p) => p.trim());
8
+ return patterns.some((p) => {
9
+ if (p === "*" || p === "ctxo:*") return true;
10
+ if (p === namespace) return true;
11
+ if (p.endsWith(":*") && namespace.startsWith(p.slice(0, -1))) return true;
12
+ return false;
13
+ });
14
+ }
15
+ function createLogger(namespace) {
16
+ const prefix = `[${namespace}]`;
17
+ return {
18
+ debug(message, ...args) {
19
+ if (isDebugEnabled(namespace)) {
20
+ console.error(`${prefix} DEBUG ${message}`, ...args);
21
+ }
22
+ },
23
+ info(message, ...args) {
24
+ console.error(`${prefix} ${message}`, ...args);
25
+ },
26
+ warn(message, ...args) {
27
+ console.error(`${prefix} WARN ${message}`, ...args);
28
+ },
29
+ error(message, ...args) {
30
+ console.error(`${prefix} ERROR ${message}`, ...args);
31
+ }
32
+ };
33
+ }
34
+
35
+ export {
36
+ createLogger
37
+ };
38
+ //# sourceMappingURL=chunk-5DSKZJNN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/logger.ts"],"sourcesContent":["/**\n * Structured logger for Ctxo — all output goes to stderr (stdout reserved for MCP JSON-RPC).\n *\n * Supports debug mode via DEBUG environment variable:\n * DEBUG=ctxo:* — all debug output\n * DEBUG=ctxo:git — only git adapter debug\n * DEBUG=ctxo:index — only indexing debug\n * DEBUG=ctxo:mcp — only MCP handler debug\n * DEBUG=ctxo:storage — only storage adapter debug\n * DEBUG=ctxo:masking — only masking pipeline debug\n *\n * Always outputs: info, warn, error (regardless of DEBUG).\n * Only outputs debug when matching namespace is enabled.\n */\n\nfunction isDebugEnabled(namespace: string): boolean {\n const debugEnv = process.env['DEBUG'] ?? '';\n if (!debugEnv) return false;\n const patterns = debugEnv.split(',').map(p => p.trim());\n return patterns.some(p => {\n if (p === '*' || p === 'ctxo:*') return true;\n if (p === namespace) return true;\n if (p.endsWith(':*') && namespace.startsWith(p.slice(0, -1))) return true;\n return false;\n });\n}\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\nexport function createLogger(namespace: string): Logger {\n const prefix = `[${namespace}]`;\n\n return {\n debug(message: string, ...args: unknown[]) {\n if (isDebugEnabled(namespace)) {\n console.error(`${prefix} DEBUG ${message}`, ...args);\n }\n },\n info(message: string, ...args: unknown[]) {\n console.error(`${prefix} ${message}`, ...args);\n },\n warn(message: string, ...args: unknown[]) {\n console.error(`${prefix} WARN ${message}`, ...args);\n },\n error(message: string, ...args: unknown[]) {\n console.error(`${prefix} ERROR ${message}`, ...args);\n },\n };\n}\n"],"mappings":";;;AAeA,SAAS,eAAe,WAA4B;AAClD,QAAM,WAAW,QAAQ,IAAI,OAAO,KAAK;AACzC,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACtD,SAAO,SAAS,KAAK,OAAK;AACxB,QAAI,MAAM,OAAO,MAAM,SAAU,QAAO;AACxC,QAAI,MAAM,UAAW,QAAO;AAC5B,QAAI,EAAE,SAAS,IAAI,KAAK,UAAU,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,QAAO;AACrE,WAAO;AAAA,EACT,CAAC;AACH;AASO,SAAS,aAAa,WAA2B;AACtD,QAAM,SAAS,IAAI,SAAS;AAE5B,SAAO;AAAA,IACL,MAAM,YAAoB,MAAiB;AACzC,UAAI,eAAe,SAAS,GAAG;AAC7B,gBAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,MACrD;AAAA,IACF;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,IAAI,OAAO,IAAI,GAAG,IAAI;AAAA,IAC/C;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,SAAS,OAAO,IAAI,GAAG,IAAI;AAAA,IACpD;AAAA,IACA,MAAM,YAAoB,MAAiB;AACzC,cAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,IACrD;AAAA,EACF;AACF;","names":[]}
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createLogger
4
+ } from "./chunk-5DSKZJNN.js";
2
5
 
3
6
  // src/adapters/storage/json-index-reader.ts
4
7
  import { readFileSync, readdirSync, existsSync, realpathSync } from "fs";
@@ -39,6 +42,16 @@ var SymbolIdSchema = z.string().min(1).refine(
39
42
  },
40
43
  { message: 'Symbol ID must match format "<file>::<name>::<kind>"' }
41
44
  );
45
+ var EdgeTargetSchema = z.string().refine(
46
+ (s) => {
47
+ const parts = s.split("::");
48
+ if (parts.length !== 3 && parts.length !== 2) return false;
49
+ if (parts.some((p) => p.length === 0)) return false;
50
+ const kind = parts[parts.length - 1];
51
+ return SYMBOL_KINDS.includes(kind);
52
+ },
53
+ { message: "edge target must be <file>::<name>::<kind> or <name>::<kind>" }
54
+ );
42
55
  var SymbolNodeSchema = z.object({
43
56
  symbolId: SymbolIdSchema,
44
57
  name: z.string().min(1),
@@ -52,7 +65,9 @@ var SymbolNodeSchema = z.object({
52
65
  });
53
66
  var GraphEdgeSchema = z.object({
54
67
  from: SymbolIdSchema,
55
- to: SymbolIdSchema,
68
+ // strict 3-part: from is always a local, resolved symbol
69
+ to: EdgeTargetSchema,
70
+ // 3-part OR 2-part name-ref for cross-file/unresolved targets
56
71
  kind: EdgeKindSchema,
57
72
  typeOnly: z.boolean().optional()
58
73
  });
@@ -83,6 +98,7 @@ var FileIndexSchema = z.object({
83
98
  });
84
99
 
85
100
  // src/adapters/storage/json-index-reader.ts
101
+ var log = createLogger("ctxo:json-reader");
86
102
  var JsonIndexReader = class {
87
103
  indexDir;
88
104
  constructor(ctxoRoot) {
@@ -107,14 +123,48 @@ var JsonIndexReader = class {
107
123
  const raw = readFileSync(absolutePath, "utf-8");
108
124
  const data = JSON.parse(raw);
109
125
  const result = FileIndexSchema.safeParse(data);
110
- if (!result.success) {
111
- const rel = relative(this.indexDir, absolutePath);
126
+ if (result.success) {
127
+ return result.data;
128
+ }
129
+ const rel = relative(this.indexDir, absolutePath);
130
+ const raw_data = data;
131
+ if (typeof raw_data["file"] !== "string" || !raw_data["file"] || typeof raw_data["lastModified"] !== "number") {
132
+ console.error(
133
+ `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`
134
+ );
135
+ return void 0;
136
+ }
137
+ const rawSymbols = Array.isArray(raw_data["symbols"]) ? raw_data["symbols"] : [];
138
+ const rawEdges = Array.isArray(raw_data["edges"]) ? raw_data["edges"] : [];
139
+ const rawIntent = Array.isArray(raw_data["intent"]) ? raw_data["intent"] : [];
140
+ const rawAntiPatterns = Array.isArray(raw_data["antiPatterns"]) ? raw_data["antiPatterns"] : [];
141
+ const validSymbols = rawSymbols.filter((s) => SymbolNodeSchema.safeParse(s).success);
142
+ const validEdges = rawEdges.filter((e) => GraphEdgeSchema.safeParse(e).success);
143
+ const validIntent = rawIntent.filter((i) => CommitIntentSchema.safeParse(i).success);
144
+ const validAntiPatterns = rawAntiPatterns.filter((a) => AntiPatternSchema.safeParse(a).success);
145
+ const droppedSymbols = rawSymbols.length - validSymbols.length;
146
+ const droppedEdges = rawEdges.length - validEdges.length;
147
+ const droppedIntent = rawIntent.length - validIntent.length;
148
+ const droppedAntiPatterns = rawAntiPatterns.length - validAntiPatterns.length;
149
+ if (droppedSymbols > 0 || droppedEdges > 0 || droppedIntent > 0 || droppedAntiPatterns > 0) {
150
+ log.warn(
151
+ `${rel}: dropped ${droppedSymbols} invalid symbol(s), ${droppedEdges} invalid edge(s), ${droppedIntent} invalid intent(s), ${droppedAntiPatterns} invalid antiPattern(s) \u2014 kept ${validSymbols.length} symbol(s), ${validEdges.length} edge(s), ${validIntent.length} intent(s), ${validAntiPatterns.length} antiPattern(s)`
152
+ );
153
+ }
154
+ const recovered = FileIndexSchema.safeParse({
155
+ ...raw_data,
156
+ symbols: validSymbols,
157
+ edges: validEdges,
158
+ intent: validIntent,
159
+ antiPatterns: validAntiPatterns
160
+ });
161
+ if (!recovered.success) {
112
162
  console.error(
113
163
  `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`
114
164
  );
115
165
  return void 0;
116
166
  }
117
- return result.data;
167
+ return recovered.data;
118
168
  } catch (err) {
119
169
  const rel = relative(this.indexDir, absolutePath);
120
170
  console.error(
@@ -146,4 +196,4 @@ export {
146
196
  DetailLevelSchema,
147
197
  JsonIndexReader
148
198
  };
149
- //# sourceMappingURL=chunk-2TVHKCCU.js.map
199
+ //# sourceMappingURL=chunk-CDGQGCVN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/storage/json-index-reader.ts","../src/core/types.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport {\n FileIndexSchema,\n SymbolNodeSchema,\n GraphEdgeSchema,\n CommitIntentSchema,\n AntiPatternSchema,\n type FileIndex,\n} from '../../core/types.js';\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:json-reader');\n\nexport class JsonIndexReader {\n private readonly indexDir: string;\n\n constructor(ctxoRoot: string) {\n this.indexDir = join(ctxoRoot, 'index');\n }\n\n readAll(): FileIndex[] {\n if (!existsSync(this.indexDir)) {\n return [];\n }\n\n const jsonFiles = this.collectJsonFiles(this.indexDir);\n const results: FileIndex[] = [];\n\n for (const filePath of jsonFiles) {\n const parsed = this.readSingle(filePath);\n if (parsed) {\n results.push(parsed);\n }\n }\n\n return results;\n }\n\n readSingle(absolutePath: string): FileIndex | undefined {\n try {\n const raw = readFileSync(absolutePath, 'utf-8');\n const data: unknown = JSON.parse(raw);\n const result = FileIndexSchema.safeParse(data);\n\n if (result.success) {\n return result.data;\n }\n\n // Strict parse failed — attempt resilient recovery: keep valid items, drop invalid ones.\n const rel = relative(this.indexDir, absolutePath);\n const raw_data = data as Record<string, unknown>;\n\n // Must have at least a valid file name and lastModified to be recoverable.\n if (typeof raw_data['file'] !== 'string' || !raw_data['file'] ||\n typeof raw_data['lastModified'] !== 'number') {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n const rawSymbols = Array.isArray(raw_data['symbols']) ? raw_data['symbols'] : [];\n const rawEdges = Array.isArray(raw_data['edges']) ? raw_data['edges'] : [];\n const rawIntent = Array.isArray(raw_data['intent']) ? raw_data['intent'] : [];\n const rawAntiPatterns = Array.isArray(raw_data['antiPatterns']) ? raw_data['antiPatterns'] : [];\n\n const validSymbols = rawSymbols.filter((s) => SymbolNodeSchema.safeParse(s).success);\n const validEdges = rawEdges.filter((e) => GraphEdgeSchema.safeParse(e).success);\n const validIntent = rawIntent.filter((i) => CommitIntentSchema.safeParse(i).success);\n const validAntiPatterns = rawAntiPatterns.filter((a) => AntiPatternSchema.safeParse(a).success);\n\n const droppedSymbols = rawSymbols.length - validSymbols.length;\n const droppedEdges = rawEdges.length - validEdges.length;\n const droppedIntent = rawIntent.length - validIntent.length;\n const droppedAntiPatterns = rawAntiPatterns.length - validAntiPatterns.length;\n\n if (droppedSymbols > 0 || droppedEdges > 0 || droppedIntent > 0 || droppedAntiPatterns > 0) {\n log.warn(\n `${rel}: dropped ${droppedSymbols} invalid symbol(s), ${droppedEdges} invalid edge(s), ${droppedIntent} invalid intent(s), ${droppedAntiPatterns} invalid antiPattern(s) — kept ${validSymbols.length} symbol(s), ${validEdges.length} edge(s), ${validIntent.length} intent(s), ${validAntiPatterns.length} antiPattern(s)`,\n );\n }\n\n // Re-parse with the filtered arrays to get a fully-typed FileIndex.\n const recovered = FileIndexSchema.safeParse({\n ...raw_data,\n symbols: validSymbols,\n edges: validEdges,\n intent: validIntent,\n antiPatterns: validAntiPatterns,\n });\n\n if (!recovered.success) {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n return recovered.data;\n } catch (err) {\n const rel = relative(this.indexDir, absolutePath);\n console.error(\n `[ctxo:json-reader] Failed to read ${rel}: ${(err as Error).message}`,\n );\n return undefined;\n }\n }\n\n private collectJsonFiles(dir: string, visited: Set<string> = new Set()): string[] {\n const realDir = realpathSync(dir);\n if (visited.has(realDir)) return []; // Guard against symlink loops\n visited.add(realDir);\n\n const files: string[] = [];\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue; // Skip symlinks entirely\n\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Skip non-FileIndex directories under .ctxo/index/\n if (entry.name === 'communities.history') continue;\n files.push(...this.collectJsonFiles(fullPath, visited));\n } else if (\n entry.name.endsWith('.json') &&\n entry.name !== 'co-changes.json' &&\n entry.name !== 'communities.json' &&\n entry.name !== 'drift-events.json' &&\n entry.name !== 'boundary-violations.json'\n ) {\n files.push(fullPath);\n }\n }\n\n return files;\n }\n}\n","import { z } from 'zod';\n\n// ── Symbol Kinds ────────────────────────────────────────────────\n\nexport const SYMBOL_KINDS = [\n 'function',\n 'class',\n 'interface',\n 'method',\n 'variable',\n 'type',\n] as const;\n\nexport const SymbolKindSchema = z.enum(SYMBOL_KINDS);\nexport type SymbolKind = z.infer<typeof SymbolKindSchema>;\n\n// ── Edge Kinds ──────────────────────────────────────────────────\n\nexport const EDGE_KINDS = [\n 'imports',\n 'calls',\n 'extends',\n 'implements',\n 'uses',\n] as const;\n\nexport const EdgeKindSchema = z.enum(EDGE_KINDS);\nexport type EdgeKind = z.infer<typeof EdgeKindSchema>;\n\n// ── Detail Levels ───────────────────────────────────────────────\n\nexport const DETAIL_LEVELS = [1, 2, 3, 4] as const;\n\nexport const DetailLevelSchema = z.union([\n z.literal(1),\n z.literal(2),\n z.literal(3),\n z.literal(4),\n]);\nexport type DetailLevel = z.infer<typeof DetailLevelSchema>;\n\n// ── Symbol ID ───────────────────────────────────────────────────\n\n/**\n * Format: \"<relativeFile>::<name>::<kind>\"\n * Example: \"src/foo.ts::myFn::function\"\n */\nexport const SymbolIdSchema = z\n .string()\n .min(1)\n .refine(\n (value) => {\n const parts = value.split('::');\n if (parts.length !== 3) return false;\n const [file, name, kind] = parts;\n if (!file || !name || !kind) return false;\n return SymbolKindSchema.safeParse(kind).success;\n },\n { message: 'Symbol ID must match format \"<file>::<name>::<kind>\"' },\n );\nexport type SymbolId = z.infer<typeof SymbolIdSchema>;\n\n// ── Edge Target ─────────────────────────────────────────────────\n\n/**\n * An edge target: either a full 3-part symbol ID (\"<file>::<name>::<kind>\")\n * or a 2-part name-reference (\"<name>::<kind>\") for cross-file / unresolved\n * targets. The graph builder resolves name-references by name at query time.\n */\nexport const EdgeTargetSchema = z.string().refine(\n (s) => {\n const parts = s.split('::');\n if (parts.length !== 3 && parts.length !== 2) return false;\n if (parts.some((p) => p.length === 0)) return false;\n const kind = parts[parts.length - 1];\n return (SYMBOL_KINDS as readonly string[]).includes(kind as string);\n },\n { message: 'edge target must be <file>::<name>::<kind> or <name>::<kind>' },\n);\nexport type EdgeTarget = z.infer<typeof EdgeTargetSchema>;\n\n// ── Symbol Node ─────────────────────────────────────────────────\n\nexport const SymbolNodeSchema = z\n .object({\n symbolId: SymbolIdSchema,\n name: z.string().min(1),\n kind: SymbolKindSchema,\n startLine: z.number().int().nonnegative(),\n endLine: z.number().int().nonnegative(),\n startOffset: z.number().int().nonnegative().optional(),\n endOffset: z.number().int().nonnegative().optional(),\n })\n .refine((node) => node.endLine >= node.startLine, {\n message: 'endLine must be >= startLine',\n });\nexport type SymbolNode = z.infer<typeof SymbolNodeSchema>;\n\n// ── Graph Edge ──────────────────────────────────────────────────\n\nexport const GraphEdgeSchema = z.object({\n from: SymbolIdSchema, // strict 3-part: from is always a local, resolved symbol\n to: EdgeTargetSchema, // 3-part OR 2-part name-ref for cross-file/unresolved targets\n kind: EdgeKindSchema,\n typeOnly: z.boolean().optional(),\n});\nexport type GraphEdge = z.infer<typeof GraphEdgeSchema>;\n\n// ── Commit Intent ───────────────────────────────────────────────\n\nexport const CommitIntentSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n kind: z.literal('commit'),\n});\nexport type CommitIntent = z.infer<typeof CommitIntentSchema>;\n\n// ── Anti-Pattern ────────────────────────────────────────────────\n\nexport const AntiPatternSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n});\nexport type AntiPattern = z.infer<typeof AntiPatternSchema>;\n\n// ── File Index ──────────────────────────────────────────────────\n\nexport const ComplexityMetricsSchema = z.object({\n symbolId: z.string().min(1),\n cyclomatic: z.number().nonnegative(),\n});\n\nexport const FileIndexSchema = z.object({\n file: z.string().min(1),\n lastModified: z.number().nonnegative(),\n contentHash: z.string().optional(),\n symbols: z.array(SymbolNodeSchema),\n edges: z.array(GraphEdgeSchema),\n complexity: z.array(ComplexityMetricsSchema).optional(),\n intent: z.array(CommitIntentSchema),\n antiPatterns: z.array(AntiPatternSchema),\n});\nexport type FileIndex = z.infer<typeof FileIndexSchema>;\n\n// ── Logic-Slice Result ──────────────────────────────────────────\n\nexport interface LogicSliceResult {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n}\n\n// ── Formatted Slice ─────────────────────────────────────────────\n\nexport interface TruncationInfo {\n readonly truncated: true;\n readonly reason: 'token_budget_exceeded';\n}\n\nexport interface FormattedSlice {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n readonly level: DetailLevel;\n readonly levelDescription?: string;\n readonly truncation?: TruncationInfo;\n}\n\n// ── Complexity & Churn ──────────────────────────────────────────\n\nexport interface ComplexityMetrics {\n readonly symbolId: string;\n readonly cyclomatic: number;\n}\n\nexport interface ChurnData {\n readonly filePath: string;\n readonly commitCount: number;\n}\n\nexport const SCORE_BANDS = ['low', 'medium', 'high'] as const;\nexport type ScoreBand = (typeof SCORE_BANDS)[number];\n\nexport interface ChangeIntelligenceScore {\n readonly symbolId: string;\n readonly complexity: number;\n readonly churn: number;\n readonly composite: number;\n readonly band: ScoreBand;\n}\n\n// ── Why-Context Result ──────────────────────────────────────────\n\nexport interface WhyContextResult {\n readonly commitHistory: readonly CommitIntent[];\n readonly antiPatternWarnings: readonly AntiPattern[];\n readonly changeIntelligence?: ChangeIntelligenceScore;\n}\n\n// ── Co-Change Analysis ─────────────────────────────────────────\n\nexport interface CoChangeEntry {\n readonly file1: string;\n readonly file2: string;\n readonly sharedCommits: number;\n readonly frequency: number;\n}\n\nexport interface CoChangeMatrix {\n readonly version: 1;\n readonly timestamp: number;\n readonly entries: readonly CoChangeEntry[];\n}\n\n// ── Commit Record (from git adapter) ────────────────────────────\n\nexport interface CommitRecord {\n readonly hash: string;\n readonly message: string;\n readonly date: string;\n readonly author: string;\n}\n\n// ── Blame Line (from git adapter) ───────────────────────────────\n\nexport interface BlameLine {\n readonly hash: string;\n readonly lineNumber: number;\n readonly author: string;\n readonly date: string;\n}\n\n// ── Search Types ───────────────────────────────────────────────\n\nexport interface SearchResult {\n symbolId: string;\n name: string;\n kind: string;\n filePath: string;\n /** BM25 relevance score (higher = more relevant) */\n relevanceScore: number;\n /** PageRank importance score (0-1) */\n importanceScore: number;\n /** Final combined score after all boosts */\n combinedScore: number;\n}\n\nexport interface SearchMetrics {\n porterHits: number;\n trigramHits: number;\n phase2Activated: boolean;\n fuzzyApplied: boolean;\n latencyMs: number;\n}\n\nexport interface FuzzyCorrection {\n originalQuery: string;\n correctedQuery: string;\n corrections: Array<{\n original: string;\n corrected: string;\n distance: number;\n }>;\n}\n\nexport interface SearchResponse {\n query: string;\n results: SearchResult[];\n metrics: SearchMetrics;\n fuzzyCorrection?: FuzzyCorrection;\n}\n\n// ── Community Detection ────────────────────────────────────────\n\nexport interface CommunityEntry {\n readonly symbolId: string;\n readonly communityId: number;\n readonly communityLabel: string;\n}\n\nexport interface GodNode {\n readonly symbolId: string;\n readonly bridgedCommunities: readonly number[];\n readonly centralityScore: number;\n}\n\nexport const EDGE_QUALITY = ['full', 'syntax-only', 'mixed'] as const;\nexport type EdgeQuality = (typeof EDGE_QUALITY)[number];\n\nexport interface CommunitySnapshot {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly modularity: number;\n readonly communities: readonly CommunityEntry[];\n readonly godNodes: readonly GodNode[];\n readonly edgeQuality: EdgeQuality;\n readonly crossClusterEdges: number;\n}\n\nexport interface DriftEvent {\n readonly symbolId: string;\n readonly movedFrom: { readonly id: number; readonly label: string };\n readonly movedTo: { readonly id: number; readonly label: string };\n readonly firstSeenInNewCluster: string;\n}\n\nexport interface BoundaryViolation {\n readonly from: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly to: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly edgeKind: EdgeKind;\n readonly historicalEdgesBetweenClusters: number;\n readonly severity: 'high' | 'medium';\n}\n\n// ── Persisted architectural artifacts ──────────────────────────\n// Written by `ctxo index` next to communities.json so the static\n// HTML visualizer can render Architecture views without re-running\n// detectors in the browser.\n\nexport type ConfidenceLevel = 'high' | 'medium' | 'low';\n\nexport interface DriftEventsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n readonly snapshotsAvailable: number;\n readonly hint?: string;\n readonly events: readonly DriftEvent[];\n}\n\nexport interface BoundaryViolationsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n readonly snapshotsAvailable: number;\n readonly hint?: string;\n readonly violations: readonly BoundaryViolation[];\n}\n"],"mappings":"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","names":[]}
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- atomicWrite,
4
- createLogger
5
- } from "./chunk-6EKERTJ7.js";
3
+ atomicWrite
4
+ } from "./chunk-RFEXMBTG.js";
6
5
  import {
7
6
  JsonIndexReader
8
- } from "./chunk-2TVHKCCU.js";
7
+ } from "./chunk-CDGQGCVN.js";
8
+ import {
9
+ createLogger
10
+ } from "./chunk-5DSKZJNN.js";
9
11
 
10
12
  // src/adapters/storage/sqlite-storage-adapter.ts
11
13
  import initSqlJs from "sql.js";
@@ -776,12 +778,21 @@ var WatchConfigSchema = z.object({
776
778
  */
777
779
  snapshotMinFileChanges: z.number().int().min(1).optional()
778
780
  }).strict();
781
+ var GateConfigSchema = z.object({
782
+ enabled: z.boolean().optional(),
783
+ sensitivity: z.enum(["strict", "balanced", "lenient"]).optional(),
784
+ /** Absolute floor: minimum confirmed+likely dependents before the gate can fire. */
785
+ minDependents: z.number().int().min(0).optional(),
786
+ /** Repo-relative significance: symbol must be in the top `percentile`% by PageRank. */
787
+ percentile: z.number().int().min(1).max(100).optional()
788
+ }).strict();
779
789
  var CtxoConfigSchema = z.object({
780
790
  version: z.union([z.string(), z.number()]).optional(),
781
791
  stats: StatsConfigSchema.optional(),
782
792
  index: IndexConfigSchema.optional(),
783
793
  masking: MaskingConfigSchema.optional(),
784
- watch: WatchConfigSchema.optional()
794
+ watch: WatchConfigSchema.optional(),
795
+ gate: GateConfigSchema.optional()
785
796
  }).passthrough();
786
797
  var DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES = 5;
787
798
  var DEFAULT_CONFIG = {
@@ -789,7 +800,8 @@ var DEFAULT_CONFIG = {
789
800
  stats: { enabled: true },
790
801
  index: { ignore: [], ignoreProjects: [] },
791
802
  masking: { clusterLabels: [] },
792
- watch: { snapshotMinFileChanges: DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES }
803
+ watch: { snapshotMinFileChanges: DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES },
804
+ gate: { enabled: true, sensitivity: "balanced" }
793
805
  };
794
806
 
795
807
  // src/core/config/load-config.ts
@@ -878,15 +890,23 @@ function makeGlobMatcher(patterns) {
878
890
  var SymbolGraph = class {
879
891
  nodes = /* @__PURE__ */ new Map();
880
892
  nodesByFileAndName = /* @__PURE__ */ new Map();
893
+ nodesByNameAndKind = /* @__PURE__ */ new Map();
881
894
  forwardEdges = /* @__PURE__ */ new Map();
882
895
  reverseEdges = /* @__PURE__ */ new Map();
883
896
  edgeSet = /* @__PURE__ */ new Set();
884
897
  addNode(node) {
885
898
  this.nodes.set(node.symbolId, node);
886
- const [p0, p1] = node.symbolId.split("::");
899
+ const parts = node.symbolId.split("::");
900
+ const [p0, p1] = parts;
887
901
  if (p0 !== void 0 && p1 !== void 0) {
888
902
  this.nodesByFileAndName.set(`${p0}::${p1}`, node);
889
903
  }
904
+ if (parts.length === 3) {
905
+ const nk = `${parts[1]}::${parts[2]}`;
906
+ const list = this.nodesByNameAndKind.get(nk);
907
+ if (list) list.push(node);
908
+ else this.nodesByNameAndKind.set(nk, [node]);
909
+ }
890
910
  }
891
911
  addEdge(edge) {
892
912
  const resolvedFrom = this.resolveNodeId(edge.from);
@@ -907,7 +927,8 @@ var SymbolGraph = class {
907
927
  }
908
928
  resolveNodeId(id) {
909
929
  if (this.nodes.has(id)) return id;
910
- const [p0, p1] = id.split("::");
930
+ const segs = id.split("::");
931
+ const [p0, p1] = segs;
911
932
  if (p0 !== void 0 && p1 !== void 0) {
912
933
  const fuzzyKey = `${p0}::${p1}`;
913
934
  const match = this.nodesByFileAndName.get(fuzzyKey);
@@ -919,18 +940,41 @@ var SymbolGraph = class {
919
940
  if (altMatch) return altMatch.symbolId;
920
941
  }
921
942
  }
943
+ const nameKind = this.extractNameKind(segs);
944
+ if (nameKind) {
945
+ const candidates = this.nodesByNameAndKind.get(nameKind);
946
+ if (candidates && candidates.length === 1) return candidates[0].symbolId;
947
+ }
922
948
  return id;
923
949
  }
924
950
  resolveNodeFuzzy(id) {
925
- const [p0, p1] = id.split("::");
951
+ const segs = id.split("::");
952
+ const [p0, p1] = segs;
926
953
  if (p0 !== void 0 && p1 !== void 0) {
927
954
  const match = this.nodesByFileAndName.get(`${p0}::${p1}`);
928
955
  if (match) return match;
929
956
  const jsToTs = p0.replace(/\.js$/, ".ts");
930
957
  if (jsToTs !== p0) {
931
- return this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
958
+ const tsMatch = this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
959
+ if (tsMatch) return tsMatch;
932
960
  }
933
961
  }
962
+ const nameKind = this.extractNameKind(segs);
963
+ if (nameKind) {
964
+ const candidates = this.nodesByNameAndKind.get(nameKind);
965
+ if (candidates && candidates.length === 1) return candidates[0];
966
+ }
967
+ return void 0;
968
+ }
969
+ /**
970
+ * Extracts a `name::kind` key from the segments of a reference ID, supporting:
971
+ * - 2-part: `Bar::class` → `Bar::class`
972
+ * - 3-part: `pkg.Bar::helper::method` → `helper::method`
973
+ * Returns undefined for 1-part IDs (no kind information available).
974
+ */
975
+ extractNameKind(segs) {
976
+ if (segs.length === 3) return `${segs[1]}::${segs[2]}`;
977
+ if (segs.length === 2) return `${segs[0]}::${segs[1]}`;
934
978
  return void 0;
935
979
  }
936
980
  getForwardEdges(symbolId) {
@@ -1141,6 +1185,92 @@ function hintFor(snapshots) {
1141
1185
  return "Install post-commit hook or run `ctxo index` more often to build a richer drift history.";
1142
1186
  }
1143
1187
 
1188
+ // src/core/blast-radius/blast-radius-calculator.ts
1189
+ var CONFIRMED_KINDS = /* @__PURE__ */ new Set(["calls", "extends", "implements"]);
1190
+ var LIKELY_KINDS = /* @__PURE__ */ new Set(["uses"]);
1191
+ var CONFIDENCE_RANK = {
1192
+ confirmed: 2,
1193
+ likely: 1,
1194
+ potential: 0
1195
+ };
1196
+ function edgeKindToConfidence(kind) {
1197
+ if (CONFIRMED_KINDS.has(kind)) return "confirmed";
1198
+ if (LIKELY_KINDS.has(kind)) return "likely";
1199
+ return "potential";
1200
+ }
1201
+ var BlastRadiusCalculator = class {
1202
+ calculate(graph, symbolId, coChangeMap) {
1203
+ if (!graph.hasNode(symbolId)) {
1204
+ return { impactedSymbols: [], directDependentsCount: 0, confirmedCount: 0, likelyCount: 0, potentialCount: 0, overallRiskScore: 0 };
1205
+ }
1206
+ const sourceFile = symbolId.split("::")[0];
1207
+ const visited = /* @__PURE__ */ new Set([symbolId]);
1208
+ const rawEntries = [];
1209
+ const queue = [{ id: symbolId, depth: 0 }];
1210
+ while (queue.length > 0) {
1211
+ const current = queue.shift();
1212
+ const reverseEdges = graph.getReverseEdges(current.id);
1213
+ const bestByNode = /* @__PURE__ */ new Map();
1214
+ for (const edge of reverseEdges) {
1215
+ if (visited.has(edge.from)) continue;
1216
+ const conf = edgeKindToConfidence(edge.kind);
1217
+ const existing = bestByNode.get(edge.from);
1218
+ if (!existing) {
1219
+ bestByNode.set(edge.from, { confidence: conf, kinds: /* @__PURE__ */ new Set([edge.kind]) });
1220
+ } else {
1221
+ existing.kinds.add(edge.kind);
1222
+ if (CONFIDENCE_RANK[conf] > CONFIDENCE_RANK[existing.confidence]) {
1223
+ existing.confidence = conf;
1224
+ }
1225
+ }
1226
+ }
1227
+ for (const [nodeId, info] of bestByNode) {
1228
+ visited.add(nodeId);
1229
+ const depth = current.depth + 1;
1230
+ const riskScore = 1 / Math.pow(depth, 0.7);
1231
+ let confidence = info.confidence;
1232
+ let coChangeFrequency;
1233
+ if (coChangeMap) {
1234
+ const nodeFile = nodeId.split("::")[0];
1235
+ const coEntries = coChangeMap.get(sourceFile);
1236
+ if (coEntries) {
1237
+ const match = coEntries.find((e) => e.file1 === nodeFile || e.file2 === nodeFile);
1238
+ if (match) {
1239
+ coChangeFrequency = match.frequency;
1240
+ if (confidence === "potential" && match.frequency > 0.5) {
1241
+ confidence = "likely";
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+ rawEntries.push({
1247
+ symbolId: nodeId,
1248
+ depth,
1249
+ riskScore: Math.round(riskScore * 1e3) / 1e3,
1250
+ confidence,
1251
+ edgeKinds: [...info.kinds],
1252
+ coChangeFrequency
1253
+ });
1254
+ queue.push({ id: nodeId, depth });
1255
+ }
1256
+ }
1257
+ const blastSet = new Set(rawEntries.map((e) => e.symbolId));
1258
+ blastSet.add(symbolId);
1259
+ const entries = rawEntries.map((e) => ({
1260
+ ...e,
1261
+ dependentCount: graph.getReverseEdges(e.symbolId).filter((re) => blastSet.has(re.from)).length
1262
+ }));
1263
+ entries.sort((a, b) => a.depth - b.depth);
1264
+ const directDependentsCount = entries.filter((e) => e.depth === 1).length;
1265
+ const totalRisk = entries.reduce((sum, e) => sum + e.riskScore, 0);
1266
+ const overallRiskScore = Math.round(Math.min(totalRisk / Math.max(directDependentsCount, 1), 1) * 1e3) / 1e3;
1267
+ const confirmedCount = entries.filter((e) => e.confidence === "confirmed").length;
1268
+ const likelyCount = entries.filter((e) => e.confidence === "likely").length;
1269
+ const potentialCount = entries.filter((e) => e.confidence === "potential").length;
1270
+ return { impactedSymbols: entries, directDependentsCount, confirmedCount, likelyCount, potentialCount, overallRiskScore };
1271
+ }
1272
+ };
1273
+
1144
1274
  // src/core/overlay/architectural-overlay.ts
1145
1275
  var DEFAULT_RULES = [
1146
1276
  // Test layer (matched first — __tests__, .test.ts, tests/, fixtures)
@@ -1649,6 +1779,7 @@ export {
1649
1779
  RevertDetector,
1650
1780
  DriftDetector,
1651
1781
  SimpleGitAdapter,
1782
+ BlastRadiusCalculator,
1652
1783
  ArchitecturalOverlay,
1653
1784
  DeadCodeDetector,
1654
1785
  PageRankCalculator,
@@ -1665,4 +1796,4 @@ export {
1665
1796
  watchSnapshotMinFileChanges,
1666
1797
  makeGlobMatcher
1667
1798
  };
1668
- //# sourceMappingURL=chunk-HXEPS2XY.js.map
1799
+ //# sourceMappingURL=chunk-HZ4UGA6J.js.map