cto-ai-cli 5.1.0 → 6.1.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.
@@ -1,3695 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli/v2/index.ts
4
- import { Command as Command7 } from "commander";
5
- import chalk7 from "chalk";
6
-
7
- // src/cli/v2/init.ts
8
- import { Command } from "commander";
9
- import chalk from "chalk";
10
- import { resolve, join as join2 } from "path";
11
- import { writeFile as writeFile2 } from "fs/promises";
12
-
13
- // src/engine/config.ts
14
- import { readFile, writeFile, mkdir } from "fs/promises";
15
- import { join } from "path";
16
- import { existsSync } from "fs";
17
- import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
18
-
19
- // src/types/config.ts
20
- var DEFAULT_CONFIG = {
21
- version: "2.0",
22
- analysis: {
23
- extensions: {
24
- code: ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "kt", "rb", "php", "c", "cpp", "h", "hpp", "cs"],
25
- config: ["json", "yml", "yaml", "toml"],
26
- docs: ["md", "txt", "rst"]
27
- },
28
- ignore: {
29
- dirs: ["node_modules", "dist", "build", ".git", "coverage", "__pycache__", ".next", "vendor", ".cto"],
30
- patterns: ["*.min.js", "*.map", "*.lock", "*.generated.*"]
31
- },
32
- maxDepth: 20
33
- },
34
- risk: {
35
- weights: {
36
- hub: 30,
37
- typeProvider: 25,
38
- complexity: 15,
39
- recency: 15,
40
- config: 10,
41
- churn: 5
42
- }
43
- },
44
- interaction: {
45
- defaultBudget: 5e4,
46
- defaultModel: "claude-sonnet-4"
47
- },
48
- tokens: {
49
- method: "chars4"
50
- },
51
- governance: {
52
- auditEnabled: true,
53
- secretDetection: true,
54
- retentionDays: 90
55
- }
56
- };
57
-
58
- // src/engine/config.ts
59
- var CONFIG_DIR = ".cto";
60
- var CONFIG_FILE = "config.yml";
61
- var POLICY_FILE = "policy.yml";
62
- function getConfigPath(projectPath) {
63
- return join(projectPath, CONFIG_DIR, CONFIG_FILE);
64
- }
65
- function getPolicyPath(projectPath) {
66
- return join(projectPath, CONFIG_DIR, POLICY_FILE);
67
- }
68
- function getCTODir(projectPath) {
69
- return join(projectPath, CONFIG_DIR);
70
- }
71
- async function saveConfig(projectPath, config) {
72
- const ctoDir = getCTODir(projectPath);
73
- await mkdir(ctoDir, { recursive: true });
74
- const configPath = getConfigPath(projectPath);
75
- const yamlContent = stringifyYAML(config, { indent: 2 });
76
- await writeFile(configPath, yamlContent, "utf-8");
77
- return configPath;
78
- }
79
- async function initProjectConfig(projectPath) {
80
- const ctoDir = getCTODir(projectPath);
81
- await mkdir(join(ctoDir, "snapshots"), { recursive: true });
82
- const created = [];
83
- const configPath = getConfigPath(projectPath);
84
- if (!existsSync(configPath)) {
85
- await saveConfig(projectPath, DEFAULT_CONFIG);
86
- created.push(configPath);
87
- }
88
- const policyPath = getPolicyPath(projectPath);
89
- if (!existsSync(policyPath)) {
90
- const defaultPolicy = {
91
- version: "1.0",
92
- name: "default",
93
- rules: [
94
- {
95
- id: "types-always",
96
- type: "include-always",
97
- pattern: "**/types/**",
98
- reason: "Type definitions provide essential context",
99
- enabled: true
100
- },
101
- {
102
- id: "no-env",
103
- type: "exclude-always",
104
- pattern: "**/*.env*",
105
- reason: "Environment files must never be sent to AI",
106
- enabled: true
107
- },
108
- {
109
- id: "no-secrets",
110
- type: "secret-block",
111
- reason: "Files with detected secrets are blocked",
112
- enabled: true
113
- },
114
- {
115
- id: "test-budget",
116
- type: "budget-limit",
117
- category: "test",
118
- threshold: 20,
119
- reason: "Test files should not exceed 20% of budget",
120
- enabled: true
121
- },
122
- {
123
- id: "min-coverage",
124
- type: "coverage-minimum",
125
- threshold: 70,
126
- reason: "Warn if context coverage drops below 70%",
127
- enabled: true
128
- }
129
- ]
130
- };
131
- const yamlContent = stringifyYAML(defaultPolicy, { indent: 2 });
132
- await writeFile(policyPath, yamlContent, "utf-8");
133
- created.push(policyPath);
134
- }
135
- return { configPath, policyPath, created };
136
- }
137
-
138
- // src/cli/v2/init.ts
139
- var initCommand = new Command("init").description("Initialize CTO v2 for a project").argument("[path]", "Project path", ".").option("--defaults", "Skip prompts and use defaults").action(async (path, opts) => {
140
- try {
141
- const projectPath = resolve(path);
142
- const ctoDir = join2(projectPath, ".cto");
143
- console.log("");
144
- console.log(chalk.bold.cyan(" \u{1F527} CTO v2.0 \u2014 Context Operating System"));
145
- console.log(chalk.dim(" AI Interaction Infrastructure for Engineering Teams"));
146
- console.log("");
147
- const { configPath, policyPath, created } = await initProjectConfig(projectPath);
148
- const gitignorePath = join2(ctoDir, ".gitignore");
149
- const gitignoreContent = [
150
- "# CTO v2 \u2014 local state (do not commit)",
151
- "audit/",
152
- "snapshots/",
153
- "",
154
- "# Config and policy should be committed",
155
- "!config.yml",
156
- "!policy.yml",
157
- ""
158
- ].join("\n");
159
- await writeFile2(gitignorePath, gitignoreContent, "utf-8");
160
- console.log(chalk.green(" \u2705 Initialized .cto/ directory"));
161
- console.log(chalk.dim(` ${ctoDir}/`));
162
- console.log(chalk.dim(" \u251C\u2500\u2500 config.yml (commit this)"));
163
- console.log(chalk.dim(" \u251C\u2500\u2500 policy.yml (commit this)"));
164
- console.log(chalk.dim(" \u251C\u2500\u2500 snapshots/ (local)"));
165
- console.log(chalk.dim(" \u2514\u2500\u2500 .gitignore"));
166
- if (created.length > 0) {
167
- console.log(chalk.dim(` Created: ${created.length} file(s)`));
168
- }
169
- console.log("");
170
- console.log(chalk.bold(" Next steps:"));
171
- console.log(chalk.dim(" cto analyze \u2014 Analyze project structure & risk"));
172
- console.log(chalk.dim(' cto interact "refactor auth module" \u2014 Build optimized AI context'));
173
- console.log(chalk.dim(" cto snapshot create baseline \u2014 Save a reproducible snapshot"));
174
- console.log(chalk.dim(" cto policy show \u2014 View context policies"));
175
- console.log("");
176
- } catch (err) {
177
- console.error(chalk.red(`
178
- Error: ${err.message}
179
- `));
180
- process.exit(1);
181
- }
182
- });
183
-
184
- // src/cli/v2/analyze.ts
185
- import { Command as Command2 } from "commander";
186
- import chalk2 from "chalk";
187
- import { resolve as resolve4 } from "path";
188
-
189
- // src/engine/analyzer.ts
190
- import { readFile as readFile3, readdir, stat as stat2 } from "fs/promises";
191
- import { join as join4, extname, relative as relative2, resolve as resolve3, basename as basename2 } from "path";
192
- import { createHash } from "crypto";
193
-
194
- // src/types/engine.ts
195
- var DEFAULT_RISK_WEIGHTS = {
196
- hub: 30,
197
- typeProvider: 25,
198
- complexity: 15,
199
- recency: 15,
200
- config: 10,
201
- churn: 5
202
- };
203
-
204
- // src/engine/tokenizer.ts
205
- import { encodingForModel } from "js-tiktoken";
206
- import { readFile as readFile2, stat } from "fs/promises";
207
- var CHARS_PER_TOKEN = 4;
208
- var encoder = null;
209
- function getEncoder() {
210
- if (!encoder) {
211
- encoder = encodingForModel("claude-3-5-sonnet-20241022");
212
- }
213
- return encoder;
214
- }
215
- function countTokensTiktoken(text) {
216
- try {
217
- const enc = getEncoder();
218
- const tokens = enc.encode(text);
219
- return tokens.length;
220
- } catch {
221
- return Math.ceil(text.length / CHARS_PER_TOKEN);
222
- }
223
- }
224
- function countTokensChars4(sizeInBytes) {
225
- return Math.ceil(sizeInBytes / CHARS_PER_TOKEN);
226
- }
227
- function estimateTokens(content, sizeInBytes, method = "chars4") {
228
- if (method === "tiktoken") {
229
- return countTokensTiktoken(content);
230
- }
231
- return countTokensChars4(sizeInBytes);
232
- }
233
-
234
- // src/engine/graph.ts
235
- import { Project, SyntaxKind } from "ts-morph";
236
- import { resolve as resolve2, relative, dirname, join as join3 } from "path";
237
- import { existsSync as existsSync2 } from "fs";
238
- var TS_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs", "cts", "cjs"]);
239
- function createProject(projectPath, filePaths) {
240
- const tsConfigPath = join3(projectPath, "tsconfig.json");
241
- const hasTsConfig = existsSync2(tsConfigPath);
242
- const project = new Project({
243
- tsConfigFilePath: hasTsConfig ? tsConfigPath : void 0,
244
- skipAddingFilesFromTsConfig: true,
245
- compilerOptions: hasTsConfig ? void 0 : {
246
- allowJs: true,
247
- jsx: 4,
248
- // JsxEmit.ReactJSX
249
- esModuleInterop: true,
250
- moduleResolution: 100
251
- // Bundler
252
- }
253
- });
254
- const tsFiles = filePaths.filter((f) => {
255
- const ext = f.split(".").pop()?.toLowerCase() ?? "";
256
- return TS_EXTENSIONS.has(ext);
257
- });
258
- for (const filePath of tsFiles) {
259
- try {
260
- project.addSourceFileAtPath(filePath);
261
- } catch {
262
- }
263
- }
264
- return project;
265
- }
266
- function buildProjectGraph(projectPath, files) {
267
- const absPath = resolve2(projectPath);
268
- const tsFiles = files.filter((f) => TS_EXTENSIONS.has(f.extension)).map((f) => f.path);
269
- if (tsFiles.length === 0) {
270
- return emptyGraph(files);
271
- }
272
- let project;
273
- try {
274
- project = createProject(projectPath, tsFiles);
275
- } catch {
276
- return emptyGraph(files);
277
- }
278
- const edges = [];
279
- const nodeSet = /* @__PURE__ */ new Set();
280
- for (const sourceFile of project.getSourceFiles()) {
281
- const fromRel = relative(absPath, sourceFile.getFilePath());
282
- if (fromRel.startsWith("..") || fromRel.includes("node_modules")) continue;
283
- nodeSet.add(fromRel);
284
- for (const imp of sourceFile.getImportDeclarations()) {
285
- const moduleSpecifier = imp.getModuleSpecifierValue();
286
- const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
287
- if (resolved) {
288
- nodeSet.add(resolved);
289
- edges.push({ from: fromRel, to: resolved, type: "import" });
290
- }
291
- }
292
- for (const exp of sourceFile.getExportDeclarations()) {
293
- const moduleSpecifier = exp.getModuleSpecifierValue();
294
- if (moduleSpecifier) {
295
- const resolved = resolveImport(sourceFile, moduleSpecifier, absPath);
296
- if (resolved) {
297
- nodeSet.add(resolved);
298
- edges.push({ from: fromRel, to: resolved, type: "re-export" });
299
- }
300
- }
301
- }
302
- }
303
- const nodes = Array.from(nodeSet);
304
- const importedByCount = /* @__PURE__ */ new Map();
305
- const importCount = /* @__PURE__ */ new Map();
306
- for (const edge of edges) {
307
- importedByCount.set(edge.to, (importedByCount.get(edge.to) ?? 0) + 1);
308
- importCount.set(edge.from, (importCount.get(edge.from) ?? 0) + 1);
309
- }
310
- const N = Math.max(nodes.length, 1);
311
- const hubs = nodes.map((node) => {
312
- const inDeg = importedByCount.get(node) ?? 0;
313
- const outDeg = importCount.get(node) ?? 0;
314
- const centrality = N > 1 ? inDeg / (N - 1) * 100 : 0;
315
- const score = Math.round(centrality + outDeg * (100 / (2 * N)));
316
- return {
317
- relativePath: node,
318
- dependents: inDeg,
319
- dependencies: outDeg,
320
- score: Math.min(100, score)
321
- };
322
- }).filter((h) => h.dependents >= 3 || h.score >= 15).sort((a, b) => b.score - a.score);
323
- const leaves = nodes.filter(
324
- (node) => (importedByCount.get(node) ?? 0) === 0 && (importCount.get(node) ?? 0) > 0
325
- );
326
- const connectedNodes = /* @__PURE__ */ new Set();
327
- for (const edge of edges) {
328
- connectedNodes.add(edge.from);
329
- connectedNodes.add(edge.to);
330
- }
331
- const allFileNodes = new Set(files.map((f) => f.relativePath));
332
- const orphans = Array.from(allFileNodes).filter((n) => !connectedNodes.has(n));
333
- const clusters = detectClusters(nodes, edges, files);
334
- enrichComplexity(project, absPath, files);
335
- return { nodes, edges, hubs, leaves, orphans, clusters };
336
- }
337
- var UnionFind = class {
338
- parent;
339
- rank;
340
- constructor(nodes) {
341
- this.parent = /* @__PURE__ */ new Map();
342
- this.rank = /* @__PURE__ */ new Map();
343
- for (const n of nodes) {
344
- this.parent.set(n, n);
345
- this.rank.set(n, 0);
346
- }
347
- }
348
- find(x) {
349
- const p = this.parent.get(x);
350
- if (p === void 0) return x;
351
- if (p !== x) {
352
- this.parent.set(x, this.find(p));
353
- }
354
- return this.parent.get(x);
355
- }
356
- union(a, b) {
357
- const ra = this.find(a);
358
- const rb = this.find(b);
359
- if (ra === rb) return;
360
- const rankA = this.rank.get(ra) ?? 0;
361
- const rankB = this.rank.get(rb) ?? 0;
362
- if (rankA < rankB) {
363
- this.parent.set(ra, rb);
364
- } else if (rankA > rankB) {
365
- this.parent.set(rb, ra);
366
- } else {
367
- this.parent.set(rb, ra);
368
- this.rank.set(ra, rankA + 1);
369
- }
370
- }
371
- };
372
- function detectClusters(nodes, edges, files) {
373
- const uf = new UnionFind(nodes);
374
- for (const edge of edges) {
375
- uf.union(edge.from, edge.to);
376
- }
377
- const components = /* @__PURE__ */ new Map();
378
- for (const node of nodes) {
379
- const root = uf.find(node);
380
- if (!components.has(root)) components.set(root, []);
381
- components.get(root).push(node);
382
- }
383
- const tokenMap = new Map(files.map((f) => [f.relativePath, f.tokens]));
384
- const clusters = [];
385
- for (const [, groupFiles] of components) {
386
- if (groupFiles.length < 2) continue;
387
- const name = commonPrefix(groupFiles);
388
- const fileSet = new Set(groupFiles);
389
- let internalEdges = 0;
390
- let externalEdges = 0;
391
- for (const edge of edges) {
392
- const fromIn = fileSet.has(edge.from);
393
- const toIn = fileSet.has(edge.to);
394
- if (fromIn && toIn) internalEdges++;
395
- else if (fromIn || toIn) externalEdges++;
396
- }
397
- const totalEdges = internalEdges + externalEdges;
398
- const cohesion = totalEdges > 0 ? internalEdges / totalEdges : 0;
399
- const totalTokens = groupFiles.reduce((s, f) => s + (tokenMap.get(f) ?? 0), 0);
400
- clusters.push({
401
- id: name.replace(/[^a-zA-Z0-9]/g, "-") || `cluster-${clusters.length}`,
402
- name: name || `cluster-${clusters.length}`,
403
- files: groupFiles,
404
- totalTokens,
405
- internalEdges,
406
- externalEdges,
407
- cohesion: Math.round(cohesion * 100) / 100
408
- });
409
- }
410
- return clusters.sort((a, b) => b.files.length - a.files.length);
411
- }
412
- function commonPrefix(paths) {
413
- if (paths.length === 0) return "";
414
- const parts = paths.map((p) => p.split("/"));
415
- const prefix = [];
416
- for (let i = 0; i < parts[0].length - 1; i++) {
417
- const segment = parts[0][i];
418
- if (parts.every((p) => p[i] === segment)) {
419
- prefix.push(segment);
420
- } else break;
421
- }
422
- return prefix.join("/") || parts[0][0];
423
- }
424
- function enrichComplexity(project, absPath, files) {
425
- const fileMap = new Map(files.map((f) => [f.relativePath, f]));
426
- for (const sourceFile of project.getSourceFiles()) {
427
- const relPath = relative(absPath, sourceFile.getFilePath());
428
- if (relPath.startsWith("..") || relPath.includes("node_modules")) continue;
429
- const file = fileMap.get(relPath);
430
- if (!file) continue;
431
- let totalComplexity = 0;
432
- for (const func of sourceFile.getFunctions()) {
433
- totalComplexity += calculateCyclomaticComplexity(func);
434
- }
435
- for (const cls of sourceFile.getClasses()) {
436
- for (const method of cls.getMethods()) {
437
- totalComplexity += calculateCyclomaticComplexity(method);
438
- }
439
- }
440
- for (const varDecl of sourceFile.getVariableDeclarations()) {
441
- const init = varDecl.getInitializer();
442
- if (init && (init.getKind() === SyntaxKind.ArrowFunction || init.getKind() === SyntaxKind.FunctionExpression)) {
443
- totalComplexity += calculateCyclomaticComplexity(init);
444
- }
445
- }
446
- file.complexity = Math.max(1, totalComplexity);
447
- }
448
- }
449
- function calculateCyclomaticComplexity(node) {
450
- let complexity = 1;
451
- node.forEachDescendant((descendant) => {
452
- switch (descendant.getKind()) {
453
- case SyntaxKind.IfStatement:
454
- case SyntaxKind.ConditionalExpression:
455
- case SyntaxKind.ForStatement:
456
- case SyntaxKind.ForInStatement:
457
- case SyntaxKind.ForOfStatement:
458
- case SyntaxKind.WhileStatement:
459
- case SyntaxKind.DoStatement:
460
- case SyntaxKind.CaseClause:
461
- case SyntaxKind.CatchClause:
462
- complexity++;
463
- break;
464
- case SyntaxKind.BinaryExpression: {
465
- const opToken = descendant.getOperatorToken?.();
466
- if (opToken) {
467
- const kind = opToken.getKind();
468
- if (kind === SyntaxKind.AmpersandAmpersandToken || kind === SyntaxKind.BarBarToken || kind === SyntaxKind.QuestionQuestionToken) {
469
- complexity++;
470
- }
471
- }
472
- break;
473
- }
474
- }
475
- });
476
- return complexity;
477
- }
478
- function resolveImport(sourceFile, moduleSpecifier, projectRoot) {
479
- if (!moduleSpecifier.startsWith(".")) return null;
480
- const sourceDir = dirname(sourceFile.getFilePath());
481
- const basePath = resolve2(sourceDir, moduleSpecifier);
482
- const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
483
- for (const ext of extensions) {
484
- const candidate = basePath.endsWith(ext) ? basePath : basePath + ext;
485
- if (existsSync2(candidate)) {
486
- const rel = relative(projectRoot, candidate);
487
- if (!rel.startsWith("..")) return rel;
488
- }
489
- }
490
- if (moduleSpecifier.endsWith(".js")) {
491
- const tsPath = basePath.replace(/\.js$/, ".ts");
492
- if (existsSync2(tsPath)) {
493
- const rel = relative(projectRoot, tsPath);
494
- if (!rel.startsWith("..")) return rel;
495
- }
496
- }
497
- return null;
498
- }
499
- function emptyGraph(files) {
500
- return {
501
- nodes: files.map((f) => f.relativePath),
502
- edges: [],
503
- hubs: [],
504
- leaves: [],
505
- orphans: files.map((f) => f.relativePath),
506
- clusters: []
507
- };
508
- }
509
-
510
- // src/engine/risk.ts
511
- function scoreAllFiles(files, graph, weights = DEFAULT_RISK_WEIGHTS) {
512
- const typeProviderUsage = computeTypeProviderUsage(files, graph);
513
- for (const file of files) {
514
- const factors = computeRiskFactors(file, graph, typeProviderUsage, weights);
515
- file.riskFactors = factors;
516
- file.riskScore = computeWeightedScore(factors);
517
- file.exclusionImpact = scoreToImpact(file.riskScore);
518
- }
519
- }
520
- function computeRiskFactors(file, graph, typeProviderUsage, weights) {
521
- const factors = [];
522
- factors.push(computeHubFactor(file, weights.hub));
523
- factors.push(computeTypeProviderFactor(file, typeProviderUsage, weights.typeProvider));
524
- factors.push(computeComplexityFactor(file, weights.complexity));
525
- factors.push(computeRecencyFactor(file, weights.recency));
526
- factors.push(computeConfigFactor(file, weights.config));
527
- factors.push(computeChurnFactor(file, weights.churn));
528
- return factors;
529
- }
530
- function computeHubFactor(file, weight) {
531
- const dependents = file.importedBy.length;
532
- const K = 12;
533
- const score = dependents === 0 ? 0 : Math.min(100, Math.round(100 * Math.log2(1 + dependents) / Math.log2(1 + K)));
534
- const detail = dependents === 0 ? "No dependents" : `Hub: ${dependents} file(s) depend on this (score ${score}/100)`;
535
- return { type: "hub", score, weight, detail };
536
- }
537
- function computeTypeProviderFactor(file, usage, weight) {
538
- const isTypeFile = file.kind === "type";
539
- const consumers = usage.get(file.relativePath) ?? 0;
540
- let score;
541
- let detail;
542
- if (isTypeFile && consumers >= 4) {
543
- score = 100;
544
- detail = `Type provider: used by ${consumers} files (critical type source)`;
545
- } else if (isTypeFile && consumers >= 1) {
546
- score = 50;
547
- detail = `Type provider: used by ${consumers} files`;
548
- } else if (isTypeFile) {
549
- score = 30;
550
- detail = "Type file (no detected consumers)";
551
- } else {
552
- score = 0;
553
- detail = "Not a type provider";
554
- }
555
- return { type: "type-provider", score, weight, detail };
556
- }
557
- function computeComplexityFactor(file, weight) {
558
- const c = file.complexity;
559
- const K = 30;
560
- const score = Math.min(100, Math.round(100 * Math.log(1 + c) / Math.log(1 + K)));
561
- const detail = c >= 30 ? `Very high complexity: ${c} (AI needs full context)` : c >= 10 ? `High complexity: ${c}` : `Complexity: ${c}`;
562
- return { type: "complexity", score, weight, detail };
563
- }
564
- function computeRecencyFactor(file, weight) {
565
- const now = Date.now();
566
- const modified = new Date(file.lastModified).getTime();
567
- const daysAgo = (now - modified) / (1e3 * 60 * 60 * 24);
568
- const HALF_LIFE = 7;
569
- const score = Math.round(100 * Math.pow(2, -daysAgo / HALF_LIFE));
570
- const detail = daysAgo <= 1 ? "Modified today" : `Modified ${Math.round(daysAgo)} days ago (decay score ${score})`;
571
- return { type: "recency", score, weight, detail };
572
- }
573
- function computeConfigFactor(file, weight) {
574
- let score;
575
- let detail;
576
- if (file.kind === "entry") {
577
- score = 90;
578
- detail = "Entry point \u2014 critical for understanding app structure";
579
- } else if (file.kind === "config") {
580
- score = 80;
581
- detail = "Configuration file \u2014 affects runtime behavior";
582
- } else {
583
- score = 0;
584
- detail = "Regular source file";
585
- }
586
- return { type: "config", score, weight, detail };
587
- }
588
- function computeChurnFactor(file, weight) {
589
- const complexitySignal = Math.min(file.complexity / 20, 1);
590
- const now = Date.now();
591
- const daysAgo = (now - new Date(file.lastModified).getTime()) / (1e3 * 60 * 60 * 24);
592
- const recencySignal = Math.pow(2, -daysAgo / 7);
593
- const score = Math.round(Math.sqrt(complexitySignal * recencySignal) * 100);
594
- const detail = score >= 50 ? "Likely under active development (complex + recent)" : score >= 20 ? "Some recent activity" : "Stable \u2014 low churn (proxy estimate)";
595
- return { type: "churn", score, weight, detail };
596
- }
597
- function computeWeightedScore(factors) {
598
- let totalWeightedScore = 0;
599
- let totalWeight = 0;
600
- for (const factor of factors) {
601
- totalWeightedScore += factor.score * factor.weight;
602
- totalWeight += factor.weight;
603
- }
604
- if (totalWeight === 0) return 0;
605
- return Math.round(totalWeightedScore / totalWeight);
606
- }
607
- function scoreToImpact(score) {
608
- if (score >= 80) return "critical";
609
- if (score >= 60) return "high";
610
- if (score >= 30) return "medium";
611
- if (score > 0) return "low";
612
- return "none";
613
- }
614
- function computeTypeProviderUsage(files, graph) {
615
- const usage = /* @__PURE__ */ new Map();
616
- const typeFiles = new Set(
617
- files.filter((f) => f.kind === "type").map((f) => f.relativePath)
618
- );
619
- for (const edge of graph.edges) {
620
- if (typeFiles.has(edge.to)) {
621
- usage.set(edge.to, (usage.get(edge.to) ?? 0) + 1);
622
- }
623
- }
624
- return usage;
625
- }
626
-
627
- // src/engine/analyzer.ts
628
- function matchesPattern(filename, patterns) {
629
- for (const pattern of patterns) {
630
- if (pattern.startsWith("*.")) {
631
- const ext = pattern.slice(1);
632
- if (filename.endsWith(ext)) return true;
633
- } else if (filename === pattern) {
634
- return true;
635
- }
636
- }
637
- return false;
638
- }
639
- async function walkProject(rootPath, options) {
640
- const results = [];
641
- const { ignoreDirs, ignorePatterns, extensions, maxDepth = 20 } = options;
642
- const ignoreDirSet = new Set(ignoreDirs);
643
- async function walk(dir, depth) {
644
- if (depth > maxDepth) return;
645
- let entries;
646
- try {
647
- entries = await readdir(dir, { withFileTypes: true });
648
- } catch {
649
- return;
650
- }
651
- const promises = [];
652
- for (const entry of entries) {
653
- const fullPath = join4(dir, entry.name);
654
- if (entry.isDirectory()) {
655
- if (!ignoreDirSet.has(entry.name) && !entry.name.startsWith(".")) {
656
- promises.push(walk(fullPath, depth + 1));
657
- }
658
- } else if (entry.isFile()) {
659
- const ext = extname(entry.name).slice(1).toLowerCase();
660
- if (ext && extensions.includes(ext) && !matchesPattern(entry.name, ignorePatterns)) {
661
- promises.push(
662
- (async () => {
663
- const fileStat = await stat2(fullPath).catch(() => null);
664
- if (!fileStat) return;
665
- let lines = 0;
666
- try {
667
- const content = await readFile3(fullPath, "utf-8");
668
- lines = content.split("\n").length;
669
- } catch {
670
- lines = 0;
671
- }
672
- results.push({
673
- path: fullPath,
674
- relativePath: relative2(rootPath, fullPath),
675
- extension: ext,
676
- size: fileStat.size,
677
- lastModified: fileStat.mtime,
678
- lines
679
- });
680
- })()
681
- );
682
- }
683
- }
684
- }
685
- await Promise.all(promises);
686
- }
687
- await walk(rootPath, 0);
688
- return results;
689
- }
690
- var TYPE_PATTERNS = [/types?\//i, /\.d\.ts$/, /interfaces?\//i];
691
- var TEST_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\/__tests__\//, /\/tests?\//];
692
- var CONFIG_PATTERNS = [/\.config\.[jt]s$/, /rc\.[jt]s$/, /\.env/, /tsconfig/, /package\.json$/, /\.yml$/, /\.yaml$/, /\.toml$/];
693
- var ENTRY_PATTERNS = [/^index\.[jt]sx?$/, /^main\.[jt]sx?$/, /^app\.[jt]sx?$/, /^server\.[jt]sx?$/];
694
- function classifyFileKind(relativePath) {
695
- const filename = basename2(relativePath);
696
- if (TYPE_PATTERNS.some((p) => p.test(relativePath))) return "type";
697
- if (TEST_PATTERNS.some((p) => p.test(relativePath))) return "test";
698
- if (CONFIG_PATTERNS.some((p) => p.test(relativePath) || p.test(filename))) return "config";
699
- if (ENTRY_PATTERNS.some((p) => p.test(filename))) return "entry";
700
- return "source";
701
- }
702
- function detectStack(files) {
703
- const stack = [];
704
- const extensions = new Set(files.map((f) => f.extension));
705
- const paths = files.map((f) => f.relativePath.toLowerCase());
706
- if (extensions.has("ts") || extensions.has("tsx")) stack.push("TypeScript");
707
- else if (extensions.has("js") || extensions.has("jsx")) stack.push("JavaScript");
708
- if (extensions.has("py")) stack.push("Python");
709
- if (extensions.has("go")) stack.push("Go");
710
- if (extensions.has("rs")) stack.push("Rust");
711
- if (extensions.has("java")) stack.push("Java");
712
- if (extensions.has("kt")) stack.push("Kotlin");
713
- if (extensions.has("rb")) stack.push("Ruby");
714
- if (extensions.has("php")) stack.push("PHP");
715
- if (extensions.has("cs")) stack.push("C#");
716
- if (extensions.has("c") || extensions.has("cpp")) stack.push("C/C++");
717
- if (paths.some((p) => p.includes("next.config"))) stack.push("Next.js");
718
- if (paths.some((p) => p.includes("nuxt.config"))) stack.push("Nuxt");
719
- if (paths.some((p) => p.includes("angular.json"))) stack.push("Angular");
720
- return stack;
721
- }
722
- async function analyzeProject(projectPath, config) {
723
- const absPath = resolve3(projectPath);
724
- const projectName = basename2(absPath);
725
- const mergedConfig = mergeConfig(DEFAULT_CONFIG, config);
726
- const allExtensions = [
727
- ...mergedConfig.analysis.extensions.code,
728
- ...mergedConfig.analysis.extensions.config,
729
- ...mergedConfig.analysis.extensions.docs
730
- ];
731
- const walkEntries = await walkProject(absPath, {
732
- ignoreDirs: mergedConfig.analysis.ignore.dirs,
733
- ignorePatterns: mergedConfig.analysis.ignore.patterns,
734
- extensions: allExtensions,
735
- maxDepth: mergedConfig.analysis.maxDepth
736
- });
737
- const tokenMethod = mergedConfig.tokens.method;
738
- const files = [];
739
- for (const entry of walkEntries) {
740
- let tokens;
741
- if (tokenMethod === "tiktoken") {
742
- try {
743
- const content = await readFile3(entry.path, "utf-8");
744
- tokens = estimateTokens(content, entry.size, "tiktoken");
745
- } catch {
746
- tokens = countTokensChars4(entry.size);
747
- }
748
- } else {
749
- tokens = countTokensChars4(entry.size);
750
- }
751
- files.push({
752
- path: entry.path,
753
- relativePath: entry.relativePath,
754
- extension: entry.extension,
755
- size: entry.size,
756
- tokens,
757
- lines: entry.lines,
758
- lastModified: entry.lastModified,
759
- kind: classifyFileKind(entry.relativePath),
760
- // Graph data — populated by graph analysis
761
- imports: [],
762
- importedBy: [],
763
- isHub: false,
764
- complexity: 0,
765
- // Risk data — populated by risk analysis
766
- riskScore: 0,
767
- riskFactors: [],
768
- exclusionImpact: "none"
769
- });
770
- }
771
- const graph = buildProjectGraph(absPath, files);
772
- for (const file of files) {
773
- const nodeImports = [];
774
- const nodeImportedBy = [];
775
- for (const edge of graph.edges) {
776
- if (edge.from === file.relativePath) nodeImports.push(edge.to);
777
- if (edge.to === file.relativePath) nodeImportedBy.push(edge.from);
778
- }
779
- file.imports = nodeImports;
780
- file.importedBy = nodeImportedBy;
781
- file.isHub = graph.hubs.some((h) => h.relativePath === file.relativePath);
782
- }
783
- const riskWeights = mergedConfig.risk.weights;
784
- scoreAllFiles(files, graph, riskWeights);
785
- const riskProfile = {
786
- distribution: {
787
- critical: files.filter((f) => f.riskScore >= 80).length,
788
- high: files.filter((f) => f.riskScore >= 60 && f.riskScore < 80).length,
789
- medium: files.filter((f) => f.riskScore >= 30 && f.riskScore < 60).length,
790
- low: files.filter((f) => f.riskScore < 30).length
791
- },
792
- topRiskFiles: [...files].sort((a, b) => b.riskScore - a.riskScore).slice(0, 10),
793
- overallComplexity: files.length > 0 ? files.reduce((s, f) => s + f.complexity, 0) / files.length : 0
794
- };
795
- const totalTokens = files.reduce((s, f) => s + f.tokens, 0);
796
- const hashInput = files.map((f) => `${f.relativePath}:${f.tokens}:${f.riskScore}`).sort().join("|");
797
- const hash = createHash("sha256").update(hashInput).digest("hex").substring(0, 16);
798
- const stack = detectStack(walkEntries);
799
- return {
800
- projectPath: absPath,
801
- projectName,
802
- analyzedAt: /* @__PURE__ */ new Date(),
803
- hash,
804
- files,
805
- totalFiles: files.length,
806
- totalTokens,
807
- graph,
808
- riskProfile,
809
- stack,
810
- tokenMethod
811
- };
812
- }
813
- function mergeConfig(base, overrides) {
814
- if (!overrides) return base;
815
- return {
816
- ...base,
817
- ...overrides,
818
- analysis: {
819
- ...base.analysis,
820
- ...overrides.analysis,
821
- extensions: {
822
- ...base.analysis.extensions,
823
- ...overrides.analysis?.extensions
824
- },
825
- ignore: {
826
- ...base.analysis.ignore,
827
- ...overrides.analysis?.ignore
828
- }
829
- },
830
- risk: {
831
- ...base.risk,
832
- ...overrides.risk,
833
- weights: {
834
- ...base.risk.weights,
835
- ...overrides.risk?.weights
836
- }
837
- },
838
- interaction: {
839
- ...base.interaction,
840
- ...overrides.interaction
841
- },
842
- tokens: {
843
- ...base.tokens,
844
- ...overrides.tokens
845
- },
846
- governance: {
847
- ...base.governance,
848
- ...overrides.governance
849
- }
850
- };
851
- }
852
-
853
- // src/cli/v2/analyze.ts
854
- var analyzeCommand = new Command2("analyze").description("Analyze project structure, dependencies, and risk profile").argument("[path]", "Project path to analyze", ".").option("--json", "Output as JSON").option("--method <method>", "Token counting method: chars4 | tiktoken", "chars4").option("--risk-only", "Show only risk distribution and top risk files").option("--graph", "Show detailed dependency graph info (hubs, clusters, orphans)").action(async (path, opts) => {
855
- try {
856
- const projectPath = resolve4(path);
857
- const method = opts.method === "tiktoken" ? "tiktoken" : "chars4";
858
- console.log(chalk2.dim(`
859
- Analyzing ${projectPath}...
860
- `));
861
- const analysis = await analyzeProject(projectPath, { tokens: { method } });
862
- if (opts.json) {
863
- console.log(JSON.stringify(analysis, null, 2));
864
- return;
865
- }
866
- console.log(chalk2.bold.cyan(" \u{1F4CA} Project Analysis"));
867
- console.log("");
868
- console.log(` ${chalk2.bold("Project:")} ${analysis.projectName}`);
869
- console.log(` ${chalk2.bold("Stack:")} ${analysis.stack.join(", ") || "Unknown"}`);
870
- console.log(` ${chalk2.bold("Files:")} ${analysis.totalFiles}`);
871
- console.log(` ${chalk2.bold("Tokens:")} ~${Math.round(analysis.totalTokens / 1e3)}K`);
872
- console.log(` ${chalk2.bold("Method:")} ${analysis.tokenMethod}`);
873
- console.log(` ${chalk2.bold("Hash:")} ${analysis.hash.substring(0, 12)}`);
874
- console.log("");
875
- const d = analysis.riskProfile.distribution;
876
- console.log(chalk2.bold(" Risk Distribution:"));
877
- console.log(` ${chalk2.red("Critical:")} ${d.critical} ${chalk2.yellow("High:")} ${d.high} ${chalk2.blue("Medium:")} ${d.medium} ${chalk2.dim("Low:")} ${d.low}`);
878
- console.log(` ${chalk2.bold("Overall complexity:")} ${analysis.riskProfile.overallComplexity.toFixed(1)}`);
879
- console.log("");
880
- if (opts.riskOnly) {
881
- console.log(chalk2.dim(` Run ${chalk2.bold("cto interact")} to build optimized AI context`));
882
- console.log("");
883
- return;
884
- }
885
- const g = analysis.graph;
886
- console.log(chalk2.bold(" Dependency Graph:"));
887
- console.log(` Nodes: ${g.nodes.length} Edges: ${g.edges.length}`);
888
- console.log(` Hubs: ${g.hubs.length} Leaves: ${g.leaves.length} Orphans: ${g.orphans.length} Clusters: ${g.clusters.length}`);
889
- if (opts.graph) {
890
- if (g.hubs.length > 0) {
891
- console.log("");
892
- console.log(chalk2.bold(" Hub Files (imported by 3+ files):"));
893
- for (const hub of g.hubs.slice(0, 15)) {
894
- console.log(` ${hub.relativePath} \u2014 ${hub.dependents} dependents, score ${hub.score}`);
895
- }
896
- }
897
- if (g.clusters.length > 0) {
898
- console.log("");
899
- console.log(chalk2.bold(" Clusters:"));
900
- for (const c of g.clusters.slice(0, 10)) {
901
- console.log(` ${c.name} \u2014 ${c.files.length} files, ${Math.round(c.totalTokens / 1e3)}K tokens, cohesion ${c.cohesion}`);
902
- }
903
- }
904
- if (g.orphans.length > 0) {
905
- console.log("");
906
- console.log(chalk2.bold(` Orphans (${g.orphans.length} files with no connections):`));
907
- for (const o of g.orphans.slice(0, 10)) {
908
- console.log(` ${chalk2.dim(o)}`);
909
- }
910
- if (g.orphans.length > 10) {
911
- console.log(chalk2.dim(` ... and ${g.orphans.length - 10} more`));
912
- }
913
- }
914
- }
915
- console.log("");
916
- if (analysis.riskProfile.topRiskFiles.length > 0) {
917
- console.log(chalk2.bold(" Top Risk Files:"));
918
- for (const f of analysis.riskProfile.topRiskFiles.slice(0, 10)) {
919
- const impact = f.exclusionImpact;
920
- const impactColor = impact === "critical" ? chalk2.red : impact === "high" ? chalk2.yellow : chalk2.dim;
921
- console.log(` ${impactColor(`[${impact}]`)} ${f.relativePath} \u2014 risk ${f.riskScore}, ~${Math.round(f.tokens / 1e3)}K tokens`);
922
- }
923
- console.log("");
924
- }
925
- console.log(chalk2.dim(` Run ${chalk2.bold("cto interact")} to build optimized AI context`));
926
- console.log("");
927
- } catch (err) {
928
- console.error(chalk2.red(`
929
- Error: ${err.message}
930
- `));
931
- process.exit(1);
932
- }
933
- });
934
-
935
- // src/cli/v2/interact.ts
936
- import { Command as Command3 } from "commander";
937
- import chalk3 from "chalk";
938
- import { resolve as resolve8, join as join8 } from "path";
939
- import { writeFile as writeFile4 } from "fs/promises";
940
-
941
- // src/engine/cache.ts
942
- import { createHash as createHash2 } from "crypto";
943
- import { readdir as readdir2, stat as stat3 } from "fs/promises";
944
- import { join as join5, extname as extname2, relative as relative3, resolve as resolve5 } from "path";
945
- var DEFAULT_CACHE_OPTIONS = {
946
- maxAgeMs: 5 * 60 * 1e3,
947
- // 5 minutes
948
- maxEntries: 10,
949
- enabled: true
950
- };
951
- var cache = /* @__PURE__ */ new Map();
952
- var cacheOptions = { ...DEFAULT_CACHE_OPTIONS };
953
- async function computeFingerprint(rootPath, config = DEFAULT_CONFIG) {
954
- const entries = [];
955
- const allExtensions = /* @__PURE__ */ new Set([
956
- ...config.analysis.extensions.code,
957
- ...config.analysis.extensions.config,
958
- ...config.analysis.extensions.docs
959
- ]);
960
- const ignoreDirSet = new Set(config.analysis.ignore.dirs);
961
- async function walk(dir, depth) {
962
- if (depth > config.analysis.maxDepth) return;
963
- let dirEntries;
964
- try {
965
- dirEntries = await readdir2(dir, { withFileTypes: true });
966
- } catch {
967
- return;
968
- }
969
- const promises = [];
970
- for (const entry of dirEntries) {
971
- const fullPath = join5(dir, entry.name);
972
- if (entry.isDirectory()) {
973
- if (!ignoreDirSet.has(entry.name) && !entry.name.startsWith(".")) {
974
- promises.push(walk(fullPath, depth + 1));
975
- }
976
- } else if (entry.isFile()) {
977
- const ext = extname2(entry.name).slice(1).toLowerCase();
978
- if (ext && allExtensions.has(ext)) {
979
- promises.push(
980
- (async () => {
981
- try {
982
- const s = await stat3(fullPath);
983
- const rel = relative3(rootPath, fullPath);
984
- entries.push(`${rel}:${s.mtimeMs.toFixed(0)}:${s.size}`);
985
- } catch {
986
- }
987
- })()
988
- );
989
- }
990
- }
991
- }
992
- await Promise.all(promises);
993
- }
994
- await walk(rootPath, 0);
995
- entries.sort();
996
- return createHash2("sha256").update(entries.join("|")).digest("hex").substring(0, 16);
997
- }
998
- async function getCachedAnalysis(projectPath, config) {
999
- const absPath = resolve5(projectPath);
1000
- if (!cacheOptions.enabled) {
1001
- return analyzeProject(absPath, config);
1002
- }
1003
- const existing = cache.get(absPath);
1004
- if (existing) {
1005
- const age = Date.now() - existing.createdAt;
1006
- if (age > cacheOptions.maxAgeMs) {
1007
- cache.delete(absPath);
1008
- }
1009
- }
1010
- const mergedConfig = config ? { ...DEFAULT_CONFIG, ...config } : DEFAULT_CONFIG;
1011
- const fingerprint = await computeFingerprint(absPath, mergedConfig);
1012
- const cached = cache.get(absPath);
1013
- if (cached && cached.fingerprint === fingerprint) {
1014
- cached.hits++;
1015
- return cached.analysis;
1016
- }
1017
- const analysis = await analyzeProject(absPath, config);
1018
- if (cache.size >= cacheOptions.maxEntries) {
1019
- const oldest = [...cache.entries()].sort(
1020
- (a, b) => a[1].createdAt - b[1].createdAt
1021
- )[0];
1022
- if (oldest) cache.delete(oldest[0]);
1023
- }
1024
- cache.set(absPath, {
1025
- analysis,
1026
- fingerprint,
1027
- createdAt: Date.now(),
1028
- hits: 0
1029
- });
1030
- return analysis;
1031
- }
1032
-
1033
- // src/engine/pr-context.ts
1034
- import { resolve as resolve6 } from "path";
1035
- import { execFile } from "child_process";
1036
- import { promisify } from "util";
1037
-
1038
- // src/engine/graph-utils.ts
1039
- function buildAdjacencyList(edges) {
1040
- const forward = /* @__PURE__ */ new Map();
1041
- const reverse = /* @__PURE__ */ new Map();
1042
- for (const edge of edges) {
1043
- if (!forward.has(edge.from)) forward.set(edge.from, []);
1044
- forward.get(edge.from).push(edge.to);
1045
- if (!reverse.has(edge.to)) reverse.set(edge.to, []);
1046
- reverse.get(edge.to).push(edge.from);
1047
- }
1048
- return { forward, reverse };
1049
- }
1050
- function bfsBidirectional(seeds, adj, depth) {
1051
- const result = new Set(seeds);
1052
- let frontier = [...seeds];
1053
- const visited = /* @__PURE__ */ new Set();
1054
- for (let d = 0; d < depth; d++) {
1055
- const nextFrontier = [];
1056
- for (const node of frontier) {
1057
- if (visited.has(node)) continue;
1058
- visited.add(node);
1059
- const fwd = adj.forward.get(node);
1060
- if (fwd) {
1061
- for (const neighbor of fwd) {
1062
- if (!visited.has(neighbor)) {
1063
- result.add(neighbor);
1064
- nextFrontier.push(neighbor);
1065
- }
1066
- }
1067
- }
1068
- const rev = adj.reverse.get(node);
1069
- if (rev) {
1070
- for (const neighbor of rev) {
1071
- if (!visited.has(neighbor)) {
1072
- result.add(neighbor);
1073
- nextFrontier.push(neighbor);
1074
- }
1075
- }
1076
- }
1077
- }
1078
- frontier = nextFrontier;
1079
- }
1080
- return result;
1081
- }
1082
- function matchGlob(path, pattern) {
1083
- const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\xA7\xA7").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, ".");
1084
- try {
1085
- return new RegExp(`^${regexStr}$`).test(path);
1086
- } catch {
1087
- return false;
1088
- }
1089
- }
1090
-
1091
- // src/engine/pr-context.ts
1092
- var exec = promisify(execFile);
1093
- async function git(args, cwd) {
1094
- try {
1095
- const { stdout } = await exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
1096
- return stdout.trim();
1097
- } catch {
1098
- return "";
1099
- }
1100
- }
1101
- async function isGitRepo(projectPath) {
1102
- const result = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
1103
- return result === "true";
1104
- }
1105
- async function getCurrentBranch(projectPath) {
1106
- return git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
1107
- }
1108
- async function getChangedFilesFromDiff(projectPath, baseBranch) {
1109
- const results = /* @__PURE__ */ new Map();
1110
- const workingDiff = await git(["diff", "--numstat", "HEAD"], projectPath);
1111
- parseDiffNumstat(workingDiff, results, "modified");
1112
- const stagedDiff = await git(["diff", "--numstat", "--cached"], projectPath);
1113
- parseDiffNumstat(stagedDiff, results, "modified");
1114
- const untracked = await git(["ls-files", "--others", "--exclude-standard"], projectPath);
1115
- for (const line of untracked.split("\n")) {
1116
- const f = line.trim();
1117
- if (f && !results.has(f)) {
1118
- results.set(f, { relativePath: f, changeType: "added", linesAdded: 0, linesRemoved: 0 });
1119
- }
1120
- }
1121
- if (baseBranch) {
1122
- const branchExists = await git(["rev-parse", "--verify", baseBranch], projectPath);
1123
- if (branchExists) {
1124
- const branchDiff = await git(["diff", "--numstat", `${baseBranch}...HEAD`], projectPath);
1125
- parseDiffNumstat(branchDiff, results, "modified");
1126
- const nameStatus = await git(["diff", "--name-status", `${baseBranch}...HEAD`], projectPath);
1127
- for (const line of nameStatus.split("\n")) {
1128
- const parts = line.trim().split(" ");
1129
- if (parts.length >= 2) {
1130
- const status = parts[0];
1131
- const filePath = parts[parts.length - 1];
1132
- if (results.has(filePath)) {
1133
- const existing = results.get(filePath);
1134
- if (status === "A") existing.changeType = "added";
1135
- else if (status === "D") existing.changeType = "deleted";
1136
- else if (status.startsWith("R")) existing.changeType = "renamed";
1137
- }
1138
- }
1139
- }
1140
- }
1141
- }
1142
- return [...results.values()];
1143
- }
1144
- function parseDiffNumstat(output, results, defaultType) {
1145
- for (const line of output.split("\n")) {
1146
- const parts = line.trim().split(" ");
1147
- if (parts.length < 3) continue;
1148
- const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
1149
- const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
1150
- const filePath = parts[2];
1151
- if (!filePath) continue;
1152
- const existing = results.get(filePath);
1153
- if (existing) {
1154
- existing.linesAdded = Math.max(existing.linesAdded, added);
1155
- existing.linesRemoved = Math.max(existing.linesRemoved, removed);
1156
- } else {
1157
- results.set(filePath, {
1158
- relativePath: filePath,
1159
- changeType: defaultType,
1160
- linesAdded: added,
1161
- linesRemoved: removed
1162
- });
1163
- }
1164
- }
1165
- }
1166
- async function generatePRContext(analysis, options = {}) {
1167
- const projectPath = resolve6(analysis.projectPath);
1168
- const baseBranch = options.baseBranch ?? "main";
1169
- const depth = options.depth ?? 2;
1170
- const includeTests = options.includeTests ?? false;
1171
- const gitRepo = await isGitRepo(projectPath);
1172
- if (!gitRepo) {
1173
- return emptyResult(baseBranch);
1174
- }
1175
- const currentBranch = await getCurrentBranch(projectPath);
1176
- const changedFiles = await getChangedFilesFromDiff(projectPath, baseBranch);
1177
- if (changedFiles.length === 0) {
1178
- return {
1179
- ...emptyResult(baseBranch),
1180
- currentBranch,
1181
- isGitRepo: true,
1182
- renderedSummary: "# PR Context\n\nNo changed files detected."
1183
- };
1184
- }
1185
- const analysisFileSet = new Set(analysis.files.map((f) => f.relativePath));
1186
- const validChangedPaths = changedFiles.filter((c) => c.changeType !== "deleted" && analysisFileSet.has(c.relativePath)).map((c) => c.relativePath);
1187
- const adj = buildAdjacencyList(analysis.graph.edges);
1188
- const expanded = bfsBidirectional(validChangedPaths, adj, depth);
1189
- const changedSet = new Set(validChangedPaths);
1190
- const dependencyFiles = [...expanded].filter((f) => !changedSet.has(f));
1191
- const allRelevantPaths = new Set(expanded);
1192
- if (!includeTests) {
1193
- for (const path of allRelevantPaths) {
1194
- const file = analysis.files.find((f) => f.relativePath === path);
1195
- if (file && file.kind === "test") {
1196
- if (!changedSet.has(path)) {
1197
- allRelevantPaths.delete(path);
1198
- }
1199
- }
1200
- }
1201
- }
1202
- const allRelevantFiles = analysis.files.filter((f) => allRelevantPaths.has(f.relativePath)).sort((a, b) => b.riskScore - a.riskScore);
1203
- const changedAnalyzed = allRelevantFiles.filter((f) => changedSet.has(f.relativePath));
1204
- const totalChangedTokens = changedAnalyzed.reduce((s, f) => s + f.tokens, 0);
1205
- const totalContextTokens = allRelevantFiles.reduce((s, f) => s + f.tokens, 0);
1206
- const riskSummary = {
1207
- critical: allRelevantFiles.filter((f) => f.riskScore >= 80).length,
1208
- high: allRelevantFiles.filter((f) => f.riskScore >= 60 && f.riskScore < 80).length,
1209
- medium: allRelevantFiles.filter((f) => f.riskScore >= 30 && f.riskScore < 60).length,
1210
- low: allRelevantFiles.filter((f) => f.riskScore < 30).length,
1211
- maxRiskFile: allRelevantFiles[0]?.relativePath ?? "",
1212
- maxRiskScore: allRelevantFiles[0]?.riskScore ?? 0
1213
- };
1214
- const renderedSummary = renderSummary(
1215
- analysis,
1216
- currentBranch,
1217
- baseBranch,
1218
- changedFiles,
1219
- dependencyFiles,
1220
- allRelevantFiles,
1221
- changedSet,
1222
- riskSummary,
1223
- totalChangedTokens,
1224
- totalContextTokens
1225
- );
1226
- return {
1227
- baseBranch,
1228
- currentBranch,
1229
- isGitRepo: true,
1230
- changedFiles,
1231
- dependencyFiles: dependencyFiles.filter((f) => allRelevantPaths.has(f)),
1232
- allRelevantFiles,
1233
- totalChangedTokens,
1234
- totalContextTokens,
1235
- riskSummary,
1236
- renderedSummary
1237
- };
1238
- }
1239
- function renderSummary(analysis, currentBranch, baseBranch, changedFiles, dependencyFiles, allRelevant, changedSet, risk, changedTokens, totalTokens) {
1240
- const lines = [];
1241
- lines.push(`## PR Context \u2014 ${analysis.projectName}`);
1242
- lines.push("");
1243
- lines.push(`**Branch:** ${currentBranch} \u2190 ${baseBranch}`);
1244
- lines.push(`**Changed:** ${changedFiles.length} files | **Dependencies:** ${dependencyFiles.length} files`);
1245
- lines.push(`**Tokens:** ~${Math.round(changedTokens / 1e3)}K changed + ~${Math.round((totalTokens - changedTokens) / 1e3)}K context = ~${Math.round(totalTokens / 1e3)}K total`);
1246
- lines.push("");
1247
- if (risk.critical > 0 || risk.high > 0) {
1248
- lines.push(`\u26A0\uFE0F **Risk:** ${risk.critical} critical + ${risk.high} high-risk files affected`);
1249
- lines.push(`**Highest risk:** ${risk.maxRiskFile} (score: ${risk.maxRiskScore})`);
1250
- lines.push("");
1251
- }
1252
- lines.push("### Changed Files");
1253
- lines.push("");
1254
- const sortedChanged = changedFiles.filter((c) => c.changeType !== "deleted").sort((a, b) => {
1255
- const fa = allRelevant.find((f) => f.relativePath === a.relativePath);
1256
- const fb = allRelevant.find((f) => f.relativePath === b.relativePath);
1257
- return (fb?.riskScore ?? 0) - (fa?.riskScore ?? 0);
1258
- });
1259
- for (const c of sortedChanged) {
1260
- const file = allRelevant.find((f) => f.relativePath === c.relativePath);
1261
- const risk2 = file ? ` risk:${file.riskScore}` : "";
1262
- const delta = c.linesAdded || c.linesRemoved ? ` (+${c.linesAdded}/-${c.linesRemoved})` : "";
1263
- const badge = c.changeType === "added" ? " \u{1F195}" : c.changeType === "renamed" ? " \u{1F4DD}" : "";
1264
- lines.push(`- \`${c.relativePath}\`${delta}${risk2}${badge}`);
1265
- }
1266
- const deleted = changedFiles.filter((c) => c.changeType === "deleted");
1267
- if (deleted.length > 0) {
1268
- lines.push("");
1269
- lines.push(`**Deleted:** ${deleted.map((d) => `\`${d.relativePath}\``).join(", ")}`);
1270
- }
1271
- if (dependencyFiles.length > 0) {
1272
- lines.push("");
1273
- lines.push("### Dependencies (included for context)");
1274
- lines.push("");
1275
- const depWithInfo = dependencyFiles.map((d) => {
1276
- const file = allRelevant.find((f) => f.relativePath === d);
1277
- return { path: d, riskScore: file?.riskScore ?? 0, tokens: file?.tokens ?? 0 };
1278
- }).sort((a, b) => b.riskScore - a.riskScore);
1279
- for (const d of depWithInfo) {
1280
- lines.push(`- \`${d.path}\` risk:${d.riskScore} ~${Math.round(d.tokens / 1e3)}K tokens`);
1281
- }
1282
- }
1283
- lines.push("");
1284
- lines.push("### Review Focus");
1285
- lines.push("");
1286
- lines.push("- Review changed files for correctness, especially high-risk files");
1287
- lines.push("- Dependencies are included for type/interface context \u2014 not for review");
1288
- lines.push(`- ${analysis.totalFiles - allRelevant.length} files excluded (not affected by this change)`);
1289
- return lines.join("\n");
1290
- }
1291
- function emptyResult(baseBranch) {
1292
- return {
1293
- baseBranch,
1294
- currentBranch: "",
1295
- isGitRepo: false,
1296
- changedFiles: [],
1297
- dependencyFiles: [],
1298
- allRelevantFiles: [],
1299
- totalChangedTokens: 0,
1300
- totalContextTokens: 0,
1301
- riskSummary: { critical: 0, high: 0, medium: 0, low: 0, maxRiskFile: "", maxRiskScore: 0 },
1302
- renderedSummary: "# PR Context\n\nNot a git repository."
1303
- };
1304
- }
1305
-
1306
- // src/interact/orchestrator.ts
1307
- import { randomUUID as randomUUID2 } from "crypto";
1308
-
1309
- // src/engine/selector.ts
1310
- import { createHash as createHash4 } from "crypto";
1311
-
1312
- // src/govern/secrets.ts
1313
- import { readFile as readFile4 } from "fs/promises";
1314
- import { readFileSync, existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
1315
- import { resolve as resolve7, relative as relative4, join as join6, dirname as dirname2 } from "path";
1316
- import { createHash as createHash3 } from "crypto";
1317
- var BUILTIN_PATTERNS = [
1318
- // API Keys
1319
- { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
1320
- { type: "api-key", source: "sk-[a-zA-Z0-9]{20,}", flags: "g", severity: "critical", description: "OpenAI/Anthropic API Key" },
1321
- { type: "api-key", source: "sk-ant-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "Anthropic API Key" },
1322
- // AWS
1323
- { type: "aws-key", source: "AKIA[0-9A-Z]{16}", flags: "g", severity: "critical", description: "AWS Access Key ID" },
1324
- { 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" },
1325
- // Private Keys
1326
- { type: "private-key", source: "-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----", flags: "g", severity: "critical", description: "Private Key" },
1327
- { type: "private-key", source: "-----BEGIN OPENSSH PRIVATE KEY-----", flags: "g", severity: "critical", description: "SSH Private Key" },
1328
- // Passwords
1329
- { type: "password", source: `(?:password|passwd|pwd)\\s*[:=]\\s*['"]([^'"]{8,})['"](?!\\s*\\{)`, flags: "gi", severity: "high", description: "Hardcoded Password" },
1330
- { type: "password", source: `(?:DB_PASSWORD|DATABASE_PASSWORD|MYSQL_PASSWORD|POSTGRES_PASSWORD)\\s*[:=]\\s*['"]?([^'"{}\\s]{4,})['"]?`, flags: "gi", severity: "high", description: "Database Password" },
1331
- // Tokens
1332
- { 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" },
1333
- { type: "token", source: "ghp_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub Personal Access Token" },
1334
- { type: "token", source: "gho_[a-zA-Z0-9]{36}", flags: "g", severity: "critical", description: "GitHub OAuth Token" },
1335
- { type: "token", source: "glpat-[a-zA-Z0-9\\-]{20,}", flags: "g", severity: "critical", description: "GitLab Personal Access Token" },
1336
- { type: "token", source: "npm_[a-zA-Z0-9]{36}", flags: "g", severity: "high", description: "npm Token" },
1337
- // Connection strings
1338
- { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
1339
- { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
1340
- // Environment variables with secrets
1341
- { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
1342
- // Stripe
1343
- { type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
1344
- { type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
1345
- { type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
1346
- // Slack
1347
- { type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
1348
- { type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
1349
- { type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
1350
- // Google
1351
- { type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
1352
- { type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
1353
- // Azure
1354
- { type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
1355
- // Twilio
1356
- { type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
1357
- // SendGrid
1358
- { type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
1359
- // JWT
1360
- { type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
1361
- // Datadog
1362
- { type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
1363
- { type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
1364
- // Sentry
1365
- { type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
1366
- // Firebase
1367
- { type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
1368
- { type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
1369
- // Supabase
1370
- { type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
1371
- { type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
1372
- // Vercel
1373
- { type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
1374
- // Heroku
1375
- { type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
1376
- // DigitalOcean
1377
- { type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
1378
- { type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
1379
- // Mailgun
1380
- { type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
1381
- // PII
1382
- { type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
1383
- { type: "pii", source: "\\b(?!000|666|9\\d{2})(\\d{3})[-.]?(?!00)(\\d{2})[-.]?(?!0000)(\\d{4})\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
1384
- ];
1385
- var _cachedBuiltinPatterns = null;
1386
- function getBuiltinPatterns() {
1387
- if (!_cachedBuiltinPatterns) {
1388
- _cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
1389
- type: def.type,
1390
- pattern: new RegExp(def.source, def.flags),
1391
- severity: def.severity,
1392
- description: def.description
1393
- }));
1394
- }
1395
- return _cachedBuiltinPatterns;
1396
- }
1397
- function buildPatterns(customPatterns = []) {
1398
- const builtins = getBuiltinPatterns();
1399
- if (customPatterns.length === 0) return builtins;
1400
- const patterns = [...builtins];
1401
- for (const custom of customPatterns) {
1402
- try {
1403
- patterns.push({
1404
- type: "custom",
1405
- pattern: new RegExp(custom, "gi"),
1406
- severity: "medium",
1407
- description: `Custom pattern: ${custom}`
1408
- });
1409
- } catch {
1410
- }
1411
- }
1412
- return patterns;
1413
- }
1414
- function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
1415
- const findings = [];
1416
- const lines = content.split("\n");
1417
- const allPatterns = buildPatterns(customPatterns);
1418
- for (const secretPattern of allPatterns) {
1419
- for (let i = 0; i < lines.length; i++) {
1420
- const line = lines[i];
1421
- secretPattern.pattern.lastIndex = 0;
1422
- let match;
1423
- while ((match = secretPattern.pattern.exec(line)) !== null) {
1424
- const matchText = match[0];
1425
- if (isTemplateOrPlaceholder(matchText)) continue;
1426
- if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
1427
- findings.push({
1428
- type: secretPattern.type,
1429
- file: filePath,
1430
- line: i + 1,
1431
- match: matchText,
1432
- redacted: redactSecret(matchText),
1433
- severity: secretPattern.severity
1434
- });
1435
- }
1436
- }
1437
- }
1438
- return deduplicateFindings(findings);
1439
- }
1440
- async function scanFileForSecrets(filePath, projectPath, customPatterns = []) {
1441
- try {
1442
- const content = await readFile4(filePath, "utf-8");
1443
- const relPath = relative4(resolve7(projectPath), resolve7(filePath));
1444
- return scanContentForSecrets(content, relPath, customPatterns);
1445
- } catch {
1446
- return [];
1447
- }
1448
- }
1449
- function redactSecret(value) {
1450
- if (value.length <= 8) return "***REDACTED***";
1451
- const prefix = value.substring(0, 4);
1452
- const suffix = value.substring(value.length - 2);
1453
- return `${prefix}${"*".repeat(Math.min(value.length - 6, 20))}${suffix}`;
1454
- }
1455
- function isTemplateOrPlaceholder(value) {
1456
- const placeholders = [
1457
- /\$\{.*\}/,
1458
- /\{\{.*\}\}/,
1459
- /%[sd]/,
1460
- /<[A-Z_]+>/,
1461
- /YOUR_.*_HERE/i,
1462
- /\bCHANGE_ME\b/i,
1463
- /\bPLACEHOLDER\b/i,
1464
- /\bexample\b/i,
1465
- /\bTODO\b/i,
1466
- /xxx+/i,
1467
- /\breplace.?me\b/i,
1468
- /\bdummy\b/i,
1469
- /\btest_?key\b/i,
1470
- /\bsample\b/i
1471
- ];
1472
- return placeholders.some((p) => p.test(value));
1473
- }
1474
- var PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
1475
- "example.com",
1476
- "example.org",
1477
- "example.net",
1478
- "test.com",
1479
- "test.org",
1480
- "test.net",
1481
- "localhost",
1482
- "localhost.localdomain",
1483
- "email.com",
1484
- "mail.com",
1485
- "foo.com",
1486
- "bar.com",
1487
- "baz.com",
1488
- "acme.com",
1489
- "company.com",
1490
- "corp.com",
1491
- "noreply.com",
1492
- "no-reply.com",
1493
- "users.noreply.github.com",
1494
- "placeholder.com"
1495
- ]);
1496
- function isSafeEmail(value, extraDomains) {
1497
- const match = value.match(/@([a-zA-Z0-9.-]+)$/);
1498
- if (!match) return false;
1499
- const domain = match[1].toLowerCase();
1500
- if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
1501
- if (extraDomains && extraDomains.has(domain)) return true;
1502
- return false;
1503
- }
1504
- function deduplicateFindings(findings) {
1505
- const seen = /* @__PURE__ */ new Set();
1506
- return findings.filter((f) => {
1507
- const key = `${f.file}:${f.line}:${f.type}:${f.match}`;
1508
- if (seen.has(key)) return false;
1509
- seen.add(key);
1510
- return true;
1511
- });
1512
- }
1513
-
1514
- // src/engine/pruner.ts
1515
- import { readFile as readFile5 } from "fs/promises";
1516
- var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
1517
- async function pruneFile(file, level) {
1518
- if (level === "excluded") {
1519
- return emptyResult2(file, "excluded");
1520
- }
1521
- if (level === "full") {
1522
- return fullContent(file);
1523
- }
1524
- const ext = file.extension.toLowerCase();
1525
- const isTS = TS_EXTENSIONS2.has(ext);
1526
- if (isTS) {
1527
- return pruneTypeScript(file, level);
1528
- }
1529
- return pruneGeneric(file, level);
1530
- }
1531
- async function pruneTypeScript(file, level) {
1532
- let content;
1533
- try {
1534
- content = await readFile5(file.path, "utf-8");
1535
- } catch {
1536
- return emptyResult2(file, level);
1537
- }
1538
- const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
1539
- const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
1540
- const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
1541
- return {
1542
- relativePath: file.relativePath,
1543
- originalTokens: file.tokens,
1544
- prunedTokens,
1545
- pruneLevel: level,
1546
- content: prunedContent,
1547
- savingsPercent: Math.max(0, savingsPercent)
1548
- };
1549
- }
1550
- function extractSignaturesRegex(content) {
1551
- const lines = content.split("\n");
1552
- const parts = [];
1553
- let i = 0;
1554
- while (i < lines.length) {
1555
- const line = lines[i];
1556
- const trimmed = line.trim();
1557
- if (trimmed === "") {
1558
- i++;
1559
- continue;
1560
- }
1561
- if (trimmed.startsWith("/**")) {
1562
- const docLines = [];
1563
- while (i < lines.length) {
1564
- docLines.push(lines[i]);
1565
- if (lines[i].includes("*/")) {
1566
- i++;
1567
- break;
1568
- }
1569
- i++;
1570
- }
1571
- parts.push(docLines.join("\n"));
1572
- continue;
1573
- }
1574
- if (trimmed.startsWith("//")) {
1575
- parts.push(line);
1576
- i++;
1577
- continue;
1578
- }
1579
- if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
1580
- const block = collectBracedLine(lines, i);
1581
- parts.push(block.text);
1582
- i = block.nextIndex;
1583
- continue;
1584
- }
1585
- if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
1586
- const block = collectBracedLine(lines, i);
1587
- parts.push(block.text);
1588
- i = block.nextIndex;
1589
- continue;
1590
- }
1591
- if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
1592
- const block = collectBalanced(lines, i);
1593
- parts.push(block.text);
1594
- i = block.nextIndex;
1595
- continue;
1596
- }
1597
- if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
1598
- const block = collectBalanced(lines, i);
1599
- parts.push(block.text);
1600
- i = block.nextIndex;
1601
- continue;
1602
- }
1603
- if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
1604
- const block = collectBalanced(lines, i);
1605
- parts.push(block.text);
1606
- i = block.nextIndex;
1607
- continue;
1608
- }
1609
- const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
1610
- if (fnMatch) {
1611
- const sig = extractFnSignature(lines, i);
1612
- parts.push(`${sig} { /* ... */ }`);
1613
- i = skipBlock(lines, i);
1614
- continue;
1615
- }
1616
- const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
1617
- if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
1618
- const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
1619
- if (prefix) {
1620
- parts.push(`${prefix} /* ... */;`);
1621
- }
1622
- i = skipBlock(lines, i);
1623
- continue;
1624
- }
1625
- if (arrowMatch) {
1626
- const block = collectStatement(lines, i);
1627
- parts.push(block.text);
1628
- i = block.nextIndex;
1629
- continue;
1630
- }
1631
- if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
1632
- const classOutline = extractClassOutline(lines, i);
1633
- parts.push(classOutline.text);
1634
- i = classOutline.nextIndex;
1635
- continue;
1636
- }
1637
- i++;
1638
- }
1639
- return parts.join("\n");
1640
- }
1641
- function extractSkeletonRegex(content) {
1642
- const lines = content.split("\n");
1643
- const parts = [];
1644
- let i = 0;
1645
- while (i < lines.length) {
1646
- const trimmed = lines[i].trim();
1647
- if (/^import\s/.test(trimmed)) {
1648
- const block = collectBracedLine(lines, i);
1649
- parts.push(block.text);
1650
- i = block.nextIndex;
1651
- continue;
1652
- }
1653
- if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
1654
- const block = collectBalanced(lines, i);
1655
- parts.push(block.text);
1656
- i = block.nextIndex;
1657
- continue;
1658
- }
1659
- if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
1660
- const block = collectBalanced(lines, i);
1661
- parts.push(block.text);
1662
- i = block.nextIndex;
1663
- continue;
1664
- }
1665
- if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
1666
- const sig = extractFnSignature(lines, i);
1667
- parts.push(`${sig};`);
1668
- i = skipBlock(lines, i);
1669
- continue;
1670
- }
1671
- if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
1672
- const nameMatch = trimmed.match(/class\s+(\w+)/);
1673
- const name = nameMatch?.[1] ?? "Unknown";
1674
- const end = skipBlock(lines, i);
1675
- const methods = [];
1676
- for (let j = i + 1; j < end; j++) {
1677
- const mt = lines[j].trim();
1678
- const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
1679
- if (mm && mm[1] !== "constructor") methods.push(mm[1]);
1680
- }
1681
- parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
1682
- i = end;
1683
- continue;
1684
- }
1685
- if (/^export\s*(\{|\*)/.test(trimmed)) {
1686
- const block = collectBracedLine(lines, i);
1687
- parts.push(block.text);
1688
- i = block.nextIndex;
1689
- continue;
1690
- }
1691
- i++;
1692
- }
1693
- return parts.join("\n");
1694
- }
1695
- function collectBracedLine(lines, start) {
1696
- let text = lines[start];
1697
- let i = start + 1;
1698
- while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
1699
- text += "\n" + lines[i];
1700
- i++;
1701
- }
1702
- return { text, nextIndex: i };
1703
- }
1704
- function collectBalanced(lines, start) {
1705
- let depth = 0;
1706
- let text = "";
1707
- let i = start;
1708
- let started = false;
1709
- while (i < lines.length) {
1710
- const line = lines[i];
1711
- text += (text ? "\n" : "") + line;
1712
- for (const ch of line) {
1713
- if (ch === "{" || ch === "(") {
1714
- depth++;
1715
- started = true;
1716
- }
1717
- if (ch === "}" || ch === ")") depth--;
1718
- }
1719
- i++;
1720
- if (started && depth <= 0) break;
1721
- if (!started && line.includes(";")) break;
1722
- }
1723
- return { text, nextIndex: i };
1724
- }
1725
- function collectStatement(lines, start) {
1726
- let text = lines[start];
1727
- let i = start + 1;
1728
- if (text.includes(";")) return { text, nextIndex: i };
1729
- let depth = 0;
1730
- for (const ch of text) {
1731
- if (ch === "{" || ch === "(" || ch === "[") depth++;
1732
- if (ch === "}" || ch === ")" || ch === "]") depth--;
1733
- }
1734
- while (i < lines.length && depth > 0) {
1735
- text += "\n" + lines[i];
1736
- for (const ch of lines[i]) {
1737
- if (ch === "{" || ch === "(" || ch === "[") depth++;
1738
- if (ch === "}" || ch === ")" || ch === "]") depth--;
1739
- }
1740
- i++;
1741
- }
1742
- return { text, nextIndex: i };
1743
- }
1744
- function extractFnSignature(lines, start) {
1745
- let sig = "";
1746
- let i = start;
1747
- while (i < lines.length) {
1748
- const line = lines[i].trim();
1749
- sig += (sig ? " " : "") + line;
1750
- if (line.includes("{")) {
1751
- sig = sig.replace(/\s*\{[^]*$/, "").trim();
1752
- break;
1753
- }
1754
- i++;
1755
- }
1756
- return sig;
1757
- }
1758
- function skipBlock(lines, start) {
1759
- let depth = 0;
1760
- let i = start;
1761
- let foundBrace = false;
1762
- while (i < lines.length) {
1763
- for (const ch of lines[i]) {
1764
- if (ch === "{") {
1765
- depth++;
1766
- foundBrace = true;
1767
- }
1768
- if (ch === "}") depth--;
1769
- }
1770
- i++;
1771
- if (foundBrace && depth <= 0) break;
1772
- if (!foundBrace && lines[i - 1].includes(";")) break;
1773
- }
1774
- return i;
1775
- }
1776
- function looksLikeFunctionDecl(lines, start) {
1777
- const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
1778
- return /=>/.test(chunk) || /=\s*function/.test(chunk);
1779
- }
1780
- function extractClassOutline(lines, start) {
1781
- const header = lines[start].trim();
1782
- let headerText = header;
1783
- let i = start + 1;
1784
- if (!header.includes("{")) {
1785
- while (i < lines.length) {
1786
- headerText += " " + lines[i].trim();
1787
- if (lines[i].includes("{")) {
1788
- i++;
1789
- break;
1790
- }
1791
- i++;
1792
- }
1793
- } else {
1794
- i = start + 1;
1795
- }
1796
- const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
1797
- let depth = 1;
1798
- while (i < lines.length && depth > 0) {
1799
- const line = lines[i];
1800
- const trimmed = line.trim();
1801
- for (const ch of line) {
1802
- if (ch === "{") depth++;
1803
- if (ch === "}") depth--;
1804
- }
1805
- if (depth <= 0) {
1806
- i++;
1807
- break;
1808
- }
1809
- if (depth === 1) {
1810
- if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
1811
- bodyParts.push(` ${trimmed}`);
1812
- } else if (/^constructor\s*\(/.test(trimmed)) {
1813
- const sig = extractFnSignature(lines, i);
1814
- bodyParts.push(` ${sig} { /* ... */ }`);
1815
- } else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
1816
- const sig = extractFnSignature(lines, i);
1817
- bodyParts.push(` ${sig} { /* ... */ }`);
1818
- }
1819
- }
1820
- i++;
1821
- }
1822
- bodyParts.push("}");
1823
- return { text: bodyParts.join("\n"), nextIndex: i };
1824
- }
1825
- async function pruneGeneric(file, level) {
1826
- let content;
1827
- try {
1828
- content = await readFile5(file.path, "utf-8");
1829
- } catch {
1830
- return emptyResult2(file, level);
1831
- }
1832
- return pruneGenericFromContent(file, content, level);
1833
- }
1834
- function pruneGenericFromContent(file, content, level) {
1835
- const lines = content.split("\n");
1836
- let result;
1837
- if (level === "signatures") {
1838
- result = lines.filter((line) => {
1839
- const t = line.trim();
1840
- 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);
1841
- });
1842
- } else {
1843
- result = lines.filter((line) => {
1844
- const t = line.trim();
1845
- 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);
1846
- });
1847
- }
1848
- const prunedContent = result.join("\n");
1849
- const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
1850
- const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
1851
- return {
1852
- relativePath: file.relativePath,
1853
- originalTokens: file.tokens,
1854
- prunedTokens,
1855
- pruneLevel: level,
1856
- content: prunedContent,
1857
- savingsPercent: Math.max(0, savingsPercent)
1858
- };
1859
- }
1860
- async function fullContent(file) {
1861
- let content = "";
1862
- try {
1863
- content = await readFile5(file.path, "utf-8");
1864
- } catch {
1865
- }
1866
- return {
1867
- relativePath: file.relativePath,
1868
- originalTokens: file.tokens,
1869
- prunedTokens: file.tokens,
1870
- pruneLevel: "full",
1871
- content,
1872
- savingsPercent: 0
1873
- };
1874
- }
1875
- function emptyResult2(file, level) {
1876
- return {
1877
- relativePath: file.relativePath,
1878
- originalTokens: file.tokens,
1879
- prunedTokens: 0,
1880
- pruneLevel: level,
1881
- content: "",
1882
- savingsPercent: 100
1883
- };
1884
- }
1885
-
1886
- // src/engine/coverage.ts
1887
- function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
1888
- const adj = buildAdjacencyList(graph.edges);
1889
- const relevantSet = targetPaths.length > 0 ? bfsBidirectional(targetPaths, adj, depth) : /* @__PURE__ */ new Set();
1890
- const includedSet = new Set(includedPaths);
1891
- const tempFileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
1892
- for (const path of includedPaths) {
1893
- const file = tempFileMap.get(path);
1894
- if (!file) continue;
1895
- for (const imp of file.imports) {
1896
- const impFile = tempFileMap.get(imp);
1897
- if (impFile && impFile.kind === "type") {
1898
- relevantSet.add(imp);
1899
- }
1900
- }
1901
- }
1902
- const relevantFiles = Array.from(relevantSet);
1903
- const includedRelevant = relevantFiles.filter((f) => includedSet.has(f));
1904
- const missingRelevant = relevantFiles.filter((f) => !includedSet.has(f));
1905
- const missingCritical = missingRelevant.filter((f) => {
1906
- const file = tempFileMap.get(f);
1907
- return file && (file.exclusionImpact === "critical" || file.exclusionImpact === "high");
1908
- });
1909
- const fileMap = new Map(allFiles.map((f) => [f.relativePath, f]));
1910
- let totalRelevantRisk = 0;
1911
- let includedRelevantRisk = 0;
1912
- for (const f of relevantFiles) {
1913
- const risk = fileMap.get(f)?.riskScore ?? 1;
1914
- totalRelevantRisk += risk;
1915
- if (includedSet.has(f)) {
1916
- includedRelevantRisk += risk;
1917
- }
1918
- }
1919
- const score = totalRelevantRisk > 0 ? Math.round(includedRelevantRisk / totalRelevantRisk * 100) : relevantFiles.length > 0 ? Math.round(includedRelevant.length / relevantFiles.length * 100) : 100;
1920
- let explanation;
1921
- if (score >= 90) {
1922
- explanation = `Excellent coverage (${score}%): AI has nearly all relevant context.`;
1923
- } else if (score >= 70) {
1924
- explanation = `Good coverage (${score}%): Most relevant files included.`;
1925
- if (missingCritical.length > 0) {
1926
- explanation += ` Warning: ${missingCritical.length} critical file(s) missing.`;
1927
- }
1928
- } else if (score >= 50) {
1929
- explanation = `Partial coverage (${score}%): Significant context is missing.`;
1930
- if (missingCritical.length > 0) {
1931
- explanation += ` ${missingCritical.length} critical file(s) not included \u2014 AI quality will degrade.`;
1932
- }
1933
- } else {
1934
- explanation = `Low coverage (${score}%): Most relevant files are excluded. AI response quality will be poor.`;
1935
- }
1936
- return {
1937
- score,
1938
- relevantFiles,
1939
- includedRelevant,
1940
- missingRelevant,
1941
- missingCritical,
1942
- explanation
1943
- };
1944
- }
1945
-
1946
- // src/engine/budget.ts
1947
- function getPruneLevelForRisk(riskScore) {
1948
- if (riskScore >= 80) return "full";
1949
- if (riskScore >= 60) return "full";
1950
- if (riskScore >= 30) return "signatures";
1951
- return "skeleton";
1952
- }
1953
-
1954
- // src/engine/selector.ts
1955
- async function selectContext(input) {
1956
- const { task, analysis, budget, policies, depth = 2 } = input;
1957
- const decisions = [];
1958
- const targetPaths = identifyTargetFiles(task, analysis.files);
1959
- if (targetPaths.length > 0) {
1960
- decisions.push({
1961
- file: targetPaths.join(", "),
1962
- action: "include-full",
1963
- reason: `Target file(s) identified from task description`
1964
- });
1965
- }
1966
- const adj = buildAdjacencyList(analysis.graph.edges);
1967
- const expandedPaths = targetPaths.length > 0 ? Array.from(bfsBidirectional(targetPaths, adj, depth)) : [];
1968
- const expansionCount = expandedPaths.length - targetPaths.length;
1969
- if (expansionCount > 0) {
1970
- decisions.push({
1971
- file: `${expansionCount} dependencies`,
1972
- action: "include-full",
1973
- reason: `Expanded ${targetPaths.length} target(s) to ${expandedPaths.length} files via dependency graph (depth ${depth})`
1974
- });
1975
- }
1976
- const allFileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
1977
- if (targetPaths.length > 0) {
1978
- for (const path of expandedPaths) {
1979
- const file = allFileMap.get(path);
1980
- if (!file) continue;
1981
- for (const imp of file.imports) {
1982
- const impFile = allFileMap.get(imp);
1983
- if (impFile && impFile.kind === "type") {
1984
- expandedPaths.push(imp);
1985
- }
1986
- }
1987
- }
1988
- }
1989
- const { mustInclude, mustExclude } = applyPolicies(analysis.files, policies);
1990
- const candidateSet = /* @__PURE__ */ new Set([...expandedPaths, ...mustInclude]);
1991
- if (targetPaths.length === 0) {
1992
- for (const f of analysis.files) {
1993
- candidateSet.add(f.relativePath);
1994
- }
1995
- }
1996
- for (const ex of mustExclude) {
1997
- candidateSet.delete(ex);
1998
- decisions.push({
1999
- file: ex,
2000
- action: "exclude",
2001
- reason: "Excluded by policy"
2002
- });
2003
- }
2004
- const hasSecretBlock = policies?.rules.some(
2005
- (r) => r.type === "secret-block" && r.enabled
2006
- );
2007
- if (hasSecretBlock) {
2008
- for (const path of Array.from(candidateSet)) {
2009
- const file = allFileMap.get(path);
2010
- if (!file) continue;
2011
- const findings = await scanFileForSecrets(
2012
- file.path,
2013
- analysis.projectPath
2014
- );
2015
- if (findings.length > 0) {
2016
- candidateSet.delete(path);
2017
- decisions.push({
2018
- file: path,
2019
- action: "exclude",
2020
- reason: `Blocked: ${findings.length} secret(s) detected (${findings.map((f) => f.type).join(", ")})`
2021
- });
2022
- }
2023
- }
2024
- }
2025
- const candidates = Array.from(candidateSet).map((p) => allFileMap.get(p)).filter((f) => f !== void 0).sort((a, b) => {
2026
- const aIsTarget = targetPaths.includes(a.relativePath) ? 0 : 1;
2027
- const bIsTarget = targetPaths.includes(b.relativePath) ? 0 : 1;
2028
- if (aIsTarget !== bIsTarget) return aIsTarget - bIsTarget;
2029
- const aIsMust = mustInclude.has(a.relativePath) ? 0 : 1;
2030
- const bIsMust = mustInclude.has(b.relativePath) ? 0 : 1;
2031
- if (aIsMust !== bIsMust) return aIsMust - bIsMust;
2032
- return b.riskScore - a.riskScore;
2033
- });
2034
- const selectedFiles = [];
2035
- let usedTokens = 0;
2036
- for (const file of candidates) {
2037
- const isTarget = targetPaths.includes(file.relativePath);
2038
- const isMustInclude = mustInclude.has(file.relativePath);
2039
- const defaultLevel = isTarget ? "full" : getPruneLevelForRisk(file.riskScore);
2040
- const levels = getCascadeLevels(defaultLevel);
2041
- let included = false;
2042
- for (const level of levels) {
2043
- if (level === "excluded") break;
2044
- let tokens;
2045
- if (level === "full") {
2046
- tokens = file.tokens;
2047
- } else {
2048
- const pruned = await pruneFile(file, level);
2049
- tokens = pruned.prunedTokens;
2050
- }
2051
- if (usedTokens + tokens <= budget) {
2052
- usedTokens += tokens;
2053
- selectedFiles.push({
2054
- relativePath: file.relativePath,
2055
- tokens,
2056
- originalTokens: file.tokens,
2057
- pruneLevel: level,
2058
- riskScore: file.riskScore,
2059
- reason: buildReason(file, level, isTarget, isMustInclude)
2060
- });
2061
- if (level !== defaultLevel) {
2062
- decisions.push({
2063
- file: file.relativePath,
2064
- action: `include-${level}`,
2065
- reason: `Downgraded from ${defaultLevel} to ${level} due to budget constraint`,
2066
- alternatives: `Would need ${file.tokens - tokens} more tokens for ${defaultLevel}`
2067
- });
2068
- }
2069
- included = true;
2070
- break;
2071
- }
2072
- }
2073
- if (!included) {
2074
- decisions.push({
2075
- file: file.relativePath,
2076
- action: "exclude",
2077
- reason: `Budget exhausted (risk: ${file.riskScore}, needs ${file.tokens} tokens)`
2078
- });
2079
- }
2080
- }
2081
- const includedPaths = selectedFiles.map((f) => f.relativePath);
2082
- const coverage = calculateCoverage(
2083
- targetPaths,
2084
- includedPaths,
2085
- analysis.files,
2086
- analysis.graph,
2087
- depth
2088
- );
2089
- const includedSet = new Set(includedPaths);
2090
- const excludedFiles = analysis.files.filter(
2091
- (f) => !includedSet.has(f.relativePath)
2092
- );
2093
- const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
2094
- const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
2095
- const hash = createHash4("sha256").update(hashInput).digest("hex").substring(0, 16);
2096
- return {
2097
- files: selectedFiles,
2098
- totalTokens: usedTokens,
2099
- budget,
2100
- usedPercent: budget > 0 ? Math.round(usedTokens / budget * 100 * 10) / 10 : 0,
2101
- coverage,
2102
- riskScore: excludedRisk,
2103
- deterministic: true,
2104
- hash,
2105
- decisions
2106
- };
2107
- }
2108
- function identifyTargetFiles(task, files) {
2109
- const targets = [];
2110
- const pathPattern = /(?:^|\s|["'`])([.\w/-]+\.[a-zA-Z]{1,4})(?:\s|$|["'`]|,|:)/g;
2111
- let match;
2112
- while ((match = pathPattern.exec(task)) !== null) {
2113
- const candidate = match[1];
2114
- const found = files.find(
2115
- (f) => f.relativePath === candidate || f.relativePath.endsWith(candidate)
2116
- );
2117
- if (found && !targets.includes(found.relativePath)) {
2118
- targets.push(found.relativePath);
2119
- }
2120
- }
2121
- return targets;
2122
- }
2123
- function applyPolicies(files, policies) {
2124
- const mustInclude = /* @__PURE__ */ new Set();
2125
- const mustExclude = /* @__PURE__ */ new Set();
2126
- if (!policies) return { mustInclude, mustExclude };
2127
- for (const rule of policies.rules) {
2128
- if (!rule.enabled) continue;
2129
- if (rule.type === "include-always" && rule.pattern) {
2130
- for (const file of files) {
2131
- if (matchGlob(file.relativePath, rule.pattern)) {
2132
- mustInclude.add(file.relativePath);
2133
- }
2134
- }
2135
- }
2136
- if (rule.type === "exclude-always" && rule.pattern) {
2137
- for (const file of files) {
2138
- if (matchGlob(file.relativePath, rule.pattern)) {
2139
- mustExclude.add(file.relativePath);
2140
- }
2141
- }
2142
- }
2143
- }
2144
- return { mustInclude, mustExclude };
2145
- }
2146
- function getCascadeLevels(startLevel) {
2147
- const all = ["full", "signatures", "skeleton", "excluded"];
2148
- const startIdx = all.indexOf(startLevel);
2149
- return all.slice(startIdx);
2150
- }
2151
- function buildReason(file, level, isTarget, isMustInclude) {
2152
- if (isTarget) return "Target file";
2153
- if (isMustInclude) return "Required by policy";
2154
- const impact = file.exclusionImpact;
2155
- const levelStr = level === "full" ? "full content" : level;
2156
- if (impact === "critical") return `Critical dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
2157
- if (impact === "high") return `High-risk dependency (risk ${file.riskScore}) \u2014 ${levelStr}`;
2158
- if (impact === "medium") return `Medium relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
2159
- return `Low relevance (risk ${file.riskScore}) \u2014 ${levelStr}`;
2160
- }
2161
-
2162
- // src/interact/router.ts
2163
- var MODEL_REGISTRY = [
2164
- {
2165
- id: "claude-haiku-3.5",
2166
- name: "Claude 3.5 Haiku",
2167
- tier: "fast",
2168
- pricing: { inputPerMillion: 0.8, outputPerMillion: 4, cacheReadPerMillion: 0.08 },
2169
- contextWindow: 2e5,
2170
- strengths: ["speed", "simple-tasks", "low-cost"]
2171
- },
2172
- {
2173
- id: "claude-sonnet-4",
2174
- name: "Claude Sonnet 4",
2175
- tier: "balanced",
2176
- pricing: { inputPerMillion: 3, outputPerMillion: 15, cacheReadPerMillion: 0.3 },
2177
- contextWindow: 2e5,
2178
- strengths: ["code-generation", "refactoring", "balanced-reasoning"]
2179
- },
2180
- {
2181
- id: "claude-opus-4",
2182
- name: "Claude Opus 4",
2183
- tier: "reasoning",
2184
- pricing: { inputPerMillion: 15, outputPerMillion: 75, cacheReadPerMillion: 1.5 },
2185
- contextWindow: 2e5,
2186
- strengths: ["deep-reasoning", "architecture", "complex-debugging"]
2187
- }
2188
- ];
2189
- var ROUTING_RULES = [
2190
- {
2191
- task: "simple-edit",
2192
- defaultModel: "claude-haiku-3.5",
2193
- upgradeIf: () => false,
2194
- upgradeTo: "claude-haiku-3.5",
2195
- reason: "Simple edits are best handled by fast models",
2196
- upgradeReason: ""
2197
- },
2198
- {
2199
- task: "docs",
2200
- defaultModel: "claude-haiku-3.5",
2201
- upgradeIf: (a) => a.totalTokens > 1e5,
2202
- upgradeTo: "claude-sonnet-4",
2203
- reason: "Documentation tasks are straightforward",
2204
- upgradeReason: "Large codebase \u2014 Sonnet provides better understanding"
2205
- },
2206
- {
2207
- task: "test",
2208
- defaultModel: "claude-sonnet-4",
2209
- upgradeIf: (a) => a.riskProfile.overallComplexity > 15,
2210
- upgradeTo: "claude-opus-4",
2211
- reason: "Test generation requires good code understanding",
2212
- upgradeReason: "High complexity codebase \u2014 Opus for better test coverage"
2213
- },
2214
- {
2215
- task: "debug",
2216
- defaultModel: "claude-sonnet-4",
2217
- upgradeIf: (a) => a.riskProfile.distribution.critical > 5,
2218
- upgradeTo: "claude-opus-4",
2219
- reason: "Debugging requires solid reasoning about code flow",
2220
- upgradeReason: "Many critical files involved \u2014 Opus for deeper analysis"
2221
- },
2222
- {
2223
- task: "refactor",
2224
- defaultModel: "claude-sonnet-4",
2225
- upgradeIf: (a) => a.totalFiles > 50 && a.riskProfile.overallComplexity > 10,
2226
- upgradeTo: "claude-opus-4",
2227
- reason: "Refactoring needs good structural understanding",
2228
- upgradeReason: "Large + complex project \u2014 Opus for safer refactoring"
2229
- },
2230
- {
2231
- task: "review",
2232
- defaultModel: "claude-sonnet-4",
2233
- upgradeIf: (a) => a.riskProfile.distribution.critical > 3,
2234
- upgradeTo: "claude-opus-4",
2235
- reason: "Code review benefits from balanced reasoning",
2236
- upgradeReason: "Critical code under review \u2014 Opus for thorough analysis"
2237
- },
2238
- {
2239
- task: "feature",
2240
- defaultModel: "claude-sonnet-4",
2241
- upgradeIf: (a) => a.totalFiles > 100,
2242
- upgradeTo: "claude-opus-4",
2243
- reason: "Feature development needs code generation + understanding",
2244
- upgradeReason: "Large codebase \u2014 Opus for better integration"
2245
- },
2246
- {
2247
- task: "architecture",
2248
- defaultModel: "claude-opus-4",
2249
- upgradeIf: () => false,
2250
- upgradeTo: "claude-opus-4",
2251
- reason: "Architecture decisions require deep reasoning",
2252
- upgradeReason: ""
2253
- }
2254
- ];
2255
- var TASK_KEYWORDS = {
2256
- debug: ["debug", "fix", "bug", "error", "issue", "broken", "crash", "failing", "wrong"],
2257
- review: ["review", "check", "assess", "evaluate", "audit", "inspect", "critique"],
2258
- refactor: ["refactor", "restructure", "reorganize", "clean up", "simplify", "extract", "move"],
2259
- test: ["test", "spec", "coverage", "unit test", "integration test", "e2e"],
2260
- docs: ["document", "docs", "readme", "jsdoc", "comment", "explain"],
2261
- feature: ["add", "implement", "create", "build", "new", "feature", "endpoint"],
2262
- architecture: ["architecture", "design", "system", "structure", "migrate", "pattern"],
2263
- "simple-edit": ["rename", "typo", "update", "change", "modify", "tweak", "adjust"]
2264
- };
2265
- function classifyTask(taskDescription) {
2266
- const lower = taskDescription.toLowerCase();
2267
- let bestType = "simple-edit";
2268
- let bestScore = 0;
2269
- for (const [type, keywords] of Object.entries(TASK_KEYWORDS)) {
2270
- let score = 0;
2271
- for (const kw of keywords) {
2272
- if (lower.includes(kw)) score++;
2273
- }
2274
- if (score > bestScore) {
2275
- bestScore = score;
2276
- bestType = type;
2277
- }
2278
- }
2279
- return bestType;
2280
- }
2281
- function routeModel(taskType, analysis, preferredModel) {
2282
- if (preferredModel) {
2283
- const spec = MODEL_REGISTRY.find((m) => m.id === preferredModel);
2284
- if (spec) {
2285
- return {
2286
- model: preferredModel,
2287
- reason: "User-specified model",
2288
- confidence: 1,
2289
- alternatives: buildAlternatives(preferredModel, taskType)
2290
- };
2291
- }
2292
- }
2293
- const rule = ROUTING_RULES.find((r) => r.task === taskType);
2294
- if (!rule) {
2295
- return {
2296
- model: "claude-sonnet-4",
2297
- reason: "Default model for unrecognized task type",
2298
- confidence: 0.5,
2299
- alternatives: buildAlternatives("claude-sonnet-4", taskType)
2300
- };
2301
- }
2302
- const shouldUpgrade = rule.upgradeIf(analysis);
2303
- const model = shouldUpgrade ? rule.upgradeTo : rule.defaultModel;
2304
- const reason = shouldUpgrade ? rule.upgradeReason : rule.reason;
2305
- return {
2306
- model,
2307
- reason,
2308
- confidence: shouldUpgrade ? 0.8 : 0.9,
2309
- alternatives: buildAlternatives(model, taskType)
2310
- };
2311
- }
2312
- function buildAlternatives(chosenModel, taskType) {
2313
- return MODEL_REGISTRY.filter((m) => m.id !== chosenModel).map((m) => {
2314
- const chosen = MODEL_REGISTRY.find((r) => r.id === chosenModel);
2315
- const costDelta = m.pricing.inputPerMillion - chosen.pricing.inputPerMillion;
2316
- const tradeoff = costDelta > 0 ? `More capable but ${(costDelta / chosen.pricing.inputPerMillion * 100).toFixed(0)}% more expensive` : `${Math.abs(costDelta / chosen.pricing.inputPerMillion * 100).toFixed(0)}% cheaper but less capable`;
2317
- return { model: m.id, costDelta, tradeoff };
2318
- });
2319
- }
2320
- function getModelSpec(modelId) {
2321
- return MODEL_REGISTRY.find((m) => m.id === modelId);
2322
- }
2323
-
2324
- // src/interact/estimator.ts
2325
- function estimateCost(modelId, inputTokens, totalProjectTokens, estimatedOutputRatio = 0.3) {
2326
- const spec = getModelSpec(modelId) ?? MODEL_REGISTRY[1];
2327
- const estimatedOutputTokens = Math.round(inputTokens * estimatedOutputRatio);
2328
- const inputCost = inputTokens / 1e6 * spec.pricing.inputPerMillion;
2329
- const outputCost = estimatedOutputTokens / 1e6 * spec.pricing.outputPerMillion;
2330
- const totalCost = inputCost + outputCost;
2331
- const woInputCost = totalProjectTokens / 1e6 * spec.pricing.inputPerMillion;
2332
- const woOutputCost = Math.round(totalProjectTokens * estimatedOutputRatio) / 1e6 * spec.pricing.outputPerMillion;
2333
- const woTotalCost = woInputCost + woOutputCost;
2334
- const tokensSaved = totalProjectTokens - inputTokens;
2335
- const costSaved = woTotalCost - totalCost;
2336
- const savingsPercent = woTotalCost > 0 ? costSaved / woTotalCost * 100 : 0;
2337
- return {
2338
- model: modelId,
2339
- inputTokens,
2340
- estimatedOutputTokens,
2341
- inputCost: round(inputCost),
2342
- outputCost: round(outputCost),
2343
- totalCost: round(totalCost),
2344
- formatted: formatCost(totalCost),
2345
- withoutOptimization: {
2346
- inputTokens: totalProjectTokens,
2347
- totalCost: round(woTotalCost),
2348
- formatted: formatCost(woTotalCost)
2349
- },
2350
- savings: {
2351
- tokensSaved: Math.max(0, tokensSaved),
2352
- costSaved: round(Math.max(0, costSaved)),
2353
- percent: Math.max(0, Math.round(savingsPercent)),
2354
- formatted: costSaved > 0 ? `saved ${formatCost(costSaved)} (${Math.round(savingsPercent)}%)` : "no savings"
2355
- }
2356
- };
2357
- }
2358
- function round(n) {
2359
- return Math.round(n * 1e6) / 1e6;
2360
- }
2361
- function formatCost(cost) {
2362
- if (cost < 1e-3) return "<$0.001";
2363
- if (cost < 0.01) return `$${cost.toFixed(4)}`;
2364
- if (cost < 1) return `$${cost.toFixed(3)}`;
2365
- return `$${cost.toFixed(2)}`;
2366
- }
2367
-
2368
- // src/interact/prompt.ts
2369
- function buildPrompt(options) {
2370
- const {
2371
- task,
2372
- taskType,
2373
- analysis,
2374
- selection,
2375
- enableCoT = true,
2376
- enableConstraints = true,
2377
- enableAntiHallucination = true
2378
- } = options;
2379
- const sections = [];
2380
- sections.push(buildSystemSection(analysis.stack, taskType));
2381
- sections.push(buildContextSection(analysis, selection));
2382
- sections.push(buildTaskSection(task, taskType));
2383
- if (enableConstraints) {
2384
- sections.push(buildConstraintsSection(analysis.stack, taskType));
2385
- }
2386
- if (enableCoT) {
2387
- sections.push(buildCoTSection(taskType));
2388
- }
2389
- if (enableAntiHallucination) {
2390
- sections.push(buildAntiHallucinationSection());
2391
- }
2392
- sections.push(buildFormatSection(taskType));
2393
- const rendered = sections.map((s) => s.content).join("\n\n---\n\n");
2394
- const totalTokens = sections.reduce((s, sec) => s + sec.tokens, 0);
2395
- return { sections, totalTokens, rendered };
2396
- }
2397
- function buildSystemSection(stack, taskType) {
2398
- const stackStr = stack.length > 0 ? stack.join(", ") : "software";
2399
- const taskRole = TASK_ROLES[taskType] ?? "engineer";
2400
- const content = [
2401
- `You are a senior ${stackStr} ${taskRole} with deep expertise in clean architecture, testing, and production-quality code.`,
2402
- "You prioritize correctness, readability, and maintainability.",
2403
- "You never make assumptions without evidence from the code."
2404
- ].join(" ");
2405
- return makeSection("system", "system", content);
2406
- }
2407
- function buildContextSection(analysis, selection) {
2408
- const lines = [];
2409
- lines.push(`## Project: ${analysis.projectName}`);
2410
- lines.push(`Stack: ${analysis.stack.join(", ") || "Unknown"}`);
2411
- lines.push(`Files analyzed: ${analysis.totalFiles} | Tokens: ~${Math.round(analysis.totalTokens / 1e3)}K`);
2412
- lines.push(`Context coverage: ${selection.coverage.score}% | Risk score: ${selection.riskScore}/100`);
2413
- lines.push("");
2414
- lines.push("### Included Files");
2415
- lines.push("");
2416
- const fullFiles = selection.files.filter((f) => f.pruneLevel === "full");
2417
- const sigFiles = selection.files.filter((f) => f.pruneLevel === "signatures");
2418
- const skelFiles = selection.files.filter((f) => f.pruneLevel === "skeleton");
2419
- if (fullFiles.length > 0) {
2420
- lines.push("**Full content (read these first):**");
2421
- for (const f of fullFiles) {
2422
- lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens) \u2014 ${f.reason}`);
2423
- }
2424
- lines.push("");
2425
- }
2426
- if (sigFiles.length > 0) {
2427
- lines.push("**Signatures only (reference as needed):**");
2428
- for (const f of sigFiles) {
2429
- lines.push(`- \`${f.relativePath}\` (~${Math.round(f.tokens / 1e3)}K tokens)`);
2430
- }
2431
- lines.push("");
2432
- }
2433
- if (skelFiles.length > 0) {
2434
- lines.push("**Skeleton (structure overview):**");
2435
- for (const f of skelFiles) {
2436
- lines.push(`- \`${f.relativePath}\``);
2437
- }
2438
- lines.push("");
2439
- }
2440
- if (selection.coverage.missingCritical.length > 0) {
2441
- lines.push("\u26A0\uFE0F **Missing critical files** (not included due to budget):");
2442
- for (const f of selection.coverage.missingCritical) {
2443
- lines.push(`- \`${f}\``);
2444
- }
2445
- lines.push("");
2446
- }
2447
- const content = lines.join("\n");
2448
- return makeSection("context", "context", content);
2449
- }
2450
- function buildTaskSection(task, taskType) {
2451
- const content = [
2452
- "## Task",
2453
- "",
2454
- task,
2455
- "",
2456
- `Task type: **${taskType}**`
2457
- ].join("\n");
2458
- return makeSection("task", "task", content);
2459
- }
2460
- function buildConstraintsSection(stack, taskType) {
2461
- const lines = ["## Constraints", ""];
2462
- lines.push("- **Do NOT** delete or modify existing tests unless explicitly asked");
2463
- lines.push("- **Do NOT** change function signatures that are part of the public API");
2464
- lines.push("- **Do NOT** introduce new dependencies without mentioning it");
2465
- lines.push("- **Always** handle errors explicitly (no silent catches)");
2466
- lines.push("- **Always** preserve existing code style and conventions");
2467
- lines.push("- **Prefer** minimal changes \u2014 smallest diff that solves the problem");
2468
- if (stack.includes("TypeScript")) {
2469
- lines.push("- **Always** use strict TypeScript types (no `any` unless unavoidable)");
2470
- lines.push("- **Always** add explicit return types to exported functions");
2471
- }
2472
- if (taskType === "refactor") {
2473
- lines.push("- **Do NOT** change behavior \u2014 refactoring must be behavior-preserving");
2474
- lines.push("- **Verify** all existing tests still pass after changes");
2475
- }
2476
- if (taskType === "test") {
2477
- lines.push("- Use AAA pattern: Arrange, Act, Assert");
2478
- lines.push("- Test boundaries, null/undefined, async errors, type edges");
2479
- lines.push('- Use descriptive test names: "should [expected] when [condition]"');
2480
- }
2481
- return makeSection("constraints", "constraints", lines.join("\n"));
2482
- }
2483
- function buildCoTSection(taskType) {
2484
- const steps = COT_STEPS[taskType] ?? COT_STEPS["simple-edit"];
2485
- const lines = ["## Thinking Process", "", "Before writing any code:"];
2486
- steps.forEach((step, i) => {
2487
- lines.push(`${i + 1}. ${step}`);
2488
- });
2489
- return makeSection("cot", "constraints", lines.join("\n"));
2490
- }
2491
- function buildAntiHallucinationSection() {
2492
- const content = [
2493
- "## Important",
2494
- "",
2495
- "- Only reference files, functions, and APIs that exist in the provided context",
2496
- "- If you are unsure about something, say so explicitly",
2497
- "- Do NOT invent function signatures, types, or module paths",
2498
- "- If the context is insufficient to complete the task, explain what is missing"
2499
- ].join("\n");
2500
- return makeSection("anti-hallucination", "constraints", content);
2501
- }
2502
- function buildFormatSection(taskType) {
2503
- const lines = ["## Output Format", ""];
2504
- if (taskType === "review") {
2505
- lines.push("Provide findings in priority order: Critical > Major > Minor > Nitpick");
2506
- lines.push("For each finding: file, line, issue, and concrete suggestion with code");
2507
- } else if (taskType === "architecture") {
2508
- lines.push("Present 2-3 options with trade-offs, then recommend one with justification");
2509
- } else if (taskType === "debug") {
2510
- lines.push("1. Root cause analysis");
2511
- lines.push("2. Minimal fix");
2512
- lines.push("3. Explanation of why the fix works");
2513
- lines.push("4. Edge cases to consider");
2514
- } else {
2515
- lines.push("Provide clean, production-ready code with brief explanations of key decisions");
2516
- }
2517
- return makeSection("format", "format", lines.join("\n"));
2518
- }
2519
- var TASK_ROLES = {
2520
- debug: "debugger",
2521
- review: "code reviewer",
2522
- refactor: "architect",
2523
- test: "test engineer",
2524
- docs: "technical writer",
2525
- feature: "engineer",
2526
- architecture: "systems architect",
2527
- "simple-edit": "engineer"
2528
- };
2529
- var COT_STEPS = {
2530
- debug: [
2531
- "**Reproduce** \u2014 Understand the exact symptom and when it occurs",
2532
- "**Hypothesize** \u2014 List the most likely root causes (max 3)",
2533
- "**Verify** \u2014 Check each hypothesis against the code",
2534
- "**Fix** \u2014 Apply the minimal fix that addresses the root cause",
2535
- "**Validate** \u2014 Explain why the fix works and what edge cases it covers"
2536
- ],
2537
- review: [
2538
- "**Understand** \u2014 Read the code and understand its purpose",
2539
- "**Assess** \u2014 Evaluate correctness, readability, performance, security",
2540
- "**Prioritize** \u2014 Rank issues by severity (critical > major > minor)",
2541
- "**Suggest** \u2014 Provide concrete, actionable improvements with code"
2542
- ],
2543
- refactor: [
2544
- "**Analyze** \u2014 Identify code smells and structural issues",
2545
- "**Plan** \u2014 Define the target structure before changing anything",
2546
- "**Preserve** \u2014 Ensure behavior doesn't change",
2547
- "**Refactor** \u2014 Apply changes incrementally",
2548
- "**Verify** \u2014 Confirm all existing tests still pass"
2549
- ],
2550
- test: [
2551
- "**Identify** \u2014 What needs testing? (happy path, edge cases, errors)",
2552
- "**Structure** \u2014 Use AAA pattern: Arrange, Act, Assert",
2553
- "**Cover** \u2014 Test boundaries, null/undefined, async errors",
2554
- "**Isolate** \u2014 Mock external dependencies, test units independently"
2555
- ],
2556
- docs: [
2557
- "**Read** \u2014 Understand the code before documenting",
2558
- "**Structure** \u2014 Organize by audience (API users, contributors, operators)",
2559
- "**Write** \u2014 Clear, concise, with examples"
2560
- ],
2561
- feature: [
2562
- "**Clarify** \u2014 Restate the requirement in your own words",
2563
- "**Design** \u2014 Plan the approach (types, interfaces, flow)",
2564
- "**Implement** \u2014 Build incrementally, starting with types",
2565
- "**Test** \u2014 Write tests alongside implementation",
2566
- "**Integrate** \u2014 Ensure no regressions"
2567
- ],
2568
- architecture: [
2569
- "**Context** \u2014 Understand current architecture and constraints",
2570
- "**Options** \u2014 Present 2-3 viable approaches with trade-offs",
2571
- "**Recommend** \u2014 Choose the best and explain why",
2572
- "**Plan** \u2014 Define migration steps",
2573
- "**Risks** \u2014 Identify risks and mitigation strategies"
2574
- ],
2575
- "simple-edit": [
2576
- "**Understand** \u2014 Read the relevant code",
2577
- "**Plan** \u2014 Think before writing",
2578
- "**Implement** \u2014 Write clean, well-typed code",
2579
- "**Verify** \u2014 Check for edge cases"
2580
- ]
2581
- };
2582
- function makeSection(id, role, content) {
2583
- const tokens = countTokensChars4(Buffer.byteLength(content, "utf-8"));
2584
- return { id, role, content, tokens };
2585
- }
2586
-
2587
- // src/govern/audit.ts
2588
- import { randomUUID, createHash as createHash5 } from "crypto";
2589
- import { readdir as readdir3, chmod } from "fs/promises";
2590
- import { join as join7 } from "path";
2591
- import { userInfo } from "os";
2592
- import { homedir } from "os";
2593
- var CTO_DIR = ".cto-ai";
2594
- var AUDIT_DIR = "audit";
2595
- var MAX_ENTRIES_PER_FILE = 500;
2596
- function getAuditDir() {
2597
- return join7(homedir(), CTO_DIR, AUDIT_DIR);
2598
- }
2599
- function getCurrentAuditFile() {
2600
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
2601
- return join7(getAuditDir(), `audit_${date}.json`);
2602
- }
2603
- function computeIntegrityHash(entry) {
2604
- const payload = JSON.stringify({
2605
- id: entry.id,
2606
- timestamp: entry.timestamp,
2607
- action: entry.action,
2608
- user: entry.user,
2609
- projectPath: entry.projectPath,
2610
- details: entry.details
2611
- });
2612
- return createHash5("sha256").update(payload).digest("hex");
2613
- }
2614
- async function ensureDir(dirPath) {
2615
- const { mkdir: mkdir5 } = await import("fs/promises");
2616
- await mkdir5(dirPath, { recursive: true });
2617
- }
2618
- async function readJSON(filePath) {
2619
- const { readFile: readFile9 } = await import("fs/promises");
2620
- try {
2621
- const content = await readFile9(filePath, "utf-8");
2622
- return JSON.parse(content);
2623
- } catch {
2624
- return null;
2625
- }
2626
- }
2627
- async function writeJSON(filePath, data) {
2628
- const { writeFile: writeFile7 } = await import("fs/promises");
2629
- await ensureDir(join7(filePath, ".."));
2630
- await writeFile7(filePath, JSON.stringify(data, null, 2), "utf-8");
2631
- }
2632
- async function logAudit(action, projectPath, details = {}) {
2633
- const auditDir = getAuditDir();
2634
- await ensureDir(auditDir);
2635
- let currentUser;
2636
- try {
2637
- currentUser = userInfo().username;
2638
- } catch {
2639
- currentUser = process.env.USER ?? process.env.USERNAME ?? "unknown";
2640
- }
2641
- const partialEntry = {
2642
- id: randomUUID().substring(0, 12),
2643
- timestamp: /* @__PURE__ */ new Date(),
2644
- action,
2645
- user: currentUser,
2646
- projectPath,
2647
- details
2648
- };
2649
- const entry = {
2650
- ...partialEntry,
2651
- integrityHash: computeIntegrityHash(partialEntry)
2652
- };
2653
- const auditFile = getCurrentAuditFile();
2654
- let entries = await readJSON(auditFile) ?? [];
2655
- entries.push(entry);
2656
- if (entries.length > MAX_ENTRIES_PER_FILE) {
2657
- entries = entries.slice(-MAX_ENTRIES_PER_FILE);
2658
- }
2659
- await writeJSON(auditFile, entries);
2660
- try {
2661
- await chmod(auditFile, 384);
2662
- } catch {
2663
- }
2664
- return entry;
2665
- }
2666
- async function getAuditEntries(options = {}) {
2667
- const auditDir = getAuditDir();
2668
- let files;
2669
- try {
2670
- files = await readdir3(auditDir);
2671
- } catch {
2672
- return [];
2673
- }
2674
- const auditFiles = files.filter((f) => f.startsWith("audit_") && f.endsWith(".json")).sort().reverse();
2675
- const allEntries = [];
2676
- const limit = options.limit ?? 100;
2677
- for (const file of auditFiles) {
2678
- if (allEntries.length >= limit) break;
2679
- const entries = await readJSON(join7(auditDir, file));
2680
- if (!entries) continue;
2681
- for (const entry of entries.reverse()) {
2682
- if (allEntries.length >= limit) break;
2683
- if (options.projectPath && entry.projectPath !== options.projectPath) continue;
2684
- if (options.action && entry.action !== options.action) continue;
2685
- if (options.since && new Date(entry.timestamp) < options.since) continue;
2686
- allEntries.push(entry);
2687
- }
2688
- }
2689
- return allEntries;
2690
- }
2691
- function verifyAuditEntry(entry) {
2692
- const { integrityHash, ...rest } = entry;
2693
- const expected = computeIntegrityHash(rest);
2694
- return expected === integrityHash;
2695
- }
2696
- async function verifyAuditIntegrity() {
2697
- const entries = await getAuditEntries({ limit: 1e4 });
2698
- const invalidEntries = [];
2699
- for (const entry of entries) {
2700
- if (!verifyAuditEntry(entry)) {
2701
- invalidEntries.push(entry);
2702
- }
2703
- }
2704
- return {
2705
- totalEntries: entries.length,
2706
- validEntries: entries.length - invalidEntries.length,
2707
- invalidEntries
2708
- };
2709
- }
2710
- async function purgeOldAuditEntries(retentionDays) {
2711
- const auditDir = getAuditDir();
2712
- let files;
2713
- try {
2714
- files = await readdir3(auditDir);
2715
- } catch {
2716
- return 0;
2717
- }
2718
- const cutoff = /* @__PURE__ */ new Date();
2719
- cutoff.setDate(cutoff.getDate() - retentionDays);
2720
- const cutoffStr = cutoff.toISOString().split("T")[0].replace(/-/g, "");
2721
- let purged = 0;
2722
- const { unlink } = await import("fs/promises");
2723
- for (const file of files) {
2724
- if (!file.startsWith("audit_") || !file.endsWith(".json")) continue;
2725
- const dateStr = file.replace("audit_", "").replace(".json", "");
2726
- if (dateStr < cutoffStr) {
2727
- try {
2728
- await unlink(join7(auditDir, file));
2729
- purged++;
2730
- } catch {
2731
- }
2732
- }
2733
- }
2734
- return purged;
2735
- }
2736
-
2737
- // src/interact/orchestrator.ts
2738
- async function planInteraction(input) {
2739
- const {
2740
- task,
2741
- analysis,
2742
- budget = 5e4,
2743
- model: preferredModel,
2744
- policies,
2745
- depth = 2,
2746
- enableCoT = true,
2747
- enableConstraints = true,
2748
- enableAntiHallucination = true
2749
- } = input;
2750
- const decisions = [];
2751
- const taskType = classifyTask(task);
2752
- decisions.push({
2753
- step: "classify",
2754
- decision: taskType,
2755
- reason: `Task classified as "${taskType}" based on keyword analysis`,
2756
- data: { task, taskType }
2757
- });
2758
- const context = await selectContext({
2759
- task,
2760
- analysis,
2761
- budget,
2762
- policies,
2763
- depth
2764
- });
2765
- decisions.push({
2766
- step: "select-context",
2767
- decision: `${context.files.length} files selected (${context.totalTokens} tokens)`,
2768
- reason: `Coverage: ${context.coverage.score}%, Risk: ${context.riskScore}/100`,
2769
- data: {
2770
- filesIncluded: context.files.length,
2771
- tokensUsed: context.totalTokens,
2772
- budget,
2773
- coverage: context.coverage.score,
2774
- risk: context.riskScore
2775
- }
2776
- });
2777
- const modelChoice = routeModel(taskType, analysis, preferredModel);
2778
- decisions.push({
2779
- step: "choose-model",
2780
- decision: modelChoice.model,
2781
- reason: modelChoice.reason,
2782
- data: {
2783
- confidence: modelChoice.confidence,
2784
- alternatives: modelChoice.alternatives.length
2785
- }
2786
- });
2787
- const prompt = buildPrompt({
2788
- task,
2789
- taskType,
2790
- analysis,
2791
- selection: context,
2792
- enableCoT,
2793
- enableConstraints,
2794
- enableAntiHallucination
2795
- });
2796
- decisions.push({
2797
- step: "build-prompt",
2798
- decision: `${prompt.sections.length} sections, ${prompt.totalTokens} tokens`,
2799
- reason: `Sections: ${prompt.sections.map((s) => s.id).join(", ")}`
2800
- });
2801
- const cost = estimateCost(
2802
- modelChoice.model,
2803
- context.totalTokens + prompt.totalTokens,
2804
- analysis.totalTokens
2805
- );
2806
- decisions.push({
2807
- step: "estimate-cost",
2808
- decision: cost.formatted,
2809
- reason: cost.savings.formatted,
2810
- data: {
2811
- inputTokens: cost.inputTokens,
2812
- totalCost: cost.totalCost,
2813
- savings: cost.savings.percent
2814
- }
2815
- });
2816
- const plan = {
2817
- id: randomUUID2().substring(0, 8),
2818
- task,
2819
- taskType,
2820
- timestamp: /* @__PURE__ */ new Date(),
2821
- context,
2822
- model: modelChoice,
2823
- prompt,
2824
- cost,
2825
- decisions
2826
- };
2827
- try {
2828
- await logAudit("interact", analysis.projectPath, {
2829
- interactionId: plan.id,
2830
- task,
2831
- taskType,
2832
- contextHash: context.hash,
2833
- filesIncluded: context.files.length,
2834
- filesExcluded: analysis.totalFiles - context.files.length,
2835
- tokensUsed: context.totalTokens,
2836
- coverageScore: context.coverage.score,
2837
- riskScore: context.riskScore,
2838
- model: modelChoice.model,
2839
- estimatedCost: cost.totalCost
2840
- });
2841
- } catch {
2842
- }
2843
- return plan;
2844
- }
2845
-
2846
- // src/cli/v2/interact.ts
2847
- var interactCommand = new Command3("interact").description("Build optimized AI context for a task").argument("<task>", 'Description of the task (e.g. "refactor the auth middleware")').option("-p, --path <path>", "Project path", ".").option("-b, --budget <tokens>", "Token budget", "50000").option("-m, --model <model>", "Force model: claude-haiku-3.5 | claude-sonnet-4 | claude-opus-4").option("--no-cot", "Disable chain-of-thought in prompt").option("--json", "Output as JSON").option("--prompt-only", "Only show the generated prompt").option("-o, --output <target>", "Output target: stdout | file | clipboard", "stdout").option("--explain", "Show detailed decision explanations").option("--pr [branch]", "PR mode: focus on changed files vs base branch (default: main)").action(async (task, opts) => {
2848
- try {
2849
- const projectPath = resolve8(opts.path ?? ".");
2850
- const budget = parseInt(opts.budget ?? "50000", 10);
2851
- console.log(chalk3.dim(`
2852
- Analyzing ${projectPath}...`));
2853
- const analysis = await getCachedAnalysis(projectPath);
2854
- if (opts.pr !== void 0) {
2855
- const baseBranch = typeof opts.pr === "string" ? opts.pr : "main";
2856
- console.log(chalk3.dim(` PR mode: comparing against ${baseBranch}...
2857
- `));
2858
- const pr = await generatePRContext(analysis, { baseBranch });
2859
- if (!pr.isGitRepo) {
2860
- console.log(chalk3.yellow(" \u26A0\uFE0F Not a git repository. Falling back to normal mode.\n"));
2861
- } else if (pr.changedFiles.length === 0) {
2862
- console.log(chalk3.yellow(` \u26A0\uFE0F No changed files vs ${baseBranch}. Falling back to normal mode.
2863
- `));
2864
- } else {
2865
- console.log(chalk3.bold.magenta(" \u{1F4CB} PR Context"));
2866
- console.log(` Branch: ${pr.currentBranch} \u2190 ${baseBranch}`);
2867
- console.log(` Changed: ${pr.changedFiles.length} files | Dependencies: ${pr.dependencyFiles.length} files`);
2868
- console.log(` Tokens: ~${Math.round(pr.totalContextTokens / 1e3)}K`);
2869
- if (pr.riskSummary.critical > 0 || pr.riskSummary.high > 0) {
2870
- console.log(chalk3.yellow(` \u26A0\uFE0F ${pr.riskSummary.critical} critical + ${pr.riskSummary.high} high-risk files`));
2871
- }
2872
- console.log("");
2873
- }
2874
- }
2875
- console.log(chalk3.dim(` Planning interaction for: "${task}"
2876
- `));
2877
- const plan = await planInteraction({
2878
- task,
2879
- analysis,
2880
- budget,
2881
- model: opts.model,
2882
- enableCoT: opts.cot !== false
2883
- });
2884
- if (opts.json) {
2885
- console.log(JSON.stringify(plan, null, 2));
2886
- return;
2887
- }
2888
- if (opts.promptOnly) {
2889
- console.log(plan.prompt.rendered);
2890
- return;
2891
- }
2892
- console.log(chalk3.bold.cyan(" \u{1F9E0} Interaction Plan"));
2893
- console.log("");
2894
- for (const d of plan.decisions) {
2895
- const icon = d.step === "classify" ? "\u{1F3F7}\uFE0F" : d.step === "select-context" ? "\u{1F4C2}" : d.step === "choose-model" ? "\u{1F916}" : d.step === "build-prompt" ? "\u{1F4DD}" : d.step === "estimate-cost" ? "\u{1F4B0}" : "\u2022";
2896
- console.log(` ${icon} ${chalk3.bold(d.step)}: ${d.decision}`);
2897
- console.log(` ${chalk3.dim(d.reason)}`);
2898
- }
2899
- console.log("");
2900
- console.log(chalk3.bold(" Context:"));
2901
- const ctx = plan.context;
2902
- console.log(` Files: ${ctx.files.length} | Tokens: ~${Math.round(ctx.totalTokens / 1e3)}K / ${Math.round(budget / 1e3)}K (${ctx.usedPercent.toFixed(1)}%)`);
2903
- console.log(` Coverage: ${ctx.coverage.score}% | Risk: ${ctx.riskScore}/100`);
2904
- if (ctx.coverage.missingCritical.length > 0) {
2905
- console.log(chalk3.yellow(` \u26A0\uFE0F Missing critical: ${ctx.coverage.missingCritical.join(", ")}`));
2906
- }
2907
- console.log("");
2908
- const fullFiles = ctx.files.filter((f) => f.pruneLevel === "full");
2909
- const sigFiles = ctx.files.filter((f) => f.pruneLevel === "signatures");
2910
- const skelFiles = ctx.files.filter((f) => f.pruneLevel === "skeleton");
2911
- if (fullFiles.length > 0) {
2912
- console.log(chalk3.bold(" Full content:"));
2913
- for (const f of fullFiles) {
2914
- console.log(` ${chalk3.green("\u25CF")} ${f.relativePath} (~${Math.round(f.tokens / 1e3)}K)`);
2915
- }
2916
- }
2917
- if (sigFiles.length > 0) {
2918
- console.log(chalk3.bold(" Signatures:"));
2919
- for (const f of sigFiles) {
2920
- console.log(` ${chalk3.yellow("\u25CB")} ${f.relativePath} (~${Math.round(f.tokens / 1e3)}K)`);
2921
- }
2922
- }
2923
- if (skelFiles.length > 0) {
2924
- console.log(chalk3.bold(" Skeleton:"));
2925
- for (const f of skelFiles) {
2926
- console.log(` ${chalk3.dim("\u25CC")} ${f.relativePath}`);
2927
- }
2928
- }
2929
- console.log("");
2930
- console.log(chalk3.bold(" Cost Estimate:"));
2931
- console.log(` Model: ${plan.model.model}`);
2932
- console.log(` This interaction: ${plan.cost.formatted}`);
2933
- console.log(` Without optimization: ${plan.cost.withoutOptimization.formatted}`);
2934
- console.log(` ${chalk3.green(plan.cost.savings.formatted)}`);
2935
- console.log("");
2936
- console.log(chalk3.bold(" Prompt:"));
2937
- console.log(` Sections: ${plan.prompt.sections.map((s) => s.id).join(", ")}`);
2938
- console.log(` Prompt tokens: ~${Math.round(plan.prompt.totalTokens / 1e3)}K`);
2939
- console.log("");
2940
- if (opts.explain && plan.context.decisions.length > 0) {
2941
- console.log(chalk3.bold(" Decisions:"));
2942
- for (const d of plan.context.decisions.slice(0, 20)) {
2943
- const icon = d.action === "exclude" ? chalk3.red("\u2717") : chalk3.green("\u2713");
2944
- console.log(` ${icon} ${d.file} \u2014 ${d.reason}`);
2945
- if (d.alternatives) {
2946
- console.log(chalk3.dim(` Alternative: ${d.alternatives}`));
2947
- }
2948
- }
2949
- if (plan.context.decisions.length > 20) {
2950
- console.log(chalk3.dim(` ... and ${plan.context.decisions.length - 20} more decisions`));
2951
- }
2952
- console.log("");
2953
- }
2954
- if (opts.output === "file") {
2955
- const outPath = join8(resolve8(opts.path ?? "."), ".cto", `prompt-${plan.id}.md`);
2956
- await writeFile4(outPath, plan.prompt.rendered, "utf-8");
2957
- console.log(chalk3.green(` \u{1F4BE} Prompt saved to ${outPath}`));
2958
- console.log("");
2959
- } else if (opts.output === "clipboard") {
2960
- try {
2961
- const { execSync } = await import("child_process");
2962
- execSync("pbcopy", { input: plan.prompt.rendered });
2963
- console.log(chalk3.green(" \u{1F4CB} Prompt copied to clipboard"));
2964
- console.log("");
2965
- } catch {
2966
- console.log(chalk3.yellow(" \u26A0\uFE0F Could not copy to clipboard. Use --output file instead."));
2967
- console.log("");
2968
- }
2969
- }
2970
- console.log(chalk3.dim(` Use ${chalk3.bold("--prompt-only")} to get the full prompt`));
2971
- console.log(chalk3.dim(` Use ${chalk3.bold("--json")} for machine-readable output`));
2972
- console.log(chalk3.dim(` Use ${chalk3.bold("--explain")} for detailed selection decisions`));
2973
- console.log("");
2974
- } catch (err) {
2975
- console.error(chalk3.red(`
2976
- Error: ${err.message}
2977
- `));
2978
- process.exit(1);
2979
- }
2980
- });
2981
-
2982
- // src/cli/v2/snapshot.ts
2983
- import { Command as Command4 } from "commander";
2984
- import chalk4 from "chalk";
2985
- import { resolve as resolve9, join as join9 } from "path";
2986
- import { readFile as readFile7, writeFile as writeFile5, readdir as readdir4, mkdir as mkdir3 } from "fs/promises";
2987
-
2988
- // src/govern/snapshot.ts
2989
- import { randomUUID as randomUUID3, createHash as createHash6 } from "crypto";
2990
- import "fs/promises";
2991
- function createSnapshot(name, analysis, selection, metadata = {}) {
2992
- const files = selection.files.map((f) => ({
2993
- relativePath: f.relativePath,
2994
- hash: hashString(`${f.relativePath}:${f.tokens}:${f.pruneLevel}`),
2995
- tokens: f.tokens,
2996
- pruneLevel: f.pruneLevel
2997
- }));
2998
- const snapshotData = files.map((f) => `${f.relativePath}:${f.hash}:${f.pruneLevel}`).sort().join("|");
2999
- return {
3000
- id: randomUUID3().substring(0, 8),
3001
- name,
3002
- createdAt: /* @__PURE__ */ new Date(),
3003
- hash: hashString(snapshotData),
3004
- projectHash: analysis.hash,
3005
- analysisHash: analysis.hash,
3006
- selectionHash: selection.hash,
3007
- files,
3008
- totalTokens: selection.totalTokens,
3009
- coverageScore: selection.coverage.score,
3010
- riskScore: selection.riskScore,
3011
- metadata
3012
- };
3013
- }
3014
- async function verifySnapshot(snapshot, currentAnalysis, currentSelection) {
3015
- const currentFiles = new Map(
3016
- currentSelection.files.map((f) => [f.relativePath, f])
3017
- );
3018
- let filesMatched = 0;
3019
- const filesMissing = [];
3020
- const filesChanged = [];
3021
- for (const snapFile of snapshot.files) {
3022
- const current = currentFiles.get(snapFile.relativePath);
3023
- if (!current) {
3024
- filesMissing.push(snapFile.relativePath);
3025
- continue;
3026
- }
3027
- const currentHash2 = hashString(
3028
- `${current.relativePath}:${current.tokens}:${current.pruneLevel}`
3029
- );
3030
- if (currentHash2 === snapFile.hash) {
3031
- filesMatched++;
3032
- } else {
3033
- filesChanged.push(snapFile.relativePath);
3034
- }
3035
- }
3036
- const currentSnapshotData = snapshot.files.map((f) => {
3037
- const current = currentFiles.get(f.relativePath);
3038
- if (!current) return `${f.relativePath}:MISSING:MISSING`;
3039
- return `${current.relativePath}:${hashString(`${current.relativePath}:${current.tokens}:${current.pruneLevel}`)}:${current.pruneLevel}`;
3040
- }).sort().join("|");
3041
- const currentHash = hashString(currentSnapshotData);
3042
- const integrityOk = currentHash === snapshot.hash && filesMissing.length === 0 && filesChanged.length === 0;
3043
- return {
3044
- valid: integrityOk,
3045
- snapshotId: snapshot.id,
3046
- filesChecked: snapshot.files.length,
3047
- filesMatched,
3048
- filesMissing,
3049
- filesChanged,
3050
- integrityOk
3051
- };
3052
- }
3053
- function compareSnapshots(older, newer) {
3054
- const olderFiles = new Map(older.files.map((f) => [f.relativePath, f]));
3055
- const newerFiles = new Map(newer.files.map((f) => [f.relativePath, f]));
3056
- const added = [];
3057
- const removed = [];
3058
- const changed = [];
3059
- for (const [path, file] of newerFiles) {
3060
- const old = olderFiles.get(path);
3061
- if (!old) {
3062
- added.push(path);
3063
- } else if (old.hash !== file.hash) {
3064
- changed.push(path);
3065
- }
3066
- }
3067
- for (const path of olderFiles.keys()) {
3068
- if (!newerFiles.has(path)) {
3069
- removed.push(path);
3070
- }
3071
- }
3072
- return {
3073
- added,
3074
- removed,
3075
- changed,
3076
- tokenDelta: newer.totalTokens - older.totalTokens,
3077
- coverageDelta: newer.coverageScore - older.coverageScore,
3078
- riskDelta: newer.riskScore - older.riskScore
3079
- };
3080
- }
3081
- function hashString(input) {
3082
- return createHash6("sha256").update(input).digest("hex").substring(0, 16);
3083
- }
3084
-
3085
- // src/cli/v2/snapshot.ts
3086
- var SNAPSHOTS_DIR = ".cto/snapshots";
3087
- async function ensureSnapshotsDir(projectPath) {
3088
- const dir = join9(projectPath, SNAPSHOTS_DIR);
3089
- await mkdir3(dir, { recursive: true });
3090
- return dir;
3091
- }
3092
- async function loadSnapshot(filePath) {
3093
- const content = await readFile7(filePath, "utf-8");
3094
- return JSON.parse(content);
3095
- }
3096
- var snapshotCommand = new Command4("snapshot").description("Create and manage reproducible context snapshots").addCommand(
3097
- new Command4("create").description("Create a snapshot of current context selection").argument("<name>", "Snapshot name").option("-p, --path <path>", "Project path", ".").option("-b, --budget <tokens>", "Token budget", "50000").option("-t, --task <task>", "Task description for context selection", "general review").action(async (name, opts) => {
3098
- try {
3099
- const projectPath = resolve9(opts.path ?? ".");
3100
- const budget = parseInt(opts.budget ?? "50000", 10);
3101
- console.log(chalk4.dim(`
3102
- Analyzing project...`));
3103
- const analysis = await analyzeProject(projectPath);
3104
- console.log(chalk4.dim(` Selecting context...`));
3105
- const selection = await selectContext({
3106
- task: opts.task ?? "general review",
3107
- analysis,
3108
- budget
3109
- });
3110
- const snap = createSnapshot(name, analysis, selection, {
3111
- task: opts.task,
3112
- budget,
3113
- createdBy: process.env.USER ?? "unknown"
3114
- });
3115
- const dir = await ensureSnapshotsDir(projectPath);
3116
- const filePath = join9(dir, `${name}.json`);
3117
- await writeFile5(filePath, JSON.stringify(snap, null, 2), "utf-8");
3118
- console.log("");
3119
- console.log(chalk4.bold.cyan(` \u{1F4F8} Snapshot "${name}" created`));
3120
- console.log(` Files: ${snap.files.length}`);
3121
- console.log(` Tokens: ~${Math.round(snap.totalTokens / 1e3)}K`);
3122
- console.log(` Coverage: ${snap.coverageScore}%`);
3123
- console.log(` Risk: ${snap.riskScore}/100`);
3124
- console.log(` Hash: ${snap.hash}`);
3125
- console.log(` Saved: ${filePath}`);
3126
- console.log("");
3127
- } catch (err) {
3128
- console.error(chalk4.red(`
3129
- Error: ${err.message}
3130
- `));
3131
- process.exit(1);
3132
- }
3133
- })
3134
- ).addCommand(
3135
- new Command4("verify").description("Verify a snapshot against current project state").argument("<name>", "Snapshot name").option("-p, --path <path>", "Project path", ".").option("-b, --budget <tokens>", "Token budget", "50000").action(async (name, opts) => {
3136
- try {
3137
- const projectPath = resolve9(opts.path ?? ".");
3138
- const budget = parseInt(opts.budget ?? "50000", 10);
3139
- const snapPath = join9(projectPath, SNAPSHOTS_DIR, `${name}.json`);
3140
- const snap = await loadSnapshot(snapPath);
3141
- console.log(chalk4.dim(`
3142
- Re-analyzing project...`));
3143
- const analysis = await analyzeProject(projectPath);
3144
- const selection = await selectContext({
3145
- task: snap.metadata.task ?? "general review",
3146
- analysis,
3147
- budget
3148
- });
3149
- const result = await verifySnapshot(snap, analysis, selection);
3150
- console.log("");
3151
- if (result.valid) {
3152
- console.log(chalk4.bold.green(` \u2705 Snapshot "${name}" is valid`));
3153
- } else {
3154
- console.log(chalk4.bold.red(` \u274C Snapshot "${name}" has drifted`));
3155
- }
3156
- console.log(` Files checked: ${result.filesChecked}`);
3157
- console.log(` Files matched: ${result.filesMatched}`);
3158
- if (result.filesMissing.length > 0) {
3159
- console.log(chalk4.yellow(` Missing: ${result.filesMissing.join(", ")}`));
3160
- }
3161
- if (result.filesChanged.length > 0) {
3162
- console.log(chalk4.yellow(` Changed: ${result.filesChanged.join(", ")}`));
3163
- }
3164
- console.log("");
3165
- } catch (err) {
3166
- console.error(chalk4.red(`
3167
- Error: ${err.message}
3168
- `));
3169
- process.exit(1);
3170
- }
3171
- })
3172
- ).addCommand(
3173
- new Command4("compare").description("Compare two snapshots").argument("<older>", "Older snapshot name").argument("<newer>", "Newer snapshot name").option("-p, --path <path>", "Project path", ".").action(async (older, newer, opts) => {
3174
- try {
3175
- const projectPath = resolve9(opts.path ?? ".");
3176
- const dir = join9(projectPath, SNAPSHOTS_DIR);
3177
- const snap1 = await loadSnapshot(join9(dir, `${older}.json`));
3178
- const snap2 = await loadSnapshot(join9(dir, `${newer}.json`));
3179
- const diff = compareSnapshots(snap1, snap2);
3180
- console.log("");
3181
- console.log(chalk4.bold.cyan(` \u{1F4CA} Snapshot Comparison: ${older} \u2192 ${newer}`));
3182
- console.log("");
3183
- if (diff.added.length > 0) {
3184
- console.log(chalk4.green(` Added (${diff.added.length}):`));
3185
- for (const f of diff.added) console.log(` + ${f}`);
3186
- }
3187
- if (diff.removed.length > 0) {
3188
- console.log(chalk4.red(` Removed (${diff.removed.length}):`));
3189
- for (const f of diff.removed) console.log(` - ${f}`);
3190
- }
3191
- if (diff.changed.length > 0) {
3192
- console.log(chalk4.yellow(` Changed (${diff.changed.length}):`));
3193
- for (const f of diff.changed) console.log(` ~ ${f}`);
3194
- }
3195
- console.log("");
3196
- console.log(` Token delta: ${diff.tokenDelta > 0 ? "+" : ""}${diff.tokenDelta}`);
3197
- console.log(` Coverage delta: ${diff.coverageDelta > 0 ? "+" : ""}${diff.coverageDelta}%`);
3198
- console.log(` Risk delta: ${diff.riskDelta > 0 ? "+" : ""}${diff.riskDelta}`);
3199
- console.log("");
3200
- } catch (err) {
3201
- console.error(chalk4.red(`
3202
- Error: ${err.message}
3203
- `));
3204
- process.exit(1);
3205
- }
3206
- })
3207
- ).addCommand(
3208
- new Command4("list").description("List saved snapshots").option("-p, --path <path>", "Project path", ".").action(async (opts) => {
3209
- try {
3210
- const projectPath = resolve9(opts.path ?? ".");
3211
- const dir = join9(projectPath, SNAPSHOTS_DIR);
3212
- let files;
3213
- try {
3214
- files = await readdir4(dir);
3215
- } catch {
3216
- console.log(chalk4.dim("\n No snapshots found. Run `cto snapshot create <name>` first.\n"));
3217
- return;
3218
- }
3219
- const snapFiles = files.filter((f) => f.endsWith(".json")).sort();
3220
- if (snapFiles.length === 0) {
3221
- console.log(chalk4.dim("\n No snapshots found.\n"));
3222
- return;
3223
- }
3224
- console.log("");
3225
- console.log(chalk4.bold.cyan(` \u{1F4F8} Snapshots (${snapFiles.length})`));
3226
- console.log("");
3227
- for (const file of snapFiles) {
3228
- try {
3229
- const snap = await loadSnapshot(join9(dir, file));
3230
- const date = new Date(snap.createdAt).toLocaleString();
3231
- console.log(` ${chalk4.bold(snap.name)} \u2014 ${snap.files.length} files, ~${Math.round(snap.totalTokens / 1e3)}K tokens, coverage ${snap.coverageScore}%`);
3232
- console.log(` ${chalk4.dim(`Created: ${date} | Hash: ${snap.hash}`)}`);
3233
- } catch {
3234
- console.log(` ${chalk4.dim(file)} ${chalk4.red("(corrupted)")}`);
3235
- }
3236
- }
3237
- console.log("");
3238
- } catch (err) {
3239
- console.error(chalk4.red(`
3240
- Error: ${err.message}
3241
- `));
3242
- process.exit(1);
3243
- }
3244
- })
3245
- );
3246
-
3247
- // src/cli/v2/audit.ts
3248
- import { Command as Command5 } from "commander";
3249
- import chalk5 from "chalk";
3250
- var auditCommand = new Command5("audit").description("View and manage the audit trail").addCommand(
3251
- new Command5("list").description("List recent audit entries").option("-n, --limit <limit>", "Number of entries to show", "20").option("-a, --action <action>", "Filter by action type").option("-p, --project <path>", "Filter by project path").option("--json", "Output as JSON").action(async (opts) => {
3252
- try {
3253
- const entries = await getAuditEntries({
3254
- limit: parseInt(opts.limit ?? "20", 10),
3255
- action: opts.action,
3256
- projectPath: opts.project
3257
- });
3258
- if (opts.json) {
3259
- console.log(JSON.stringify(entries, null, 2));
3260
- return;
3261
- }
3262
- if (entries.length === 0) {
3263
- console.log(chalk5.dim("\n No audit entries found.\n"));
3264
- return;
3265
- }
3266
- console.log("");
3267
- console.log(chalk5.bold.cyan(` \u{1F4CB} Audit Trail (${entries.length} entries)`));
3268
- console.log("");
3269
- for (const entry of entries) {
3270
- const date = new Date(entry.timestamp).toLocaleString();
3271
- const actionColor = entry.action.includes("secret") ? chalk5.red : entry.action.includes("integrity") ? chalk5.yellow : chalk5.blue;
3272
- console.log(` ${chalk5.dim(date)} ${actionColor(entry.action)} ${chalk5.dim("by")} ${entry.user}`);
3273
- if (entry.projectPath) {
3274
- console.log(` ${chalk5.dim(`Project: ${entry.projectPath}`)}`);
3275
- }
3276
- if (entry.tokensUsed) {
3277
- console.log(` ${chalk5.dim(`Tokens: ~${Math.round(entry.tokensUsed / 1e3)}K | Coverage: ${entry.coverageScore ?? "?"}% | Risk: ${entry.riskScore ?? "?"}`)}`);
3278
- }
3279
- }
3280
- console.log("");
3281
- } catch (err) {
3282
- console.error(chalk5.red(`
3283
- Error: ${err.message}
3284
- `));
3285
- process.exit(1);
3286
- }
3287
- })
3288
- ).addCommand(
3289
- new Command5("verify").description("Verify audit trail integrity").action(async () => {
3290
- try {
3291
- console.log(chalk5.dim("\n Verifying audit integrity...\n"));
3292
- const result = await verifyAuditIntegrity();
3293
- if (result.invalidEntries.length === 0) {
3294
- console.log(chalk5.bold.green(` \u2705 Audit trail is intact`));
3295
- } else {
3296
- console.log(chalk5.bold.red(` \u274C Audit trail has been tampered with`));
3297
- }
3298
- console.log(` Total entries: ${result.totalEntries}`);
3299
- console.log(` Valid: ${result.validEntries}`);
3300
- console.log(` Invalid: ${result.invalidEntries.length}`);
3301
- if (result.invalidEntries.length > 0) {
3302
- console.log("");
3303
- console.log(chalk5.red(" Tampered entries:"));
3304
- for (const entry of result.invalidEntries.slice(0, 5)) {
3305
- console.log(` ${entry.id} \u2014 ${entry.action} at ${new Date(entry.timestamp).toLocaleString()}`);
3306
- }
3307
- }
3308
- console.log("");
3309
- } catch (err) {
3310
- console.error(chalk5.red(`
3311
- Error: ${err.message}
3312
- `));
3313
- process.exit(1);
3314
- }
3315
- })
3316
- ).addCommand(
3317
- new Command5("purge").description("Purge old audit entries").option("-d, --days <days>", "Retention days", "90").action(async (opts) => {
3318
- try {
3319
- const days = parseInt(opts.days ?? "90", 10);
3320
- const purged = await purgeOldAuditEntries(days);
3321
- console.log("");
3322
- if (purged > 0) {
3323
- console.log(chalk5.bold(` \u{1F5D1}\uFE0F Purged ${purged} audit file(s) older than ${days} days`));
3324
- } else {
3325
- console.log(chalk5.dim(` No audit files older than ${days} days found.`));
3326
- }
3327
- console.log("");
3328
- } catch (err) {
3329
- console.error(chalk5.red(`
3330
- Error: ${err.message}
3331
- `));
3332
- process.exit(1);
3333
- }
3334
- })
3335
- );
3336
-
3337
- // src/cli/v2/policy.ts
3338
- import { Command as Command6 } from "commander";
3339
- import chalk6 from "chalk";
3340
- import { resolve as resolve10, join as join10 } from "path";
3341
- import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir4 } from "fs/promises";
3342
-
3343
- // src/govern/policy.ts
3344
- var DEFAULT_POLICY = {
3345
- version: "1.0",
3346
- name: "default",
3347
- rules: [
3348
- {
3349
- id: "no-env",
3350
- type: "exclude-always",
3351
- pattern: "**/*.env*",
3352
- reason: "Environment files must never be sent to AI",
3353
- enabled: true
3354
- },
3355
- {
3356
- id: "no-secrets",
3357
- type: "secret-block",
3358
- reason: "Files with detected secrets are blocked",
3359
- enabled: true
3360
- },
3361
- {
3362
- id: "min-coverage",
3363
- type: "coverage-minimum",
3364
- threshold: 70,
3365
- reason: "Warn if context coverage drops below 70%",
3366
- enabled: true
3367
- }
3368
- ]
3369
- };
3370
- function validateSelection(selection, policies, allFiles) {
3371
- const violations = [];
3372
- const warnings = [];
3373
- const includedPaths = new Set(selection.files.map((f) => f.relativePath));
3374
- for (const rule of policies.rules) {
3375
- if (!rule.enabled) continue;
3376
- switch (rule.type) {
3377
- case "exclude-always": {
3378
- if (!rule.pattern) break;
3379
- const violatingFiles = selection.files.filter(
3380
- (f) => matchGlob(f.relativePath, rule.pattern)
3381
- );
3382
- for (const f of violatingFiles) {
3383
- violations.push({
3384
- rule,
3385
- message: `File "${f.relativePath}" is included but matches exclude-always pattern "${rule.pattern}"`,
3386
- severity: "error"
3387
- });
3388
- }
3389
- break;
3390
- }
3391
- case "include-always": {
3392
- if (!rule.pattern || !allFiles) break;
3393
- const requiredFiles = allFiles.filter(
3394
- (f) => matchGlob(f.relativePath, rule.pattern)
3395
- );
3396
- for (const f of requiredFiles) {
3397
- if (!includedPaths.has(f.relativePath)) {
3398
- violations.push({
3399
- rule,
3400
- message: `File "${f.relativePath}" matches include-always pattern "${rule.pattern}" but is not included`,
3401
- severity: "warning"
3402
- });
3403
- }
3404
- }
3405
- break;
3406
- }
3407
- case "coverage-minimum": {
3408
- const threshold = rule.threshold ?? 70;
3409
- if (selection.coverage.score < threshold) {
3410
- warnings.push({
3411
- rule,
3412
- message: `Coverage ${selection.coverage.score}% is below minimum ${threshold}%`,
3413
- currentValue: selection.coverage.score,
3414
- threshold
3415
- });
3416
- }
3417
- break;
3418
- }
3419
- case "risk-maximum": {
3420
- const threshold = rule.threshold ?? 50;
3421
- if (selection.riskScore > threshold) {
3422
- warnings.push({
3423
- rule,
3424
- message: `Exclusion risk ${selection.riskScore}/100 exceeds maximum ${threshold}`,
3425
- currentValue: selection.riskScore,
3426
- threshold
3427
- });
3428
- }
3429
- break;
3430
- }
3431
- case "budget-limit": {
3432
- if (!rule.category || !rule.threshold) break;
3433
- const categoryFiles = selection.files.filter(
3434
- (f) => fileMatchesCategory(f.relativePath, rule.category)
3435
- );
3436
- const categoryTokens = categoryFiles.reduce((s, f) => s + f.tokens, 0);
3437
- const categoryPercent = selection.totalTokens > 0 ? categoryTokens / selection.totalTokens * 100 : 0;
3438
- if (categoryPercent > rule.threshold) {
3439
- warnings.push({
3440
- rule,
3441
- message: `Category "${rule.category}" uses ${Math.round(categoryPercent)}% of budget (max: ${rule.threshold}%)`,
3442
- currentValue: Math.round(categoryPercent),
3443
- threshold: rule.threshold
3444
- });
3445
- }
3446
- break;
3447
- }
3448
- }
3449
- }
3450
- return {
3451
- passed: violations.filter((v) => v.severity === "error").length === 0,
3452
- violations,
3453
- warnings
3454
- };
3455
- }
3456
- function addRule(policies, rule) {
3457
- return {
3458
- ...policies,
3459
- rules: [...policies.rules, rule]
3460
- };
3461
- }
3462
- function removeRule(policies, ruleId) {
3463
- return {
3464
- ...policies,
3465
- rules: policies.rules.filter((r) => r.id !== ruleId)
3466
- };
3467
- }
3468
- function toggleRule(policies, ruleId, enabled) {
3469
- return {
3470
- ...policies,
3471
- rules: policies.rules.map(
3472
- (r) => r.id === ruleId ? { ...r, enabled } : r
3473
- )
3474
- };
3475
- }
3476
- function fileMatchesCategory(path, category) {
3477
- switch (category) {
3478
- case "test":
3479
- return /\.(test|spec)\.[jt]sx?$/.test(path) || /\/__tests__\//.test(path) || /\/tests?\//.test(path);
3480
- case "config":
3481
- return /\.(config|rc)\.[jt]s$/.test(path) || /\.json$/.test(path) || /\.ya?ml$/.test(path);
3482
- case "docs":
3483
- return /\.(md|txt|rst)$/.test(path);
3484
- case "types":
3485
- return /types?\//i.test(path) || /\.d\.ts$/.test(path);
3486
- default:
3487
- return path.includes(category);
3488
- }
3489
- }
3490
-
3491
- // src/cli/v2/policy.ts
3492
- var POLICY_FILE2 = ".cto/policy.json";
3493
- async function loadPolicy(projectPath) {
3494
- try {
3495
- const content = await readFile8(join10(projectPath, POLICY_FILE2), "utf-8");
3496
- return JSON.parse(content);
3497
- } catch {
3498
- return DEFAULT_POLICY;
3499
- }
3500
- }
3501
- async function savePolicy(projectPath, policy) {
3502
- const dir = join10(projectPath, ".cto");
3503
- await mkdir4(dir, { recursive: true });
3504
- await writeFile6(join10(projectPath, POLICY_FILE2), JSON.stringify(policy, null, 2), "utf-8");
3505
- }
3506
- var policyCommand = new Command6("policy").description("Manage context selection policies").addCommand(
3507
- new Command6("show").description("Show current policy rules").option("-p, --path <path>", "Project path", ".").option("--json", "Output as JSON").action(async (opts) => {
3508
- try {
3509
- const projectPath = resolve10(opts.path ?? ".");
3510
- const policy = await loadPolicy(projectPath);
3511
- if (opts.json) {
3512
- console.log(JSON.stringify(policy, null, 2));
3513
- return;
3514
- }
3515
- console.log("");
3516
- console.log(chalk6.bold.cyan(` \u{1F4DC} Policy: ${policy.name} (v${policy.version})`));
3517
- console.log("");
3518
- if (policy.rules.length === 0) {
3519
- console.log(chalk6.dim(" No rules defined."));
3520
- }
3521
- for (const rule of policy.rules) {
3522
- const status = rule.enabled ? chalk6.green("\u25CF") : chalk6.dim("\u25CB");
3523
- const typeColor = rule.type.includes("exclude") ? chalk6.red : rule.type.includes("include") ? chalk6.green : chalk6.yellow;
3524
- console.log(` ${status} ${chalk6.bold(rule.id)} \u2014 ${typeColor(rule.type)}`);
3525
- console.log(` ${chalk6.dim(rule.reason)}`);
3526
- if (rule.pattern) console.log(` ${chalk6.dim(`Pattern: ${rule.pattern}`)}`);
3527
- if (rule.threshold != null) console.log(` ${chalk6.dim(`Threshold: ${rule.threshold}`)}`);
3528
- if (rule.category) console.log(` ${chalk6.dim(`Category: ${rule.category}`)}`);
3529
- }
3530
- console.log("");
3531
- } catch (err) {
3532
- console.error(chalk6.red(`
3533
- Error: ${err.message}
3534
- `));
3535
- process.exit(1);
3536
- }
3537
- })
3538
- ).addCommand(
3539
- new Command6("add").description("Add a new policy rule").argument("<id>", "Rule ID").argument("<type>", "Rule type: exclude-always | include-always | coverage-minimum | risk-maximum | budget-limit").option("-p, --path <path>", "Project path", ".").option("--pattern <pattern>", "Glob pattern for include/exclude rules").option("--threshold <n>", "Threshold value for metric rules").option("--category <cat>", "Category for budget-limit rules").option("--reason <reason>", "Reason for the rule", "User-defined rule").action(async (id, type, opts) => {
3540
- try {
3541
- const projectPath = resolve10(opts.path ?? ".");
3542
- let policy = await loadPolicy(projectPath);
3543
- policy = addRule(policy, {
3544
- id,
3545
- type,
3546
- pattern: opts.pattern,
3547
- threshold: opts.threshold ? parseInt(opts.threshold, 10) : void 0,
3548
- category: opts.category,
3549
- reason: opts.reason ?? "User-defined rule",
3550
- enabled: true
3551
- });
3552
- await savePolicy(projectPath, policy);
3553
- console.log(chalk6.green(`
3554
- \u2705 Rule "${id}" added (${type})
3555
- `));
3556
- } catch (err) {
3557
- console.error(chalk6.red(`
3558
- Error: ${err.message}
3559
- `));
3560
- process.exit(1);
3561
- }
3562
- })
3563
- ).addCommand(
3564
- new Command6("remove").description("Remove a policy rule").argument("<id>", "Rule ID to remove").option("-p, --path <path>", "Project path", ".").action(async (id, opts) => {
3565
- try {
3566
- const projectPath = resolve10(opts.path ?? ".");
3567
- let policy = await loadPolicy(projectPath);
3568
- policy = removeRule(policy, id);
3569
- await savePolicy(projectPath, policy);
3570
- console.log(chalk6.green(`
3571
- \u2705 Rule "${id}" removed
3572
- `));
3573
- } catch (err) {
3574
- console.error(chalk6.red(`
3575
- Error: ${err.message}
3576
- `));
3577
- process.exit(1);
3578
- }
3579
- })
3580
- ).addCommand(
3581
- new Command6("toggle").description("Enable or disable a rule").argument("<id>", "Rule ID").argument("<state>", "on | off").option("-p, --path <path>", "Project path", ".").action(async (id, state, opts) => {
3582
- try {
3583
- const projectPath = resolve10(opts.path ?? ".");
3584
- let policy = await loadPolicy(projectPath);
3585
- policy = toggleRule(policy, id, state === "on");
3586
- await savePolicy(projectPath, policy);
3587
- console.log(chalk6.green(`
3588
- \u2705 Rule "${id}" ${state === "on" ? "enabled" : "disabled"}
3589
- `));
3590
- } catch (err) {
3591
- console.error(chalk6.red(`
3592
- Error: ${err.message}
3593
- `));
3594
- process.exit(1);
3595
- }
3596
- })
3597
- ).addCommand(
3598
- new Command6("validate").description("Validate current context selection against policies").option("-p, --path <path>", "Project path", ".").option("-b, --budget <tokens>", "Token budget", "50000").option("-t, --task <task>", "Task description", "general review").action(async (opts) => {
3599
- try {
3600
- const projectPath = resolve10(opts.path ?? ".");
3601
- const budget = parseInt(opts.budget ?? "50000", 10);
3602
- const policy = await loadPolicy(projectPath);
3603
- console.log(chalk6.dim("\n Analyzing project..."));
3604
- const analysis = await analyzeProject(projectPath);
3605
- console.log(chalk6.dim(" Selecting context..."));
3606
- const selection = await selectContext({
3607
- task: opts.task ?? "general review",
3608
- analysis,
3609
- budget,
3610
- policies: policy
3611
- });
3612
- const result = validateSelection(selection, policy, analysis.files);
3613
- console.log("");
3614
- if (result.passed) {
3615
- console.log(chalk6.bold.green(" \u2705 Policy validation passed"));
3616
- } else {
3617
- console.log(chalk6.bold.red(" \u274C Policy validation failed"));
3618
- }
3619
- if (result.violations.length > 0) {
3620
- console.log("");
3621
- console.log(chalk6.bold(" Violations:"));
3622
- for (const v of result.violations) {
3623
- const icon = v.severity === "error" ? chalk6.red("\u2717") : chalk6.yellow("\u26A0");
3624
- console.log(` ${icon} ${v.message}`);
3625
- }
3626
- }
3627
- if (result.warnings.length > 0) {
3628
- console.log("");
3629
- console.log(chalk6.bold(" Warnings:"));
3630
- for (const w of result.warnings) {
3631
- console.log(` ${chalk6.yellow("\u26A0")} ${w.message}`);
3632
- }
3633
- }
3634
- console.log("");
3635
- } catch (err) {
3636
- console.error(chalk6.red(`
3637
- Error: ${err.message}
3638
- `));
3639
- process.exit(1);
3640
- }
3641
- })
3642
- ).addCommand(
3643
- new Command6("init").description("Initialize default policy file").option("-p, --path <path>", "Project path", ".").action(async (opts) => {
3644
- try {
3645
- const projectPath = resolve10(opts.path ?? ".");
3646
- await savePolicy(projectPath, DEFAULT_POLICY);
3647
- console.log(chalk6.green(`
3648
- \u2705 Default policy initialized at ${join10(projectPath, POLICY_FILE2)}
3649
- `));
3650
- } catch (err) {
3651
- console.error(chalk6.red(`
3652
- Error: ${err.message}
3653
- `));
3654
- process.exit(1);
3655
- }
3656
- })
3657
- );
3658
-
3659
- // src/cli/v2/index.ts
3660
- var CTO_V2_VERSION = "2.0.0";
3661
- var program = new Command7();
3662
- program.name("cto").description("CTO \u2014 Context Operating System for AI-native Engineering").version(CTO_V2_VERSION, "-v, --version");
3663
- program.addCommand(initCommand);
3664
- program.addCommand(analyzeCommand);
3665
- program.addCommand(interactCommand);
3666
- program.addCommand(snapshotCommand);
3667
- program.addCommand(auditCommand);
3668
- program.addCommand(policyCommand);
3669
- program.action(() => {
3670
- console.log("");
3671
- console.log(chalk7.bold.cyan(" \u{1F9E0} CTO v2 \u2014 Context Operating System"));
3672
- console.log(chalk7.dim(` v${CTO_V2_VERSION}`));
3673
- console.log("");
3674
- console.log(chalk7.dim(" AI Interaction Infrastructure for Engineering Teams"));
3675
- console.log(chalk7.dim(" Formal risk modeling \xB7 Deterministic selection \xB7 Enterprise auditability"));
3676
- console.log("");
3677
- console.log(chalk7.bold(" Commands:"));
3678
- console.log(chalk7.dim(" init Setup CTO for a project"));
3679
- console.log(chalk7.dim(" analyze [path] Analyze structure, deps & risk"));
3680
- console.log(chalk7.dim(" interact <task> Build optimized AI context"));
3681
- console.log(chalk7.dim(" snapshot create|verify|compare|list Reproducible context snapshots"));
3682
- console.log(chalk7.dim(" audit list|verify|purge Tamper-evident audit trail"));
3683
- console.log(chalk7.dim(" policy show|add|remove|toggle|validate|init Context policies"));
3684
- console.log("");
3685
- console.log(chalk7.bold(" Quick Start:"));
3686
- console.log(chalk7.dim(" $ cto init"));
3687
- console.log(chalk7.dim(" $ cto analyze"));
3688
- console.log(chalk7.dim(' $ cto interact "refactor the auth middleware"'));
3689
- console.log(chalk7.dim(" $ cto snapshot create baseline"));
3690
- console.log("");
3691
- console.log(chalk7.dim(" Run cto <command> --help for details"));
3692
- console.log("");
3693
- });
3694
- program.parse();
3695
- //# sourceMappingURL=index.js.map