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/DOCS.md +201 -2
- package/README.md +216 -312
- package/dist/action/index.js +271 -156
- package/dist/api/dashboard.js +271 -156
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +276 -155
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +298 -183
- package/dist/cli/score.js +1396 -241
- package/dist/cli/v2/index.js +290 -175
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +121 -1
- package/dist/engine/index.js +1035 -212
- package/dist/engine/index.js.map +1 -1
- package/dist/fsevents-X6WP4TKM.node +0 -0
- package/dist/gateway/index.js +298 -183
- package/dist/gateway/index.js.map +1 -1
- package/dist/interact/index.js +263 -148
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +287 -172
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +8 -22
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
|
-
|
|
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
|
|
1381
|
+
function extractSignaturesRegex(content) {
|
|
1382
|
+
const lines = content.split("\n");
|
|
1401
1383
|
const parts = [];
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
if (
|
|
1465
|
-
const
|
|
1466
|
-
parts.push(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
|
1472
|
+
function extractSkeletonRegex(content) {
|
|
1473
|
+
const lines = content.split("\n");
|
|
1490
1474
|
const parts = [];
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2442
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2616
|
-
if (!
|
|
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 =
|
|
2631
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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((
|
|
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", () =>
|
|
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((
|
|
3022
|
+
return new Promise((resolve12) => {
|
|
2908
3023
|
this.server.listen(this.config.port, this.config.host, () => {
|
|
2909
|
-
|
|
3024
|
+
resolve12();
|
|
2910
3025
|
});
|
|
2911
3026
|
});
|
|
2912
3027
|
}
|
|
2913
3028
|
async stop() {
|
|
2914
|
-
return new Promise((
|
|
3029
|
+
return new Promise((resolve12) => {
|
|
2915
3030
|
if (this.server) {
|
|
2916
|
-
this.server.close(() =>
|
|
3031
|
+
this.server.close(() => resolve12());
|
|
2917
3032
|
} else {
|
|
2918
|
-
|
|
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((
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
-
|
|
3326
|
+
resolve12();
|
|
3212
3327
|
});
|
|
3213
3328
|
proxyRes.on("error", () => {
|
|
3214
3329
|
clientRes.end();
|
|
3215
|
-
|
|
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((
|
|
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
|
-
|
|
3393
|
+
resolve12();
|
|
3279
3394
|
});
|
|
3280
3395
|
proxyRes.on("error", () => {
|
|
3281
3396
|
clientRes.end();
|
|
3282
|
-
|
|
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
|
|
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
|
|
3609
|
-
const left = Math.floor(
|
|
3610
|
-
return " ".repeat(left) + str + " ".repeat(
|
|
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
|
|
3779
|
-
import { existsSync as
|
|
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 =
|
|
3801
|
-
if (
|
|
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 (
|
|
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 =
|
|
3823
|
-
if (!
|
|
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 =
|
|
3829
|
-
if (
|
|
3830
|
-
packagePaths.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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) =>
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
5893
|
+
const ctoDir = join10(projectPath, ".cto");
|
|
4841
5894
|
mkdirSync3(ctoDir, { recursive: true });
|
|
4842
|
-
writeFileSync2(
|
|
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 =
|
|
4986
|
-
const auditDir =
|
|
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 =
|
|
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(
|
|
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(
|
|
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();
|