cto-ai-cli 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/score.js CHANGED
@@ -1344,10 +1344,7 @@ var init_secrets = __esm({
1344
1344
  });
1345
1345
 
1346
1346
  // src/engine/pruner.ts
1347
- import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
1348
1347
  import { readFile as readFile4 } from "fs/promises";
1349
- import { existsSync as existsSync3 } from "fs";
1350
- import { join as join4 } from "path";
1351
1348
  async function pruneFile(file, level) {
1352
1349
  if (level === "excluded") {
1353
1350
  return emptyResult(file, "excluded");
@@ -1369,23 +1366,7 @@ async function pruneTypeScript(file, level) {
1369
1366
  } catch {
1370
1367
  return emptyResult(file, level);
1371
1368
  }
1372
- let project;
1373
- try {
1374
- const tsConfigPath = findTsConfig(file.path);
1375
- project = new Project2({
1376
- tsConfigFilePath: tsConfigPath,
1377
- skipAddingFilesFromTsConfig: true,
1378
- compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
1379
- });
1380
- project.createSourceFile(file.path, content, { overwrite: true });
1381
- } catch {
1382
- return pruneGenericFromContent(file, content, level);
1383
- }
1384
- const sourceFile = project.getSourceFiles()[0];
1385
- if (!sourceFile) {
1386
- return pruneGenericFromContent(file, content, level);
1387
- }
1388
- const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
1369
+ const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
1389
1370
  const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
1390
1371
  const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
1391
1372
  return {
@@ -1397,131 +1378,281 @@ async function pruneTypeScript(file, level) {
1397
1378
  savingsPercent: Math.max(0, savingsPercent)
1398
1379
  };
1399
1380
  }
1400
- function extractSignaturesAST(sf) {
1381
+ function extractSignaturesRegex(content) {
1382
+ const lines = content.split("\n");
1401
1383
  const parts = [];
1402
- for (const imp of sf.getImportDeclarations()) {
1403
- parts.push(imp.getText());
1404
- }
1405
- if (parts.length > 0) parts.push("");
1406
- for (const ta of sf.getTypeAliases()) {
1407
- addJSDoc(ta, parts);
1408
- parts.push(ta.getText());
1409
- }
1410
- for (const iface of sf.getInterfaces()) {
1411
- addJSDoc(iface, parts);
1412
- parts.push(iface.getText());
1413
- }
1414
- for (const en of sf.getEnums()) {
1415
- addJSDoc(en, parts);
1416
- parts.push(en.getText());
1417
- }
1418
- for (const fn of sf.getFunctions()) {
1419
- addJSDoc(fn, parts);
1420
- const isExported = fn.isExported();
1421
- const isAsync = fn.isAsync();
1422
- const name = fn.getName() ?? "<anonymous>";
1423
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1424
- const returnType = fn.getReturnTypeNode()?.getText();
1425
- const returnStr = returnType ? `: ${returnType}` : "";
1426
- const prefix = isExported ? "export " : "";
1427
- const asyncStr = isAsync ? "async " : "";
1428
- parts.push(`${prefix}${asyncStr}function ${name}(${params})${returnStr} { /* ... */ }`);
1429
- }
1430
- for (const stmt of sf.getVariableStatements()) {
1431
- for (const decl of stmt.getDeclarations()) {
1432
- const init = decl.getInitializer();
1433
- if (init && (init.getKind() === SyntaxKind2.ArrowFunction || init.getKind() === SyntaxKind2.FunctionExpression)) {
1434
- addJSDoc(stmt, parts);
1435
- const isExported = stmt.isExported();
1436
- const prefix = isExported ? "export " : "";
1437
- const kind = stmt.getDeclarationKind();
1438
- const name = decl.getName();
1439
- const typeNode = decl.getTypeNode()?.getText();
1440
- const typeStr = typeNode ? `: ${typeNode}` : "";
1441
- parts.push(`${prefix}${kind} ${name}${typeStr} = /* ... */;`);
1442
- } else {
1443
- addJSDoc(stmt, parts);
1444
- parts.push(stmt.getText());
1384
+ let i = 0;
1385
+ while (i < lines.length) {
1386
+ const line = lines[i];
1387
+ const trimmed = line.trim();
1388
+ if (trimmed === "") {
1389
+ i++;
1390
+ continue;
1391
+ }
1392
+ if (trimmed.startsWith("/**")) {
1393
+ const docLines = [];
1394
+ while (i < lines.length) {
1395
+ docLines.push(lines[i]);
1396
+ if (lines[i].includes("*/")) {
1397
+ i++;
1398
+ break;
1399
+ }
1400
+ i++;
1445
1401
  }
1402
+ parts.push(docLines.join("\n"));
1403
+ continue;
1446
1404
  }
1447
- }
1448
- for (const cls of sf.getClasses()) {
1449
- addJSDoc(cls, parts);
1450
- const isExported = cls.isExported();
1451
- const prefix = isExported ? "export " : "";
1452
- const name = cls.getName() ?? "<anonymous>";
1453
- const ext = cls.getExtends()?.getText();
1454
- const impl = cls.getImplements().map((i) => i.getText()).join(", ");
1455
- let header = `${prefix}class ${name}`;
1456
- if (ext) header += ` extends ${ext}`;
1457
- if (impl) header += ` implements ${impl}`;
1458
- header += " {";
1459
- parts.push(header);
1460
- for (const prop of cls.getProperties()) {
1461
- parts.push(` ${prop.getText()}`);
1462
- }
1463
- const ctor = cls.getConstructors()[0];
1464
- if (ctor) {
1465
- const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
1466
- parts.push(` constructor(${ctorParams}) { /* ... */ }`);
1467
- }
1468
- for (const method of cls.getMethods()) {
1469
- const isStatic = method.isStatic();
1470
- const isAsync = method.isAsync();
1471
- const methodName = method.getName();
1472
- const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
1473
- const returnType = method.getReturnTypeNode()?.getText();
1474
- const returnStr = returnType ? `: ${returnType}` : "";
1475
- const staticStr = isStatic ? "static " : "";
1476
- const asyncStr = isAsync ? "async " : "";
1477
- parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
1478
- }
1479
- parts.push("}");
1480
- }
1481
- for (const exp of sf.getExportDeclarations()) {
1482
- parts.push(exp.getText());
1483
- }
1484
- for (const exp of sf.getExportAssignments()) {
1485
- parts.push(exp.getText());
1405
+ if (trimmed.startsWith("//")) {
1406
+ parts.push(line);
1407
+ i++;
1408
+ continue;
1409
+ }
1410
+ if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
1411
+ const block = collectBracedLine(lines, i);
1412
+ parts.push(block.text);
1413
+ i = block.nextIndex;
1414
+ continue;
1415
+ }
1416
+ if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
1417
+ const block = collectBracedLine(lines, i);
1418
+ parts.push(block.text);
1419
+ i = block.nextIndex;
1420
+ continue;
1421
+ }
1422
+ if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
1423
+ const block = collectBalanced(lines, i);
1424
+ parts.push(block.text);
1425
+ i = block.nextIndex;
1426
+ continue;
1427
+ }
1428
+ if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
1429
+ const block = collectBalanced(lines, i);
1430
+ parts.push(block.text);
1431
+ i = block.nextIndex;
1432
+ continue;
1433
+ }
1434
+ if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
1435
+ const block = collectBalanced(lines, i);
1436
+ parts.push(block.text);
1437
+ i = block.nextIndex;
1438
+ continue;
1439
+ }
1440
+ const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
1441
+ if (fnMatch) {
1442
+ const sig = extractFnSignature(lines, i);
1443
+ parts.push(`${sig} { /* ... */ }`);
1444
+ i = skipBlock(lines, i);
1445
+ continue;
1446
+ }
1447
+ const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
1448
+ if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
1449
+ const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
1450
+ if (prefix) {
1451
+ parts.push(`${prefix} /* ... */;`);
1452
+ }
1453
+ i = skipBlock(lines, i);
1454
+ continue;
1455
+ }
1456
+ if (arrowMatch) {
1457
+ const block = collectStatement(lines, i);
1458
+ parts.push(block.text);
1459
+ i = block.nextIndex;
1460
+ continue;
1461
+ }
1462
+ if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
1463
+ const classOutline = extractClassOutline(lines, i);
1464
+ parts.push(classOutline.text);
1465
+ i = classOutline.nextIndex;
1466
+ continue;
1467
+ }
1468
+ i++;
1486
1469
  }
1487
1470
  return parts.join("\n");
1488
1471
  }
1489
- function extractSkeletonAST(sf) {
1472
+ function extractSkeletonRegex(content) {
1473
+ const lines = content.split("\n");
1490
1474
  const parts = [];
1491
- for (const imp of sf.getImportDeclarations()) {
1492
- parts.push(imp.getText());
1493
- }
1494
- if (parts.length > 0) parts.push("");
1495
- for (const ta of sf.getTypeAliases()) {
1496
- if (ta.isExported()) parts.push(ta.getText());
1497
- }
1498
- for (const iface of sf.getInterfaces()) {
1499
- if (!iface.isExported()) continue;
1500
- const ext = iface.getExtends().map((e) => e.getText());
1501
- const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
1502
- parts.push(`export interface ${iface.getName()}${extStr} { /* ${iface.getProperties().length} props */ }`);
1503
- }
1504
- for (const en of sf.getEnums()) {
1505
- if (!en.isExported()) continue;
1506
- const members = en.getMembers().map((m) => m.getName());
1507
- parts.push(`export enum ${en.getName()} { ${members.join(", ")} }`);
1508
- }
1509
- for (const fn of sf.getFunctions()) {
1510
- if (!fn.isExported()) continue;
1511
- const name = fn.getName() ?? "<anonymous>";
1512
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1513
- parts.push(`export function ${name}(${params});`);
1514
- }
1515
- for (const cls of sf.getClasses()) {
1516
- if (!cls.isExported()) continue;
1517
- const methods = cls.getMethods().map((m) => m.getName());
1518
- parts.push(`export class ${cls.getName()} { /* methods: ${methods.join(", ")} */ }`);
1519
- }
1520
- for (const exp of sf.getExportDeclarations()) {
1521
- parts.push(exp.getText());
1475
+ let i = 0;
1476
+ while (i < lines.length) {
1477
+ const trimmed = lines[i].trim();
1478
+ if (/^import\s/.test(trimmed)) {
1479
+ const block = collectBracedLine(lines, i);
1480
+ parts.push(block.text);
1481
+ i = block.nextIndex;
1482
+ continue;
1483
+ }
1484
+ if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
1485
+ const block = collectBalanced(lines, i);
1486
+ parts.push(block.text);
1487
+ i = block.nextIndex;
1488
+ continue;
1489
+ }
1490
+ if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
1491
+ const block = collectBalanced(lines, i);
1492
+ parts.push(block.text);
1493
+ i = block.nextIndex;
1494
+ continue;
1495
+ }
1496
+ if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
1497
+ const sig = extractFnSignature(lines, i);
1498
+ parts.push(`${sig};`);
1499
+ i = skipBlock(lines, i);
1500
+ continue;
1501
+ }
1502
+ if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
1503
+ const nameMatch = trimmed.match(/class\s+(\w+)/);
1504
+ const name = nameMatch?.[1] ?? "Unknown";
1505
+ const end = skipBlock(lines, i);
1506
+ const methods = [];
1507
+ for (let j = i + 1; j < end; j++) {
1508
+ const mt = lines[j].trim();
1509
+ const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
1510
+ if (mm && mm[1] !== "constructor") methods.push(mm[1]);
1511
+ }
1512
+ parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
1513
+ i = end;
1514
+ continue;
1515
+ }
1516
+ if (/^export\s*(\{|\*)/.test(trimmed)) {
1517
+ const block = collectBracedLine(lines, i);
1518
+ parts.push(block.text);
1519
+ i = block.nextIndex;
1520
+ continue;
1521
+ }
1522
+ i++;
1522
1523
  }
1523
1524
  return parts.join("\n");
1524
1525
  }
