cto-ai-cli 1.3.0 → 3.0.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.
@@ -0,0 +1,1971 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/score.ts
4
+ import { resolve as resolve4 } from "path";
5
+
6
+ // src/engine/analyzer.ts
7
+ import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
8
+ import { join as join2, extname, relative as relative2, resolve as resolve2, basename as basename2 } from "path";
9
+ import { createHash } from "crypto";
10
+
11
+ // src/types/engine.ts
12
+ var DEFAULT_RISK_WEIGHTS = {
13
+ hub: 30,
14
+ typeProvider: 25,
15
+ complexity: 15,
16
+ recency: 15,
17
+ config: 10,
18
+ churn: 5
19
+ };
20
+
21
+ // src/types/config.ts
22
+ var DEFAULT_CONFIG = {
23
+ version: "2.0",
24
+ analysis: {
25
+ extensions: {
26
+ code: ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "kt", "rb", "php", "c", "cpp", "h", "hpp", "cs"],
27
+ config: ["json", "yml", "yaml", "toml"],
28
+ docs: ["md", "txt", "rst"]
29
+ },
30
+ ignore: {
31
+ dirs: ["node_modules", "dist", "build", ".git", "coverage", "__pycache__", ".next", "vendor", ".cto"],
32
+ patterns: ["*.min.js", "*.map", "*.lock", "*.generated.*"]
33
+ },
34
+ maxDepth: 20
35
+ },
36
+ risk: {
37
+ weights: {
38
+ hub: 30,
39
+ typeProvider: 25,
40
+ complexity: 15,
41
+ recency: 15,
42
+ config: 10,
43
+ churn: 5
44
+ }
45
+ },
46
+ interaction: {
47
+ defaultBudget: 5e4,
48
+ defaultModel: "claude-sonnet-4"
49
+ },
50
+ tokens: {
51
+ method: "chars4"
52
+ },
53
+ governance: {
54
+ auditEnabled: true,
55
+ secretDetection: true,
56
+ retentionDays: 90
57
+ }
58
+ };
59
+
60
+ // src/engine/tokenizer.ts
61
+ import { encodingForModel } from "js-tiktoken";
62
+ import { readFile, stat } from "fs/promises";
63
+ var CHARS_PER_TOKEN = 4;
64
+ var encoder = null;
65
+ function getEncoder() {
66
+ if (!encoder) {
67
+ encoder = encodingForModel("claude-3-5-sonnet-20241022");
68
+ }
69
+ return encoder;
70
+ }
71
+ function countTokensTiktoken(text) {
72
+ try {
73
+ const enc = getEncoder();
74
+ const tokens = enc.encode(text);
75
+ return tokens.length;
76
+ } catch {
77
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
78
+ }
79
+ }
80
+ function countTokensChars4(sizeInBytes) {
81
+ return Math.ceil(sizeInBytes / CHARS_PER_TOKEN);
82
+ }
83
+ function estimateTokens(content, sizeInBytes, method = "chars4") {
84
+ if (method === "tiktoken") {
85
+ return countTokensTiktoken(content);
86
+ }
87
+ return countTokensChars4(sizeInBytes);
88
+ }
89
+
90
+ // src/engine/graph.ts
91
+ import { Project, SyntaxKind } from "ts-morph";
92
+ import { resolve, relative, dirname, join } from "path";
93
+ import { existsSync } from "fs";
94
+ var TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs", "cts", "cjs"]);
95
+ function createProject(projectPath, filePaths) {
96
+ const tsConfigPath = join(projectPath, "tsconfig.json");
97
+ const hasTsConfig = existsSync(tsConfigPath);
98
+ const project = new Project({
99
+ tsConfigFilePath: hasTsConfig ? tsConfigPath : void 0,
100
+ skipAddingFilesFromTsConfig: true,
101
+ compilerOptions: hasTsConfig ? void 0 : {
102
+ allowJs: true,
103
+ jsx: 4,
104
+ // JsxEmit.ReactJSX
105
+ esModuleInterop: true,
106
+ moduleResolution: 100
107
+ // Bundler
108
+ }
109
+ });
110
+ const tsFiles = filePaths.filter((f) => {
111
+ const ext = f.split(".").pop()?.toLowerCase() ?? "";
112
+ return TS_EXTENSIONS.has(ext);
113
+ });
114
+ for (const filePath of tsFiles) {
115
+ try {
116
+ project.addSourceFileAtPath(filePath);
117
+ } catch {
118
+ }
119
+ }
120
+ return project;
121
+ }
122
+ function buildProjectGraph(projectPath, files) {
123
+ const absPath = resolve(projectPath);
124
+ const tsFiles = files.filter((f) => TS_EXTENSIONS.has(f.extension)).map((f) => f.path);
125
+ if (tsFiles.length === 0) {
126
+ return emptyGraph(files);
127
+ }
128
+ let project;
129
+ try {
130
+ project = createProject(projectPath, tsFiles);
131
+ } catch {
132
+ return emptyGraph(files);
133
+ }
134
+ const edges = [];
135
+ const nodeSet = /* @__PURE__ */ new Set();
136
+ for (const sourceFile of project.getSourceFiles()) {
137
+ const fromRel = relative(absPath, sourceFile.getFilePath());
138
+ if (fromRel.startsWith("..") || fromRel.includes("node_modules")) continue;
139
+ nodeSet.add(fromRel);
140
+ for (const imp of sourceFile.getImportDeclarations()) {
141
+ const moduleSpecifier = imp.getModuleSpecifierValue();
142
+ const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
143
+ if (resolved) {
144
+ nodeSet.add(resolved);
145
+ edges.push({ from: fromRel, to: resolved, type: "import" });
146
+ }
147
+ }
148
+ for (const exp of sourceFile.getExportDeclarations()) {
149
+ const moduleSpecifier = exp.getModuleSpecifierValue();
150
+ if (moduleSpecifier) {
151
+ const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
152
+ if (resolved) {
153
+ nodeSet.add(resolved);
154
+ edges.push({ from: fromRel, to: resolved, type: "re-export" });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ const nodes = Array.from(nodeSet);
160
+ const importedByCount = /* @__PURE__ */ new Map();
161
+ const importCount = /* @__PURE__ */ new Map();
162
+ for (const edge of edges) {
163
+ importedByCount.set(edge.to, (importedByCount.get(edge.to) ?? 0) + 1);
164
+ importCount.set(edge.from, (importCount.get(edge.from) ?? 0) + 1);
165
+ }
166
+ const N = Math.max(nodes.length, 1);
167
+ const hubs = nodes.map((node) => {
168
+ const inDeg = importedByCount.get(node) ?? 0;
169
+ const outDeg = importCount.get(node) ?? 0;
170
+ const centrality = N > 1 ? inDeg / (N - 1) * 100 : 0;
171
+ const score = Math.round(centrality + outDeg * (100 / (2 * N)));
172
+ return {
173
+ relativePath: node,
174
+ dependents: inDeg,
175
+ dependencies: outDeg,
176
+ score: Math.min(100, score)
177
+ };
178
+ }).filter((h) => h.dependents >= 3 || h.score >= 15).sort((a, b) => b.score - a.score);
179
+ const leaves = nodes.filter(
180
+ (node) => (importedByCount.get(node) ?? 0) === 0 && (importCount.get(node) ?? 0) > 0
181
+ );
182
+ const connectedNodes = /* @__PURE__ */ new Set();
183
+ for (const edge of edges) {
184
+ connectedNodes.add(edge.from);
185
+ connectedNodes.add(edge.to);
186
+ }
187
+ const allFileNodes = new Set(files.map((f) => f.relativePath));
188
+ const orphans = Array.from(allFileNodes).filter((n) => !connectedNodes.has(n));
189
+ const clusters = detectClusters(nodes, edges, files);
190
+ enrichComplexity(project, absPath, files);
191
+ return { nodes, edges, hubs, leaves, orphans, clusters };
192
+ }
193
+ var UnionFind = class {
194
+ parent;
195
+ rank;
196
+ constructor(nodes) {
197
+ this.parent = /* @__PURE__ */ new Map();
198
+ this.rank = /* @__PURE__ */ new Map();
199
+ for (const n of nodes) {
200
+ this.parent.set(n, n);
201
+ this.rank.set(n, 0);
202
+ }
203
+ }
204
+ find(x) {
205
+ const p = this.parent.get(x);
206
+ if (p === void 0) return x;
207
+ if (p !== x) {
208
+ this.parent.set(x, this.find(p));
209
+ }
210
+ return this.parent.get(x);
211
+ }
212
+ union(a, b) {
213
+ const ra = this.find(a);
214
+ const rb = this.find(b);
215
+ if (ra === rb) return;
216
+ const rankA = this.rank.get(ra) ?? 0;
217
+ const rankB = this.rank.get(rb) ?? 0;
218
+ if (rankA < rankB) {
219
+ this.parent.set(ra, rb);
220
+ } else if (rankA > rankB) {
221
+ this.parent.set(rb, ra);
222
+ } else {
223
+ this.parent.set(rb, ra);
224
+ this.rank.set(ra, rankA + 1);
225
+ }
226
+ }
227
+ };
228
+ function detectClusters(nodes, edges, files) {
229
+ const uf = new UnionFind(nodes);
230
+ for (const edge of edges) {
231
+ uf.union(edge.from, edge.to);
232
+ }
233
+ const components = /* @__PURE__ */ new Map();
234
+ for (const node of nodes) {
235
+ const root = uf.find(node);
236
+ if (!components.has(root)) components.set(root, []);
237
+ components.get(root).push(node);
238
+ }
239
+ const tokenMap = new Map(files.map((f) => [f.relativePath, f.tokens]));
240
+ const clusters = [];
241
+ for (const [, groupFiles] of components) {
242
+ if (groupFiles.length < 2) continue;
243
+ const name = commonPrefix(groupFiles);
244
+ const fileSet = new Set(groupFiles);
245
+ let internalEdges = 0;
246
+ let externalEdges = 0;
247
+ for (const edge of edges) {
248
+ const fromIn = fileSet.has(edge.from);
249
+ const toIn = fileSet.has(edge.to);
250
+ if (fromIn && toIn) internalEdges++;
251
+ else if (fromIn || toIn) externalEdges++;
252
+ }
253
+ const totalEdges = internalEdges + externalEdges;
254
+ const cohesion = totalEdges > 0 ? internalEdges / totalEdges : 0;
255
+ const totalTokens = groupFiles.reduce((s, f) => s + (tokenMap.get(f) ?? 0), 0);
256
+ clusters.push({
257
+ id: name.replace(/[^a-zA-Z0-9]/g, "-") || `cluster-${clusters.length}`,
258
+ name: name || `cluster-${clusters.length}`,
259
+ files: groupFiles,
260
+ totalTokens,
261
+ internalEdges,
262
+ externalEdges,
263
+ cohesion: Math.round(cohesion * 100) / 100
264
+ });
265
+ }
266
+ return clusters.sort((a, b) => b.files.length - a.files.length);
267
+ }
268
+ function commonPrefix(paths) {
269
+ if (paths.length === 0) return "";
270
+ const parts = paths.map((p) => p.split("/"));
271
+ const prefix = [];
272
+ for (let i = 0; i < parts[0].length - 1; i++) {
273
+ const segment = parts[0][i];
274
+ if (parts.every((p) => p[i] === segment)) {
275
+ prefix.push(segment);
276
+ } else break;
277
+ }
278
+ return prefix.join("/") || parts[0][0];
279
+ }
280
+ function enrichComplexity(project, absPath, files) {
281
+ const fileMap = new Map(files.map((f) => [f.relativePath, f]));
282
+ for (const sourceFile of project.getSourceFiles()) {
283
+ const relPath = relative(absPath, sourceFile.getFilePath());
284
+ if (relPath.startsWith("..") || relPath.includes("node_modules")) continue;
285
+ const file = fileMap.get(relPath);
286
+ if (!file) continue;
287
+ let totalComplexity = 0;
288
+ for (const func of sourceFile.getFunctions()) {
289
+ totalComplexity += calculateCyclomaticComplexity(func);
290
+ }
291
+ for (const cls of sourceFile.getClasses()) {
292
+ for (const method of cls.getMethods()) {
293
+ totalComplexity += calculateCyclomaticComplexity(method);
294
+ }
295
+ }
296
+ for (const varDecl of sourceFile.getVariableDeclarations()) {
297
+ const init = varDecl.getInitializer();
298
+ if (init && (init.getKind() === SyntaxKind.ArrowFunction || init.getKind() === SyntaxKind.FunctionExpression)) {
299
+ totalComplexity += calculateCyclomaticComplexity(init);
300
+ }
301
+ }
302
+ file.complexity = Math.max(1, totalComplexity);
303
+ }
304
+ }
305
+ function calculateCyclomaticComplexity(node) {
306
+ let complexity = 1;
307
+ node.forEachDescendant((descendant) => {
308
+ switch (descendant.getKind()) {
309
+ case SyntaxKind.IfStatement:
310
+ case SyntaxKind.ConditionalExpression:
311
+ case SyntaxKind.ForStatement:
312
+ case SyntaxKind.ForInStatement:
313
+ case SyntaxKind.ForOfStatement:
314
+ case SyntaxKind.WhileStatement:
315
+ case SyntaxKind.DoStatement:
316
+ case SyntaxKind.CaseClause:
317
+ case SyntaxKind.CatchClause:
318
+ complexity++;
319
+ break;
320
+ case SyntaxKind.BinaryExpression: {
321
+ const opToken = descendant.getOperatorToken?.();
322
+ if (opToken) {
323
+ const kind = opToken.getKind();
324
+ if (kind === SyntaxKind.AmpersandAmpersandToken || kind === SyntaxKind.BarBarToken || kind === SyntaxKind.QuestionQuestionToken) {
325
+ complexity++;
326
+ }
327
+ }
328
+ break;
329
+ }
330
+ }
331
+ });
332
+ return complexity;
333
+ }
334
+ function resolveImport(sourceFile, moduleSpecifier, projectRoot) {
335
+ if (!moduleSpecifier.startsWith(".")) return null;
336
+ const sourceDir = dirname(sourceFile.getFilePath());
337
+ const basePath = resolve(sourceDir, moduleSpecifier);
338
+ const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
339
+ for (const ext of extensions) {
340
+ const candidate = basePath.endsWith(ext) ? basePath : basePath + ext;
341
+ if (existsSync(candidate)) {
342
+ const rel = relative(projectRoot, candidate);
343
+ if (!rel.startsWith("..")) return rel;
344
+ }
345
+ }
346
+ if (moduleSpecifier.endsWith(".js")) {
347
+ const tsPath = basePath.replace(/\.js$/, ".ts");
348
+ if (existsSync(tsPath)) {
349
+ const rel = relative(projectRoot, tsPath);
350
+ if (!rel.startsWith("..")) return rel;
351
+ }
352
+ }
353
+ return null;
354
+ }
355
+ function emptyGraph(files) {
356
+ return {
357
+ nodes: files.map((f) => f.relativePath),
358
+ edges: [],
359
+ hubs: [],
360
+ leaves: [],
361
+ orphans: files.map((f) => f.relativePath),
362
+ clusters: []
363
+ };
364
+ }
365
+
366
+ // src/engine/risk.ts
367
+ function scoreAllFiles(files, graph, weights = DEFAULT_RISK_WEIGHTS) {
368
+ const typeProviderUsage = computeTypeProviderUsage(files, graph);
369
+ for (const file of files) {
370
+ const factors = computeRiskFactors(file, graph, typeProviderUsage, weights);
371
+ file.riskFactors = factors;
372
+ file.riskScore = computeWeightedScore(factors);
373
+ file.exclusionImpact = scoreToImpact(file.riskScore);
374
+ }
375
+ }
376
+ function computeRiskFactors(file, graph, typeProviderUsage, weights) {
377
+ const factors = [];
378
+ factors.push(computeHubFactor(file, weights.hub));
379
+ factors.push(computeTypeProviderFactor(file, typeProviderUsage, weights.typeProvider));
380
+ factors.push(computeComplexityFactor(file, weights.complexity));
381
+ factors.push(computeRecencyFactor(file, weights.recency));
382
+ factors.push(computeConfigFactor(file, weights.config));
383
+ factors.push(computeChurnFactor(file, weights.churn));
384
+ return factors;
385
+ }
386
+ function computeHubFactor(file, weight) {
387
+ const dependents = file.importedBy.length;
388
+ const K = 12;
389
+ const score = dependents === 0 ? 0 : Math.min(100, Math.round(100 * Math.log2(1 + dependents) / Math.log2(1 + K)));
390
+ const detail = dependents === 0 ? "No dependents" : `Hub: ${dependents} file(s) depend on this (score ${score}/100)`;
391
+ return { type: "hub", score, weight, detail };
392
+ }
393
+ function computeTypeProviderFactor(file, usage, weight) {
394
+ const isTypeFile = file.kind === "type";
395
+ const consumers = usage.get(file.relativePath) ?? 0;
396
+ let score;
397
+ let detail;
398
+ if (isTypeFile && consumers >= 4) {
399
+ score = 100;
400
+ detail = `Type provider: used by ${consumers} files (critical type source)`;
401
+ } else if (isTypeFile && consumers >= 1) {
402
+ score = 50;
403
+ detail = `Type provider: used by ${consumers} files`;
404
+ } else if (isTypeFile) {
405
+ score = 30;
406
+ detail = "Type file (no detected consumers)";
407
+ } else {
408
+ score = 0;
409
+ detail = "Not a type provider";
410
+ }
411
+ return { type: "type-provider", score, weight, detail };
412
+ }
413
+ function computeComplexityFactor(file, weight) {
414
+ const c = file.complexity;
415
+ const K = 30;
416
+ const score = Math.min(100, Math.round(100 * Math.log(1 + c) / Math.log(1 + K)));
417
+ const detail = c >= 30 ? `Very high complexity: ${c} (AI needs full context)` : c >= 10 ? `High complexity: ${c}` : `Complexity: ${c}`;
418
+ return { type: "complexity", score, weight, detail };
419
+ }
420
+ function computeRecencyFactor(file, weight) {
421
+ const now = Date.now();
422
+ const modified = new Date(file.lastModified).getTime();
423
+ const daysAgo = (now - modified) / (1e3 * 60 * 60 * 24);
424
+ const HALF_LIFE = 7;
425
+ const score = Math.round(100 * Math.pow(2, -daysAgo / HALF_LIFE));
426
+ const detail = daysAgo <= 1 ? "Modified today" : `Modified ${Math.round(daysAgo)} days ago (decay score ${score})`;
427
+ return { type: "recency", score, weight, detail };
428
+ }
429
+ function computeConfigFactor(file, weight) {
430
+ let score;
431
+ let detail;
432
+ if (file.kind === "entry") {
433
+ score = 90;
434
+ detail = "Entry point \u2014 critical for understanding app structure";
435
+ } else if (file.kind === "config") {
436
+ score = 80;
437
+ detail = "Configuration file \u2014 affects runtime behavior";
438
+ } else {
439
+ score = 0;
440
+ detail = "Regular source file";
441
+ }
442
+ return { type: "config", score, weight, detail };
443
+ }
444
+ function computeChurnFactor(file, weight) {
445
+ const complexitySignal = Math.min(file.complexity / 20, 1);
446
+ const now = Date.now();
447
+ const daysAgo = (now - new Date(file.lastModified).getTime()) / (1e3 * 60 * 60 * 24);
448
+ const recencySignal = Math.pow(2, -daysAgo / 7);
449
+ const score = Math.round(Math.sqrt(complexitySignal * recencySignal) * 100);
450
+ const detail = score >= 50 ? "Likely under active development (complex + recent)" : score >= 20 ? "Some recent activity" : "Stable \u2014 low churn (proxy estimate)";
451
+ return { type: "churn", score, weight, detail };
452
+ }
453
+ function computeWeightedScore(factors) {
454
+ let totalWeightedScore = 0;
455
+ let totalWeight = 0;
456
+ for (const factor of factors) {
457
+ totalWeightedScore += factor.score * factor.weight;
458
+ totalWeight += factor.weight;
459
+ }
460
+ if (totalWeight === 0) return 0;
461
+ return Math.round(totalWeightedScore / totalWeight);
462
+ }
463
+ function scoreToImpact(score) {
464
+ if (score >= 80) return "critical";
465
+ if (score >= 60) return "high";
466
+ if (score >= 30) return "medium";
467
+ if (score > 0) return "low";
468
+ return "none";
469
+ }
470
+ function computeTypeProviderUsage(files, graph) {
471
+ const usage = /* @__PURE__ */ new Map();
472
+ const typeFiles = new Set(
473
+ files.filter((f) => f.kind === "type").map((f) => f.relativePath)
474
+ );
475
+ for (const edge of graph.edges) {
476
+ if (typeFiles.has(edge.to)) {
477
+ usage.set(edge.to, (usage.get(edge.to) ?? 0) + 1);
478
+ }
479
+ }
480
+ return usage;
481
+ }
482
+
483
+ // src/engine/analyzer.ts
484
+ function matchesPattern(filename, patterns) {
485
+ for (const pattern of patterns) {
486
+ if (pattern.startsWith("*.")) {
487
+ const ext = pattern.slice(1);
488
+ if (filename.endsWith(ext)) return true;
489
+ } else if (filename === pattern) {
490
+ return true;
491
+ }
492
+ }
493
+ return false;
494
+ }
495
+ async function walkProject(rootPath, options) {
496
+ const results = [];
497
+ const { ignoreDirs, ignorePatterns, extensions, maxDepth = 20 } = options;
498
+ const ignoreDirSet = new Set(ignoreDirs);
499
+ async function walk(dir, depth) {
500
+ if (depth > maxDepth) return;
501
+ let entries;
502
+ try {
503
+ entries = await readdir(dir, { withFileTypes: true });
504
+ } catch {
505
+ return;
506
+ }
507
+ const promises = [];
508
+ for (const entry of entries) {
509
+ const fullPath = join2(dir, entry.name);
510
+ if (entry.isDirectory()) {
511
+ if (!ignoreDirSet.has(entry.name) && !entry.name.startsWith(".")) {
512
+ promises.push(walk(fullPath, depth + 1));
513
+ }
514
+ } else if (entry.isFile()) {
515
+ const ext = extname(entry.name).slice(1).toLowerCase();
516
+ if (ext && extensions.includes(ext) && !matchesPattern(entry.name, ignorePatterns)) {
517
+ promises.push(
518
+ (async () => {
519
+ const fileStat = await stat2(fullPath).catch(() => null);
520
+ if (!fileStat) return;
521
+ let lines = 0;
522
+ try {
523
+ const content = await readFile2(fullPath, "utf-8");
524
+ lines = content.split("\n").length;
525
+ } catch {
526
+ lines = 0;
527
+ }
528
+ results.push({
529
+ path: fullPath,
530
+ relativePath: relative2(rootPath, fullPath),
531
+ extension: ext,
532
+ size: fileStat.size,
533
+ lastModified: fileStat.mtime,
534
+ lines
535
+ });
536
+ })()
537
+ );
538
+ }
539
+ }
540
+ }
541
+ await Promise.all(promises);
542
+ }
543
+ await walk(rootPath, 0);
544
+ return results;
545
+ }
546
+ var TYPE_PATTERNS = [/types?\//i, /\.d\.ts$/, /interfaces?\//i];
547
+ var TEST_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//];
548
+ var CONFIG_PATTERNS = [/\.config\.[jt]s$/, /rc\.[jt]s$/, /\.env/, /tsconfig/, /package\.json$/, /\.yml$/, /\.yaml$/, /\.toml$/];
549
+ var ENTRY_PATTERNS = [/^index\.[jt]sx?$/, /^main\.[jt]sx?$/, /^app\.[jt]sx?$/, /^server\.[jt]sx?$/];
550
+ function classifyFileKind(relativePath) {
551
+ const filename = basename2(relativePath);
552
+ if (TYPE_PATTERNS.some((p) => p.test(relativePath))) return "type";
553
+ if (TEST_PATTERNS.some((p) => p.test(relativePath))) return "test";
554
+ if (CONFIG_PATTERNS.some((p) => p.test(relativePath) || p.test(filename))) return "config";
555
+ if (ENTRY_PATTERNS.some((p) => p.test(filename))) return "entry";
556
+ return "source";
557
+ }
558
+ function detectStack(files) {
559
+ const stack = [];
560
+ const extensions = new Set(files.map((f) => f.extension));
561
+ const paths = files.map((f) => f.relativePath.toLowerCase());
562
+ if (extensions.has("ts") || extensions.has("tsx")) stack.push("TypeScript");
563
+ else if (extensions.has("js") || extensions.has("jsx")) stack.push("JavaScript");
564
+ if (extensions.has("py")) stack.push("Python");
565
+ if (extensions.has("go")) stack.push("Go");
566
+ if (extensions.has("rs")) stack.push("Rust");
567
+ if (extensions.has("java")) stack.push("Java");
568
+ if (extensions.has("kt")) stack.push("Kotlin");
569
+ if (extensions.has("rb")) stack.push("Ruby");
570
+ if (extensions.has("php")) stack.push("PHP");
571
+ if (extensions.has("cs")) stack.push("C#");
572
+ if (extensions.has("c") || extensions.has("cpp")) stack.push("C/C++");
573
+ if (paths.some((p) => p.includes("next.config"))) stack.push("Next.js");
574
+ if (paths.some((p) => p.includes("nuxt.config"))) stack.push("Nuxt");
575
+ if (paths.some((p) => p.includes("angular.json"))) stack.push("Angular");
576
+ return stack;
577
+ }
578
+ async function analyzeProject(projectPath, config) {
579
+ const absPath = resolve2(projectPath);
580
+ const projectName = basename2(absPath);
581
+ const mergedConfig = mergeConfig(DEFAULT_CONFIG, config);
582
+ const allExtensions = [
583
+ ...mergedConfig.analysis.extensions.code,
584
+ ...mergedConfig.analysis.extensions.config,
585
+ ...mergedConfig.analysis.extensions.docs
586
+ ];
587
+ const walkEntries = await walkProject(absPath, {
588
+ ignoreDirs: mergedConfig.analysis.ignore.dirs,
589
+ ignorePatterns: mergedConfig.analysis.ignore.patterns,
590
+ extensions: allExtensions,
591
+ maxDepth: mergedConfig.analysis.maxDepth
592
+ });
593
+ const tokenMethod = mergedConfig.tokens.method;
594
+ const files = [];
595
+ for (const entry of walkEntries) {
596
+ let tokens;
597
+ if (tokenMethod === "tiktoken") {
598
+ try {
599
+ const content = await readFile2(entry.path, "utf-8");
600
+ tokens = estimateTokens(content, entry.size, "tiktoken");
601
+ } catch {
602
+ tokens = countTokensChars4(entry.size);
603
+ }
604
+ } else {
605
+ tokens = countTokensChars4(entry.size);
606
+ }
607
+ files.push({
608
+ path: entry.path,
609
+ relativePath: entry.relativePath,
610
+ extension: entry.extension,
611
+ size: entry.size,
612
+ tokens,
613
+ lines: entry.lines,
614
+ lastModified: entry.lastModified,
615
+ kind: classifyFileKind(entry.relativePath),
616
+ // Graph data — populated by graph analysis
617
+ imports: [],
618
+ importedBy: [],
619
+ isHub: false,
620
+ complexity: 0,
621
+ // Risk data — populated by risk analysis
622
+ riskScore: 0,
623
+ riskFactors: [],
624
+ exclusionImpact: "none"
625
+ });
626
+ }
627
+ const graph = buildProjectGraph(absPath, files);
628
+ for (const file of files) {
629
+ const nodeImports = [];
630
+ const nodeImportedBy = [];
631
+ for (const edge of graph.edges) {
632
+ if (edge.from === file.relativePath) nodeImports.push(edge.to);
633
+ if (edge.to === file.relativePath) nodeImportedBy.push(edge.from);
634
+ }
635
+ file.imports = nodeImports;
636
+ file.importedBy = nodeImportedBy;
637
+ file.isHub = graph.hubs.some((h) => h.relativePath === file.relativePath);
638
+ }
639
+ const riskWeights = mergedConfig.risk.weights;
640
+ scoreAllFiles(files, graph, riskWeights);
641
+ const riskProfile = {
642
+ distribution: {
643
+ critical: files.filter((f) => f.riskScore >= 80).length,
644
+ high: files.filter((f) => f.riskScore >= 60 && f.riskScore < 80).length,
645
+ medium: files.filter((f) => f.riskScore >= 30 && f.riskScore < 60).length,
646
+ low: files.filter((f) => f.riskScore < 30).length
647
+ },
648
+ topRiskFiles: [...files].sort((a, b) => b.riskScore - a.riskScore).slice(0, 10),
649
+ overallComplexity: files.length > 0 ? files.reduce((s, f) => s + f.complexity, 0) / files.length : 0
650
+ };
651
+ const totalTokens = files.reduce((s, f) => s + f.tokens, 0);
652
+ const hashInput = files.map((f) => `${f.relativePath}:${f.tokens}:${f.riskScore}`).sort().join("|");
653
+ const hash = createHash("sha256").update(hashInput).digest("hex").substring(0, 16);
654
+ const stack = detectStack(walkEntries);
655
+ return {
656
+ projectPath: absPath,
657
+ projectName,
658
+ analyzedAt: /* @__PURE__ */ new Date(),
659
+ hash,
660
+ files,
661
+ totalFiles: files.length,
662
+ totalTokens,
663
+ graph,
664
+ riskProfile,
665
+ stack,
666
+ tokenMethod
667
+ };
668
+ }
669
+ function mergeConfig(base, overrides) {
670
+ if (!overrides) return base;
671
+ return {
672
+ ...base,
673
+ ...overrides,
674
+ analysis: {
675
+ ...base.analysis,
676
+ ...overrides.analysis,
677
+ extensions: {
678
+ ...base.analysis.extensions,
679
+ ...overrides.analysis?.extensions
680
+ },
681
+ ignore: {
682
+ ...base.analysis.ignore,
683
+ ...overrides.analysis?.ignore
684
+ }
685
+ },
686
+ risk: {
687
+ ...base.risk,
688
+ ...overrides.risk,
689
+ weights: {
690
+ ...base.risk.weights,
691
+ ...overrides.risk?.weights
692
+ }
693
+ },
694
+ interaction: {
695
+ ...base.interaction,
696
+ ...overrides.interaction
697
+ },
698
+ tokens: {
699
+ ...base.tokens,
700
+ ...overrides.tokens
701
+ },
702
+ governance: {
703
+ ...base.governance,
704
+ ...overrides.governance
705
+ }
706
+ };
707
+ }
708
+
709
+ // src/engine/selector.ts
710
+ import { createHash as createHash2 } from "crypto";
711
+
712
+ // src/govern/secrets.ts
713
+ import { readFile as readFile3 } from "fs/promises";
714
+ import { resolve as resolve3, relative as relative3 } from "path";
715
+ var BUILTIN_PATTERNS = [
716
+ // API Keys
717
+ { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
718
+ { type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
719
+ { type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
720
+ // AWS
721
+ { type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
722
+ { type: "aws-key", source: `(?:aws_secret_access_key|aws_secret)\\s*[:=]\\s*['"]?([a-zA-Z0-9/+=]{40})['"]?`, flags: "gi", severity: "critical", description: "AWS Secret Key" },
723
+ // Private Keys
724
+ { type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
725
+ { type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
726
+ // Passwords
727
+ { type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
728
+ { type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
729
+ // Tokens
730
+ { type: "token", source: `(?:bearer|token|auth_token|access_token|refresh_token)\\s*[:=]\\s*['"]([a-zA-Z0-9_\\-.]{20,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Auth Token" },
731
+ { type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
732
+ { type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
733
+ { type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
734
+ { type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
735
+ // Connection strings
736
+ { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
737
+ { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
738
+ // Environment variables with secrets
739
+ { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
740
+ ];
741
+ function buildPatterns(customPatterns = []) {
742
+ const patterns = BUILTIN_PATTERNS.map((def) => ({
743
+ type: def.type,
744
+ pattern: new RegExp(def.source, def.flags),
745
+ severity: def.severity,
746
+ description: def.description
747
+ }));
748
+ for (const custom of customPatterns) {
749
+ try {
750
+ patterns.push({
751
+ type: "custom",
752
+ pattern: new RegExp(custom, "gi"),
753
+ severity: "medium",
754
+ description: `Custom pattern: ${custom}`
755
+ });
756
+ } catch {
757
+ }
758
+ }
759
+ return patterns;
760
+ }
761
+ function scanContentForSecrets(content, filePath, customPatterns = []) {
762
+ const findings = [];
763
+ const lines = content.split("\n");
764
+ const allPatterns = buildPatterns(customPatterns);
765
+ for (const secretPattern of allPatterns) {
766
+ for (let i = 0; i < lines.length; i++) {
767
+ const line = lines[i];
768
+ secretPattern.pattern.lastIndex = 0;
769
+ let match;
770
+ while ((match = secretPattern.pattern.exec(line)) !== null) {
771
+ const matchText = match[0];
772
+ if (isTemplateOrPlaceholder(matchText)) continue;
773
+ findings.push({
774
+ type: secretPattern.type,
775
+ file: filePath,
776
+ line: i + 1,
777
+ match: matchText,
778
+ redacted: redactSecret(matchText),
779
+ severity: secretPattern.severity
780
+ });
781
+ }
782
+ }
783
+ }
784
+ return deduplicateFindings(findings);
785
+ }
786
+ async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
787
+ try {
788
+ const content = await readFile3(filePath, "utf-8");
789
+ const relPath = relative3(resolve3(projectPath), resolve3(filePath));
790
+ return scanContentForSecrets(content, relPath, customPatterns);
791
+ } catch {
792
+ return [];
793
+ }
794
+ }
795
+ function redactSecret(value) {
796
+ if (value.length <= 8) return "***REDACTED***";
797
+ const prefix = value.substring(0, 4);
798
+ const suffix = value.substring(value.length - 2);
799
+ return `${prefix}${"*".repeat(Math.min(value.length - 6, 20))}${suffix}`;
800
+ }
801
+ function isTemplateOrPlaceholder(value) {
802
+ const placeholders = [
803
+ /\$\{.*\}/,
804
+ /\{\{.*\}\}/,
805
+ /%[sd]/,
806
+ /<[A-Z_]+>/,
807
+ /YOUR_.*_HERE/i,
808
+ /\bCHANGE_ME\b/i,
809
+ /\bPLACEHOLDER\b/i,
810
+ /\bexample\b/i,
811
+ /\bTODO\b/i,
812
+ /xxx+/i,
813
+ /\breplace.?me\b/i,
814
+ /\bdummy\b/i,
815
+ /\btest_?key\b/i,
816
+ /\bsample\b/i
817
+ ];
818
+ return placeholders.some((p) => p.test(value));
819
+ }
820
+ function deduplicateFindings(findings) {
821
+ const seen = /* @__PURE__ */ new Set();
822
+ return findings.filter((f) => {
823
+ const key = `${f.file}:${f.line}:${f.type}:${f.match}`;
824
+ if (seen.has(key)) return false;
825
+ seen.add(key);
826
+ return true;
827
+ });
828
+ }
829
+
830
+ // src/engine/pruner.ts
831
+ import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
832
+ import { readFile as readFile4 } from "fs/promises";
833
+ import { existsSync as existsSync2 } from "fs";
834
+ import { join as join3 } from "path";
835
+ var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
836
+ async function pruneFile(file, level) {
837
+ if (level === "excluded") {
838
+ return emptyResult(file, "excluded");
839
+ }
840
+ if (level === "full") {
841
+ return fullContent(file);
842
+ }
843
+ const ext = file.extension.toLowerCase();
844
+ const isTS = TS_EXTENSIONS2.has(ext);
845
+ if (isTS) {
846
+ return pruneTypeScript(file, level);
847
+ }
848
+ return pruneGeneric(file, level);
849
+ }
850
+ async function pruneTypeScript(file, level) {
851
+ let content;
852
+ try {
853
+ content = await readFile4(file.path, "utf-8");
854
+ } catch {
855
+ return emptyResult(file, level);
856
+ }
857
+ let project;
858
+ try {
859
+ const tsConfigPath = findTsConfig(file.path);
860
+ project = new Project2({
861
+ tsConfigFilePath: tsConfigPath,
862
+ skipAddingFilesFromTsConfig: true,
863
+ compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
864
+ });
865
+ project.createSourceFile(file.path, content, { overwrite: true });
866
+ } catch {
867
+ return pruneGenericFromContent(file, content, level);
868
+ }
869
+ const sourceFile = project.getSourceFiles()[0];
870
+ if (!sourceFile) {
871
+ return pruneGenericFromContent(file, content, level);
872
+ }
873
+ const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
874
+ const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
875
+ const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
876
+ return {
877
+ relativePath: file.relativePath,
878
+ originalTokens: file.tokens,
879
+ prunedTokens,
880
+ pruneLevel: level,
881
+ content: prunedContent,
882
+ savingsPercent: Math.max(0, savingsPercent)
883
+ };
884
+ }
885
+ function extractSignaturesAST(sf) {
886
+ const parts = [];
887
+ for (const imp of sf.getImportDeclarations()) {
888
+ parts.push(imp.getText());
889
+ }
890
+ if (parts.length > 0) parts.push("");
891
+ for (const ta of sf.getTypeAliases()) {
892
+ addJSDoc(ta, parts);
893
+ parts.push(ta.getText());
894
+ }
895
+ for (const iface of sf.getInterfaces()) {
896
+ addJSDoc(iface, parts);
897
+ parts.push(iface.getText());
898
+ }
899
+ for (const en of sf.getEnums()) {
900
+ addJSDoc(en, parts);
901
+ parts.push(en.getText());
902
+ }
903
+ for (const fn of sf.getFunctions()) {
904
+ addJSDoc(fn, parts);
905
+ const isExported = fn.isExported();
906
+ const isAsync = fn.isAsync();
907
+ const name = fn.getName() ?? "<anonymous>";
908
+ const params = fn.getParameters().map((p) => p.getText()).join(", ");
909
+ const returnType = fn.getReturnTypeNode()?.getText();
910
+ const returnStr = returnType ? `: ${returnType}` : "";
911
+ const prefix = isExported ? "export " : "";
912
+ const asyncStr = isAsync ? "async " : "";
913
+ parts.push(`${prefix}${asyncStr}function ${name}(${params})${returnStr} { /* ... */ }`);
914
+ }
915
+ for (const stmt of sf.getVariableStatements()) {
916
+ for (const decl of stmt.getDeclarations()) {
917
+ const init = decl.getInitializer();
918
+ if (init && (init.getKind() === SyntaxKind2.ArrowFunction || init.getKind() === SyntaxKind2.FunctionExpression)) {
919
+ addJSDoc(stmt, parts);
920
+ const isExported = stmt.isExported();
921
+ const prefix = isExported ? "export " : "";
922
+ const kind = stmt.getDeclarationKind();
923
+ const name = decl.getName();
924
+ const typeNode = decl.getTypeNode()?.getText();
925
+ const typeStr = typeNode ? `: ${typeNode}` : "";
926
+ parts.push(`${prefix}${kind} ${name}${typeStr} = /* ... */;`);
927
+ } else {
928
+ addJSDoc(stmt, parts);
929
+ parts.push(stmt.getText());
930
+ }
931
+ }
932
+ }
933
+ for (const cls of sf.getClasses()) {
934
+ addJSDoc(cls, parts);
935
+ const isExported = cls.isExported();
936
+ const prefix = isExported ? "export " : "";
937
+ const name = cls.getName() ?? "<anonymous>";
938
+ const ext = cls.getExtends()?.getText();
939
+ const impl = cls.getImplements().map((i) => i.getText()).join(", ");
940
+ let header = `${prefix}class ${name}`;
941
+ if (ext) header += ` extends ${ext}`;
942
+ if (impl) header += ` implements ${impl}`;
943
+ header += " {";
944
+ parts.push(header);
945
+ for (const prop of cls.getProperties()) {
946
+ parts.push(` ${prop.getText()}`);
947
+ }
948
+ const ctor = cls.getConstructors()[0];
949
+ if (ctor) {
950
+ const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
951
+ parts.push(` constructor(${ctorParams}) { /* ... */ }`);
952
+ }
953
+ for (const method of cls.getMethods()) {
954
+ const isStatic = method.isStatic();
955
+ const isAsync = method.isAsync();
956
+ const methodName = method.getName();
957
+ const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
958
+ const returnType = method.getReturnTypeNode()?.getText();
959
+ const returnStr = returnType ? `: ${returnType}` : "";
960
+ const staticStr = isStatic ? "static " : "";
961
+ const asyncStr = isAsync ? "async " : "";
962
+ parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
963
+ }
964
+ parts.push("}");
965
+ }
966
+ for (const exp of sf.getExportDeclarations()) {
967
+ parts.push(exp.getText());
968
+ }
969
+ for (const exp of sf.getExportAssignments()) {
970
+ parts.push(exp.getText());
971
+ }
972
+ return parts.join("\n");
973
+ }
974
+ function extractSkeletonAST(sf) {
975
+ const parts = [];
976
+ for (const imp of sf.getImportDeclarations()) {
977
+ parts.push(imp.getText());
978
+ }
979
+ if (parts.length > 0) parts.push("");
980
+ for (const ta of sf.getTypeAliases()) {
981
+ if (ta.isExported()) parts.push(ta.getText());
982
+ }
983
+ for (const iface of sf.getInterfaces()) {
984
+ if (!iface.isExported()) continue;
985
+ const ext = iface.getExtends().map((e) => e.getText());
986
+ const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
987
+ parts.push(`export interface ${iface.getName()}${extStr} { /* ${iface.getProperties().length} props */ }`);
988
+ }
989
+ for (const en of sf.getEnums()) {
990
+ if (!en.isExported()) continue;
991
+ const members = en.getMembers().map((m) => m.getName());
992
+ parts.push(`export enum ${en.getName()} { ${members.join(", ")} }`);
993
+ }
994
+ for (const fn of sf.getFunctions()) {
995
+ if (!fn.isExported()) continue;
996
+ const name = fn.getName() ?? "<anonymous>";
997
+ const params = fn.getParameters().map((p) => p.getText()).join(", ");
998
+ parts.push(`export function ${name}(${params});`);
999
+ }
1000
+ for (const cls of sf.getClasses()) {
1001
+ if (!cls.isExported()) continue;
1002
+ const methods = cls.getMethods().map((m) => m.getName());
1003
+ parts.push(`export class ${cls.getName()} { /* methods: ${methods.join(", ")} */ }`);
1004
+ }
1005
+ for (const exp of sf.getExportDeclarations()) {
1006
+ parts.push(exp.getText());
1007
+ }
1008
+ return parts.join("\n");
1009
+ }
1010
+ async function pruneGeneric(file, level) {
1011
+ let content;
1012
+ try {
1013
+ content = await readFile4(file.path, "utf-8");
1014
+ } catch {
1015
+ return emptyResult(file, level);
1016
+ }
1017
+ return pruneGenericFromContent(file, content, level);
1018
+ }
1019
+ function pruneGenericFromContent(file, content, level) {
1020
+ const lines = content.split("\n");
1021
+ let result;
1022
+ if (level === "signatures") {
1023
+ result = lines.filter((line) => {
1024
+ const t = line.trim();
1025
+ return t === "" || t.startsWith("#") || t.startsWith("//") || t.startsWith("import ") || t.startsWith("from ") || t.startsWith("export ") || t.startsWith("def ") || t.startsWith("async def ") || t.startsWith("class ") || t.startsWith("function ") || t.startsWith("const ") || t.startsWith("let ") || t.startsWith("var ") || /^(pub |fn |struct |enum |impl |mod |use )/.test(t);
1026
+ });
1027
+ } else {
1028
+ result = lines.filter((line) => {
1029
+ const t = line.trim();
1030
+ return t.startsWith("import ") || t.startsWith("from ") || t.startsWith("export ") || t.startsWith("def ") || t.startsWith("class ") || t.startsWith("function ") || /^(pub |fn |struct |enum |mod |use )/.test(t);
1031
+ });
1032
+ }
1033
+ const prunedContent = result.join("\n");
1034
+ const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
1035
+ const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
1036
+ return {
1037
+ relativePath: file.relativePath,
1038
+ originalTokens: file.tokens,
1039
+ prunedTokens,
1040
+ pruneLevel: level,
1041
+ content: prunedContent,
1042
+ savingsPercent: Math.max(0, savingsPercent)
1043
+ };
1044
+ }
1045
+ async function fullContent(file) {
1046
+ let content = "";
1047
+ try {
1048
+ content = await readFile4(file.path, "utf-8");
1049
+ } catch {
1050
+ }
1051
+ return {
1052
+ relativePath: file.relativePath,
1053
+ originalTokens: file.tokens,
1054
+ prunedTokens: file.tokens,
1055
+ pruneLevel: "full",
1056
+ content,
1057
+ savingsPercent: 0
1058
+ };
1059
+ }
1060
+ function emptyResult(file, level) {
1061
+ return {
1062
+ relativePath: file.relativePath,
1063
+ originalTokens: file.tokens,
1064
+ prunedTokens: 0,
1065
+ pruneLevel: level,
1066
+ content: "",
1067
+ savingsPercent: 100
1068
+ };
1069
+ }
1070
+ function addJSDoc(node, parts) {
1071
+ if (!node.getJsDocs) return;
1072
+ const docs = node.getJsDocs();
1073
+ if (docs.length > 0) {
1074
+ parts.push(docs[0].getText());
1075
+ }
1076
+ }
1077
+ function findTsConfig(filePath) {
1078
+ let dir = filePath;
1079
+ for (let i = 0; i < 10; i++) {
1080
+ dir = join3(dir, "..");
1081
+ const candidate = join3(dir, "tsconfig.json");
1082
+ if (existsSync2(candidate)) return candidate;
1083
+ }
1084
+ return void 0;
1085
+ }
1086
+
1087
+ // src/engine/graph-utils.ts
1088
+ function buildAdjacencyList(edges) {
1089
+ const forward = /* @__PURE__ */ new Map();
1090
+ const reverse = /* @__PURE__ */ new Map();
1091
+ for (const edge of edges) {
1092
+ if (!forward.has(edge.from)) forward.set(edge.from, []);
1093
+ forward.get(edge.from).push(edge.to);
1094
+ if (!reverse.has(edge.to)) reverse.set(edge.to, []);
1095
+ reverse.get(edge.to).push(edge.from);
1096
+ }
1097
+ return { forward, reverse };
1098
+ }
1099
+ function bfsBidirectional(seeds, adj, depth) {
1100
+ const result = new Set(seeds);
1101
+ let frontier = [...seeds];
1102
+ const visited = /* @__PURE__ */ new Set();
1103
+ for (let d = 0; d < depth; d++) {
1104
+ const nextFrontier = [];
1105
+ for (const node of frontier) {
1106
+ if (visited.has(node)) continue;
1107
+ visited.add(node);
1108
+ const fwd = adj.forward.get(node);
1109
+ if (fwd) {
1110
+ for (const neighbor of fwd) {
1111
+ if (!visited.has(neighbor)) {
1112
+ result.add(neighbor);
1113
+ nextFrontier.push(neighbor);
1114
+ }
1115
+ }
1116
+ }
1117
+ const rev = adj.reverse.get(node);
1118
+ if (rev) {
1119
+ for (const neighbor of rev) {
1120
+ if (!visited.has(neighbor)) {
1121
+ result.add(neighbor);
1122
+ nextFrontier.push(neighbor);
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ frontier = nextFrontier;
1128
+ }
1129
+ return result;
1130
+ }
1131
+ function matchGlob(path, pattern) {
1132
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\xA7\xA7").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, ".");
1133
+ try {
1134
+ return new RegExp(`^${regexStr}$`).test(path);
1135
+ } catch {
1136
+ return false;
1137
+ }
1138
+ }
1139
+
1140
+ // src/engine/coverage.ts
1141
+ function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
1142
+ const adj = buildAdjacencyList(graph.edges);
1143
+ const relevantSet = targetPaths.length > 0 ? bfsBidirectional(targetPaths, adj, depth) : /* @__PURE__ */ new Set();
1144
+ const includedSet = new Set(includedPaths);
1145
+ const tempFileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
1146
+ for (const path of includedPaths) {
1147
+ const file = tempFileMap.get(path);
1148
+ if (!file) continue;
1149
+ for (const imp of file.imports) {
1150
+ const impFile = tempFileMap.get(imp);
1151
+ if (impFile && impFile.kind === "type") {
1152
+ relevantSet.add(imp);
1153
+ }
1154
+ }
1155
+ }
1156
+ const relevantFiles = Array.from(relevantSet);
1157
+ const includedRelevant = relevantFiles.filter((f) => includedSet.has(f));
1158
+ const missingRelevant = relevantFiles.filter((f) => !includedSet.has(f));
1159
+ const missingCritical = missingRelevant.filter((f) => {
1160
+ const file = tempFileMap.get(f);
1161
+ return file && (file.exclusionImpact === "critical" || file.exclusionImpact === "high");
1162
+ });
1163
+ const fileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
1164
+ let totalRelevantRisk = 0;
1165
+ let includedRelevantRisk = 0;
1166
+ for (const f of relevantFiles) {
1167
+ const risk = fileMap.get(f)?.riskScore ?? 1;
1168
+ totalRelevantRisk += risk;
1169
+ if (includedSet.has(f)) {
1170
+ includedRelevantRisk += risk;
1171
+ }
1172
+ }
1173
+ const score = totalRelevantRisk > 0 ? Math.round(includedRelevantRisk / totalRelevantRisk * 100) : relevantFiles.length > 0 ? Math.round(includedRelevant.length / relevantFiles.length * 100) : 100;
1174
+ let explanation;
1175
+ if (score >= 90) {
1176
+ explanation = `Excellent coverage (${score}%): AI has nearly all relevant context.`;
1177
+ } else if (score >= 70) {
1178
+ explanation = `Good coverage (${score}%): Most relevant files included.`;
1179
+ if (missingCritical.length > 0) {
1180
+ explanation += ` Warning: ${missingCritical.length} critical file(s) missing.`;
1181
+ }
1182
+ } else if (score >= 50) {
1183
+ explanation = `Partial coverage (${score}%): Significant context is missing.`;
1184
+ if (missingCritical.length > 0) {
1185
+ explanation += ` ${missingCritical.length} critical file(s) not included \u2014 AI quality will degrade.`;
1186
+ }
1187
+ } else {
1188
+ explanation = `Low coverage (${score}%): Most relevant files are excluded. AI response quality will be poor.`;
1189
+ }
1190
+ return {
1191
+ score,
1192
+ relevantFiles,
1193
+ includedRelevant,
1194
+ missingRelevant,
1195
+ missingCritical,
1196
+ explanation
1197
+ };
1198
+ }
1199
+
1200
+ // src/engine/budget.ts
1201
+ function getPruneLevelForRisk(riskScore) {
1202
+ if (riskScore >= 80) return "full";
1203
+ if (riskScore >= 60) return "full";
1204
+ if (riskScore >= 30) return "signatures";
1205
+ return "skeleton";
1206
+ }
1207
+
1208
+ // src/engine/selector.ts
1209
+ async function selectContext(input) {
1210
+ const { task, analysis, budget, policies, depth = 2 } = input;
1211
+ const decisions = [];
1212
+ const targetPaths = identifyTargetFiles(task, analysis.files);
1213
+ if (targetPaths.length > 0) {
1214
+ decisions.push({
1215
+ file: targetPaths.join(", "),
1216
+ action: "include-full",
1217
+ reason: `Target file(s) identified from task description`
1218
+ });
1219
+ }
1220
+ const adj = buildAdjacencyList(analysis.graph.edges);
1221
+ const expandedPaths = targetPaths.length > 0 ? Array.from(bfsBidirectional(targetPaths, adj, depth)) : [];
1222
+ const expansionCount = expandedPaths.length - targetPaths.length;
1223
+ if (expansionCount > 0) {
1224
+ decisions.push({
1225
+ file: `${expansionCount} dependencies`,
1226
+ action: "include-full",
1227
+ reason: `Expanded ${targetPaths.length} target(s) to ${expandedPaths.length} files via dependency graph (depth ${depth})`
1228
+ });
1229
+ }
1230
+ const allFileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
1231
+ if (targetPaths.length > 0) {
1232
+ for (const path of expandedPaths) {
1233
+ const file = allFileMap.get(path);
1234
+ if (!file) continue;
1235
+ for (const imp of file.imports) {
1236
+ const impFile = allFileMap.get(imp);
1237
+ if (impFile && impFile.kind === "type") {
1238
+ expandedPaths.push(imp);
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ const { mustInclude, mustExclude } = applyPolicies(analysis.files, policies);
1244
+ const candidateSet = /* @__PURE__ */ new Set([...expandedPaths, ...mustInclude]);
1245
+ if (targetPaths.length === 0) {
1246
+ for (const f of analysis.files) {
1247
+ candidateSet.add(f.relativePath);
1248
+ }
1249
+ }
1250
+ for (const ex of mustExclude) {
1251
+ candidateSet.delete(ex);
1252
+ decisions.push({
1253
+ file: ex,
1254
+ action: "exclude",
1255
+ reason: "Excluded by policy"
1256
+ });
1257
+ }
1258
+ const hasSecretBlock = policies?.rules.some(
1259
+ (r) => r.type === "secret-block" && r.enabled
1260
+ );
1261
+ if (hasSecretBlock) {
1262
+ for (const path of Array.from(candidateSet)) {
1263
+ const file = allFileMap.get(path);
1264
+ if (!file) continue;
1265
+ const findings = await scanFileForSecrets(
1266
+ file.path,
1267
+ analysis.projectPath
1268
+ );
1269
+ if (findings.length > 0) {
1270
+ candidateSet.delete(path);
1271
+ decisions.push({
1272
+ file: path,
1273
+ action: "exclude",
1274
+ reason: `Blocked: ${findings.length} secret(s) detected (${findings.map((f) => f.type).join(", ")})`
1275
+ });
1276
+ }
1277
+ }
1278
+ }
1279
+ const candidates = Array.from(candidateSet).map((p) => allFileMap.get(p)).filter((f) => f !== void 0).sort((a, b) => {
1280
+ const aIsTarget = targetPaths.includes(a.relativePath) ? 0 : 1;
1281
+ const bIsTarget = targetPaths.includes(b.relativePath) ? 0 : 1;
1282
+ if (aIsTarget !== bIsTarget) return aIsTarget - bIsTarget;
1283
+ const aIsMust = mustInclude.has(a.relativePath) ? 0 : 1;
1284
+ const bIsMust = mustInclude.has(b.relativePath) ? 0 : 1;
1285
+ if (aIsMust !== bIsMust) return aIsMust - bIsMust;
1286
+ return b.riskScore - a.riskScore;
1287
+ });
1288
+ const selectedFiles = [];
1289
+ let usedTokens = 0;
1290
+ for (const file of candidates) {
1291
+ const isTarget = targetPaths.includes(file.relativePath);
1292
+ const isMustInclude = mustInclude.has(file.relativePath);
1293
+ const defaultLevel = isTarget ? "full" : getPruneLevelForRisk(file.riskScore);
1294
+ const levels = getCascadeLevels(defaultLevel);
1295
+ let included = false;
1296
+ for (const level of levels) {
1297
+ if (level === "excluded") break;
1298
+ let tokens;
1299
+ if (level === "full") {
1300
+ tokens = file.tokens;
1301
+ } else {
1302
+ const pruned = await pruneFile(file, level);
1303
+ tokens = pruned.prunedTokens;
1304
+ }
1305
+ if (usedTokens + tokens <= budget) {
1306
+ usedTokens += tokens;
1307
+ selectedFiles.push({
1308
+ relativePath: file.relativePath,
1309
+ tokens,
1310
+ originalTokens: file.tokens,
1311
+ pruneLevel: level,
1312
+ riskScore: file.riskScore,
1313
+ reason: buildReason(file, level, isTarget, isMustInclude)
1314
+ });
1315
+ if (level !== defaultLevel) {
1316
+ decisions.push({
1317
+ file: file.relativePath,
1318
+ action: `include-${level}`,
1319
+ reason: `Downgraded from ${defaultLevel} to ${level} due to budget constraint`,
1320
+ alternatives: `Would need ${file.tokens - tokens} more tokens for ${defaultLevel}`
1321
+ });
1322
+ }
1323
+ included = true;
1324
+ break;
1325
+ }
1326
+ }
1327
+ if (!included) {
1328
+ decisions.push({
1329
+ file: file.relativePath,
1330
+ action: "exclude",
1331
+ reason: `Budget exhausted (risk: ${file.riskScore}, needs ${file.tokens} tokens)`
1332
+ });
1333
+ }
1334
+ }
1335
+ const includedPaths = selectedFiles.map((f) => f.relativePath);
1336
+ const coverage = calculateCoverage(
1337
+ targetPaths,
1338
+ includedPaths,
1339
+ analysis.files,
1340
+ analysis.graph,
1341
+ depth
1342
+ );
1343
+ const includedSet = new Set(includedPaths);
1344
+ const excludedFiles = analysis.files.filter(
1345
+ (f) => !includedSet.has(f.relativePath)
1346
+ );
1347
+ const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
1348
+ const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
1349
+ const hash = createHash2("sha256").update(hashInput).digest("hex").substring(0, 16);
1350
+ return {
1351
+ files: selectedFiles,
1352
+ totalTokens: usedTokens,
1353
+ budget,
1354
+ usedPercent: budget > 0 ? Math.round(usedTokens / budget * 100 * 10) / 10 : 0,
1355
+ coverage,
1356
+ riskScore: excludedRisk,
1357
+ deterministic: true,
1358
+ hash,
1359
+ decisions
1360
+ };
1361
+ }
1362
+ function identifyTargetFiles(task, files) {
1363
+ const targets = [];
1364
+ const pathPattern = /(?:^|\s|["'`])([.\w/-]+\.[a-zA-Z]{1,4})(?:\s|$|["'`]|,|:)/g;
1365
+ let match;
1366
+ while ((match = pathPattern.exec(task)) !== null) {
1367
+ const candidate = match[1];
1368
+ const found = files.find(
1369
+ (f) => f.relativePath === candidate || f.relativePath.endsWith(candidate)
1370
+ );
1371
+ if (found && !targets.includes(found.relativePath)) {
1372
+ targets.push(found.relativePath);
1373
+ }
1374
+ }
1375
+ return targets;
1376
+ }
1377
+ function applyPolicies(files, policies) {
1378
+ const mustInclude = /* @__PURE__ */ new Set();
1379
+ const mustExclude = /* @__PURE__ */ new Set();
1380
+ if (!policies) return { mustInclude, mustExclude };
1381
+ for (const rule of policies.rules) {
1382
+ if (!rule.enabled) continue;
1383
+ if (rule.type === "include-always" && rule.pattern) {
1384
+ for (const file of files) {
1385
+ if (matchGlob(file.relativePath, rule.pattern)) {
1386
+ mustInclude.add(file.relativePath);
1387
+ }
1388
+ }
1389
+ }
1390
+ if (rule.type === "exclude-always" && rule.pattern) {
1391
+ for (const file of files) {
1392
+ if (matchGlob(file.relativePath, rule.pattern)) {
1393
+ mustExclude.add(file.relativePath);
1394
+ }
1395
+ }
1396
+ }
1397
+ }
1398
+ return { mustInclude, mustExclude };
1399
+ }
1400
+ function getCascadeLevels(startLevel) {
1401
+ const all = ["full", "signatures", "skeleton", "excluded"];
1402
+ const startIdx = all.indexOf(startLevel);
1403
+ return all.slice(startIdx);
1404
+ }
1405
+ function buildReason(file, level, isTarget, isMustInclude) {
1406
+ if (isTarget) return "Target file";
1407
+ if (isMustInclude) return "Required by policy";
1408
+ const impact = file.exclusionImpact;
1409
+ const levelStr = level === "full" ? "full content" : level;
1410
+ if (impact === "critical") return `Critical dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
1411
+ if (impact === "high") return `High-risk dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
1412
+ if (impact === "medium") return `Medium relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
1413
+ return `Low relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
1414
+ }
1415
+
1416
+ // src/engine/score.ts
1417
+ async function computeContextScore(analysis, task = "general code review and refactoring", budget = 5e4) {
1418
+ const selection = await selectContext({ task, analysis, budget });
1419
+ const insights = [];
1420
+ const efficiency = scoreEfficiency(analysis, selection, insights);
1421
+ const coverage = scoreCoverage(analysis, selection, insights);
1422
+ const riskControl = scoreRiskControl(analysis, selection, insights);
1423
+ const structure = scoreStructure(analysis, insights);
1424
+ const governance = scoreGovernance(analysis, insights);
1425
+ const overall = Math.round(
1426
+ efficiency.weighted + coverage.weighted + riskControl.weighted + structure.weighted + governance.weighted
1427
+ );
1428
+ const grade = scoreToGrade(overall);
1429
+ const naiveTokens = analysis.totalTokens;
1430
+ const optimizedTokens = selection.totalTokens;
1431
+ const savedTokens = naiveTokens - optimizedTokens;
1432
+ const savedPercent = naiveTokens > 0 ? Math.round(savedTokens / naiveTokens * 100) : 0;
1433
+ const interactionsPerMonth = 40 * 20;
1434
+ const costPerMToken = 3;
1435
+ const naiveMonthlyCost = naiveTokens / 1e6 * costPerMToken * interactionsPerMonth;
1436
+ const optimizedMonthlyCost = optimizedTokens / 1e6 * costPerMToken * interactionsPerMonth;
1437
+ const monthlySavingsUSD = Math.round((naiveMonthlyCost - optimizedMonthlyCost) * 100) / 100;
1438
+ return {
1439
+ overall,
1440
+ grade,
1441
+ dimensions: {
1442
+ efficiency,
1443
+ coverage,
1444
+ riskControl,
1445
+ structure,
1446
+ governance
1447
+ },
1448
+ insights: insights.sort((a, b) => {
1449
+ const order = { high: 0, medium: 1, low: 2 };
1450
+ return order[a.impact] - order[b.impact];
1451
+ }),
1452
+ comparison: {
1453
+ naiveTokens,
1454
+ optimizedTokens,
1455
+ savedTokens,
1456
+ savedPercent,
1457
+ monthlySavingsUSD
1458
+ },
1459
+ meta: {
1460
+ projectName: analysis.projectName,
1461
+ totalFiles: analysis.totalFiles,
1462
+ totalTokens: analysis.totalTokens,
1463
+ analyzedAt: analysis.analyzedAt
1464
+ }
1465
+ };
1466
+ }
1467
+ function scoreEfficiency(analysis, selection, insights) {
1468
+ const weight = 30;
1469
+ const ratio = analysis.totalTokens > 0 ? 1 - selection.totalTokens / analysis.totalTokens : 0;
1470
+ const selectivity = analysis.totalFiles > 0 ? 1 - selection.files.length / analysis.totalFiles : 0;
1471
+ const prunedFiles = selection.files.filter(
1472
+ (f) => f.pruneLevel === "signatures" || f.pruneLevel === "skeleton"
1473
+ ).length;
1474
+ const pruneRatio = selection.files.length > 0 ? prunedFiles / selection.files.length : 0;
1475
+ const raw = (ratio * 0.5 + selectivity * 0.3 + pruneRatio * 0.2) * 100;
1476
+ const score = Math.min(100, Math.max(0, Math.round(raw)));
1477
+ const weighted = score / 100 * weight;
1478
+ if (ratio > 0.7) {
1479
+ insights.push({
1480
+ type: "strength",
1481
+ title: "Excellent compression",
1482
+ detail: `${Math.round(ratio * 100)}% token reduction while maintaining context quality`,
1483
+ impact: "high"
1484
+ });
1485
+ }
1486
+ if (ratio < 0.3 && analysis.totalTokens > 2e4) {
1487
+ insights.push({
1488
+ type: "weakness",
1489
+ title: "Low compression opportunity",
1490
+ detail: "Most files are needed. Consider splitting the project into smaller modules.",
1491
+ impact: "medium"
1492
+ });
1493
+ }
1494
+ return {
1495
+ score,
1496
+ weight,
1497
+ weighted,
1498
+ detail: `${Math.round(ratio * 100)}% compression, ${prunedFiles}/${selection.files.length} files pruned`
1499
+ };
1500
+ }
1501
+ function scoreCoverage(analysis, selection, insights) {
1502
+ const weight = 25;
1503
+ const coverageScore = selection.coverage.score;
1504
+ const missingCritical = selection.coverage.missingCritical.length;
1505
+ let penalty = 0;
1506
+ if (missingCritical > 0) {
1507
+ penalty = Math.min(30, missingCritical * 10);
1508
+ insights.push({
1509
+ type: "weakness",
1510
+ title: `${missingCritical} critical file(s) missing from context`,
1511
+ detail: `Missing: ${selection.coverage.missingCritical.slice(0, 3).join(", ")}${missingCritical > 3 ? ` +${missingCritical - 3} more` : ""}`,
1512
+ impact: "high"
1513
+ });
1514
+ }
1515
+ const score = Math.min(100, Math.max(0, Math.round(coverageScore - penalty)));
1516
+ const weighted = score / 100 * weight;
1517
+ if (coverageScore >= 90 && missingCritical === 0) {
1518
+ insights.push({
1519
+ type: "strength",
1520
+ title: "Excellent context coverage",
1521
+ detail: `${coverageScore}% of the relevant universe captured with zero critical gaps`,
1522
+ impact: "high"
1523
+ });
1524
+ }
1525
+ return {
1526
+ score,
1527
+ weight,
1528
+ weighted,
1529
+ detail: `${coverageScore}% coverage, ${missingCritical} critical gaps`
1530
+ };
1531
+ }
1532
+ function scoreRiskControl(analysis, selection, insights) {
1533
+ const weight = 20;
1534
+ const dist = analysis.riskProfile.distribution;
1535
+ const totalFiles = analysis.totalFiles;
1536
+ const criticalFiles = analysis.files.filter((f) => f.riskScore >= 80);
1537
+ const highFiles = analysis.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80);
1538
+ const selectedPaths = new Set(selection.files.map((f) => f.relativePath));
1539
+ const criticalIncluded = criticalFiles.filter((f) => selectedPaths.has(f.relativePath)).length;
1540
+ const highIncluded = highFiles.filter((f) => selectedPaths.has(f.relativePath)).length;
1541
+ const criticalCoverage = criticalFiles.length > 0 ? criticalIncluded / criticalFiles.length : 1;
1542
+ const highCoverage = highFiles.length > 0 ? highIncluded / highFiles.length : 1;
1543
+ const criticalRatio = totalFiles > 0 ? dist.critical / totalFiles : 0;
1544
+ const healthScore = Math.max(0, 1 - criticalRatio * 5);
1545
+ const raw = (criticalCoverage * 0.5 + highCoverage * 0.3 + healthScore * 0.2) * 100;
1546
+ const score = Math.min(100, Math.max(0, Math.round(raw)));
1547
+ const weighted = score / 100 * weight;
1548
+ if (criticalCoverage === 1 && criticalFiles.length > 0) {
1549
+ insights.push({
1550
+ type: "strength",
1551
+ title: "All critical files included",
1552
+ detail: `${criticalFiles.length} critical-risk files are captured in context`,
1553
+ impact: "high"
1554
+ });
1555
+ }
1556
+ if (criticalRatio > 0.2) {
1557
+ insights.push({
1558
+ type: "opportunity",
1559
+ title: "High concentration of critical files",
1560
+ detail: `${dist.critical} files (${Math.round(criticalRatio * 100)}%) are critical risk. Consider refactoring complex modules.`,
1561
+ impact: "medium"
1562
+ });
1563
+ }
1564
+ return {
1565
+ score,
1566
+ weight,
1567
+ weighted,
1568
+ detail: `${criticalIncluded}/${criticalFiles.length} critical + ${highIncluded}/${highFiles.length} high-risk files included`
1569
+ };
1570
+ }
1571
+ function scoreStructure(analysis, insights) {
1572
+ const weight = 15;
1573
+ const graph = analysis.graph;
1574
+ const totalFiles = analysis.totalFiles;
1575
+ const avgCohesion = graph.clusters.length > 0 ? graph.clusters.reduce((s, c) => s + c.cohesion, 0) / graph.clusters.length : 0;
1576
+ const orphanRatio = totalFiles > 0 ? graph.orphans.length / totalFiles : 0;
1577
+ const hubRatio = totalFiles > 0 ? graph.hubs.length / totalFiles : 0;
1578
+ const hubHealth = hubRatio > 0.02 && hubRatio < 0.15 ? 1 : Math.max(0, 1 - Math.abs(hubRatio - 0.08) * 10);
1579
+ const typeFiles = analysis.files.filter((f) => f.kind === "type").length;
1580
+ const typeRatio = totalFiles > 0 ? typeFiles / totalFiles : 0;
1581
+ const typeScore = Math.min(1, typeRatio * 10);
1582
+ const raw = (avgCohesion * 0.3 + (1 - orphanRatio) * 0.3 + hubHealth * 0.2 + typeScore * 0.2) * 100;
1583
+ const score = Math.min(100, Math.max(0, Math.round(raw)));
1584
+ const weighted = score / 100 * weight;
1585
+ if (orphanRatio > 0.5) {
1586
+ insights.push({
1587
+ type: "weakness",
1588
+ title: "Many orphan files",
1589
+ detail: `${graph.orphans.length} files (${Math.round(orphanRatio * 100)}%) have no imports/exports. AI gets less context from the dependency graph.`,
1590
+ impact: "medium"
1591
+ });
1592
+ }
1593
+ if (graph.clusters.length > 0 && avgCohesion > 0.7) {
1594
+ insights.push({
1595
+ type: "strength",
1596
+ title: "Well-organized module structure",
1597
+ detail: `${graph.clusters.length} cohesive clusters (avg cohesion: ${(avgCohesion * 100).toFixed(0)}%). CTO can efficiently select relevant modules.`,
1598
+ impact: "medium"
1599
+ });
1600
+ }
1601
+ return {
1602
+ score,
1603
+ weight,
1604
+ weighted,
1605
+ detail: `${graph.clusters.length} clusters, ${graph.orphans.length} orphans, ${graph.hubs.length} hubs`
1606
+ };
1607
+ }
1608
+ function scoreGovernance(analysis, insights) {
1609
+ const weight = 10;
1610
+ const hasTypes = analysis.files.some((f) => f.kind === "type");
1611
+ const hasConfig = analysis.files.some((f) => f.kind === "config");
1612
+ const hasTests = analysis.files.some((f) => f.kind === "test");
1613
+ let score = 50;
1614
+ if (hasTypes) {
1615
+ score += 15;
1616
+ }
1617
+ if (hasConfig) {
1618
+ score += 10;
1619
+ }
1620
+ if (hasTests) {
1621
+ score += 15;
1622
+ }
1623
+ if (analysis.stack.length > 0) {
1624
+ score += 10;
1625
+ }
1626
+ score = Math.min(100, score);
1627
+ const weighted = score / 100 * weight;
1628
+ if (!hasTests) {
1629
+ insights.push({
1630
+ type: "opportunity",
1631
+ title: "No test files detected",
1632
+ detail: "Adding tests helps CTO understand code intent and provides better context boundaries.",
1633
+ impact: "low"
1634
+ });
1635
+ }
1636
+ if (!hasTypes) {
1637
+ insights.push({
1638
+ type: "opportunity",
1639
+ title: "No type definition files",
1640
+ detail: "Type files dramatically improve AI code generation accuracy. Consider adding interfaces/types.",
1641
+ impact: "medium"
1642
+ });
1643
+ }
1644
+ return {
1645
+ score,
1646
+ weight,
1647
+ weighted,
1648
+ detail: `types:${hasTypes ? "\u2713" : "\u2717"} tests:${hasTests ? "\u2713" : "\u2717"} config:${hasConfig ? "\u2713" : "\u2717"} stack:${analysis.stack.join(",") || "unknown"}`
1649
+ };
1650
+ }
1651
+ function scoreToGrade(score) {
1652
+ if (score >= 95) return "A+";
1653
+ if (score >= 90) return "A";
1654
+ if (score >= 85) return "A-";
1655
+ if (score >= 80) return "B+";
1656
+ if (score >= 75) return "B";
1657
+ if (score >= 70) return "B-";
1658
+ if (score >= 65) return "C+";
1659
+ if (score >= 60) return "C";
1660
+ if (score >= 55) return "C-";
1661
+ if (score >= 40) return "D";
1662
+ return "F";
1663
+ }
1664
+ function renderContextScore(score) {
1665
+ const lines = [];
1666
+ const gradeColor = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
1667
+ lines.push("");
1668
+ lines.push(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
1669
+ lines.push(` \u2551 \u2551`);
1670
+ lines.push(` \u2551 ${gradeColor} Context Score\u2122 ${padCenter(score.overall.toString(), 3)} / 100 Grade: ${padCenter(score.grade, 2)} \u2551`);
1671
+ lines.push(` \u2551 \u2551`);
1672
+ lines.push(` \u2551 ${score.meta.projectName.padEnd(20).substring(0, 20)} \u2551`);
1673
+ lines.push(` \u2551 ${score.meta.totalFiles} files \xB7 ${Math.round(score.meta.totalTokens / 1e3)}K tokens \u2551`);
1674
+ lines.push(` \u2551 \u2551`);
1675
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
1676
+ const dims = [
1677
+ { label: "Efficiency", dim: score.dimensions.efficiency },
1678
+ { label: "Coverage", dim: score.dimensions.coverage },
1679
+ { label: "Risk Control", dim: score.dimensions.riskControl },
1680
+ { label: "Structure", dim: score.dimensions.structure },
1681
+ { label: "Governance", dim: score.dimensions.governance }
1682
+ ];
1683
+ lines.push(` \u2551 \u2551`);
1684
+ for (const { label, dim } of dims) {
1685
+ const bar = renderBar(dim.score, 20);
1686
+ const pct = dim.score.toString().padStart(3);
1687
+ lines.push(` \u2551 ${label.padEnd(14)} ${bar} ${pct}% (\xD7${dim.weight}%) \u2551`);
1688
+ }
1689
+ lines.push(` \u2551 \u2551`);
1690
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
1691
+ lines.push(` \u2551 \u2551`);
1692
+ lines.push(` \u2551 \u{1F4B0} vs. Sending Everything: \u2551`);
1693
+ lines.push(` \u2551 Tokens saved: ${formatNumber(score.comparison.savedTokens).padEnd(12)} (${score.comparison.savedPercent}%) \u2551`);
1694
+ lines.push(` \u2551 Monthly savings: $${score.comparison.monthlySavingsUSD.toFixed(2).padEnd(10)} \u2551`);
1695
+ lines.push(` \u2551 (${score.comparison.optimizedTokens.toLocaleString()} vs ${score.comparison.naiveTokens.toLocaleString()} tokens) \u2551`);
1696
+ lines.push(` \u2551 \u2551`);
1697
+ if (score.insights.length > 0) {
1698
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
1699
+ lines.push(` \u2551 \u2551`);
1700
+ const top = score.insights.slice(0, 5);
1701
+ for (const insight of top) {
1702
+ const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
1703
+ const title = insight.title.substring(0, 44).padEnd(44);
1704
+ lines.push(` \u2551 ${icon} ${title} \u2551`);
1705
+ }
1706
+ lines.push(` \u2551 \u2551`);
1707
+ }
1708
+ lines.push(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
1709
+ lines.push("");
1710
+ return lines.join("\n");
1711
+ }
1712
+ function renderBar(pct, width) {
1713
+ const filled = Math.round(pct / 100 * width);
1714
+ const empty = width - filled;
1715
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
1716
+ }
1717
+ function padCenter(str, width) {
1718
+ const pad2 = Math.max(0, width - str.length);
1719
+ const left = Math.floor(pad2 / 2);
1720
+ return " ".repeat(left) + str + " ".repeat(pad2 - left);
1721
+ }
1722
+ function formatNumber(n) {
1723
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1724
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1725
+ return n.toString();
1726
+ }
1727
+
1728
+ // src/engine/benchmark.ts
1729
+ async function runBenchmark(analysis, task = "general code review and refactoring", budget = 5e4) {
1730
+ const criticalFiles = analysis.files.filter((f) => f.riskScore >= 80);
1731
+ const highRiskFiles = analysis.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80);
1732
+ const ctoStart = performance.now();
1733
+ const ctoSelection = await selectContext({ task, analysis, budget });
1734
+ const ctoTime = performance.now() - ctoStart;
1735
+ const ctoSelectedPaths = new Set(ctoSelection.files.map((f) => f.relativePath));
1736
+ const ctoCritical = criticalFiles.filter((f) => ctoSelectedPaths.has(f.relativePath)).length;
1737
+ const ctoHigh = highRiskFiles.filter((f) => ctoSelectedPaths.has(f.relativePath)).length;
1738
+ const cto = {
1739
+ filesSelected: ctoSelection.files.length,
1740
+ tokensUsed: ctoSelection.totalTokens,
1741
+ coverageScore: ctoSelection.coverage.score,
1742
+ criticalFilesCovered: ctoCritical,
1743
+ criticalFilesTotal: criticalFiles.length,
1744
+ highRiskCovered: ctoHigh,
1745
+ highRiskTotal: highRiskFiles.length,
1746
+ costPerInteractionUSD: ctoSelection.totalTokens / 1e6 * 3,
1747
+ // Sonnet pricing
1748
+ timeMs: Math.round(ctoTime)
1749
+ };
1750
+ const naiveStart = performance.now();
1751
+ const naiveFiles = [...analysis.files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1752
+ let naiveTokens = 0;
1753
+ let naiveCount = 0;
1754
+ const naiveSelectedPaths = /* @__PURE__ */ new Set();
1755
+ for (const f of naiveFiles) {
1756
+ if (naiveTokens + f.tokens <= budget) {
1757
+ naiveTokens += f.tokens;
1758
+ naiveCount++;
1759
+ naiveSelectedPaths.add(f.relativePath);
1760
+ }
1761
+ }
1762
+ if (naiveTokens === 0 && analysis.totalTokens <= budget) {
1763
+ naiveTokens = analysis.totalTokens;
1764
+ naiveCount = analysis.totalFiles;
1765
+ for (const f of analysis.files) naiveSelectedPaths.add(f.relativePath);
1766
+ }
1767
+ const naiveTime = performance.now() - naiveStart;
1768
+ const naiveCritical = criticalFiles.filter((f) => naiveSelectedPaths.has(f.relativePath)).length;
1769
+ const naiveHigh = highRiskFiles.filter((f) => naiveSelectedPaths.has(f.relativePath)).length;
1770
+ const naiveCoverage = analysis.totalFiles > 0 ? Math.round(naiveCount / analysis.totalFiles * 100) : 0;
1771
+ const naive = {
1772
+ filesSelected: naiveCount,
1773
+ tokensUsed: naiveTokens > 0 ? naiveTokens : analysis.totalTokens,
1774
+ coverageScore: naiveTokens > 0 ? naiveCoverage : 100,
1775
+ criticalFilesCovered: naiveCritical,
1776
+ criticalFilesTotal: criticalFiles.length,
1777
+ highRiskCovered: naiveHigh,
1778
+ highRiskTotal: highRiskFiles.length,
1779
+ costPerInteractionUSD: (naiveTokens > 0 ? naiveTokens : analysis.totalTokens) / 1e6 * 3,
1780
+ timeMs: Math.round(naiveTime)
1781
+ };
1782
+ const randomStart = performance.now();
1783
+ const shuffled = [...analysis.files].sort(() => Math.random() - 0.5);
1784
+ let randomTokens = 0;
1785
+ let randomCount = 0;
1786
+ const randomSelectedPaths = /* @__PURE__ */ new Set();
1787
+ for (const f of shuffled) {
1788
+ if (randomTokens + f.tokens <= budget) {
1789
+ randomTokens += f.tokens;
1790
+ randomCount++;
1791
+ randomSelectedPaths.add(f.relativePath);
1792
+ }
1793
+ }
1794
+ const randomTime = performance.now() - randomStart;
1795
+ const randomCritical = criticalFiles.filter((f) => randomSelectedPaths.has(f.relativePath)).length;
1796
+ const randomHigh = highRiskFiles.filter((f) => randomSelectedPaths.has(f.relativePath)).length;
1797
+ const randomCoverage = analysis.totalFiles > 0 ? Math.round(randomCount / analysis.totalFiles * 100) : 0;
1798
+ const random = {
1799
+ filesSelected: randomCount,
1800
+ tokensUsed: randomTokens,
1801
+ coverageScore: randomCoverage,
1802
+ criticalFilesCovered: randomCritical,
1803
+ criticalFilesTotal: criticalFiles.length,
1804
+ highRiskCovered: randomHigh,
1805
+ highRiskTotal: highRiskFiles.length,
1806
+ costPerInteractionUSD: randomTokens / 1e6 * 3,
1807
+ timeMs: Math.round(randomTime)
1808
+ };
1809
+ const ctoScore = computeStrategyScore(cto, budget);
1810
+ const naiveScore = computeStrategyScore(naive, budget);
1811
+ const randomScore = computeStrategyScore(random, budget);
1812
+ const winner = ctoScore >= naiveScore && ctoScore >= randomScore ? "cto" : naiveScore >= randomScore ? "naive" : "random";
1813
+ const interactionsPerMonth = 800;
1814
+ const vsNaiveCostSaved = (naive.costPerInteractionUSD - cto.costPerInteractionUSD) * interactionsPerMonth;
1815
+ return {
1816
+ project: analysis.projectName,
1817
+ totalFiles: analysis.totalFiles,
1818
+ totalTokens: analysis.totalTokens,
1819
+ budget,
1820
+ task,
1821
+ strategies: { cto, naive, random },
1822
+ winner,
1823
+ ctoAdvantage: {
1824
+ vsNaiveTokensSaved: naive.tokensUsed - cto.tokensUsed,
1825
+ vsNaiveTokensSavedPercent: naive.tokensUsed > 0 ? Math.round((naive.tokensUsed - cto.tokensUsed) / naive.tokensUsed * 100) : 0,
1826
+ vsRandomCoverageGain: cto.coverageScore - random.coverageScore,
1827
+ vsNaiveCostSavedMonthlyUSD: Math.round(vsNaiveCostSaved * 100) / 100
1828
+ }
1829
+ };
1830
+ }
1831
+ function computeStrategyScore(strategy, budget) {
1832
+ const coverageWeight = strategy.coverageScore / 100;
1833
+ const criticalWeight = strategy.criticalFilesTotal > 0 ? strategy.criticalFilesCovered / strategy.criticalFilesTotal : 1;
1834
+ const efficiency = budget > 0 ? 1 - strategy.tokensUsed / budget : 0;
1835
+ return coverageWeight * 0.5 + criticalWeight * 0.3 + Math.max(0, efficiency) * 0.2;
1836
+ }
1837
+ function renderBenchmark(result) {
1838
+ const lines = [];
1839
+ const { cto, naive, random } = result.strategies;
1840
+ lines.push("");
1841
+ lines.push(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
1842
+ lines.push(` \u2551 \u26A1 CTO Benchmark \u2014 ${result.project.substring(0, 20).padEnd(20)} \u2551`);
1843
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
1844
+ lines.push(` \u2551 \u2551`);
1845
+ lines.push(` \u2551 ${pad("Metric", 20)} ${pad("CTO", 12)} ${pad("Naive", 12)} ${pad("Random", 12)} \u2551`);
1846
+ lines.push(` \u2551 ${"\u2500".repeat(56)} \u2551`);
1847
+ lines.push(` \u2551 ${pad("Files selected", 20)} ${pad(cto.filesSelected.toString(), 12)} ${pad(naive.filesSelected.toString(), 12)} ${pad(random.filesSelected.toString(), 12)} \u2551`);
1848
+ lines.push(` \u2551 ${pad("Tokens used", 20)} ${pad(fmt(cto.tokensUsed), 12)} ${pad(fmt(naive.tokensUsed), 12)} ${pad(fmt(random.tokensUsed), 12)} \u2551`);
1849
+ lines.push(` \u2551 ${pad("Coverage", 20)} ${pad(cto.coverageScore + "%", 12)} ${pad(naive.coverageScore + "%", 12)} ${pad(random.coverageScore + "%", 12)} \u2551`);
1850
+ lines.push(` \u2551 ${pad("Critical files", 20)} ${pad(`${cto.criticalFilesCovered}/${cto.criticalFilesTotal}`, 12)} ${pad(`${naive.criticalFilesCovered}/${naive.criticalFilesTotal}`, 12)} ${pad(`${random.criticalFilesCovered}/${random.criticalFilesTotal}`, 12)} \u2551`);
1851
+ lines.push(` \u2551 ${pad("High-risk files", 20)} ${pad(`${cto.highRiskCovered}/${cto.highRiskTotal}`, 12)} ${pad(`${naive.highRiskCovered}/${naive.highRiskTotal}`, 12)} ${pad(`${random.highRiskCovered}/${random.highRiskTotal}`, 12)} \u2551`);
1852
+ lines.push(` \u2551 ${pad("Cost/interaction", 20)} ${pad("$" + cto.costPerInteractionUSD.toFixed(4), 12)} ${pad("$" + naive.costPerInteractionUSD.toFixed(4), 12)} ${pad("$" + random.costPerInteractionUSD.toFixed(4), 12)} \u2551`);
1853
+ lines.push(` \u2551 ${pad("Time", 20)} ${pad(cto.timeMs + "ms", 12)} ${pad(naive.timeMs + "ms", 12)} ${pad(random.timeMs + "ms", 12)} \u2551`);
1854
+ lines.push(` \u2551 \u2551`);
1855
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
1856
+ lines.push(` \u2551 \u2551`);
1857
+ lines.push(` \u2551 \u{1F3C6} Winner: ${result.winner.toUpperCase().padEnd(48)} \u2551`);
1858
+ if (result.ctoAdvantage.vsNaiveTokensSavedPercent > 0) {
1859
+ lines.push(` \u2551 \u{1F4C9} ${result.ctoAdvantage.vsNaiveTokensSavedPercent}% fewer tokens than naive \u2551`);
1860
+ }
1861
+ if (result.ctoAdvantage.vsRandomCoverageGain > 0) {
1862
+ lines.push(` \u2551 \u{1F4C8} +${result.ctoAdvantage.vsRandomCoverageGain}% better coverage than random \u2551`);
1863
+ }
1864
+ if (result.ctoAdvantage.vsNaiveCostSavedMonthlyUSD > 0) {
1865
+ lines.push(` \u2551 \u{1F4B0} $${result.ctoAdvantage.vsNaiveCostSavedMonthlyUSD.toFixed(2)}/mo saved vs naive (800 interactions) \u2551`);
1866
+ }
1867
+ lines.push(` \u2551 \u2551`);
1868
+ lines.push(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
1869
+ lines.push("");
1870
+ return lines.join("\n");
1871
+ }
1872
+ function pad(s, w) {
1873
+ return s.padEnd(w).substring(0, w);
1874
+ }
1875
+ function fmt(n) {
1876
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1877
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1878
+ return n.toString();
1879
+ }
1880
+
1881
+ // src/cli/score.ts
1882
+ async function main() {
1883
+ const args = process.argv.slice(2);
1884
+ const jsonMode = args.includes("--json");
1885
+ const benchmarkMode = args.includes("--benchmark");
1886
+ const helpMode = args.includes("--help") || args.includes("-h");
1887
+ const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-"));
1888
+ const projectPath = resolve4(pathArg ?? ".");
1889
+ if (helpMode) {
1890
+ console.log(`
1891
+ \u26A1 cto-score \u2014 How AI-ready is your codebase?
1892
+
1893
+ Usage:
1894
+ npx cto-score Scan current directory
1895
+ npx cto-score ./path Scan a specific project
1896
+ npx cto-score --benchmark Include CTO vs naive vs random comparison
1897
+ npx cto-score --json Output as JSON (for CI/scripts)
1898
+
1899
+ What it does:
1900
+ Analyzes your project's structure, dependencies, and risk profile.
1901
+ Gives you a single 0-100 score showing how efficiently AI tools
1902
+ can work with your codebase.
1903
+
1904
+ No data leaves your machine. No API keys needed. MIT licensed.
1905
+ Learn more: https://github.com/cto-ai/cto-ai-cli
1906
+ `);
1907
+ process.exit(0);
1908
+ }
1909
+ console.log("");
1910
+ console.log(" \u26A1 cto-score \u2014 analyzing your project...");
1911
+ console.log("");
1912
+ try {
1913
+ const startTime = Date.now();
1914
+ const analysis = await analyzeProject(projectPath);
1915
+ const score = await computeContextScore(analysis);
1916
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
1917
+ if (jsonMode) {
1918
+ console.log(JSON.stringify({
1919
+ project: analysis.projectName,
1920
+ files: analysis.totalFiles,
1921
+ tokens: analysis.totalTokens,
1922
+ score: score.overall,
1923
+ grade: score.grade,
1924
+ dimensions: {
1925
+ efficiency: score.dimensions.efficiency.score,
1926
+ coverage: score.dimensions.coverage.score,
1927
+ riskControl: score.dimensions.riskControl.score,
1928
+ structure: score.dimensions.structure.score,
1929
+ governance: score.dimensions.governance.score
1930
+ },
1931
+ savings: {
1932
+ percent: score.comparison.savedPercent,
1933
+ monthlyUSD: score.comparison.monthlySavingsUSD,
1934
+ tokensOptimized: score.comparison.optimizedTokens,
1935
+ tokensNaive: score.comparison.naiveTokens
1936
+ }
1937
+ }, null, 2));
1938
+ process.exit(0);
1939
+ }
1940
+ console.log(renderContextScore(score));
1941
+ if (benchmarkMode) {
1942
+ const benchmark = await runBenchmark(analysis);
1943
+ console.log(renderBenchmark(benchmark));
1944
+ }
1945
+ console.log("");
1946
+ console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
1947
+ console.log("");
1948
+ console.log(" What does this mean?");
1949
+ console.log(` Your project scores ${score.overall}/100 (${score.grade}) for AI context efficiency.`);
1950
+ if (score.overall >= 80) {
1951
+ console.log(" \u2705 Great! AI tools can work effectively with your codebase.");
1952
+ } else if (score.overall >= 60) {
1953
+ console.log(" \u{1F7E1} Good, but there's room to improve. Run with --benchmark for details.");
1954
+ } else {
1955
+ console.log(" \u{1F534} AI tools are likely wasting tokens on your project. Run with --benchmark.");
1956
+ }
1957
+ console.log("");
1958
+ console.log(" Next steps:");
1959
+ console.log(" npx cto-score --benchmark See CTO vs naive comparison");
1960
+ console.log(" npm i -g cto-ai-cli Install for full CLI + MCP server");
1961
+ console.log(" https://github.com/cto-ai/cto-ai-cli Full docs");
1962
+ console.log("");
1963
+ } catch (err) {
1964
+ console.error(` \u274C Error: ${err.message}`);
1965
+ console.error("");
1966
+ console.error(" Make sure you're running this in a project directory with source files.");
1967
+ console.error(" Supported: TypeScript, JavaScript, Python, Go, Rust, Java, C/C++");
1968
+ process.exit(1);
1969
+ }
1970
+ }
1971
+ main();