@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 +57 -0
- package/dist/{atomic-write-AQNYJFAG.js → atomic-write-BAFH464Q.js} +3 -2
- package/dist/chunk-5DSKZJNN.js +38 -0
- package/dist/chunk-5DSKZJNN.js.map +1 -0
- package/dist/{chunk-2TVHKCCU.js → chunk-CDGQGCVN.js} +55 -5
- package/dist/chunk-CDGQGCVN.js.map +1 -0
- package/dist/{chunk-HXEPS2XY.js → chunk-HZ4UGA6J.js} +142 -11
- package/dist/chunk-HZ4UGA6J.js.map +1 -0
- package/dist/{chunk-6EKERTJ7.js → chunk-RFEXMBTG.js} +4 -36
- package/dist/chunk-RFEXMBTG.js.map +1 -0
- package/dist/{cli-router-CQJ7CN6E.js → cli-router-NFAGLJ46.js} +802 -198
- package/dist/cli-router-NFAGLJ46.js.map +1 -0
- package/dist/index.js +8 -94
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-UYS74USB.js +9 -0
- package/llms-full.txt +48 -1
- package/llms.txt +8 -0
- package/package.json +6 -5
- package/skills/ctxo-review-pr/SKILL.md +10 -0
- package/skills/ctxo-safe-edit/SKILL.md +15 -0
- package/skills/ctxo-understand/SKILL.md +14 -0
- package/dist/chunk-2TVHKCCU.js.map +0 -1
- package/dist/chunk-6EKERTJ7.js.map +0 -1
- package/dist/chunk-HXEPS2XY.js.map +0 -1
- package/dist/cli-router-CQJ7CN6E.js.map +0 -1
- package/dist/json-index-reader-XOEE6VMD.js +0 -8
- /package/dist/{atomic-write-AQNYJFAG.js.map → atomic-write-BAFH464Q.js.map} +0 -0
- /package/dist/{json-index-reader-XOEE6VMD.js.map → json-index-reader-UYS74USB.js.map} +0 -0
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-
|
|
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-
|
|
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
|
-
|
|
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 (
|
|
111
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
5
|
-
} from "./chunk-6EKERTJ7.js";
|
|
3
|
+
atomicWrite
|
|
4
|
+
} from "./chunk-RFEXMBTG.js";
|
|
6
5
|
import {
|
|
7
6
|
JsonIndexReader
|
|
8
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
1799
|
+
//# sourceMappingURL=chunk-HZ4UGA6J.js.map
|