1526
+ function collectBracedLine(lines, start) {
1527
+ let text = lines[start];
1528
+ let i = start + 1;
1529
+ while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
1530
+ text += "\n" + lines[i];
1531
+ i++;
1532
+ }
1533
+ return { text, nextIndex: i };
1534
+ }
1535
+ function collectBalanced(lines, start) {
1536
+ let depth = 0;
1537
+ let text = "";
1538
+ let i = start;
1539
+ let started = false;
1540
+ while (i < lines.length) {
1541
+ const line = lines[i];
1542
+ text += (text ? "\n" : "") + line;
1543
+ for (const ch of line) {
1544
+ if (ch === "{" || ch === "(") {
1545
+ depth++;
1546
+ started = true;
1547
+ }
1548
+ if (ch === "}" || ch === ")") depth--;
1549
+ }
1550
+ i++;
1551
+ if (started && depth <= 0) break;
1552
+ if (!started && line.includes(";")) break;
1553
+ }
1554
+ return { text, nextIndex: i };
1555
+ }
1556
+ function collectStatement(lines, start) {
1557
+ let text = lines[start];
1558
+ let i = start + 1;
1559
+ if (text.includes(";")) return { text, nextIndex: i };
1560
+ let depth = 0;
1561
+ for (const ch of text) {
1562
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
1563
+ if (ch === "}" || ch === ")" || ch === "]") depth--;
1564
+ }
1565
+ while (i < lines.length && depth > 0) {
1566
+ text += "\n" + lines[i];
1567
+ for (const ch of lines[i]) {
1568
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
1569
+ if (ch === "}" || ch === ")" || ch === "]") depth--;
1570
+ }
1571
+ i++;
1572
+ }
1573
+ return { text, nextIndex: i };
1574
+ }
1575
+ function extractFnSignature(lines, start) {
1576
+ let sig = "";
1577
+ let i = start;
1578
+ while (i < lines.length) {
1579
+ const line = lines[i].trim();
1580
+ sig += (sig ? " " : "") + line;
1581
+ if (line.includes("{")) {
1582
+ sig = sig.replace(/\s*\{[^]*$/, "").trim();
1583
+ break;
1584
+ }
1585
+ i++;
1586
+ }
1587
+ return sig;
1588
+ }
1589
+ function skipBlock(lines, start) {
1590
+ let depth = 0;
1591
+ let i = start;
1592
+ let foundBrace = false;
1593
+ while (i < lines.length) {
1594
+ for (const ch of lines[i]) {
1595
+ if (ch === "{") {
1596
+ depth++;
1597
+ foundBrace = true;
1598
+ }
1599
+ if (ch === "}") depth--;
1600
+ }
1601
+ i++;
1602
+ if (foundBrace && depth <= 0) break;
1603
+ if (!foundBrace && lines[i - 1].includes(";")) break;
1604
+ }
1605
+ return i;
1606
+ }
1607
+ function looksLikeFunctionDecl(lines, start) {
1608
+ const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
1609
+ return /=>/.test(chunk) || /=\s*function/.test(chunk);
1610
+ }
1611
+ function extractClassOutline(lines, start) {
1612
+ const header = lines[start].trim();
1613
+ let headerText = header;
1614
+ let i = start + 1;
1615
+ if (!header.includes("{")) {
1616
+ while (i < lines.length) {
1617
+ headerText += " " + lines[i].trim();
1618
+ if (lines[i].includes("{")) {
1619
+ i++;
1620
+ break;
1621
+ }
1622
+ i++;
1623
+ }
1624
+ } else {
1625
+ i = start + 1;
1626
+ }
1627
+ const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
1628
+ let depth = 1;
1629
+ while (i < lines.length && depth > 0) {
1630
+ const line = lines[i];
1631
+ const trimmed = line.trim();
1632
+ for (const ch of line) {
1633
+ if (ch === "{") depth++;
1634
+ if (ch === "}") depth--;
1635
+ }
1636
+ if (depth <= 0) {
1637
+ i++;
1638
+ break;
1639
+ }
1640
+ if (depth === 1) {
1641
+ if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
1642
+ bodyParts.push(` ${trimmed}`);
1643
+ } else if (/^constructor\s*\(/.test(trimmed)) {
1644
+ const sig = extractFnSignature(lines, i);
1645
+ bodyParts.push(` ${sig} { /* ... */ }`);
1646
+ } else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
1647
+ const sig = extractFnSignature(lines, i);
1648
+ bodyParts.push(` ${sig} { /* ... */ }`);
1649
+ }
1650
+ }
1651
+ i++;
1652
+ }
1653
+ bodyParts.push("}");
1654
+ return { text: bodyParts.join("\n"), nextIndex: i };
1655
+ }
1525
1656
  async function pruneGeneric(file, level) {
1526
1657
  let content;
1527
1658
  try {
@@ -1582,22 +1713,6 @@ function emptyResult(file, level) {
1582
1713
  savingsPercent: 100
1583
1714
  };
1584
1715
  }
1585
- function addJSDoc(node, parts) {
1586
- if (!node.getJsDocs) return;
1587
- const docs = node.getJsDocs();
1588
- if (docs.length > 0) {
1589
- parts.push(docs[0].getText());
1590
- }
1591
- }
1592
- function findTsConfig(filePath) {
1593
- let dir = filePath;
1594
- for (let i = 0; i < 10; i++) {
1595
- dir = join4(dir, "..");
1596
- const candidate = join4(dir, "tsconfig.json");
1597
- if (existsSync3(candidate)) return candidate;
1598
- }
1599
- return void 0;
1600
- }
1601
1716
  var TS_EXTENSIONS2;
1602
1717
  var init_pruner = __esm({
1603
1718
  "src/engine/pruner.ts"() {
@@ -2241,7 +2356,7 @@ var init_providers = __esm({
2241
2356
 
2242
2357
  // src/gateway/interceptor.ts
2243
2358
  import { readFileSync as readFileSync2 } from "fs";
2244
- import { resolve as resolve6 } from "path";
2359
+ import { resolve as resolve10 } from "path";
2245
2360
  function estimateTokensFromString(s) {
2246
2361
  return Math.ceil(Buffer.byteLength(s, "utf-8") / 4);
2247
2362
  }
@@ -2355,7 +2470,7 @@ async function optimizeContext(messages, analysis, config) {
2355
2470
  continue;
2356
2471
  }
2357
2472
  try {
2358
- const fullPath = resolve6(config.projectPath, f.relativePath);
2473
+ const fullPath = resolve10(config.projectPath, f.relativePath);
2359
2474
  const content = readFileSync2(fullPath, "utf-8");
2360
2475
  const fileTokens = estimateTokensFromString(content);
2361
2476
  const remainingBudget = contentBudget - usedTokens;
@@ -2438,8 +2553,8 @@ var init_interceptor = __esm({
2438
2553
  });
2439
2554
 
2440
2555
  // src/gateway/tracker.ts
2441
- import { mkdirSync as mkdirSync2, appendFileSync, readFileSync as readFileSync3, readdirSync, existsSync as existsSync6 } from "fs";
2442
- import { join as join7 } from "path";
2556
+ import { mkdirSync as mkdirSync2, appendFileSync, readFileSync as readFileSync3, readdirSync, existsSync as existsSync5 } from "fs";
2557
+ import { join as join9 } from "path";
2443
2558
  import { randomUUID } from "crypto";
2444
2559
  var UsageTracker;
2445
2560
  var init_tracker = __esm({
@@ -2455,7 +2570,7 @@ var init_tracker = __esm({
2455
2570
  memRecords = [];
2456
2571
  constructor(config) {
2457
2572
  this.config = config;
2458
- this.logDir = join7(config.logDir, "usage");
2573
+ this.logDir = join9(config.logDir, "usage");
2459
2574
  mkdirSync2(this.logDir, { recursive: true });
2460
2575
  }
2461
2576
  // ===== EVENT SYSTEM =====
@@ -2478,7 +2593,7 @@ var init_tracker = __esm({
2478
2593
  ...params
2479
2594
  };
2480
2595
  const monthKey = this.getMonthKey(record.timestamp);
2481
- const logFile = join7(this.logDir, `${monthKey}.jsonl`);
2596
+ const logFile = join9(this.logDir, `${monthKey}.jsonl`);
2482
2597
  const line = JSON.stringify({
2483
2598
  ...record,
2484
2599
  timestamp: record.timestamp.toISOString()
@@ -2612,8 +2727,8 @@ var init_tracker = __esm({
2612
2727
  return date.toISOString().slice(0, 7);
2613
2728
  }
2614
2729
  getMonthRecordsByKey(monthKey) {
2615
- const filePath = join7(this.logDir, `${monthKey}.jsonl`);
2616
- if (!existsSync6(filePath)) return [];
2730
+ const filePath = join9(this.logDir, `${monthKey}.jsonl`);
2731
+ if (!existsSync5(filePath)) return [];
2617
2732
  return readFileSync3(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
2618
2733
  try {
2619
2734
  const parsed = JSON.parse(line);
@@ -2627,8 +2742,8 @@ var init_tracker = __esm({
2627
2742
  getMonthRecords(date) {
2628
2743
  const monthKey = this.getMonthKey(date);
2629
2744
  if (this.cache && this.cacheMonth === monthKey) return this.cache;
2630
- const filePath = join7(this.logDir, `${monthKey}.jsonl`);
2631
- if (!existsSync6(filePath)) return [];
2745
+ const filePath = join9(this.logDir, `${monthKey}.jsonl`);
2746
+ if (!existsSync5(filePath)) return [];
2632
2747
  const records = readFileSync3(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
2633
2748
  try {
2634
2749
  const parsed = JSON.parse(line);
@@ -2643,11 +2758,11 @@ var init_tracker = __esm({
2643
2758
  return records;
2644
2759
  }
2645
2760
  getAllRecords() {
2646
- if (!existsSync6(this.logDir)) return [];
2761
+ if (!existsSync5(this.logDir)) return [];
2647
2762
  const files = readdirSync(this.logDir).filter((f) => f.endsWith(".jsonl")).sort();
2648
2763
  const allRecords = [];
2649
2764
  for (const file of files) {
2650
- const content = readFileSync3(join7(this.logDir, file), "utf-8");
2765
+ const content = readFileSync3(join9(this.logDir, file), "utf-8");
2651
2766
  const records = content.split("\n").filter((line) => line.trim()).map((line) => {
2652
2767
  try {
2653
2768
  const parsed = JSON.parse(line);
@@ -2676,7 +2791,7 @@ import { request as httpRequest } from "http";
2676
2791
  import { URL } from "url";
2677
2792
  import { lookup } from "dns/promises";
2678
2793
  function readBody(req, maxBytes = 0) {
2679
- return new Promise((resolve8, reject) => {
2794
+ return new Promise((resolve12, reject) => {
2680
2795
  const chunks = [];
2681
2796
  let totalBytes = 0;
2682
2797
  req.on("data", (chunk) => {
@@ -2688,7 +2803,7 @@ function readBody(req, maxBytes = 0) {
2688
2803
  }
2689
2804
  chunks.push(chunk);
2690
2805
  });
2691
- req.on("end", () => resolve8(Buffer.concat(chunks).toString()));
2806
+ req.on("end", () => resolve12(Buffer.concat(chunks).toString()));
2692
2807
  req.on("error", reject);
2693
2808
  });
2694
2809
  }
@@ -2904,18 +3019,18 @@ var init_server = __esm({
2904
3019
  this.analysisPromise = this.refreshAnalysis();
2905
3020
  }
2906
3021
  this.server = createServer((req, res) => this.handleRequest(req, res));
2907
- return new Promise((resolve8) => {
3022
+ return new Promise((resolve12) => {
2908
3023
  this.server.listen(this.config.port, this.config.host, () => {
2909
- resolve8();
3024
+ resolve12();
2910
3025
  });
2911
3026
  });
2912
3027
  }
2913
3028
  async stop() {
2914
- return new Promise((resolve8) => {
3029
+ return new Promise((resolve12) => {
2915
3030
  if (this.server) {
2916
- this.server.close(() => resolve8());
3031
+ this.server.close(() => resolve12());
2917
3032
  } else {
2918
- resolve8();
3033
+ resolve12();
2919
3034
  }
2920
3035
  });
2921
3036
  }
@@ -3097,7 +3212,7 @@ var init_server = __esm({
3097
3212
  if (value) forwardHeaders[key] = Array.isArray(value) ? value[0] : value;
3098
3213
  }
3099
3214
  forwardHeaders["content-length"] = Buffer.byteLength(body).toString();
3100
- return new Promise((resolve8, reject) => {
3215
+ return new Promise((resolve12, reject) => {
3101
3216
  const proxyReq = requester(
3102
3217
  {
3103
3218
  hostname: url.hostname,
@@ -3118,7 +3233,7 @@ var init_server = __esm({
3118
3233
  parsed,
3119
3234
  interceptResult,
3120
3235
  startTime
3121
- ).then(resolve8).catch(reject);
3236
+ ).then(resolve12).catch(reject);
3122
3237
  } else {
3123
3238
  this.handleBufferedResponse(
3124
3239
  proxyRes,
@@ -3127,7 +3242,7 @@ var init_server = __esm({
3127
3242
  parsed,
3128
3243
  interceptResult,
3129
3244
  startTime
3130
- ).then(resolve8).catch(reject);
3245
+ ).then(resolve12).catch(reject);
3131
3246
  }
3132
3247
  }
3133
3248
  );
@@ -3147,7 +3262,7 @@ var init_server = __esm({
3147
3262
  let inputTokens = 0;
3148
3263
  let outputTokens = 0;
3149
3264
  let sseBuffer = "";
3150
- return new Promise((resolve8) => {
3265
+ return new Promise((resolve12) => {
3151
3266
  proxyRes.on("data", (chunk) => {
3152
3267
  clientRes.write(chunk);
3153
3268
  sseBuffer += chunk.toString();
@@ -3208,17 +3323,17 @@ var init_server = __esm({
3208
3323
  latencyMs: Date.now() - startTime,
3209
3324
  stream: true
3210
3325
  });
3211
- resolve8();
3326
+ resolve12();
3212
3327
  });
3213
3328
  proxyRes.on("error", () => {
3214
3329
  clientRes.end();
3215
- resolve8();
3330
+ resolve12();
3216
3331
  });
3217
3332
  });
3218
3333
  }
3219
3334
  // ===== BUFFERED HANDLER =====
3220
3335
  async handleBufferedResponse(proxyRes, clientRes, provider, parsed, interceptResult, startTime) {
3221
- return new Promise((resolve8) => {
3336
+ return new Promise((resolve12) => {
3222
3337
  const chunks = [];
3223
3338
  proxyRes.on("data", (chunk) => chunks.push(chunk));
3224
3339
  proxyRes.on("end", () => {
@@ -3275,11 +3390,11 @@ var init_server = __esm({
3275
3390
  error: "response-parse-failed"
3276
3391
  });
3277
3392
  }
3278
- resolve8();
3393
+ resolve12();
3279
3394
  });
3280
3395
  proxyRes.on("error", () => {
3281
3396
  clientRes.end();
3282
- resolve8();
3397
+ resolve12();
3283
3398
  });
3284
3399
  });
3285
3400
  }
@@ -3297,7 +3412,7 @@ var init_server = __esm({
3297
3412
 
3298
3413
  // src/cli/score.ts
3299
3414
  init_analyzer();
3300
- import { resolve as resolve7, join as join8 } from "path";
3415
+ import { resolve as resolve11, join as join10 } from "path";
3301
3416
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync4, appendFileSync as appendFileSync2 } from "fs";
3302
3417
 
3303
3418
  // src/engine/score.ts
@@ -3605,9 +3720,9 @@ function renderBar(pct, width) {
3605
3720
  return "\u2588".repeat(filled) + "\u2591".repeat(empty);
3606
3721
  }
3607
3722
  function padCenter(str, width) {
3608
- const pad2 = Math.max(0, width - str.length);
3609
- const left = Math.floor(pad2 / 2);
3610
- return " ".repeat(left) + str + " ".repeat(pad2 - left);
3723
+ const pad3 = Math.max(0, width - str.length);
3724
+ const left = Math.floor(pad3 / 2);
3725
+ return " ".repeat(left) + str + " ".repeat(pad3 - left);
3611
3726
  }
3612
3727
  function formatNumber(n) {
3613
3728
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
@@ -3775,8 +3890,8 @@ init_secrets();
3775
3890
 
3776
3891
  // src/engine/monorepo.ts
3777
3892
  import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
3778
- import { join as join5, relative as relative4, basename as basename3 } from "path";
3779
- import { existsSync as existsSync4 } from "fs";
3893
+ import { join as join4, relative as relative4, basename as basename3 } from "path";
3894
+ import { existsSync as existsSync3 } from "fs";
3780
3895
  async function detectMonorepoTool(rootPath) {
3781
3896
  const checks = [
3782
3897
  { file: "nx.json", tool: "nx" },
@@ -3797,14 +3912,14 @@ async function detectMonorepoTool(rootPath) {
3797
3912
  }
3798
3913
  ];
3799
3914
  for (const check of checks) {
3800
- const filePath = join5(rootPath, check.file);
3801
- if (existsSync4(filePath)) {
3915
+ const filePath = join4(rootPath, check.file);
3916
+ if (existsSync3(filePath)) {
3802
3917
  if (!check.validate) return check.tool;
3803
3918
  try {
3804
3919
  const content = await readFile5(filePath, "utf-8");
3805
3920
  if (check.validate(content)) {
3806
3921
  if (check.tool === "npm-workspaces") {
3807
- if (existsSync4(join5(rootPath, "yarn.lock"))) return "yarn-workspaces";
3922
+ if (existsSync3(join4(rootPath, "yarn.lock"))) return "yarn-workspaces";
3808
3923
  return "npm-workspaces";
3809
3924
  }
3810
3925
  return check.tool;
@@ -3819,15 +3934,15 @@ async function resolveWorkspaceGlobs(rootPath, globs) {
3819
3934
  const packagePaths = [];
3820
3935
  for (const glob of globs) {
3821
3936
  const cleanGlob = glob.replace(/\/?\*\*?$/, "");
3822
- const searchDir = join5(rootPath, cleanGlob);
3823
- if (!existsSync4(searchDir)) continue;
3937
+ const searchDir = join4(rootPath, cleanGlob);
3938
+ if (!existsSync3(searchDir)) continue;
3824
3939
  try {
3825
3940
  const entries = await readdir2(searchDir, { withFileTypes: true });
3826
3941
  for (const entry of entries) {
3827
3942
  if (!entry.isDirectory()) continue;
3828
- const pkgJsonPath = join5(searchDir, entry.name, "package.json");
3829
- if (existsSync4(pkgJsonPath)) {
3830
- packagePaths.push(join5(searchDir, entry.name));
3943
+ const pkgJsonPath = join4(searchDir, entry.name, "package.json");
3944
+ if (existsSync3(pkgJsonPath)) {
3945
+ packagePaths.push(join4(searchDir, entry.name));
3831
3946
  }
3832
3947
  }
3833
3948
  } catch {
@@ -3839,12 +3954,12 @@ async function discoverPackages(rootPath, tool) {
3839
3954
  switch (tool) {
3840
3955
  case "npm-workspaces":
3841
3956
  case "yarn-workspaces": {
3842
- const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
3957
+ const pkgJson = JSON.parse(await readFile5(join4(rootPath, "package.json"), "utf-8"));
3843
3958
  const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
3844
3959
  return resolveWorkspaceGlobs(rootPath, workspaces);
3845
3960
  }
3846
3961
  case "pnpm-workspaces": {
3847
- const content = await readFile5(join5(rootPath, "pnpm-workspace.yaml"), "utf-8");
3962
+ const content = await readFile5(join4(rootPath, "pnpm-workspace.yaml"), "utf-8");
3848
3963
  const packages = [];
3849
3964
  let inPackages = false;
3850
3965
  for (const line of content.split("\n")) {
@@ -3862,20 +3977,20 @@ async function discoverPackages(rootPath, tool) {
3862
3977
  return resolveWorkspaceGlobs(rootPath, packages);
3863
3978
  }
3864
3979
  case "turborepo": {
3865
- const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
3980
+ const pkgJson = JSON.parse(await readFile5(join4(rootPath, "package.json"), "utf-8"));
3866
3981
  const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
3867
3982
  if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
3868
- if (existsSync4(join5(rootPath, "pnpm-workspace.yaml"))) {
3983
+ if (existsSync3(join4(rootPath, "pnpm-workspace.yaml"))) {
3869
3984
  return discoverPackages(rootPath, "pnpm-workspaces");
3870
3985
  }
3871
3986
  return [];
3872
3987
  }
3873
3988
  case "nx": {
3874
3989
  const standardDirs = ["packages", "apps", "libs"];
3875
- const globs = standardDirs.filter((d) => existsSync4(join5(rootPath, d)));
3990
+ const globs = standardDirs.filter((d) => existsSync3(join4(rootPath, d)));
3876
3991
  if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
3877
3992
  try {
3878
- const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
3993
+ const pkgJson = JSON.parse(await readFile5(join4(rootPath, "package.json"), "utf-8"));
3879
3994
  const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
3880
3995
  if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
3881
3996
  } catch {
@@ -3883,7 +3998,7 @@ async function discoverPackages(rootPath, tool) {
3883
3998
  return [];
3884
3999
  }
3885
4000
  case "lerna": {
3886
- const lernaJson = JSON.parse(await readFile5(join5(rootPath, "lerna.json"), "utf-8"));
4001
+ const lernaJson = JSON.parse(await readFile5(join4(rootPath, "lerna.json"), "utf-8"));
3887
4002
  const packages = lernaJson.packages || ["packages/*"];
3888
4003
  return resolveWorkspaceGlobs(rootPath, packages);
3889
4004
  }
@@ -3937,7 +4052,7 @@ async function analyzeMonorepo(rootPath, analysis) {
3937
4052
  const packages = [];
3938
4053
  const packageTokenMap = {};
3939
4054
  for (const pkgPath of packagePaths) {
3940
- const pkgJsonPath = join5(pkgPath, "package.json");
4055
+ const pkgJsonPath = join4(pkgPath, "package.json");
3941
4056
  let name = basename3(pkgPath);
3942
4057
  let pkgDeps = [];
3943
4058
  try {
@@ -3980,7 +4095,7 @@ async function analyzeMonorepo(rootPath, analysis) {
3980
4095
  }
3981
4096
  const pkgNames = new Set(packages.map((p) => p.name));
3982
4097
  for (const pkg of packages) {
3983
- const pkgJsonPath = join5(pkg.path, "package.json");
4098
+ const pkgJsonPath = join4(pkg.path, "package.json");
3984
4099
  try {
3985
4100
  const pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf-8"));
3986
4101
  const allDeps = {
@@ -4134,7 +4249,7 @@ function renderPackageContext(result) {
4134
4249
  // src/engine/quality-gate.ts
4135
4250
  import { readFile as readFile6, writeFile as writeFile2, mkdir } from "fs/promises";
4136
4251
  import { resolve as resolve5 } from "path";
4137
- import { existsSync as existsSync5 } from "fs";
4252
+ import { existsSync as existsSync4 } from "fs";
4138
4253
  var DEFAULT_GATE_CONFIG = {
4139
4254
  threshold: 70,
4140
4255
  failOnSecrets: true,
@@ -4145,7 +4260,7 @@ var DEFAULT_GATE_CONFIG = {
4145
4260
  };
4146
4261
  async function loadBaseline(projectPath, baselinePath) {
4147
4262
  const filePath = resolve5(projectPath, baselinePath || ".cto/baseline.json");
4148
- if (!existsSync5(filePath)) return null;
4263
+ if (!existsSync4(filePath)) return null;
4149
4264
  try {
4150
4265
  const content = await readFile6(filePath, "utf-8");
4151
4266
  return JSON.parse(content);
@@ -4155,7 +4270,7 @@ async function loadBaseline(projectPath, baselinePath) {
4155
4270
  }
4156
4271
  async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
4157
4272
  const dir = resolve5(projectPath, ".cto");
4158
- if (!existsSync5(dir)) await mkdir(dir, { recursive: true });
4273
+ if (!existsSync4(dir)) await mkdir(dir, { recursive: true });
4159
4274
  const baseline = {
4160
4275
  score: score.overall,
4161
4276
  grade: score.grade,
@@ -4303,6 +4418,914 @@ Warnings: ${warnings.map((c) => c.name).join(", ")}`;
4303
4418
  return summary;
4304
4419
  }
4305
4420
 
4421
+ // src/engine/feedback.ts
4422
+ import { resolve as resolve6, join as join6 } from "path";
4423
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
4424
+
4425
+ // src/interact/router.ts
4426
+ var TASK_KEYWORDS = {
4427
+ debug: ["debug", "fix", "bug", "error", "issue", "broken", "crash", "failing", "wrong"],
4428
+ review: ["review", "check", "assess", "evaluate", "audit", "inspect", "critique"],
4429
+ refactor: ["refactor", "restructure", "reorganize", "clean up", "simplify", "extract", "move"],
4430
+ test: ["test", "spec", "coverage", "unit test", "integration test", "e2e"],
4431
+ docs: ["document", "docs", "readme", "jsdoc", "comment", "explain"],
4432
+ feature: ["add", "implement", "create", "build", "new", "feature", "endpoint"],
4433
+ architecture: ["architecture", "design", "system", "structure", "migrate", "pattern"],
4434
+ "simple-edit": ["rename", "typo", "update", "change", "modify", "tweak", "adjust"]
4435
+ };
4436
+ function classifyTask(taskDescription) {
4437
+ const lower = taskDescription.toLowerCase();
4438
+ let bestType = "simple-edit";
4439
+ let bestScore = 0;
4440
+ for (const [type, keywords] of Object.entries(TASK_KEYWORDS)) {
4441
+ let score = 0;
4442
+ for (const kw of keywords) {
4443
+ if (lower.includes(kw)) score++;
4444
+ }
4445
+ if (score > bestScore) {
4446
+ bestScore = score;
4447
+ bestType = type;
4448
+ }
4449
+ }
4450
+ return bestType;
4451
+ }
4452
+
4453
+ // src/engine/feedback.ts
4454
+ async function getFeedbackPath(projectPath) {
4455
+ const ctoDir = join6(resolve6(projectPath), ".cto");
4456
+ await mkdir2(ctoDir, { recursive: true });
4457
+ return join6(ctoDir, "feedback.json");
4458
+ }
4459
+ async function getModelPath(projectPath) {
4460
+ const ctoDir = join6(resolve6(projectPath), ".cto");
4461
+ await mkdir2(ctoDir, { recursive: true });
4462
+ return join6(ctoDir, "feedback-model.json");
4463
+ }
4464
+ async function loadFeedback(projectPath) {
4465
+ try {
4466
+ const raw = await readFile7(await getFeedbackPath(projectPath), "utf-8");
4467
+ return JSON.parse(raw);
4468
+ } catch {
4469
+ return [];
4470
+ }
4471
+ }
4472
+ async function loadFeedbackModel(projectPath) {
4473
+ try {
4474
+ const raw = await readFile7(await getModelPath(projectPath), "utf-8");
4475
+ return JSON.parse(raw);
4476
+ } catch {
4477
+ return createEmptyModel();
4478
+ }
4479
+ }
4480
+ function createEmptyModel() {
4481
+ return {
4482
+ version: 2,
4483
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4484
+ totalFeedback: 0,
4485
+ acceptRate: 0,
4486
+ fileAcceptance: {},
4487
+ taskTypeAcceptance: {},
4488
+ pairAcceptance: {},
4489
+ sessions: {},
4490
+ strategyComparison: {},
4491
+ insights: []
4492
+ };
4493
+ }
4494
+ async function exportFeedbackForTeam(projectPath, projectName) {
4495
+ const model = await loadFeedbackModel(projectPath);
4496
+ const entries = await loadFeedback(projectPath);
4497
+ return {
4498
+ version: 2,
4499
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4500
+ projectName,
4501
+ model,
4502
+ entrySummary: {
4503
+ total: entries.length,
4504
+ accepted: entries.filter((e) => e.outcome.accepted).length,
4505
+ sessions: new Set(entries.map((e) => e.sessionId).filter(Boolean)).size
4506
+ }
4507
+ };
4508
+ }
4509
+ function renderFeedbackReport(model) {
4510
+ const lines = [];
4511
+ lines.push("");
4512
+ lines.push(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
4513
+ lines.push(` \u2551 \u{1F4CA} Feedback Loop Report \u2551`);
4514
+ lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
4515
+ lines.push(` \u2551 \u2551`);
4516
+ lines.push(` \u2551 Total feedback: ${pad2(model.totalFeedback.toString(), 8)} \u2551`);
4517
+ lines.push(` \u2551 Accept rate: ${pad2(Math.round(model.acceptRate * 100) + "%", 8)} \u2551`);
4518
+ lines.push(` \u2551 Tracked files: ${pad2(Object.keys(model.fileAcceptance).length.toString(), 8)} \u2551`);
4519
+ lines.push(` \u2551 Task types: ${pad2(Object.keys(model.taskTypeAcceptance).length.toString(), 8)} \u2551`);
4520
+ lines.push(` \u2551 \u2551`);
4521
+ if (model.insights.length > 0) {
4522
+ lines.push(` \u2551 Insights: \u2551`);
4523
+ for (const insight of model.insights.slice(0, 5)) {
4524
+ const icon = insight.type === "positive" ? "\u2705" : insight.type === "negative" ? "\u26A0\uFE0F" : "\u{1F4A1}";
4525
+ lines.push(` \u2551 ${icon} ${pad2(insight.title, 43)} \u2551`);
4526
+ }
4527
+ }
4528
+ lines.push(` \u2551 \u2551`);
4529
+ lines.push(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
4530
+ return lines.join("\n");
4531
+ }
4532
+ function pad2(s, w) {
4533
+ return s.padEnd(w).substring(0, w);
4534
+ }
4535
+ function renderCrossRepoReport(stats) {
4536
+ const lines = [];
4537
+ lines.push("");
4538
+ lines.push(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
4539
+ lines.push(" \u2551 \u{1F310} Cross-Repo Intelligence \u2551");
4540
+ lines.push(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
4541
+ lines.push(" \u2551 \u2551");
4542
+ lines.push(` \u2551 Projects learned: ${pad2(stats.totalProjects.toString(), 8)} \u2551`);
4543
+ lines.push(` \u2551 Total observations: ${pad2(stats.totalObservations.toString(), 8)} \u2551`);
4544
+ lines.push(` \u2551 Archetypes: ${pad2(stats.archetypes.length.toString(), 8)} \u2551`);
4545
+ lines.push(` \u2551 Universal patterns: ${pad2(stats.universalPatterns.toString(), 8)} \u2551`);
4546
+ lines.push(" \u2551 \u2551");
4547
+ if (stats.archetypes.length > 0) {
4548
+ lines.push(" \u2551 Archetypes: \u2551");
4549
+ for (const a of stats.archetypes.slice(0, 5)) {
4550
+ lines.push(` \u2551 ${pad2(a.name, 24)} ${pad2(a.observations + " obs", 12)} \u2551`);
4551
+ }
4552
+ }
4553
+ lines.push(" \u2551 \u2551");
4554
+ lines.push(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
4555
+ return lines.join("\n");
4556
+ }
4557
+
4558
+ // src/engine/predictor.ts
4559
+ import { resolve as resolve7, join as join7 } from "path";
4560
+ import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
4561
+ init_graph_utils();
4562
+ var DEFAULT_PREDICTOR_CONFIG = {
4563
+ maxCoSelectionPairs: 500,
4564
+ decayFactor: 0.95,
4565
+ // slight decay to favor recent patterns
4566
+ minObservations: 2
4567
+ // need at least 2 observations before predicting
4568
+ };
4569
+ async function getModelPath2(projectPath) {
4570
+ const ctoDir = join7(resolve7(projectPath), ".cto");
4571
+ await mkdir3(ctoDir, { recursive: true });
4572
+ return join7(ctoDir, "predictor.json");
4573
+ }
4574
+ async function loadModel(projectPath) {
4575
+ try {
4576
+ const path = await getModelPath2(projectPath);
4577
+ const raw = await readFile8(path, "utf-8");
4578
+ return JSON.parse(raw);
4579
+ } catch {
4580
+ return createEmptyModel2();
4581
+ }
4582
+ }
4583
+ function createEmptyModel2() {
4584
+ return {
4585
+ version: 1,
4586
+ trainedAt: (/* @__PURE__ */ new Date()).toISOString(),
4587
+ totalObservations: 0,
4588
+ taskTypeFrequency: {},
4589
+ keywordFrequency: {},
4590
+ fileStats: {},
4591
+ coSelection: {}
4592
+ };
4593
+ }
4594
+ async function predictRelevantFiles(projectPath, task, analysis, config = {}) {
4595
+ const cfg = { ...DEFAULT_PREDICTOR_CONFIG, ...config };
4596
+ const model = await loadModel(projectPath);
4597
+ if (model.totalObservations < cfg.minObservations) {
4598
+ return [];
4599
+ }
4600
+ const taskType = classifyTask(task);
4601
+ const keywords = extractKeywords(task);
4602
+ const scores = /* @__PURE__ */ new Map();
4603
+ const boost = (path, amount, reason) => {
4604
+ const existing = scores.get(path) ?? { score: 0, reasons: [] };
4605
+ existing.score += amount;
4606
+ existing.reasons.push(reason);
4607
+ scores.set(path, existing);
4608
+ };
4609
+ const taskFreqs = model.taskTypeFrequency[taskType];
4610
+ if (taskFreqs) {
4611
+ const maxFreq = Math.max(...Object.values(taskFreqs));
4612
+ for (const [path, freq] of Object.entries(taskFreqs)) {
4613
+ const normalized = maxFreq > 0 ? freq / maxFreq : 0;
4614
+ boost(path, normalized * 30, `${taskType} task history (${freq}\xD7 selected)`);
4615
+ }
4616
+ }
4617
+ for (const kw of keywords) {
4618
+ const kwFreqs = model.keywordFrequency[kw];
4619
+ if (kwFreqs) {
4620
+ const maxFreq = Math.max(...Object.values(kwFreqs));
4621
+ for (const [path, freq] of Object.entries(kwFreqs)) {
4622
+ const normalized = maxFreq > 0 ? freq / maxFreq : 0;
4623
+ boost(path, normalized * 20, `keyword "${kw}" (${freq}\xD7 selected)`);
4624
+ }
4625
+ }
4626
+ }
4627
+ for (const [path, stats] of Object.entries(model.fileStats)) {
4628
+ const freqRatio = model.totalObservations > 0 ? stats.totalSelections / model.totalObservations : 0;
4629
+ if (freqRatio > 0.3) {
4630
+ boost(path, freqRatio * 10, `frequently selected (${stats.totalSelections}/${model.totalObservations})`);
4631
+ }
4632
+ }
4633
+ const topPredicted = [...scores.entries()].sort((a, b) => b[1].score - a[1].score).slice(0, 10).map(([path]) => path);
4634
+ for (const topPath of topPredicted) {
4635
+ const coFiles = model.coSelection[topPath];
4636
+ if (coFiles) {
4637
+ for (const [coPath, count] of Object.entries(coFiles)) {
4638
+ if (count >= 2) {
4639
+ boost(coPath, count * 2, `co-selected with ${topPath} (${count}\xD7)`);
4640
+ }
4641
+ }
4642
+ }
4643
+ }
4644
+ const existingPaths = new Set(analysis.files.map((f) => f.relativePath));
4645
+ const results = [];
4646
+ for (const [path, data] of scores) {
4647
+ if (existingPaths.has(path)) {
4648
+ results.push({
4649
+ filePath: path,
4650
+ predictedScore: Math.round(data.score * 10) / 10,
4651
+ reasons: data.reasons.slice(0, 5)
4652
+ });
4653
+ }
4654
+ }
4655
+ return results.sort((a, b) => b.predictedScore - a.predictedScore).slice(0, 50);
4656
+ }
4657
+ function getModelStats(model) {
4658
+ const coSelectionPairs = Object.values(model.coSelection).reduce((s, pairs) => s + Object.keys(pairs).length, 0) / 2;
4659
+ return {
4660
+ observations: model.totalObservations,
4661
+ taskTypes: Object.keys(model.taskTypeFrequency).length,
4662
+ keywords: Object.keys(model.keywordFrequency).length,
4663
+ trackedFiles: Object.keys(model.fileStats).length,
4664
+ coSelectionPairs: Math.round(coSelectionPairs),
4665
+ trainedAt: model.trainedAt
4666
+ };
4667
+ }
4668
+ function extractKeywords(task) {
4669
+ const stopWords = /* @__PURE__ */ new Set([
4670
+ "the",
4671
+ "a",
4672
+ "an",
4673
+ "is",
4674
+ "are",
4675
+ "was",
4676
+ "were",
4677
+ "be",
4678
+ "been",
4679
+ "being",
4680
+ "have",
4681
+ "has",
4682
+ "had",
4683
+ "do",
4684
+ "does",
4685
+ "did",
4686
+ "will",
4687
+ "would",
4688
+ "could",
4689
+ "should",
4690
+ "may",
4691
+ "might",
4692
+ "can",
4693
+ "shall",
4694
+ "to",
4695
+ "of",
4696
+ "in",
4697
+ "for",
4698
+ "on",
4699
+ "with",
4700
+ "at",
4701
+ "by",
4702
+ "from",
4703
+ "as",
4704
+ "into",
4705
+ "through",
4706
+ "and",
4707
+ "but",
4708
+ "or",
4709
+ "not",
4710
+ "this",
4711
+ "that",
4712
+ "it",
4713
+ "its",
4714
+ "fix",
4715
+ "add",
4716
+ "remove",
4717
+ "update",
4718
+ "change",
4719
+ "refactor",
4720
+ "implement",
4721
+ "create",
4722
+ "build",
4723
+ "make",
4724
+ "code",
4725
+ "file",
4726
+ "module",
4727
+ "function"
4728
+ ]);
4729
+ return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
4730
+ }
4731
+
4732
+ // src/engine/cross-repo.ts
4733
+ import { join as join8, basename as basename4 } from "path";
4734
+ import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
4735
+ function getGlobalModelPath() {
4736
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
4737
+ return join8(home, ".cto", "global-intelligence.json");
4738
+ }
4739
+ async function loadGlobalModel() {
4740
+ try {
4741
+ const raw = await readFile9(getGlobalModelPath(), "utf-8");
4742
+ return JSON.parse(raw);
4743
+ } catch {
4744
+ return createEmptyModel3();
4745
+ }
4746
+ }
4747
+ function createEmptyModel3() {
4748
+ return {
4749
+ version: 1,
4750
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4751
+ totalProjects: 0,
4752
+ totalObservations: 0,
4753
+ archetypes: {},
4754
+ universalPatterns: []
4755
+ };
4756
+ }
4757
+ function getCrossRepoStats(model) {
4758
+ return {
4759
+ totalProjects: model.totalProjects,
4760
+ totalObservations: model.totalObservations,
4761
+ archetypes: Object.values(model.archetypes).map((a) => ({
4762
+ name: a.name,
4763
+ projects: a.projectCount,
4764
+ observations: a.observationCount
4765
+ })),
4766
+ universalPatterns: model.universalPatterns.length
4767
+ };
4768
+ }
4769
+
4770
+ // src/engine/code-review.ts
4771
+ init_graph_utils();
4772
+ import { resolve as resolve9, basename as basename5, dirname as dirname4 } from "path";
4773
+ import { readFile as readFile10 } from "fs/promises";
4774
+ import { execFile } from "child_process";
4775
+ import { promisify } from "util";
4776
+ var exec = promisify(execFile);
4777
+ async function git(args, cwd) {
4778
+ try {
4779
+ const { stdout } = await exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
4780
+ return stdout.trim();
4781
+ } catch {
4782
+ return "";
4783
+ }
4784
+ }
4785
+ async function analyzeForReview(analysis, options = {}) {
4786
+ const projectPath = resolve9(analysis.projectPath);
4787
+ const baseBranch = options.baseBranch ?? "main";
4788
+ const depth = options.depth ?? 2;
4789
+ const maxPromptFiles = options.maxPromptFiles ?? 20;
4790
+ const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath) === "true";
4791
+ if (!isRepo) {
4792
+ return emptyResult2(baseBranch);
4793
+ }
4794
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
4795
+ const changedFiles = await getChangedFilesWithHunks(projectPath, baseBranch, analysis);
4796
+ if (changedFiles.length === 0) {
4797
+ return {
4798
+ ...emptyResult2(baseBranch),
4799
+ branch,
4800
+ isGitRepo: true,
4801
+ renderedSummary: "# Code Review\n\nNo changed files detected."
4802
+ };
4803
+ }
4804
+ const totalLinesChanged = changedFiles.reduce((s, f) => s + f.linesAdded + f.linesRemoved, 0);
4805
+ const breakingChanges = detectBreakingChanges(changedFiles, analysis);
4806
+ const missingFiles = findMissingFiles(changedFiles, analysis, depth);
4807
+ const impactRadius = computeImpactRadius(changedFiles, analysis, depth);
4808
+ const reviewQuality = calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged);
4809
+ const reviewPrompt = await generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxPromptFiles);
4810
+ const renderedSummary = renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality);
4811
+ return {
4812
+ branch,
4813
+ baseBranch,
4814
+ isGitRepo: true,
4815
+ changedFiles,
4816
+ totalLinesChanged,
4817
+ breakingChanges,
4818
+ missingFiles,
4819
+ impactRadius,
4820
+ reviewQuality,
4821
+ reviewPrompt,
4822
+ renderedSummary
4823
+ };
4824
+ }
4825
+ async function getChangedFilesWithHunks(projectPath, baseBranch, analysis) {
4826
+ const files = [];
4827
+ const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
4828
+ const numstat = await git(["diff", "--numstat", "HEAD"], projectPath);
4829
+ const branchNumstat = await git(["diff", "--numstat", `${baseBranch}...HEAD`], projectPath);
4830
+ const nameStatus = await git(["diff", "--name-status", `${baseBranch}...HEAD`], projectPath);
4831
+ const changeTypes = /* @__PURE__ */ new Map();
4832
+ for (const line of nameStatus.split("\n")) {
4833
+ const parts = line.trim().split(" ");
4834
+ if (parts.length < 2) continue;
4835
+ const status = parts[0];
4836
+ const filePath = parts[parts.length - 1];
4837
+ if (status === "A") changeTypes.set(filePath, "added");
4838
+ else if (status === "D") changeTypes.set(filePath, "deleted");
4839
+ else if (status.startsWith("R")) changeTypes.set(filePath, "renamed");
4840
+ else changeTypes.set(filePath, "modified");
4841
+ }
4842
+ const allNumstat = (numstat + "\n" + branchNumstat).split("\n");
4843
+ const seen = /* @__PURE__ */ new Set();
4844
+ for (const line of allNumstat) {
4845
+ const parts = line.trim().split(" ");
4846
+ if (parts.length < 3) continue;
4847
+ const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
4848
+ const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
4849
+ const filePath = parts[2];
4850
+ if (!filePath || seen.has(filePath)) continue;
4851
+ seen.add(filePath);
4852
+ const af = fileMap.get(filePath);
4853
+ const changeType = changeTypes.get(filePath) ?? "modified";
4854
+ const hunks = await parseDiffHunks(projectPath, baseBranch, filePath);
4855
+ const hasExportChanges = hunks.some(
4856
+ (h) => h.additions.some((l) => /^\s*export\s/.test(l)) || h.deletions.some((l) => /^\s*export\s/.test(l))
4857
+ );
4858
+ const hasTypeChanges = hunks.some(
4859
+ (h) => h.additions.some((l) => /^\s*(interface|type|enum)\s/.test(l)) || h.deletions.some((l) => /^\s*(interface|type|enum)\s/.test(l))
4860
+ );
4861
+ files.push({
4862
+ relativePath: filePath,
4863
+ changeType,
4864
+ linesAdded: added,
4865
+ linesRemoved: removed,
4866
+ riskScore: af?.riskScore ?? 0,
4867
+ kind: af?.kind ?? "unknown",
4868
+ hunks,
4869
+ hasExportChanges,
4870
+ hasTypeChanges
4871
+ });
4872
+ }
4873
+ const workingNumstat = await git(["diff", "--numstat"], projectPath);
4874
+ for (const line of workingNumstat.split("\n")) {
4875
+ const parts = line.trim().split(" ");
4876
+ if (parts.length < 3) continue;
4877
+ const filePath = parts[2];
4878
+ if (!filePath || seen.has(filePath)) continue;
4879
+ seen.add(filePath);
4880
+ const af = fileMap.get(filePath);
4881
+ const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
4882
+ const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
4883
+ files.push({
4884
+ relativePath: filePath,
4885
+ changeType: "modified",
4886
+ linesAdded: added,
4887
+ linesRemoved: removed,
4888
+ riskScore: af?.riskScore ?? 0,
4889
+ kind: af?.kind ?? "unknown",
4890
+ hunks: [],
4891
+ hasExportChanges: false,
4892
+ hasTypeChanges: false
4893
+ });
4894
+ }
4895
+ return files.sort((a, b) => b.riskScore - a.riskScore);
4896
+ }
4897
+ async function parseDiffHunks(projectPath, baseBranch, filePath) {
4898
+ const diff = await git(["diff", "-U3", `${baseBranch}...HEAD`, "--", filePath], projectPath);
4899
+ if (!diff) return [];
4900
+ const hunks = [];
4901
+ const lines = diff.split("\n");
4902
+ let currentHunk = null;
4903
+ for (const line of lines) {
4904
+ const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@\s*(.*)/);
4905
+ if (hunkMatch) {
4906
+ if (currentHunk) hunks.push(currentHunk);
4907
+ currentHunk = {
4908
+ startLine: parseInt(hunkMatch[2], 10),
4909
+ endLine: parseInt(hunkMatch[2], 10),
4910
+ header: hunkMatch[3] || "",
4911
+ additions: [],
4912
+ deletions: []
4913
+ };
4914
+ continue;
4915
+ }
4916
+ if (!currentHunk) continue;
4917
+ if (line.startsWith("+") && !line.startsWith("+++")) {
4918
+ currentHunk.additions.push(line.substring(1));
4919
+ currentHunk.endLine++;
4920
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
4921
+ currentHunk.deletions.push(line.substring(1));
4922
+ } else if (!line.startsWith("\\")) {
4923
+ if (currentHunk) currentHunk.endLine++;
4924
+ }
4925
+ }
4926
+ if (currentHunk) hunks.push(currentHunk);
4927
+ return hunks;
4928
+ }
4929
+ function detectBreakingChanges(changedFiles, analysis) {
4930
+ const breaks = [];
4931
+ const adj = buildAdjacencyList(analysis.graph.edges);
4932
+ for (const file of changedFiles) {
4933
+ if (file.changeType === "deleted") {
4934
+ const dependents = findDependents(file.relativePath, analysis);
4935
+ if (dependents.length > 0) {
4936
+ breaks.push({
4937
+ file: file.relativePath,
4938
+ type: "export-removed",
4939
+ severity: "critical",
4940
+ description: `File deleted but ${dependents.length} files depend on it`,
4941
+ affectedFiles: dependents
4942
+ });
4943
+ }
4944
+ continue;
4945
+ }
4946
+ for (const hunk of file.hunks) {
4947
+ for (const del of hunk.deletions) {
4948
+ const exportMatch = del.match(/^\s*export\s+(function|const|class|type|interface|enum)\s+(\w+)/);
4949
+ if (exportMatch) {
4950
+ const [, kind, name] = exportMatch;
4951
+ const wasReAdded = hunk.additions.some(
4952
+ (a) => new RegExp(`export\\s+${kind}\\s+${name}\\b`).test(a)
4953
+ );
4954
+ if (!wasReAdded) {
4955
+ const dependents = findDependents(file.relativePath, analysis);
4956
+ breaks.push({
4957
+ file: file.relativePath,
4958
+ type: kind === "type" || kind === "interface" ? "type-changed" : "export-removed",
4959
+ severity: dependents.length > 3 ? "critical" : dependents.length > 0 ? "high" : "medium",
4960
+ description: `Removed export ${kind} ${name}`,
4961
+ affectedFiles: dependents
4962
+ });
4963
+ }
4964
+ }
4965
+ const propMatch = del.match(/^\s+(\w+)\s*[?:].*[;,]?\s*$/);
4966
+ if (propMatch && file.hasTypeChanges) {
4967
+ const propName = propMatch[1];
4968
+ const wasReAdded = hunk.additions.some(
4969
+ (a) => new RegExp(`\\b${propName}\\s*[?:]`).test(a)
4970
+ );
4971
+ if (!wasReAdded) {
4972
+ breaks.push({
4973
+ file: file.relativePath,
4974
+ type: "interface-changed",
4975
+ severity: "high",
4976
+ description: `Removed property "${propName}" from type/interface`,
4977
+ affectedFiles: findDependents(file.relativePath, analysis)
4978
+ });
4979
+ }
4980
+ }
4981
+ }
4982
+ for (const add of hunk.additions) {
4983
+ const fnMatch = add.match(/^\s*export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
4984
+ if (fnMatch) {
4985
+ const [, , fnName, newParams] = fnMatch;
4986
+ for (const del of hunk.deletions) {
4987
+ const origMatch = del.match(new RegExp(`export\\s+(async\\s+)?function\\s+${fnName}\\s*\\(([^)]*)\\)`));
4988
+ if (origMatch) {
4989
+ const oldParams = origMatch[2];
4990
+ if (oldParams !== newParams) {
4991
+ const oldCount = oldParams.split(",").filter((p) => p.trim()).length;
4992
+ const newCount = newParams.split(",").filter((p) => p.trim()).length;
4993
+ if (oldCount !== newCount || !paramsCompatible(oldParams, newParams)) {
4994
+ breaks.push({
4995
+ file: file.relativePath,
4996
+ type: "function-signature",
4997
+ severity: "high",
4998
+ description: `Function "${fnName}" signature changed: (${oldParams.trim()}) \u2192 (${newParams.trim()})`,
4999
+ affectedFiles: findDependents(file.relativePath, analysis)
5000
+ });
5001
+ }
5002
+ }
5003
+ }
5004
+ }
5005
+ }
5006
+ }
5007
+ }
5008
+ }
5009
+ return breaks.sort((a, b) => {
5010
+ const sev = { critical: 0, high: 1, medium: 2 };
5011
+ return sev[a.severity] - sev[b.severity];
5012
+ });
5013
+ }
5014
+ function paramsCompatible(oldParams, newParams) {
5015
+ const oldParts = oldParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
5016
+ const newParts = newParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
5017
+ let j = 0;
5018
+ for (let i = 0; i < oldParts.length && j < newParts.length; i++) {
5019
+ if (oldParts[i] === newParts[j]) j++;
5020
+ }
5021
+ return j >= oldParts.length;
5022
+ }
5023
+ function findDependents(filePath, analysis) {
5024
+ return analysis.files.filter((f) => f.imports.includes(filePath) || f.imports.some((imp) => imp.endsWith(filePath))).map((f) => f.relativePath);
5025
+ }
5026
+ function findMissingFiles(changedFiles, analysis, depth) {
5027
+ const missing = [];
5028
+ const changedPaths = new Set(changedFiles.map((f) => f.relativePath));
5029
+ const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
5030
+ for (const changed of changedFiles) {
5031
+ if (changed.changeType === "deleted") continue;
5032
+ const af = fileMap.get(changed.relativePath);
5033
+ if (!af) continue;
5034
+ if (changed.hasTypeChanges || changed.hasExportChanges) {
5035
+ const dir2 = dirname4(changed.relativePath);
5036
+ const base = basename5(changed.relativePath).replace(/\.[^.]+$/, "");
5037
+ const typeVariants = [
5038
+ `${dir2}/${base}.types.ts`,
5039
+ `${dir2}/${base}.types.tsx`,
5040
+ `${dir2}/types.ts`,
5041
+ `${dir2}/types/${base}.ts`,
5042
+ `${dir2}/index.d.ts`
5043
+ ];
5044
+ for (const variant of typeVariants) {
5045
+ if (fileMap.has(variant) && !changedPaths.has(variant)) {
5046
+ missing.push({
5047
+ file: variant,
5048
+ reason: `Type file for ${changed.relativePath} \u2014 may need updates`,
5049
+ severity: changed.hasExportChanges ? "high" : "medium",
5050
+ relatedChangedFile: changed.relativePath,
5051
+ relationship: "sibling-type"
5052
+ });
5053
+ }
5054
+ }
5055
+ }
5056
+ const testVariants = [
5057
+ changed.relativePath.replace(/\.([^.]+)$/, ".test.$1"),
5058
+ changed.relativePath.replace(/\.([^.]+)$/, ".spec.$1"),
5059
+ changed.relativePath.replace(/^src\//, "tests/").replace(/\.([^.]+)$/, ".test.$1"),
5060
+ changed.relativePath.replace(/^src\//, "__tests__/").replace(/\.([^.]+)$/, ".test.$1")
5061
+ ];
5062
+ for (const testPath of testVariants) {
5063
+ if (fileMap.has(testPath) && !changedPaths.has(testPath)) {
5064
+ missing.push({
5065
+ file: testPath,
5066
+ reason: `Test file for ${changed.relativePath} \u2014 should be updated`,
5067
+ severity: "medium",
5068
+ relatedChangedFile: changed.relativePath,
5069
+ relationship: "test"
5070
+ });
5071
+ break;
5072
+ }
5073
+ }
5074
+ if (changed.hasExportChanges) {
5075
+ const importers = analysis.files.filter(
5076
+ (f) => f.imports.includes(af.relativePath) && !changedPaths.has(f.relativePath)
5077
+ );
5078
+ for (const importer of importers.slice(0, 5)) {
5079
+ missing.push({
5080
+ file: importer.relativePath,
5081
+ reason: `Imports ${changed.relativePath} which has export changes`,
5082
+ severity: "high",
5083
+ relatedChangedFile: changed.relativePath,
5084
+ relationship: "imported-by"
5085
+ });
5086
+ }
5087
+ }
5088
+ const dir = dirname4(changed.relativePath);
5089
+ const indexFile = `${dir}/index.ts`;
5090
+ if (fileMap.has(indexFile) && !changedPaths.has(indexFile) && changed.hasExportChanges) {
5091
+ missing.push({
5092
+ file: indexFile,
5093
+ reason: `Barrel export may need updating after changes to ${changed.relativePath}`,
5094
+ severity: "medium",
5095
+ relatedChangedFile: changed.relativePath,
5096
+ relationship: "co-located"
5097
+ });
5098
+ }
5099
+ }
5100
+ const seen = /* @__PURE__ */ new Set();
5101
+ return missing.filter((m) => {
5102
+ if (seen.has(m.file)) return false;
5103
+ seen.add(m.file);
5104
+ return true;
5105
+ }).sort((a, b) => {
5106
+ const sev = { high: 0, medium: 1, low: 2 };
5107
+ return sev[a.severity] - sev[b.severity];
5108
+ });
5109
+ }
5110
+ function computeImpactRadius(changedFiles, analysis, depth) {
5111
+ const changedPaths = changedFiles.map((f) => f.relativePath);
5112
+ const adj = buildAdjacencyList(analysis.graph.edges);
5113
+ const direct = /* @__PURE__ */ new Set();
5114
+ for (const path of changedPaths) {
5115
+ const importers = adj.reverse.get(path) ?? [];
5116
+ const imports = adj.forward.get(path) ?? [];
5117
+ for (const n of [...importers, ...imports]) {
5118
+ if (!changedPaths.includes(n)) direct.add(n);
5119
+ }
5120
+ }
5121
+ const allAffected = bfsBidirectional(changedPaths, adj, depth);
5122
+ const transitive = /* @__PURE__ */ new Set();
5123
+ for (const path of allAffected) {
5124
+ if (!changedPaths.includes(path) && !direct.has(path)) {
5125
+ transitive.add(path);
5126
+ }
5127
+ }
5128
+ const affectedTests = [...allAffected].filter((p) => {
5129
+ const f = analysis.files.find((af) => af.relativePath === p);
5130
+ return f?.kind === "test";
5131
+ }).length;
5132
+ const hotspots = changedFiles.map((f) => ({
5133
+ file: f.relativePath,
5134
+ dependents: adj.reverse.get(f.relativePath)?.length ?? 0,
5135
+ riskScore: f.riskScore
5136
+ })).sort((a, b) => b.dependents * b.riskScore - a.dependents * a.riskScore).slice(0, 5);
5137
+ const totalAffected = direct.size + transitive.size;
5138
+ const maxRisk = Math.max(...changedFiles.map((f) => f.riskScore), 0);
5139
+ const avgRisk = changedFiles.length > 0 ? changedFiles.reduce((s, f) => s + f.riskScore, 0) / changedFiles.length : 0;
5140
+ const riskScore = Math.min(100, Math.round(
5141
+ avgRisk * 0.3 + maxRisk * 0.2 + Math.min(100, totalAffected * 3) * 0.3 + Math.min(100, changedFiles.length * 5) * 0.2
5142
+ ));
5143
+ return {
5144
+ directlyAffected: direct.size,
5145
+ transitivelyAffected: transitive.size,
5146
+ totalAffected,
5147
+ affectedTests,
5148
+ riskScore,
5149
+ hotspots
5150
+ };
5151
+ }
5152
+ function calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged) {
5153
+ const factors = [];
5154
+ const sizeScore = totalLinesChanged <= 50 ? 100 : totalLinesChanged <= 200 ? 85 : totalLinesChanged <= 500 ? 65 : totalLinesChanged <= 1e3 ? 40 : 20;
5155
+ factors.push({
5156
+ name: "PR Size",
5157
+ score: sizeScore,
5158
+ weight: 0.25,
5159
+ detail: `${totalLinesChanged} lines changed \u2014 ${sizeScore >= 80 ? "easy" : sizeScore >= 50 ? "manageable" : "too large"} to review`
5160
+ });
5161
+ const focusScore = changedFiles.length <= 3 ? 100 : changedFiles.length <= 8 ? 80 : changedFiles.length <= 15 ? 55 : 25;
5162
+ factors.push({
5163
+ name: "Focus",
5164
+ score: focusScore,
5165
+ weight: 0.2,
5166
+ detail: `${changedFiles.length} files \u2014 ${focusScore >= 80 ? "focused" : focusScore >= 50 ? "moderate scope" : "unfocused"}`
5167
+ });
5168
+ const criticalBreaks = breakingChanges.filter((b) => b.severity === "critical").length;
5169
+ const highBreaks = breakingChanges.filter((b) => b.severity === "high").length;
5170
+ const breakScore = criticalBreaks > 0 ? 10 : highBreaks > 2 ? 30 : highBreaks > 0 ? 60 : breakingChanges.length > 0 ? 80 : 100;
5171
+ factors.push({
5172
+ name: "Breaking Changes",
5173
+ score: breakScore,
5174
+ weight: 0.25,
5175
+ detail: `${breakingChanges.length} breaking changes (${criticalBreaks} critical, ${highBreaks} high)`
5176
+ });
5177
+ const highMissing = missingFiles.filter((m) => m.severity === "high").length;
5178
+ const completenessScore = highMissing > 3 ? 20 : highMissing > 0 ? 50 : missingFiles.length > 3 ? 65 : missingFiles.length > 0 ? 80 : 100;
5179
+ factors.push({
5180
+ name: "Completeness",
5181
+ score: completenessScore,
5182
+ weight: 0.15,
5183
+ detail: `${missingFiles.length} potentially missing files (${highMissing} high priority)`
5184
+ });
5185
+ const radiusScore = impactRadius.totalAffected <= 5 ? 100 : impactRadius.totalAffected <= 15 ? 75 : impactRadius.totalAffected <= 30 ? 50 : 25;
5186
+ factors.push({
5187
+ name: "Blast Radius",
5188
+ score: radiusScore,
5189
+ weight: 0.15,
5190
+ detail: `${impactRadius.totalAffected} files affected (${impactRadius.directlyAffected} direct, ${impactRadius.transitivelyAffected} transitive)`
5191
+ });
5192
+ const overall = Math.round(factors.reduce((s, f) => s + f.score * f.weight, 0));
5193
+ const grade = overall >= 95 ? "A+" : overall >= 90 ? "A" : overall >= 85 ? "A-" : overall >= 80 ? "B+" : overall >= 75 ? "B" : overall >= 70 ? "B-" : overall >= 65 ? "C+" : overall >= 60 ? "C" : overall >= 55 ? "C-" : overall >= 50 ? "D+" : overall >= 45 ? "D" : overall >= 40 ? "D-" : "F";
5194
+ return { score: overall, grade, factors };
5195
+ }
5196
+ async function generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxFiles) {
5197
+ const lines = [];
5198
+ lines.push("# Code Review Context");
5199
+ lines.push("");
5200
+ lines.push("## Project: " + analysis.projectName);
5201
+ lines.push("## Stack: " + analysis.stack.join(", "));
5202
+ lines.push("");
5203
+ if (breakingChanges.length > 0) {
5204
+ lines.push("## \u26A0\uFE0F BREAKING CHANGES DETECTED");
5205
+ lines.push("");
5206
+ for (const bc of breakingChanges) {
5207
+ lines.push("- **" + bc.severity.toUpperCase() + "** " + bc.file + ": " + bc.description);
5208
+ if (bc.affectedFiles.length > 0) {
5209
+ lines.push(" Affected: " + bc.affectedFiles.slice(0, 5).join(", "));
5210
+ }
5211
+ }
5212
+ lines.push("");
5213
+ }
5214
+ if (missingFiles.length > 0) {
5215
+ lines.push("## \u{1F4CB} Potentially Missing Files");
5216
+ lines.push("");
5217
+ for (const mf of missingFiles.slice(0, 10)) {
5218
+ lines.push("- " + mf.file + " \u2014 " + mf.reason);
5219
+ }
5220
+ lines.push("");
5221
+ }
5222
+ lines.push("## Changed Files");
5223
+ lines.push("");
5224
+ const topFiles = changedFiles.slice(0, maxFiles);
5225
+ for (const file of topFiles) {
5226
+ const icon = file.changeType === "added" ? "\u{1F195}" : file.changeType === "deleted" ? "\u{1F5D1}\uFE0F" : "\u{1F4DD}";
5227
+ lines.push("### " + icon + " " + file.relativePath + " (risk: " + file.riskScore + ", " + file.kind + ")");
5228
+ lines.push("+" + file.linesAdded + "/-" + file.linesRemoved + " lines");
5229
+ lines.push("");
5230
+ if (file.riskScore >= 40 && file.changeType !== "deleted") {
5231
+ try {
5232
+ const content = await readFile10(resolve9(projectPath, file.relativePath), "utf-8");
5233
+ const ext = file.relativePath.split(".").pop() ?? "";
5234
+ const maxChars = 4e3;
5235
+ const truncated = content.length > maxChars;
5236
+ lines.push("```" + ext);
5237
+ lines.push(content.slice(0, maxChars));
5238
+ if (truncated) lines.push("// ... [truncated]");
5239
+ lines.push("```");
5240
+ lines.push("");
5241
+ } catch {
5242
+ }
5243
+ }
5244
+ }
5245
+ lines.push("## Review Instructions");
5246
+ lines.push("");
5247
+ lines.push("1. Check breaking changes above for correctness");
5248
+ lines.push("2. Verify all affected files have been updated");
5249
+ lines.push("3. Review changed files for bugs, security issues, and code quality");
5250
+ lines.push("4. Ensure tests cover the changes");
5251
+ if (missingFiles.length > 0) {
5252
+ lines.push('5. Consider whether the "potentially missing files" need updates');
5253
+ }
5254
+ return lines.join("\n");
5255
+ }
5256
+ function renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality) {
5257
+ const lines = [];
5258
+ const qIcon = reviewQuality.score >= 80 ? "\u{1F7E2}" : reviewQuality.score >= 60 ? "\u{1F7E1}" : "\u{1F534}";
5259
+ lines.push("");
5260
+ lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
5261
+ lines.push(" " + qIcon + " Code Review: " + reviewQuality.score + "/100 (" + reviewQuality.grade + ")");
5262
+ lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
5263
+ lines.push("");
5264
+ lines.push(" Branch: " + branch + " \u2190 " + baseBranch);
5265
+ lines.push(" Files: " + changedFiles.length + " changed");
5266
+ lines.push(" Lines: +" + changedFiles.reduce((s, f) => s + f.linesAdded, 0) + "/-" + changedFiles.reduce((s, f) => s + f.linesRemoved, 0));
5267
+ lines.push("");
5268
+ for (const f of reviewQuality.factors) {
5269
+ const icon = f.score >= 80 ? "\u2705" : f.score >= 50 ? "\u26A0\uFE0F" : "\u274C";
5270
+ lines.push(" " + icon + " " + f.name + ": " + f.score + "/100 \u2014 " + f.detail);
5271
+ }
5272
+ if (breakingChanges.length > 0) {
5273
+ lines.push("");
5274
+ lines.push(" \u26A0\uFE0F BREAKING CHANGES (" + breakingChanges.length + "):");
5275
+ for (const bc of breakingChanges.slice(0, 5)) {
5276
+ const icon = bc.severity === "critical" ? "\u{1F534}" : bc.severity === "high" ? "\u{1F7E0}" : "\u{1F7E1}";
5277
+ lines.push(" " + icon + " " + bc.file + ": " + bc.description);
5278
+ }
5279
+ }
5280
+ if (missingFiles.length > 0) {
5281
+ lines.push("");
5282
+ lines.push(" \u{1F4CB} Potentially missing (" + missingFiles.length + "):");
5283
+ for (const mf of missingFiles.slice(0, 5)) {
5284
+ lines.push(" \u2192 " + mf.file + " (" + mf.reason + ")");
5285
+ }
5286
+ }
5287
+ lines.push("");
5288
+ lines.push(" \u{1F4A5} Impact: " + impactRadius.totalAffected + " files affected (" + impactRadius.directlyAffected + " direct, " + impactRadius.transitivelyAffected + " transitive)");
5289
+ if (impactRadius.affectedTests > 0) {
5290
+ lines.push(" \u{1F9EA} Tests: " + impactRadius.affectedTests + " test files in blast radius");
5291
+ }
5292
+ if (impactRadius.hotspots.length > 0) {
5293
+ lines.push("");
5294
+ lines.push(" \u{1F525} Hotspots:");
5295
+ for (const h of impactRadius.hotspots.slice(0, 3)) {
5296
+ lines.push(" " + h.file + " (risk: " + h.riskScore + ", " + h.dependents + " dependents)");
5297
+ }
5298
+ }
5299
+ lines.push("");
5300
+ return lines.join("\n");
5301
+ }
5302
+ function emptyResult2(baseBranch) {
5303
+ return {
5304
+ branch: "",
5305
+ baseBranch,
5306
+ isGitRepo: false,
5307
+ changedFiles: [],
5308
+ totalLinesChanged: 0,
5309
+ breakingChanges: [],
5310
+ missingFiles: [],
5311
+ impactRadius: {
5312
+ directlyAffected: 0,
5313
+ transitivelyAffected: 0,
5314
+ totalAffected: 0,
5315
+ affectedTests: 0,
5316
+ riskScore: 0,
5317
+ hotspots: []
5318
+ },
5319
+ reviewQuality: {
5320
+ score: 0,
5321
+ grade: "F",
5322
+ factors: []
5323
+ },
5324
+ reviewPrompt: "",
5325
+ renderedSummary: "# Code Review\n\nNot a git repository."
5326
+ };
5327
+ }
5328
+
4306
5329
  // src/cli/score.ts
4307
5330
  async function main() {
4308
5331
  const args = process.argv.slice(2);
@@ -4318,6 +5341,9 @@ async function main() {
4318
5341
  const monorepoMode = args.includes("--monorepo");
4319
5342
  const gatewayMode = args.includes("--gateway");
4320
5343
  const ciMode = args.includes("--ci");
5344
+ const learnMode = args.includes("--learn") || args.includes("--feedback");
5345
+ const predictMode = args.includes("--predict");
5346
+ const reviewMode = args.includes("--review");
4321
5347
  const helpMode = args.includes("--help") || args.includes("-h");
4322
5348
  const pkgIdx = args.indexOf("--package");
4323
5349
  const targetPackage = pkgIdx !== -1 && args[pkgIdx + 1] ? args[pkgIdx + 1] : null;
@@ -4326,7 +5352,7 @@ async function main() {
4326
5352
  const contextIdx = args.indexOf("--context");
4327
5353
  const contextTask = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
4328
5354
  const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask && a !== targetPackage);
4329
- const projectPath = resolve7(pathArg ?? ".");
5355
+ const projectPath = resolve11(pathArg ?? ".");
4330
5356
  if (helpMode) {
4331
5357
  console.log(`
4332
5358
  \u26A1 cto-score \u2014 How AI-ready is your codebase?
@@ -4363,6 +5389,15 @@ async function main() {
4363
5389
  npx cto-ai-cli --ci --threshold 80 Set minimum score (default: 70)
4364
5390
  npx cto-ai-cli --ci --json JSON output for CI pipelines
4365
5391
 
5392
+ Phase 7 \u2014 Learning Mode:
5393
+ npx cto-ai-cli --learn Show feedback model & cross-repo intelligence
5394
+ npx cto-ai-cli --predict Predict relevant files for a task
5395
+ npx cto-ai-cli --learn --json Export learning data as JSON
5396
+
5397
+ Phase 8 \u2014 Code Review:
5398
+ npx cto-ai-cli --review Smart PR review analysis
5399
+ npx cto-ai-cli --review --json JSON output for CI
5400
+
4366
5401
  Options:
4367
5402
  npx cto-ai-cli --json Output as JSON (for CI/scripts)
4368
5403
 
@@ -4444,29 +5479,35 @@ async function main() {
4444
5479
  const score = await computeContextScore(analysis);
4445
5480
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
4446
5481
  if (jsonMode) {
4447
- console.log(JSON.stringify({
4448
- project: analysis.projectName,
4449
- files: analysis.totalFiles,
4450
- tokens: analysis.totalTokens,
4451
- score: score.overall,
4452
- grade: score.grade,
4453
- dimensions: {
4454
- efficiency: score.dimensions.efficiency.score,
4455
- coverage: score.dimensions.coverage.score,
4456
- riskControl: score.dimensions.riskControl.score,
4457
- structure: score.dimensions.structure.score,
4458
- governance: score.dimensions.governance.score
4459
- },
4460
- savings: {
4461
- percent: score.comparison.savedPercent,
4462
- monthlyUSD: score.comparison.monthlySavingsUSD,
4463
- tokensOptimized: score.comparison.optimizedTokens,
4464
- tokensNaive: score.comparison.naiveTokens
4465
- }
4466
- }, null, 2));
4467
- process.exit(0);
5482
+ const hasModeFlag2 = learnMode || predictMode || reviewMode || ciMode;
5483
+ if (!hasModeFlag2) {
5484
+ console.log(JSON.stringify({
5485
+ project: analysis.projectName,
5486
+ files: analysis.totalFiles,
5487
+ tokens: analysis.totalTokens,
5488
+ score: score.overall,
5489
+ grade: score.grade,
5490
+ dimensions: {
5491
+ efficiency: score.dimensions.efficiency.score,
5492
+ coverage: score.dimensions.coverage.score,
5493
+ riskControl: score.dimensions.riskControl.score,
5494
+ structure: score.dimensions.structure.score,
5495
+ governance: score.dimensions.governance.score
5496
+ },
5497
+ savings: {
5498
+ percent: score.comparison.savedPercent,
5499
+ monthlyUSD: score.comparison.monthlySavingsUSD,
5500
+ tokensOptimized: score.comparison.optimizedTokens,
5501
+ tokensNaive: score.comparison.naiveTokens
5502
+ }
5503
+ }, null, 2));
5504
+ process.exit(0);
5505
+ }
5506
+ }
5507
+ const hasModeFlag = learnMode || predictMode || reviewMode || ciMode;
5508
+ if (!(jsonMode && hasModeFlag)) {
5509
+ console.log(renderContextScore(score));
4468
5510
  }
4469
- console.log(renderContextScore(score));
4470
5511
  if (benchmarkMode) {
4471
5512
  const benchmark = await runBenchmark(analysis);
4472
5513
  console.log(renderBenchmark(benchmark));
@@ -4492,6 +5533,18 @@ async function main() {
4492
5533
  if (ciMode) {
4493
5534
  await runCIGate(projectPath, analysis, score, thresholdArg, jsonMode);
4494
5535
  }
5536
+ if (learnMode) {
5537
+ await runLearn(projectPath, analysis, jsonMode);
5538
+ }
5539
+ if (predictMode) {
5540
+ await runPredict(projectPath, analysis, contextTask ?? "general code review", jsonMode);
5541
+ }
5542
+ if (reviewMode) {
5543
+ await runReview(projectPath, analysis, jsonMode);
5544
+ }
5545
+ if (jsonMode && hasModeFlag) {
5546
+ process.exit(0);
5547
+ }
4495
5548
  console.log("");
4496
5549
  console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
4497
5550
  console.log("");
@@ -4521,7 +5574,7 @@ async function main() {
4521
5574
  }
4522
5575
  }
4523
5576
  async function runFix(projectPath, analysis, score) {
4524
- const ctoDir = join8(projectPath, ".cto");
5577
+ const ctoDir = join10(projectPath, ".cto");
4525
5578
  mkdirSync3(ctoDir, { recursive: true });
4526
5579
  const selection = await selectContext({
4527
5580
  task: "general code review and refactoring",
@@ -4580,7 +5633,7 @@ async function runFix(projectPath, analysis, score) {
4580
5633
  `;
4581
5634
  contextMd += `## Savings: ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)})
4582
5635
  `;
4583
- writeFileSync2(join8(ctoDir, "context.md"), contextMd);
5636
+ writeFileSync2(join10(ctoDir, "context.md"), contextMd);
4584
5637
  const config = {
4585
5638
  version: "3.0",
4586
5639
  project: analysis.projectName,
@@ -4608,7 +5661,7 @@ async function runFix(projectPath, analysis, score) {
4608
5661
  impact: i.impact
4609
5662
  }))
4610
5663
  };
4611
- writeFileSync2(join8(ctoDir, "config.json"), JSON.stringify(config, null, 2));
5664
+ writeFileSync2(join10(ctoDir, "config.json"), JSON.stringify(config, null, 2));
4612
5665
  const ignoreContent = [
4613
5666
  "# CTO AI-ignore \u2014 files that add noise to AI context",
4614
5667
  "# Generated by cto-ai-cli",
@@ -4634,7 +5687,7 @@ async function runFix(projectPath, analysis, score) {
4634
5687
  }).slice(0, 20).map((o) => `${o} # orphan, low-risk`),
4635
5688
  ""
4636
5689
  ].join("\n");
4637
- writeFileSync2(join8(ctoDir, ".cteignore"), ignoreContent);
5690
+ writeFileSync2(join10(ctoDir, ".cteignore"), ignoreContent);
4638
5691
  console.log("");
4639
5692
  console.log(" \u2705 Auto-fix complete! Generated:");
4640
5693
  console.log("");
@@ -4647,7 +5700,7 @@ async function runFix(projectPath, analysis, score) {
4647
5700
  console.log("");
4648
5701
  }
4649
5702
  async function runContext(projectPath, analysis, task) {
4650
- const ctoDir = join8(projectPath, ".cto");
5703
+ const ctoDir = join10(projectPath, ".cto");
4651
5704
  mkdirSync3(ctoDir, { recursive: true });
4652
5705
  const selection = await selectContext({
4653
5706
  task,
@@ -4744,7 +5797,7 @@ ${content.slice(0, maxChars)}${truncated ? "\n// ... [truncated \u2014 " + (cont
4744
5797
  }
4745
5798
  const safeName = task.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 40);
4746
5799
  const filename = `context-${safeName}.md`;
4747
- writeFileSync2(join8(ctoDir, filename), contextMd);
5800
+ writeFileSync2(join10(ctoDir, filename), contextMd);
4748
5801
  console.log("");
4749
5802
  console.log(` \u2705 Task context generated!`);
4750
5803
  console.log("");
@@ -4837,9 +5890,9 @@ async function runReport(projectPath, analysis, score) {
4837
5890
  `;
4838
5891
  report += `*Run \`npx cto-ai-cli\` to generate your own report. [Learn more](https://npmjs.com/package/cto-ai-cli)*
4839
5892
  `;
4840
- const ctoDir = join8(projectPath, ".cto");
5893
+ const ctoDir = join10(projectPath, ".cto");
4841
5894
  mkdirSync3(ctoDir, { recursive: true });
4842
- writeFileSync2(join8(ctoDir, "report.md"), report);
5895
+ writeFileSync2(join10(ctoDir, "report.md"), report);
4843
5896
  console.log("");
4844
5897
  console.log(" \u2705 Report generated!");
4845
5898
  console.log("");
@@ -4982,12 +6035,12 @@ async function runAudit(projectPath, analysis, flags = {}) {
4982
6035
  console.log(` ${icon} ${rec}`);
4983
6036
  }
4984
6037
  }
4985
- const ctoDir = join8(projectPath, ".cto");
4986
- const auditDir = join8(ctoDir, "audit");
6038
+ const ctoDir = join10(projectPath, ".cto");
6039
+ const auditDir = join10(ctoDir, "audit");
4987
6040
  mkdirSync3(auditDir, { recursive: true });
4988
6041
  const now = /* @__PURE__ */ new Date();
4989
6042
  const dateStr = now.toISOString().split("T")[0];
4990
- const logFile = join8(auditDir, `${dateStr}.jsonl`);
6043
+ const logFile = join10(auditDir, `${dateStr}.jsonl`);
4991
6044
  const logEntry = {
4992
6045
  timestamp: now.toISOString(),
4993
6046
  version: "3.2.0",
@@ -5056,7 +6109,7 @@ async function runAudit(projectPath, analysis, flags = {}) {
5056
6109
  `;
5057
6110
  }
5058
6111
  }
5059
- writeFileSync2(join8(auditDir, "report.md"), report);
6112
+ writeFileSync2(join10(auditDir, "report.md"), report);
5060
6113
  const envSecrets = findings.filter(
5061
6114
  (f) => f.type === "env-variable" || f.type === "password" || f.type === "api-key" || f.type === "aws-key" || f.type === "connection-string"
5062
6115
  );
@@ -5078,7 +6131,7 @@ async function runAudit(projectPath, analysis, flags = {}) {
5078
6131
  envExample += `${name}=your_${name.toLowerCase()}_here
5079
6132
  `;
5080
6133
  }
5081
- writeFileSync2(join8(ctoDir, ".env.example"), envExample);
6134
+ writeFileSync2(join10(ctoDir, ".env.example"), envExample);
5082
6135
  }
5083
6136
  }
5084
6137
  console.log("");
@@ -5148,4 +6201,106 @@ async function runCIGate(projectPath, analysis, score, threshold, jsonMode) {
5148
6201
  process.exit(1);
5149
6202
  }
5150
6203
  }
6204
+ async function runLearn(projectPath, analysis, jsonMode) {
6205
+ const feedbackModel = await loadFeedbackModel(projectPath);
6206
+ const predictorModel = await loadModel(projectPath);
6207
+ const predictorStats = getModelStats(predictorModel);
6208
+ const globalModel = await loadGlobalModel();
6209
+ const crossRepoStats = getCrossRepoStats(globalModel);
6210
+ if (jsonMode) {
6211
+ const exported = await exportFeedbackForTeam(projectPath, analysis.projectName);
6212
+ console.log(JSON.stringify({
6213
+ feedback: feedbackModel,
6214
+ predictor: predictorStats,
6215
+ crossRepo: crossRepoStats,
6216
+ teamExport: exported
6217
+ }, null, 2));
6218
+ return;
6219
+ }
6220
+ console.log(renderFeedbackReport(feedbackModel));
6221
+ console.log("");
6222
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6223
+ console.log(" \u{1F9E0} Predictor Model");
6224
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6225
+ console.log("");
6226
+ console.log(` Observations: ${predictorStats.observations}`);
6227
+ console.log(` Task types: ${predictorStats.taskTypes}`);
6228
+ console.log(` Keywords: ${predictorStats.keywords}`);
6229
+ console.log(` Tracked files: ${predictorStats.trackedFiles}`);
6230
+ console.log(` Co-selection: ${predictorStats.coSelectionPairs} pairs`);
6231
+ console.log(` Last trained: ${predictorStats.trainedAt || "never"}`);
6232
+ console.log("");
6233
+ console.log(renderCrossRepoReport(crossRepoStats));
6234
+ if (feedbackModel.insights.length > 0) {
6235
+ console.log("");
6236
+ console.log(" \u{1F4A1} Top Insights:");
6237
+ console.log("");
6238
+ for (const insight of feedbackModel.insights.slice(0, 8)) {
6239
+ const icon = insight.type === "positive" ? "\u2705" : insight.type === "negative" ? "\u26A0\uFE0F" : "\u{1F4A1}";
6240
+ console.log(` ${icon} ${insight.title}`);
6241
+ console.log(` ${insight.detail}`);
6242
+ }
6243
+ console.log("");
6244
+ }
6245
+ const strategies = Object.entries(feedbackModel.strategyComparison);
6246
+ if (strategies.length > 1) {
6247
+ console.log("");
6248
+ console.log(" \u{1F52C} A/B Strategy Comparison:");
6249
+ console.log("");
6250
+ for (const [name, sc] of strategies) {
6251
+ console.log(` ${name}: ${Math.round(sc.acceptRate * 100)}% accept (n=${sc.totalCount}), avg ${Math.round(sc.avgTimeToAccept / 1e3)}s`);
6252
+ }
6253
+ console.log("");
6254
+ }
6255
+ }
6256
+ async function runPredict(projectPath, analysis, task, jsonMode) {
6257
+ const predictions = await predictRelevantFiles(projectPath, task, analysis);
6258
+ if (jsonMode) {
6259
+ console.log(JSON.stringify({ task, predictions }, null, 2));
6260
+ return;
6261
+ }
6262
+ console.log("");
6263
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6264
+ console.log(` \u{1F52E} Predictions for: "${task}"`);
6265
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6266
+ console.log("");
6267
+ if (predictions.length === 0) {
6268
+ console.log(" Not enough learning data yet. Use the tool more and run --learn to see insights.");
6269
+ console.log("");
6270
+ return;
6271
+ }
6272
+ for (const p of predictions.slice(0, 15)) {
6273
+ const bar = "\u2588".repeat(Math.round(p.predictedScore / 5)) + "\u2591".repeat(Math.max(0, 20 - Math.round(p.predictedScore / 5)));
6274
+ console.log(` ${bar} ${p.predictedScore.toFixed(1).padStart(5)} ${p.filePath}`);
6275
+ if (p.reasons.length > 0) {
6276
+ console.log(` ${p.reasons[0]}`);
6277
+ }
6278
+ }
6279
+ console.log("");
6280
+ console.log(` ${predictions.length} files predicted as relevant.`);
6281
+ console.log("");
6282
+ }
6283
+ async function runReview(projectPath, analysis, jsonMode) {
6284
+ const result = await analyzeForReview(analysis);
6285
+ if (jsonMode) {
6286
+ console.log(JSON.stringify({
6287
+ branch: result.branch,
6288
+ baseBranch: result.baseBranch,
6289
+ changedFiles: result.changedFiles.length,
6290
+ totalLinesChanged: result.totalLinesChanged,
6291
+ breakingChanges: result.breakingChanges,
6292
+ missingFiles: result.missingFiles,
6293
+ impactRadius: result.impactRadius,
6294
+ reviewQuality: result.reviewQuality
6295
+ }, null, 2));
6296
+ return;
6297
+ }
6298
+ console.log(result.renderedSummary);
6299
+ const ctoDir = join10(projectPath, ".cto");
6300
+ mkdirSync3(ctoDir, { recursive: true });
6301
+ writeFileSync2(join10(ctoDir, "review-prompt.md"), result.reviewPrompt);
6302
+ console.log(" \u{1F4CB} Review prompt saved: .cto/review-prompt.md");
6303
+ console.log(" Paste it into Claude/Cursor for an AI-powered code review.");
6304
+ console.log("");
6305
+ }
5151
6306
  main();