cto-ai-cli 4.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +201 -2
- package/README.md +217 -312
- package/dist/action/index.js +281 -162
- package/dist/api/dashboard.js +281 -162
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +362 -184
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +358 -229
- package/dist/cli/score.js +2426 -1225
- package/dist/cli/v2/index.js +290 -175
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +150 -1
- package/dist/engine/index.js +1130 -219
- package/dist/engine/index.js.map +1 -1
- package/dist/fsevents-X6WP4TKM.node +0 -0
- package/dist/gateway/index.d.ts +2 -2
- package/dist/gateway/index.js +358 -229
- 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 +297 -178
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +8 -22
- package/dist/core/index.d.ts +0 -717
- package/dist/core/index.js +0 -4446
- package/dist/core/index.js.map +0 -1
package/dist/engine/index.js
CHANGED
|
@@ -610,8 +610,8 @@ async function analyzeProject(projectPath, config) {
|
|
|
610
610
|
maxDepth: mergedConfig.analysis.maxDepth
|
|
611
611
|
});
|
|
612
612
|
const tokenMethod = mergedConfig.tokens.method;
|
|
613
|
-
const
|
|
614
|
-
|
|
613
|
+
const BATCH_SIZE = 50;
|
|
614
|
+
async function estimateFileTokens2(entry) {
|
|
615
615
|
let tokens;
|
|
616
616
|
if (tokenMethod === "tiktoken") {
|
|
617
617
|
try {
|
|
@@ -623,7 +623,7 @@ async function analyzeProject(projectPath, config) {
|
|
|
623
623
|
} else {
|
|
624
624
|
tokens = countTokensChars4(entry.size);
|
|
625
625
|
}
|
|
626
|
-
|
|
626
|
+
return {
|
|
627
627
|
path: entry.path,
|
|
628
628
|
relativePath: entry.relativePath,
|
|
629
629
|
extension: entry.extension,
|
|
@@ -632,16 +632,20 @@ async function analyzeProject(projectPath, config) {
|
|
|
632
632
|
lines: entry.lines,
|
|
633
633
|
lastModified: entry.lastModified,
|
|
634
634
|
kind: classifyFileKind(entry.relativePath),
|
|
635
|
-
// Graph data — populated by graph analysis
|
|
636
635
|
imports: [],
|
|
637
636
|
importedBy: [],
|
|
638
637
|
isHub: false,
|
|
639
638
|
complexity: 0,
|
|
640
|
-
// Risk data — populated by risk analysis
|
|
641
639
|
riskScore: 0,
|
|
642
640
|
riskFactors: [],
|
|
643
641
|
exclusionImpact: "none"
|
|
644
|
-
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const files = [];
|
|
645
|
+
for (let i = 0; i < walkEntries.length; i += BATCH_SIZE) {
|
|
646
|
+
const batch = walkEntries.slice(i, i + BATCH_SIZE);
|
|
647
|
+
const results = await Promise.all(batch.map(estimateFileTokens2));
|
|
648
|
+
files.push(...results);
|
|
645
649
|
}
|
|
646
650
|
const graph = buildProjectGraph(absPath, files);
|
|
647
651
|
for (const file of files) {
|
|
@@ -1565,10 +1569,7 @@ function deduplicateFindings(findings) {
|
|
|
1565
1569
|
}
|
|
1566
1570
|
|
|
1567
1571
|
// src/engine/pruner.ts
|
|
1568
|
-
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1569
1572
|
import { readFile as readFile5 } from "fs/promises";
|
|
1570
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1571
|
-
import { join as join6 } from "path";
|
|
1572
1573
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
1573
1574
|
async function pruneFile(file, level) {
|
|
1574
1575
|
if (level === "excluded") {
|
|
@@ -1600,23 +1601,7 @@ async function pruneTypeScript(file, level) {
|
|
|
1600
1601
|
} catch {
|
|
1601
1602
|
return emptyResult2(file, level);
|
|
1602
1603
|
}
|
|
1603
|
-
|
|
1604
|
-
try {
|
|
1605
|
-
const tsConfigPath = findTsConfig(file.path);
|
|
1606
|
-
project = new Project2({
|
|
1607
|
-
tsConfigFilePath: tsConfigPath,
|
|
1608
|
-
skipAddingFilesFromTsConfig: true,
|
|
1609
|
-
compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
|
|
1610
|
-
});
|
|
1611
|
-
project.createSourceFile(file.path, content, { overwrite: true });
|
|
1612
|
-
} catch {
|
|
1613
|
-
return pruneGenericFromContent(file, content, level);
|
|
1614
|
-
}
|
|
1615
|
-
const sourceFile = project.getSourceFiles()[0];
|
|
1616
|
-
if (!sourceFile) {
|
|
1617
|
-
return pruneGenericFromContent(file, content, level);
|
|
1618
|
-
}
|
|
1619
|
-
const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
|
|
1604
|
+
const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
|
|
1620
1605
|
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
1621
1606
|
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
1622
1607
|
return {
|
|
@@ -1628,131 +1613,281 @@ async function pruneTypeScript(file, level) {
|
|
|
1628
1613
|
savingsPercent: Math.max(0, savingsPercent)
|
|
1629
1614
|
};
|
|
1630
1615
|
}
|
|
1631
|
-
function
|
|
1616
|
+
function extractSignaturesRegex(content) {
|
|
1617
|
+
const lines = content.split("\n");
|
|
1632
1618
|
const parts = [];
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
addJSDoc(fn, parts);
|
|
1651
|
-
const isExported = fn.isExported();
|
|
1652
|
-
const isAsync = fn.isAsync();
|
|
1653
|
-
const name = fn.getName() ?? "<anonymous>";
|
|
1654
|
-
const params = fn.getParameters().map((p) => p.getText()).join(", ");
|
|
1655
|
-
const returnType = fn.getReturnTypeNode()?.getText();
|
|
1656
|
-
const returnStr = returnType ? `: ${returnType}` : "";
|
|
1657
|
-
const prefix = isExported ? "export " : "";
|
|
1658
|
-
const asyncStr = isAsync ? "async " : "";
|
|
1659
|
-
parts.push(`${prefix}${asyncStr}function ${name}(${params})${returnStr} { /* ... */ }`);
|
|
1660
|
-
}
|
|
1661
|
-
for (const stmt of sf.getVariableStatements()) {
|
|
1662
|
-
for (const decl of stmt.getDeclarations()) {
|
|
1663
|
-
const init = decl.getInitializer();
|
|
1664
|
-
if (init && (init.getKind() === SyntaxKind2.ArrowFunction || init.getKind() === SyntaxKind2.FunctionExpression)) {
|
|
1665
|
-
addJSDoc(stmt, parts);
|
|
1666
|
-
const isExported = stmt.isExported();
|
|
1667
|
-
const prefix = isExported ? "export " : "";
|
|
1668
|
-
const kind = stmt.getDeclarationKind();
|
|
1669
|
-
const name = decl.getName();
|
|
1670
|
-
const typeNode = decl.getTypeNode()?.getText();
|
|
1671
|
-
const typeStr = typeNode ? `: ${typeNode}` : "";
|
|
1672
|
-
parts.push(`${prefix}${kind} ${name}${typeStr} = /* ... */;`);
|
|
1673
|
-
} else {
|
|
1674
|
-
addJSDoc(stmt, parts);
|
|
1675
|
-
parts.push(stmt.getText());
|
|
1619
|
+
let i = 0;
|
|
1620
|
+
while (i < lines.length) {
|
|
1621
|
+
const line = lines[i];
|
|
1622
|
+
const trimmed = line.trim();
|
|
1623
|
+
if (trimmed === "") {
|
|
1624
|
+
i++;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (trimmed.startsWith("/**")) {
|
|
1628
|
+
const docLines = [];
|
|
1629
|
+
while (i < lines.length) {
|
|
1630
|
+
docLines.push(lines[i]);
|
|
1631
|
+
if (lines[i].includes("*/")) {
|
|
1632
|
+
i++;
|
|
1633
|
+
break;
|
|
1634
|
+
}
|
|
1635
|
+
i++;
|
|
1676
1636
|
}
|
|
1637
|
+
parts.push(docLines.join("\n"));
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (trimmed.startsWith("//")) {
|
|
1641
|
+
parts.push(line);
|
|
1642
|
+
i++;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
|
|
1646
|
+
const block = collectBracedLine(lines, i);
|
|
1647
|
+
parts.push(block.text);
|
|
1648
|
+
i = block.nextIndex;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
|
|
1652
|
+
const block = collectBracedLine(lines, i);
|
|
1653
|
+
parts.push(block.text);
|
|
1654
|
+
i = block.nextIndex;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
|
|
1658
|
+
const block = collectBalanced(lines, i);
|
|
1659
|
+
parts.push(block.text);
|
|
1660
|
+
i = block.nextIndex;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
|
|
1664
|
+
const block = collectBalanced(lines, i);
|
|
1665
|
+
parts.push(block.text);
|
|
1666
|
+
i = block.nextIndex;
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1670
|
+
const block = collectBalanced(lines, i);
|
|
1671
|
+
parts.push(block.text);
|
|
1672
|
+
i = block.nextIndex;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
|
|
1676
|
+
if (fnMatch) {
|
|
1677
|
+
const sig = extractFnSignature(lines, i);
|
|
1678
|
+
parts.push(`${sig} { /* ... */ }`);
|
|
1679
|
+
i = skipBlock(lines, i);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
|
|
1683
|
+
if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
|
|
1684
|
+
const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
|
|
1685
|
+
if (prefix) {
|
|
1686
|
+
parts.push(`${prefix} /* ... */;`);
|
|
1687
|
+
}
|
|
1688
|
+
i = skipBlock(lines, i);
|
|
1689
|
+
continue;
|
|
1677
1690
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
for (const prop of cls.getProperties()) {
|
|
1692
|
-
parts.push(` ${prop.getText()}`);
|
|
1693
|
-
}
|
|
1694
|
-
const ctor = cls.getConstructors()[0];
|
|
1695
|
-
if (ctor) {
|
|
1696
|
-
const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
|
|
1697
|
-
parts.push(` constructor(${ctorParams}) { /* ... */ }`);
|
|
1698
|
-
}
|
|
1699
|
-
for (const method of cls.getMethods()) {
|
|
1700
|
-
const isStatic = method.isStatic();
|
|
1701
|
-
const isAsync = method.isAsync();
|
|
1702
|
-
const methodName = method.getName();
|
|
1703
|
-
const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
|
|
1704
|
-
const returnType = method.getReturnTypeNode()?.getText();
|
|
1705
|
-
const returnStr = returnType ? `: ${returnType}` : "";
|
|
1706
|
-
const staticStr = isStatic ? "static " : "";
|
|
1707
|
-
const asyncStr = isAsync ? "async " : "";
|
|
1708
|
-
parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
|
|
1709
|
-
}
|
|
1710
|
-
parts.push("}");
|
|
1711
|
-
}
|
|
1712
|
-
for (const exp of sf.getExportDeclarations()) {
|
|
1713
|
-
parts.push(exp.getText());
|
|
1714
|
-
}
|
|
1715
|
-
for (const exp of sf.getExportAssignments()) {
|
|
1716
|
-
parts.push(exp.getText());
|
|
1691
|
+
if (arrowMatch) {
|
|
1692
|
+
const block = collectStatement(lines, i);
|
|
1693
|
+
parts.push(block.text);
|
|
1694
|
+
i = block.nextIndex;
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
|
|
1698
|
+
const classOutline = extractClassOutline(lines, i);
|
|
1699
|
+
parts.push(classOutline.text);
|
|
1700
|
+
i = classOutline.nextIndex;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
i++;
|
|
1717
1704
|
}
|
|
1718
1705
|
return parts.join("\n");
|
|
1719
1706
|
}
|
|
1720
|
-
function
|
|
1707
|
+
function extractSkeletonRegex(content) {
|
|
1708
|
+
const lines = content.split("\n");
|
|
1721
1709
|
const parts = [];
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1710
|
+
let i = 0;
|
|
1711
|
+
while (i < lines.length) {
|
|
1712
|
+
const trimmed = lines[i].trim();
|
|
1713
|
+
if (/^import\s/.test(trimmed)) {
|
|
1714
|
+
const block = collectBracedLine(lines, i);
|
|
1715
|
+
parts.push(block.text);
|
|
1716
|
+
i = block.nextIndex;
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
|
|
1720
|
+
const block = collectBalanced(lines, i);
|
|
1721
|
+
parts.push(block.text);
|
|
1722
|
+
i = block.nextIndex;
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1726
|
+
const block = collectBalanced(lines, i);
|
|
1727
|
+
parts.push(block.text);
|
|
1728
|
+
i = block.nextIndex;
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
|
|
1732
|
+
const sig = extractFnSignature(lines, i);
|
|
1733
|
+
parts.push(`${sig};`);
|
|
1734
|
+
i = skipBlock(lines, i);
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
|
|
1738
|
+
const nameMatch = trimmed.match(/class\s+(\w+)/);
|
|
1739
|
+
const name = nameMatch?.[1] ?? "Unknown";
|
|
1740
|
+
const end = skipBlock(lines, i);
|
|
1741
|
+
const methods = [];
|
|
1742
|
+
for (let j = i + 1; j < end; j++) {
|
|
1743
|
+
const mt = lines[j].trim();
|
|
1744
|
+
const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
|
|
1745
|
+
if (mm && mm[1] !== "constructor") methods.push(mm[1]);
|
|
1746
|
+
}
|
|
1747
|
+
parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
|
|
1748
|
+
i = end;
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
if (/^export\s*(\{|\*)/.test(trimmed)) {
|
|
1752
|
+
const block = collectBracedLine(lines, i);
|
|
1753
|
+
parts.push(block.text);
|
|
1754
|
+
i = block.nextIndex;
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
i++;
|
|
1753
1758
|
}
|
|
1754
1759
|
return parts.join("\n");
|
|
1755
1760
|
}
|
|
1761
|
+
function collectBracedLine(lines, start) {
|
|
1762
|
+
let text = lines[start];
|
|
1763
|
+
let i = start + 1;
|
|
1764
|
+
while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
|
|
1765
|
+
text += "\n" + lines[i];
|
|
1766
|
+
i++;
|
|
1767
|
+
}
|
|
1768
|
+
return { text, nextIndex: i };
|
|
1769
|
+
}
|
|
1770
|
+
function collectBalanced(lines, start) {
|
|
1771
|
+
let depth = 0;
|
|
1772
|
+
let text = "";
|
|
1773
|
+
let i = start;
|
|
1774
|
+
let started = false;
|
|
1775
|
+
while (i < lines.length) {
|
|
1776
|
+
const line = lines[i];
|
|
1777
|
+
text += (text ? "\n" : "") + line;
|
|
1778
|
+
for (const ch of line) {
|
|
1779
|
+
if (ch === "{" || ch === "(") {
|
|
1780
|
+
depth++;
|
|
1781
|
+
started = true;
|
|
1782
|
+
}
|
|
1783
|
+
if (ch === "}" || ch === ")") depth--;
|
|
1784
|
+
}
|
|
1785
|
+
i++;
|
|
1786
|
+
if (started && depth <= 0) break;
|
|
1787
|
+
if (!started && line.includes(";")) break;
|
|
1788
|
+
}
|
|
1789
|
+
return { text, nextIndex: i };
|
|
1790
|
+
}
|
|
1791
|
+
function collectStatement(lines, start) {
|
|
1792
|
+
let text = lines[start];
|
|
1793
|
+
let i = start + 1;
|
|
1794
|
+
if (text.includes(";")) return { text, nextIndex: i };
|
|
1795
|
+
let depth = 0;
|
|
1796
|
+
for (const ch of text) {
|
|
1797
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1798
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1799
|
+
}
|
|
1800
|
+
while (i < lines.length && depth > 0) {
|
|
1801
|
+
text += "\n" + lines[i];
|
|
1802
|
+
for (const ch of lines[i]) {
|
|
1803
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1804
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1805
|
+
}
|
|
1806
|
+
i++;
|
|
1807
|
+
}
|
|
1808
|
+
return { text, nextIndex: i };
|
|
1809
|
+
}
|
|
1810
|
+
function extractFnSignature(lines, start) {
|
|
1811
|
+
let sig = "";
|
|
1812
|
+
let i = start;
|
|
1813
|
+
while (i < lines.length) {
|
|
1814
|
+
const line = lines[i].trim();
|
|
1815
|
+
sig += (sig ? " " : "") + line;
|
|
1816
|
+
if (line.includes("{")) {
|
|
1817
|
+
sig = sig.replace(/\s*\{[^]*$/, "").trim();
|
|
1818
|
+
break;
|
|
1819
|
+
}
|
|
1820
|
+
i++;
|
|
1821
|
+
}
|
|
1822
|
+
return sig;
|
|
1823
|
+
}
|
|
1824
|
+
function skipBlock(lines, start) {
|
|
1825
|
+
let depth = 0;
|
|
1826
|
+
let i = start;
|
|
1827
|
+
let foundBrace = false;
|
|
1828
|
+
while (i < lines.length) {
|
|
1829
|
+
for (const ch of lines[i]) {
|
|
1830
|
+
if (ch === "{") {
|
|
1831
|
+
depth++;
|
|
1832
|
+
foundBrace = true;
|
|
1833
|
+
}
|
|
1834
|
+
if (ch === "}") depth--;
|
|
1835
|
+
}
|
|
1836
|
+
i++;
|
|
1837
|
+
if (foundBrace && depth <= 0) break;
|
|
1838
|
+
if (!foundBrace && lines[i - 1].includes(";")) break;
|
|
1839
|
+
}
|
|
1840
|
+
return i;
|
|
1841
|
+
}
|
|
1842
|
+
function looksLikeFunctionDecl(lines, start) {
|
|
1843
|
+
const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
|
|
1844
|
+
return /=>/.test(chunk) || /=\s*function/.test(chunk);
|
|
1845
|
+
}
|
|
1846
|
+
function extractClassOutline(lines, start) {
|
|
1847
|
+
const header = lines[start].trim();
|
|
1848
|
+
let headerText = header;
|
|
1849
|
+
let i = start + 1;
|
|
1850
|
+
if (!header.includes("{")) {
|
|
1851
|
+
while (i < lines.length) {
|
|
1852
|
+
headerText += " " + lines[i].trim();
|
|
1853
|
+
if (lines[i].includes("{")) {
|
|
1854
|
+
i++;
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
i++;
|
|
1858
|
+
}
|
|
1859
|
+
} else {
|
|
1860
|
+
i = start + 1;
|
|
1861
|
+
}
|
|
1862
|
+
const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
|
|
1863
|
+
let depth = 1;
|
|
1864
|
+
while (i < lines.length && depth > 0) {
|
|
1865
|
+
const line = lines[i];
|
|
1866
|
+
const trimmed = line.trim();
|
|
1867
|
+
for (const ch of line) {
|
|
1868
|
+
if (ch === "{") depth++;
|
|
1869
|
+
if (ch === "}") depth--;
|
|
1870
|
+
}
|
|
1871
|
+
if (depth <= 0) {
|
|
1872
|
+
i++;
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
if (depth === 1) {
|
|
1876
|
+
if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
|
|
1877
|
+
bodyParts.push(` ${trimmed}`);
|
|
1878
|
+
} else if (/^constructor\s*\(/.test(trimmed)) {
|
|
1879
|
+
const sig = extractFnSignature(lines, i);
|
|
1880
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1881
|
+
} else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
1882
|
+
const sig = extractFnSignature(lines, i);
|
|
1883
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
i++;
|
|
1887
|
+
}
|
|
1888
|
+
bodyParts.push("}");
|
|
1889
|
+
return { text: bodyParts.join("\n"), nextIndex: i };
|
|
1890
|
+
}
|
|
1756
1891
|
async function pruneGeneric(file, level) {
|
|
1757
1892
|
let content;
|
|
1758
1893
|
try {
|
|
@@ -1813,22 +1948,6 @@ function emptyResult2(file, level) {
|
|
|
1813
1948
|
savingsPercent: 100
|
|
1814
1949
|
};
|
|
1815
1950
|
}
|
|
1816
|
-
function addJSDoc(node, parts) {
|
|
1817
|
-
if (!node.getJsDocs) return;
|
|
1818
|
-
const docs = node.getJsDocs();
|
|
1819
|
-
if (docs.length > 0) {
|
|
1820
|
-
parts.push(docs[0].getText());
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
function findTsConfig(filePath) {
|
|
1824
|
-
let dir = filePath;
|
|
1825
|
-
for (let i = 0; i < 10; i++) {
|
|
1826
|
-
dir = join6(dir, "..");
|
|
1827
|
-
const candidate = join6(dir, "tsconfig.json");
|
|
1828
|
-
if (existsSync4(candidate)) return candidate;
|
|
1829
|
-
}
|
|
1830
|
-
return void 0;
|
|
1831
|
-
}
|
|
1832
1951
|
|
|
1833
1952
|
// src/engine/coverage.ts
|
|
1834
1953
|
function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
|
|
@@ -3252,7 +3371,7 @@ function fmt2(n) {
|
|
|
3252
3371
|
}
|
|
3253
3372
|
|
|
3254
3373
|
// src/engine/predictor.ts
|
|
3255
|
-
import { resolve as resolve8, join as
|
|
3374
|
+
import { resolve as resolve8, join as join6 } from "path";
|
|
3256
3375
|
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3257
3376
|
var DEFAULT_PREDICTOR_CONFIG = {
|
|
3258
3377
|
maxCoSelectionPairs: 500,
|
|
@@ -3262,9 +3381,9 @@ var DEFAULT_PREDICTOR_CONFIG = {
|
|
|
3262
3381
|
// need at least 2 observations before predicting
|
|
3263
3382
|
};
|
|
3264
3383
|
async function getModelPath(projectPath) {
|
|
3265
|
-
const ctoDir =
|
|
3384
|
+
const ctoDir = join6(resolve8(projectPath), ".cto");
|
|
3266
3385
|
await mkdir2(ctoDir, { recursive: true });
|
|
3267
|
-
return
|
|
3386
|
+
return join6(ctoDir, "predictor.json");
|
|
3268
3387
|
}
|
|
3269
3388
|
async function loadModel(projectPath) {
|
|
3270
3389
|
try {
|
|
@@ -3514,7 +3633,7 @@ function pruneCoSelection(model, maxPairs) {
|
|
|
3514
3633
|
}
|
|
3515
3634
|
|
|
3516
3635
|
// src/engine/cross-repo.ts
|
|
3517
|
-
import { join as
|
|
3636
|
+
import { join as join7, basename as basename3 } from "path";
|
|
3518
3637
|
import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
3519
3638
|
function computeFingerprint2(analysis) {
|
|
3520
3639
|
const totalFiles = analysis.totalFiles;
|
|
@@ -3547,7 +3666,7 @@ function fingerprintHash(fp) {
|
|
|
3547
3666
|
}
|
|
3548
3667
|
function getGlobalModelPath() {
|
|
3549
3668
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
3550
|
-
return
|
|
3669
|
+
return join7(home, ".cto", "global-intelligence.json");
|
|
3551
3670
|
}
|
|
3552
3671
|
async function loadGlobalModel() {
|
|
3553
3672
|
try {
|
|
@@ -3558,7 +3677,7 @@ async function loadGlobalModel() {
|
|
|
3558
3677
|
}
|
|
3559
3678
|
}
|
|
3560
3679
|
async function saveGlobalModel(model) {
|
|
3561
|
-
const dir =
|
|
3680
|
+
const dir = join7(getGlobalModelPath(), "..");
|
|
3562
3681
|
await mkdir3(dir, { recursive: true });
|
|
3563
3682
|
await writeFile4(getGlobalModelPath(), JSON.stringify(model, null, 2));
|
|
3564
3683
|
}
|
|
@@ -3779,17 +3898,17 @@ function getCrossRepoStats(model) {
|
|
|
3779
3898
|
}
|
|
3780
3899
|
|
|
3781
3900
|
// src/engine/feedback.ts
|
|
3782
|
-
import { resolve as resolve10, join as
|
|
3901
|
+
import { resolve as resolve10, join as join8 } from "path";
|
|
3783
3902
|
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
3784
3903
|
async function getFeedbackPath(projectPath) {
|
|
3785
|
-
const ctoDir =
|
|
3904
|
+
const ctoDir = join8(resolve10(projectPath), ".cto");
|
|
3786
3905
|
await mkdir4(ctoDir, { recursive: true });
|
|
3787
|
-
return
|
|
3906
|
+
return join8(ctoDir, "feedback.json");
|
|
3788
3907
|
}
|
|
3789
3908
|
async function getModelPath2(projectPath) {
|
|
3790
|
-
const ctoDir =
|
|
3909
|
+
const ctoDir = join8(resolve10(projectPath), ".cto");
|
|
3791
3910
|
await mkdir4(ctoDir, { recursive: true });
|
|
3792
|
-
return
|
|
3911
|
+
return join8(ctoDir, "feedback-model.json");
|
|
3793
3912
|
}
|
|
3794
3913
|
async function loadFeedback(projectPath) {
|
|
3795
3914
|
try {
|
|
@@ -3815,16 +3934,30 @@ async function saveFeedbackModel(projectPath, model) {
|
|
|
3815
3934
|
}
|
|
3816
3935
|
function createEmptyModel3() {
|
|
3817
3936
|
return {
|
|
3818
|
-
version:
|
|
3937
|
+
version: 2,
|
|
3819
3938
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3820
3939
|
totalFeedback: 0,
|
|
3821
3940
|
acceptRate: 0,
|
|
3822
3941
|
fileAcceptance: {},
|
|
3823
3942
|
taskTypeAcceptance: {},
|
|
3824
3943
|
pairAcceptance: {},
|
|
3944
|
+
sessions: {},
|
|
3945
|
+
strategyComparison: {},
|
|
3825
3946
|
insights: []
|
|
3826
3947
|
};
|
|
3827
3948
|
}
|
|
3949
|
+
var EWMA_ALPHA = 0.15;
|
|
3950
|
+
function ewmaUpdate(prev, newValue, alpha = EWMA_ALPHA) {
|
|
3951
|
+
return alpha * newValue + (1 - alpha) * prev;
|
|
3952
|
+
}
|
|
3953
|
+
function wilsonLowerBound(successes, total, z = 1.96) {
|
|
3954
|
+
if (total === 0) return 0;
|
|
3955
|
+
const phat = successes / total;
|
|
3956
|
+
const denom = 1 + z * z / total;
|
|
3957
|
+
const center = phat + z * z / (2 * total);
|
|
3958
|
+
const spread = z * Math.sqrt((phat * (1 - phat) + z * z / (4 * total)) / total);
|
|
3959
|
+
return Math.max(0, (center - spread) / denom);
|
|
3960
|
+
}
|
|
3828
3961
|
async function recordFeedback(projectPath, entry) {
|
|
3829
3962
|
const entries = await loadFeedback(projectPath);
|
|
3830
3963
|
const fullEntry = {
|
|
@@ -3853,13 +3986,20 @@ function rebuildModel(entries) {
|
|
|
3853
3986
|
includedCount: 0,
|
|
3854
3987
|
acceptedCount: 0,
|
|
3855
3988
|
acceptRate: 0,
|
|
3856
|
-
|
|
3989
|
+
ewmaAcceptRate: 0.5,
|
|
3990
|
+
// prior: 50%
|
|
3991
|
+
avgTimeToAccept: 0,
|
|
3992
|
+
lastSeen: entry.timestamp,
|
|
3993
|
+
bayesianLower: 0
|
|
3857
3994
|
};
|
|
3858
3995
|
}
|
|
3859
3996
|
const fa = model.fileAcceptance[file];
|
|
3860
3997
|
fa.includedCount++;
|
|
3861
3998
|
if (accepted) fa.acceptedCount++;
|
|
3862
3999
|
fa.acceptRate = fa.acceptedCount / fa.includedCount;
|
|
4000
|
+
fa.ewmaAcceptRate = ewmaUpdate(fa.ewmaAcceptRate, accepted ? 1 : 0);
|
|
4001
|
+
fa.bayesianLower = wilsonLowerBound(fa.acceptedCount, fa.includedCount);
|
|
4002
|
+
fa.lastSeen = entry.timestamp;
|
|
3863
4003
|
if (entry.outcome.timeToAcceptMs) {
|
|
3864
4004
|
fa.avgTimeToAccept = (fa.avgTimeToAccept * (fa.includedCount - 1) + entry.outcome.timeToAcceptMs) / fa.includedCount;
|
|
3865
4005
|
}
|
|
@@ -3870,13 +4010,15 @@ function rebuildModel(entries) {
|
|
|
3870
4010
|
totalCount: 0,
|
|
3871
4011
|
acceptedCount: 0,
|
|
3872
4012
|
acceptRate: 0,
|
|
3873
|
-
avgCompilable: 0
|
|
4013
|
+
avgCompilable: 0,
|
|
4014
|
+
ewmaAcceptRate: 0.5
|
|
3874
4015
|
};
|
|
3875
4016
|
}
|
|
3876
4017
|
const tta = model.taskTypeAcceptance[tt];
|
|
3877
4018
|
tta.totalCount++;
|
|
3878
4019
|
if (accepted) tta.acceptedCount++;
|
|
3879
4020
|
tta.acceptRate = tta.acceptedCount / tta.totalCount;
|
|
4021
|
+
tta.ewmaAcceptRate = ewmaUpdate(tta.ewmaAcceptRate, accepted ? 1 : 0);
|
|
3880
4022
|
if (entry.outcome.compilable !== void 0) {
|
|
3881
4023
|
tta.avgCompilable = (tta.avgCompilable * (tta.totalCount - 1) + (entry.outcome.compilable ? 1 : 0)) / tta.totalCount;
|
|
3882
4024
|
}
|
|
@@ -3893,6 +4035,36 @@ function rebuildModel(entries) {
|
|
|
3893
4035
|
pa.acceptRate = pa.acceptedCount / pa.count;
|
|
3894
4036
|
}
|
|
3895
4037
|
}
|
|
4038
|
+
if (entry.sessionId) {
|
|
4039
|
+
if (!model.sessions[entry.sessionId]) {
|
|
4040
|
+
model.sessions[entry.sessionId] = {
|
|
4041
|
+
strategy: entry.strategy ?? "default",
|
|
4042
|
+
entries: 0,
|
|
4043
|
+
acceptRate: 0
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
const sess = model.sessions[entry.sessionId];
|
|
4047
|
+
sess.entries++;
|
|
4048
|
+
const sessionEntries = entries.filter((e) => e.sessionId === entry.sessionId);
|
|
4049
|
+
const sessionAccepted = sessionEntries.filter((e) => e.outcome.accepted).length;
|
|
4050
|
+
sess.acceptRate = sessionEntries.length > 0 ? sessionAccepted / sessionEntries.length : 0;
|
|
4051
|
+
}
|
|
4052
|
+
const strat = entry.strategy ?? "default";
|
|
4053
|
+
if (!model.strategyComparison[strat]) {
|
|
4054
|
+
model.strategyComparison[strat] = {
|
|
4055
|
+
totalCount: 0,
|
|
4056
|
+
acceptedCount: 0,
|
|
4057
|
+
acceptRate: 0,
|
|
4058
|
+
avgTimeToAccept: 0
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
const sc = model.strategyComparison[strat];
|
|
4062
|
+
sc.totalCount++;
|
|
4063
|
+
if (accepted) sc.acceptedCount++;
|
|
4064
|
+
sc.acceptRate = sc.acceptedCount / sc.totalCount;
|
|
4065
|
+
if (entry.outcome.timeToAcceptMs) {
|
|
4066
|
+
sc.avgTimeToAccept = (sc.avgTimeToAccept * (sc.totalCount - 1) + entry.outcome.timeToAcceptMs) / sc.totalCount;
|
|
4067
|
+
}
|
|
3896
4068
|
}
|
|
3897
4069
|
model.acceptRate = entries.length > 0 ? acceptedTotal / entries.length : 0;
|
|
3898
4070
|
model.insights = generateInsights(model);
|
|
@@ -3900,43 +4072,64 @@ function rebuildModel(entries) {
|
|
|
3900
4072
|
}
|
|
3901
4073
|
function generateInsights(model) {
|
|
3902
4074
|
const insights = [];
|
|
3903
|
-
const highAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.
|
|
4075
|
+
const highAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.bayesianLower >= 0.5).sort((a, b) => b[1].bayesianLower - a[1].bayesianLower).slice(0, 5);
|
|
3904
4076
|
for (const [file, fa] of highAccept) {
|
|
3905
4077
|
insights.push({
|
|
3906
4078
|
type: "positive",
|
|
3907
4079
|
title: `${file} consistently leads to accepted output`,
|
|
3908
|
-
detail: `${Math.round(fa.acceptRate * 100)}% accept rate
|
|
3909
|
-
impact: Math.round(fa.
|
|
4080
|
+
detail: `${Math.round(fa.acceptRate * 100)}% accept rate (${fa.acceptedCount}/${fa.includedCount}), Bayesian lower: ${Math.round(fa.bayesianLower * 100)}%, EWMA: ${Math.round(fa.ewmaAcceptRate * 100)}%`,
|
|
4081
|
+
impact: Math.round(fa.bayesianLower * fa.includedCount * 10)
|
|
3910
4082
|
});
|
|
3911
4083
|
}
|
|
3912
|
-
const lowAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.acceptRate < 0.3).sort((a, b) => a[1].
|
|
4084
|
+
const lowAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.acceptRate < 0.3).sort((a, b) => a[1].ewmaAcceptRate - b[1].ewmaAcceptRate).slice(0, 5);
|
|
3913
4085
|
for (const [file, fa] of lowAccept) {
|
|
3914
4086
|
insights.push({
|
|
3915
4087
|
type: "negative",
|
|
3916
4088
|
title: `${file} often included but output rejected`,
|
|
3917
|
-
detail: `Only ${Math.round(fa.acceptRate * 100)}% accept rate \u2014
|
|
3918
|
-
impact: Math.round((1 - fa.
|
|
4089
|
+
detail: `Only ${Math.round(fa.acceptRate * 100)}% accept rate \u2014 EWMA trending ${Math.round(fa.ewmaAcceptRate * 100)}%`,
|
|
4090
|
+
impact: Math.round((1 - fa.bayesianLower) * fa.includedCount * 5)
|
|
3919
4091
|
});
|
|
3920
4092
|
}
|
|
3921
|
-
const bestTasks = Object.entries(model.taskTypeAcceptance).filter(([_, ta]) => ta.totalCount >= 2).sort((a, b) => b[1].
|
|
4093
|
+
const bestTasks = Object.entries(model.taskTypeAcceptance).filter(([_, ta]) => ta.totalCount >= 2).sort((a, b) => b[1].ewmaAcceptRate - a[1].ewmaAcceptRate);
|
|
3922
4094
|
for (const [taskType, ta] of bestTasks.slice(0, 3)) {
|
|
4095
|
+
const trending = ta.ewmaAcceptRate > ta.acceptRate ? "improving" : ta.ewmaAcceptRate < ta.acceptRate - 0.05 ? "declining" : "stable";
|
|
3923
4096
|
insights.push({
|
|
3924
4097
|
type: ta.acceptRate >= 0.7 ? "positive" : "opportunity",
|
|
3925
|
-
title: `${taskType} tasks: ${Math.round(ta.acceptRate * 100)}% acceptance`,
|
|
3926
|
-
detail: `${ta.acceptedCount}/${ta.totalCount} accepted, ${Math.round(ta.avgCompilable * 100)}% compilable
|
|
3927
|
-
impact: Math.round(ta.
|
|
4098
|
+
title: `${taskType} tasks: ${Math.round(ta.acceptRate * 100)}% acceptance (${trending})`,
|
|
4099
|
+
detail: `${ta.acceptedCount}/${ta.totalCount} accepted, ${Math.round(ta.avgCompilable * 100)}% compilable, EWMA: ${Math.round(ta.ewmaAcceptRate * 100)}%`,
|
|
4100
|
+
impact: Math.round(ta.ewmaAcceptRate * ta.totalCount * 3)
|
|
3928
4101
|
});
|
|
3929
4102
|
}
|
|
3930
|
-
const powerPairs = Object.entries(model.pairAcceptance).filter(([_, pa]) => pa.count >=
|
|
4103
|
+
const powerPairs = Object.entries(model.pairAcceptance).filter(([_, pa]) => pa.count >= 4 && pa.acceptRate >= 0.85).sort((a, b) => {
|
|
4104
|
+
const aWilson = wilsonLowerBound(a[1].acceptedCount, a[1].count);
|
|
4105
|
+
const bWilson = wilsonLowerBound(b[1].acceptedCount, b[1].count);
|
|
4106
|
+
return bWilson - aWilson;
|
|
4107
|
+
}).slice(0, 3);
|
|
3931
4108
|
for (const [pair, pa] of powerPairs) {
|
|
3932
4109
|
const [a, b] = pair.split("|");
|
|
4110
|
+
const wilson = wilsonLowerBound(pa.acceptedCount, pa.count);
|
|
3933
4111
|
insights.push({
|
|
3934
4112
|
type: "positive",
|
|
3935
4113
|
title: `Power pair: ${a} + ${b}`,
|
|
3936
|
-
detail: `${Math.round(pa.acceptRate * 100)}% acceptance
|
|
3937
|
-
impact: Math.round(
|
|
4114
|
+
detail: `${Math.round(pa.acceptRate * 100)}% acceptance (${pa.count}x), confidence: ${Math.round(wilson * 100)}%`,
|
|
4115
|
+
impact: Math.round(wilson * pa.count * 5)
|
|
3938
4116
|
});
|
|
3939
4117
|
}
|
|
4118
|
+
const strategies = Object.entries(model.strategyComparison).filter(([_, sc]) => sc.totalCount >= 3);
|
|
4119
|
+
if (strategies.length >= 2) {
|
|
4120
|
+
strategies.sort((a, b) => b[1].acceptRate - a[1].acceptRate);
|
|
4121
|
+
const best = strategies[0];
|
|
4122
|
+
const worst = strategies[strategies.length - 1];
|
|
4123
|
+
const diff = best[1].acceptRate - worst[1].acceptRate;
|
|
4124
|
+
if (diff > 0.1) {
|
|
4125
|
+
insights.push({
|
|
4126
|
+
type: "opportunity",
|
|
4127
|
+
title: `Strategy "${best[0]}" outperforms "${worst[0]}" by ${Math.round(diff * 100)}%`,
|
|
4128
|
+
detail: `${best[0]}: ${Math.round(best[1].acceptRate * 100)}% (n=${best[1].totalCount}) vs ${worst[0]}: ${Math.round(worst[1].acceptRate * 100)}% (n=${worst[1].totalCount})`,
|
|
4129
|
+
impact: Math.round(diff * 50)
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
3940
4133
|
return insights.sort((a, b) => b.impact - a.impact);
|
|
3941
4134
|
}
|
|
3942
4135
|
async function getFeedbackBoosts(projectPath, task) {
|
|
@@ -3946,18 +4139,19 @@ async function getFeedbackBoosts(projectPath, task) {
|
|
|
3946
4139
|
const taskType = classifyTask(task);
|
|
3947
4140
|
for (const [file, fa] of Object.entries(model.fileAcceptance)) {
|
|
3948
4141
|
if (fa.includedCount < 2) continue;
|
|
3949
|
-
if (fa.
|
|
3950
|
-
|
|
4142
|
+
if (fa.bayesianLower >= 0.5) {
|
|
4143
|
+
const boost = fa.ewmaAcceptRate * 5 * Math.min(1, fa.includedCount / 5);
|
|
4144
|
+
boosts.set(file, (boosts.get(file) ?? 0) + boost);
|
|
3951
4145
|
}
|
|
3952
|
-
if (fa.acceptRate < 0.2 && fa.includedCount >=
|
|
3953
|
-
boosts.set(file, (boosts.get(file) ?? 0) - 3);
|
|
4146
|
+
if (fa.acceptRate < 0.2 && fa.includedCount >= 4) {
|
|
4147
|
+
boosts.set(file, (boosts.get(file) ?? 0) - 3 * (1 - fa.bayesianLower));
|
|
3954
4148
|
}
|
|
3955
4149
|
}
|
|
3956
4150
|
const tta = model.taskTypeAcceptance[taskType];
|
|
3957
4151
|
if (tta && tta.totalCount >= 2) {
|
|
3958
4152
|
if (tta.avgCompilable >= 0.8) {
|
|
3959
4153
|
for (const [file, fa] of Object.entries(model.fileAcceptance)) {
|
|
3960
|
-
if (fa.
|
|
4154
|
+
if (fa.bayesianLower >= 0.6) {
|
|
3961
4155
|
boosts.set(file, (boosts.get(file) ?? 0) + 1);
|
|
3962
4156
|
}
|
|
3963
4157
|
}
|
|
@@ -3965,6 +4159,53 @@ async function getFeedbackBoosts(projectPath, task) {
|
|
|
3965
4159
|
}
|
|
3966
4160
|
return boosts;
|
|
3967
4161
|
}
|
|
4162
|
+
async function exportFeedbackForTeam(projectPath, projectName) {
|
|
4163
|
+
const model = await loadFeedbackModel(projectPath);
|
|
4164
|
+
const entries = await loadFeedback(projectPath);
|
|
4165
|
+
return {
|
|
4166
|
+
version: 2,
|
|
4167
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4168
|
+
projectName,
|
|
4169
|
+
model,
|
|
4170
|
+
entrySummary: {
|
|
4171
|
+
total: entries.length,
|
|
4172
|
+
accepted: entries.filter((e) => e.outcome.accepted).length,
|
|
4173
|
+
sessions: new Set(entries.map((e) => e.sessionId).filter(Boolean)).size
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
async function importTeamFeedback(projectPath, teamExport) {
|
|
4178
|
+
const localModel = await loadFeedbackModel(projectPath);
|
|
4179
|
+
for (const [file, fa] of Object.entries(teamExport.model.fileAcceptance)) {
|
|
4180
|
+
const local = localModel.fileAcceptance[file];
|
|
4181
|
+
if (!local) {
|
|
4182
|
+
localModel.fileAcceptance[file] = {
|
|
4183
|
+
...fa,
|
|
4184
|
+
includedCount: Math.max(1, Math.round(fa.includedCount * 0.5)),
|
|
4185
|
+
acceptedCount: Math.max(0, Math.round(fa.acceptedCount * 0.5)),
|
|
4186
|
+
ewmaAcceptRate: fa.ewmaAcceptRate * 0.7 + 0.5 * 0.3,
|
|
4187
|
+
// blend with prior
|
|
4188
|
+
bayesianLower: wilsonLowerBound(
|
|
4189
|
+
Math.round(fa.acceptedCount * 0.5),
|
|
4190
|
+
Math.max(1, Math.round(fa.includedCount * 0.5))
|
|
4191
|
+
)
|
|
4192
|
+
};
|
|
4193
|
+
} else {
|
|
4194
|
+
const totalLocal = local.includedCount;
|
|
4195
|
+
const totalTeam = fa.includedCount;
|
|
4196
|
+
const combinedCount = totalLocal + Math.round(totalTeam * 0.3);
|
|
4197
|
+
const combinedAccepted = local.acceptedCount + Math.round(fa.acceptedCount * 0.3);
|
|
4198
|
+
local.includedCount = combinedCount;
|
|
4199
|
+
local.acceptedCount = combinedAccepted;
|
|
4200
|
+
local.acceptRate = combinedCount > 0 ? combinedAccepted / combinedCount : 0;
|
|
4201
|
+
local.ewmaAcceptRate = local.ewmaAcceptRate * 0.7 + fa.ewmaAcceptRate * 0.3;
|
|
4202
|
+
local.bayesianLower = wilsonLowerBound(combinedAccepted, combinedCount);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
localModel.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4206
|
+
await saveFeedbackModel(projectPath, localModel);
|
|
4207
|
+
return localModel;
|
|
4208
|
+
}
|
|
3968
4209
|
function renderFeedbackReport(model) {
|
|
3969
4210
|
const lines = [];
|
|
3970
4211
|
lines.push("");
|
|
@@ -3991,6 +4232,28 @@ function renderFeedbackReport(model) {
|
|
|
3991
4232
|
function pad3(s, w) {
|
|
3992
4233
|
return s.padEnd(w).substring(0, w);
|
|
3993
4234
|
}
|
|
4235
|
+
function renderCrossRepoReport(stats) {
|
|
4236
|
+
const lines = [];
|
|
4237
|
+
lines.push("");
|
|
4238
|
+
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");
|
|
4239
|
+
lines.push(" \u2551 \u{1F310} Cross-Repo Intelligence \u2551");
|
|
4240
|
+
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");
|
|
4241
|
+
lines.push(" \u2551 \u2551");
|
|
4242
|
+
lines.push(` \u2551 Projects learned: ${pad3(stats.totalProjects.toString(), 8)} \u2551`);
|
|
4243
|
+
lines.push(` \u2551 Total observations: ${pad3(stats.totalObservations.toString(), 8)} \u2551`);
|
|
4244
|
+
lines.push(` \u2551 Archetypes: ${pad3(stats.archetypes.length.toString(), 8)} \u2551`);
|
|
4245
|
+
lines.push(` \u2551 Universal patterns: ${pad3(stats.universalPatterns.toString(), 8)} \u2551`);
|
|
4246
|
+
lines.push(" \u2551 \u2551");
|
|
4247
|
+
if (stats.archetypes.length > 0) {
|
|
4248
|
+
lines.push(" \u2551 Archetypes: \u2551");
|
|
4249
|
+
for (const a of stats.archetypes.slice(0, 5)) {
|
|
4250
|
+
lines.push(` \u2551 ${pad3(a.name, 24)} ${pad3(a.observations + " obs", 12)} \u2551`);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
lines.push(" \u2551 \u2551");
|
|
4254
|
+
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");
|
|
4255
|
+
return lines.join("\n");
|
|
4256
|
+
}
|
|
3994
4257
|
|
|
3995
4258
|
// src/engine/semantic.ts
|
|
3996
4259
|
var DOMAIN_SIGNALS = {
|
|
@@ -4486,7 +4749,7 @@ function fmt3(n) {
|
|
|
4486
4749
|
}
|
|
4487
4750
|
|
|
4488
4751
|
// src/engine/compile-proof.ts
|
|
4489
|
-
import { resolve as resolve11, join as
|
|
4752
|
+
import { resolve as resolve11, join as join9, dirname as dirname3, basename as basename4 } from "path";
|
|
4490
4753
|
import { writeFile as writeFile6, mkdir as mkdir5, rm, cp } from "fs/promises";
|
|
4491
4754
|
import { execSync } from "child_process";
|
|
4492
4755
|
async function runCompileProof(analysis, task, budget = 5e4) {
|
|
@@ -4688,21 +4951,21 @@ function extractKnownTypes(analysis, typePaths) {
|
|
|
4688
4951
|
return types;
|
|
4689
4952
|
}
|
|
4690
4953
|
async function runTscWithContext(name, projectPath, selectedPaths, tokensUsed, typePaths, consumerCode) {
|
|
4691
|
-
const tmpDir =
|
|
4954
|
+
const tmpDir = join9(projectPath, ".cto", `compile-proof-${name.toLowerCase()}`);
|
|
4692
4955
|
try {
|
|
4693
4956
|
await rm(tmpDir, { recursive: true, force: true });
|
|
4694
4957
|
await mkdir5(tmpDir, { recursive: true });
|
|
4695
4958
|
for (const filePath of selectedPaths) {
|
|
4696
|
-
const src =
|
|
4697
|
-
const dest =
|
|
4959
|
+
const src = join9(projectPath, filePath);
|
|
4960
|
+
const dest = join9(tmpDir, filePath);
|
|
4698
4961
|
try {
|
|
4699
4962
|
await mkdir5(dirname3(dest), { recursive: true });
|
|
4700
4963
|
await cp(src, dest);
|
|
4701
4964
|
} catch {
|
|
4702
4965
|
}
|
|
4703
4966
|
}
|
|
4704
|
-
await writeFile6(
|
|
4705
|
-
await writeFile6(
|
|
4967
|
+
await writeFile6(join9(tmpDir, "_compile_test.ts"), consumerCode);
|
|
4968
|
+
await writeFile6(join9(tmpDir, "tsconfig.json"), JSON.stringify({
|
|
4706
4969
|
compilerOptions: {
|
|
4707
4970
|
target: "ES2022",
|
|
4708
4971
|
module: "nodenext",
|
|
@@ -4724,7 +4987,8 @@ async function runTscWithContext(name, projectPath, selectedPaths, tokensUsed, t
|
|
|
4724
4987
|
env: { ...process.env, NODE_ENV: "production" }
|
|
4725
4988
|
});
|
|
4726
4989
|
} catch (err) {
|
|
4727
|
-
|
|
4990
|
+
const e = err;
|
|
4991
|
+
tscOutput = e.stdout ?? (err instanceof Error ? err.message : "") ?? "";
|
|
4728
4992
|
}
|
|
4729
4993
|
const errorLines = tscOutput.split("\n").filter((l) => l.includes("error TS")).map((l) => l.trim());
|
|
4730
4994
|
compileErrors = errorLines.length;
|
|
@@ -5008,10 +5272,87 @@ function fmt5(n) {
|
|
|
5008
5272
|
return n.toString();
|
|
5009
5273
|
}
|
|
5010
5274
|
|
|
5275
|
+
// src/engine/logger.ts
|
|
5276
|
+
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
5277
|
+
var currentLevel = process.env.CTO_LOG_LEVEL ?? "warn";
|
|
5278
|
+
var jsonOutput = process.env.CTO_LOG_JSON === "1";
|
|
5279
|
+
function setLogLevel(level) {
|
|
5280
|
+
currentLevel = level;
|
|
5281
|
+
}
|
|
5282
|
+
function setJsonLogging(enabled) {
|
|
5283
|
+
jsonOutput = enabled;
|
|
5284
|
+
}
|
|
5285
|
+
function shouldLog(level) {
|
|
5286
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
|
|
5287
|
+
}
|
|
5288
|
+
function emit(entry) {
|
|
5289
|
+
if (!shouldLog(entry.level)) return;
|
|
5290
|
+
if (jsonOutput) {
|
|
5291
|
+
const stream = entry.level === "error" ? process.stderr : process.stdout;
|
|
5292
|
+
stream.write(JSON.stringify(entry) + "\n");
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
const prefix = entry.module ? `[${entry.module}]` : "";
|
|
5296
|
+
const extra = Object.entries(entry).filter(([k]) => !["level", "msg", "ts", "module"].includes(k)).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(" ");
|
|
5297
|
+
const line = `${prefix} ${entry.msg}${extra ? " " + extra : ""}`.trim();
|
|
5298
|
+
if (entry.level === "error") {
|
|
5299
|
+
process.stderr.write(` \u274C ${line}
|
|
5300
|
+
`);
|
|
5301
|
+
} else if (entry.level === "warn") {
|
|
5302
|
+
process.stderr.write(` \u26A0\uFE0F ${line}
|
|
5303
|
+
`);
|
|
5304
|
+
} else {
|
|
5305
|
+
process.stdout.write(` ${line}
|
|
5306
|
+
`);
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
function createLogger(module) {
|
|
5310
|
+
const log = (level, msg, data) => {
|
|
5311
|
+
emit({ level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), module, ...data });
|
|
5312
|
+
};
|
|
5313
|
+
return {
|
|
5314
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
5315
|
+
info: (msg, data) => log("info", msg, data),
|
|
5316
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
5317
|
+
error: (msg, data) => log("error", msg, data)
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
// src/engine/errors.ts
|
|
5322
|
+
var CtoError = class extends Error {
|
|
5323
|
+
code;
|
|
5324
|
+
module;
|
|
5325
|
+
context;
|
|
5326
|
+
constructor(code, message, module, context) {
|
|
5327
|
+
super(message);
|
|
5328
|
+
this.name = "CtoError";
|
|
5329
|
+
this.code = code;
|
|
5330
|
+
this.module = module;
|
|
5331
|
+
this.context = context;
|
|
5332
|
+
}
|
|
5333
|
+
toJSON() {
|
|
5334
|
+
return {
|
|
5335
|
+
name: this.name,
|
|
5336
|
+
code: this.code,
|
|
5337
|
+
message: this.message,
|
|
5338
|
+
module: this.module,
|
|
5339
|
+
context: this.context
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
function isCtoError(err) {
|
|
5344
|
+
return err instanceof CtoError;
|
|
5345
|
+
}
|
|
5346
|
+
function wrapError(err, code, module, context) {
|
|
5347
|
+
if (isCtoError(err)) return err;
|
|
5348
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5349
|
+
return new CtoError(code, message, module, context);
|
|
5350
|
+
}
|
|
5351
|
+
|
|
5011
5352
|
// src/engine/monorepo.ts
|
|
5012
5353
|
import { readFile as readFile11, readdir as readdir4 } from "fs/promises";
|
|
5013
|
-
import { join as
|
|
5014
|
-
import { existsSync as
|
|
5354
|
+
import { join as join10, relative as relative6, basename as basename5 } from "path";
|
|
5355
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5015
5356
|
async function detectMonorepoTool(rootPath) {
|
|
5016
5357
|
const checks = [
|
|
5017
5358
|
{ file: "nx.json", tool: "nx" },
|
|
@@ -5032,14 +5373,14 @@ async function detectMonorepoTool(rootPath) {
|
|
|
5032
5373
|
}
|
|
5033
5374
|
];
|
|
5034
5375
|
for (const check of checks) {
|
|
5035
|
-
const filePath =
|
|
5036
|
-
if (
|
|
5376
|
+
const filePath = join10(rootPath, check.file);
|
|
5377
|
+
if (existsSync4(filePath)) {
|
|
5037
5378
|
if (!check.validate) return check.tool;
|
|
5038
5379
|
try {
|
|
5039
5380
|
const content = await readFile11(filePath, "utf-8");
|
|
5040
5381
|
if (check.validate(content)) {
|
|
5041
5382
|
if (check.tool === "npm-workspaces") {
|
|
5042
|
-
if (
|
|
5383
|
+
if (existsSync4(join10(rootPath, "yarn.lock"))) return "yarn-workspaces";
|
|
5043
5384
|
return "npm-workspaces";
|
|
5044
5385
|
}
|
|
5045
5386
|
return check.tool;
|
|
@@ -5054,15 +5395,15 @@ async function resolveWorkspaceGlobs(rootPath, globs) {
|
|
|
5054
5395
|
const packagePaths = [];
|
|
5055
5396
|
for (const glob of globs) {
|
|
5056
5397
|
const cleanGlob = glob.replace(/\/?\*\*?$/, "");
|
|
5057
|
-
const searchDir =
|
|
5058
|
-
if (!
|
|
5398
|
+
const searchDir = join10(rootPath, cleanGlob);
|
|
5399
|
+
if (!existsSync4(searchDir)) continue;
|
|
5059
5400
|
try {
|
|
5060
5401
|
const entries = await readdir4(searchDir, { withFileTypes: true });
|
|
5061
5402
|
for (const entry of entries) {
|
|
5062
5403
|
if (!entry.isDirectory()) continue;
|
|
5063
|
-
const pkgJsonPath =
|
|
5064
|
-
if (
|
|
5065
|
-
packagePaths.push(
|
|
5404
|
+
const pkgJsonPath = join10(searchDir, entry.name, "package.json");
|
|
5405
|
+
if (existsSync4(pkgJsonPath)) {
|
|
5406
|
+
packagePaths.push(join10(searchDir, entry.name));
|
|
5066
5407
|
}
|
|
5067
5408
|
}
|
|
5068
5409
|
} catch {
|
|
@@ -5074,12 +5415,12 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5074
5415
|
switch (tool) {
|
|
5075
5416
|
case "npm-workspaces":
|
|
5076
5417
|
case "yarn-workspaces": {
|
|
5077
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5418
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5078
5419
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5079
5420
|
return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5080
5421
|
}
|
|
5081
5422
|
case "pnpm-workspaces": {
|
|
5082
|
-
const content = await readFile11(
|
|
5423
|
+
const content = await readFile11(join10(rootPath, "pnpm-workspace.yaml"), "utf-8");
|
|
5083
5424
|
const packages = [];
|
|
5084
5425
|
let inPackages = false;
|
|
5085
5426
|
for (const line of content.split("\n")) {
|
|
@@ -5097,20 +5438,20 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5097
5438
|
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5098
5439
|
}
|
|
5099
5440
|
case "turborepo": {
|
|
5100
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5441
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5101
5442
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5102
5443
|
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5103
|
-
if (
|
|
5444
|
+
if (existsSync4(join10(rootPath, "pnpm-workspace.yaml"))) {
|
|
5104
5445
|
return discoverPackages(rootPath, "pnpm-workspaces");
|
|
5105
5446
|
}
|
|
5106
5447
|
return [];
|
|
5107
5448
|
}
|
|
5108
5449
|
case "nx": {
|
|
5109
5450
|
const standardDirs = ["packages", "apps", "libs"];
|
|
5110
|
-
const globs = standardDirs.filter((d) =>
|
|
5451
|
+
const globs = standardDirs.filter((d) => existsSync4(join10(rootPath, d)));
|
|
5111
5452
|
if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
|
|
5112
5453
|
try {
|
|
5113
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5454
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5114
5455
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
|
|
5115
5456
|
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5116
5457
|
} catch {
|
|
@@ -5118,7 +5459,7 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5118
5459
|
return [];
|
|
5119
5460
|
}
|
|
5120
5461
|
case "lerna": {
|
|
5121
|
-
const lernaJson = JSON.parse(await readFile11(
|
|
5462
|
+
const lernaJson = JSON.parse(await readFile11(join10(rootPath, "lerna.json"), "utf-8"));
|
|
5122
5463
|
const packages = lernaJson.packages || ["packages/*"];
|
|
5123
5464
|
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5124
5465
|
}
|
|
@@ -5172,7 +5513,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
5172
5513
|
const packages = [];
|
|
5173
5514
|
const packageTokenMap = {};
|
|
5174
5515
|
for (const pkgPath of packagePaths) {
|
|
5175
|
-
const pkgJsonPath =
|
|
5516
|
+
const pkgJsonPath = join10(pkgPath, "package.json");
|
|
5176
5517
|
let name = basename5(pkgPath);
|
|
5177
5518
|
let pkgDeps = [];
|
|
5178
5519
|
try {
|
|
@@ -5215,7 +5556,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
5215
5556
|
}
|
|
5216
5557
|
const pkgNames = new Set(packages.map((p) => p.name));
|
|
5217
5558
|
for (const pkg of packages) {
|
|
5218
|
-
const pkgJsonPath =
|
|
5559
|
+
const pkgJsonPath = join10(pkg.path, "package.json");
|
|
5219
5560
|
try {
|
|
5220
5561
|
const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
|
|
5221
5562
|
const allDeps = {
|
|
@@ -5369,7 +5710,7 @@ function renderPackageContext(result) {
|
|
|
5369
5710
|
// src/engine/quality-gate.ts
|
|
5370
5711
|
import { readFile as readFile12, writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
|
|
5371
5712
|
import { resolve as resolve13 } from "path";
|
|
5372
|
-
import { existsSync as
|
|
5713
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5373
5714
|
var DEFAULT_GATE_CONFIG = {
|
|
5374
5715
|
threshold: 70,
|
|
5375
5716
|
failOnSecrets: true,
|
|
@@ -5380,7 +5721,7 @@ var DEFAULT_GATE_CONFIG = {
|
|
|
5380
5721
|
};
|
|
5381
5722
|
async function loadBaseline(projectPath, baselinePath) {
|
|
5382
5723
|
const filePath = resolve13(projectPath, baselinePath || ".cto/baseline.json");
|
|
5383
|
-
if (!
|
|
5724
|
+
if (!existsSync5(filePath)) return null;
|
|
5384
5725
|
try {
|
|
5385
5726
|
const content = await readFile12(filePath, "utf-8");
|
|
5386
5727
|
return JSON.parse(content);
|
|
@@ -5390,7 +5731,7 @@ async function loadBaseline(projectPath, baselinePath) {
|
|
|
5390
5731
|
}
|
|
5391
5732
|
async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
|
|
5392
5733
|
const dir = resolve13(projectPath, ".cto");
|
|
5393
|
-
if (!
|
|
5734
|
+
if (!existsSync5(dir)) await mkdir6(dir, { recursive: true });
|
|
5394
5735
|
const baseline = {
|
|
5395
5736
|
score: score.overall,
|
|
5396
5737
|
grade: score.grade,
|
|
@@ -5537,10 +5878,570 @@ Warnings: ${warnings.map((c) => c.name).join(", ")}`;
|
|
|
5537
5878
|
}
|
|
5538
5879
|
return summary;
|
|
5539
5880
|
}
|
|
5881
|
+
|
|
5882
|
+
// src/engine/code-review.ts
|
|
5883
|
+
import { resolve as resolve14, basename as basename6, dirname as dirname5 } from "path";
|
|
5884
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
5885
|
+
import { execFile as execFile2 } from "child_process";
|
|
5886
|
+
import { promisify as promisify2 } from "util";
|
|
5887
|
+
var exec2 = promisify2(execFile2);
|
|
5888
|
+
async function git2(args, cwd) {
|
|
5889
|
+
try {
|
|
5890
|
+
const { stdout } = await exec2("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
5891
|
+
return stdout.trim();
|
|
5892
|
+
} catch {
|
|
5893
|
+
return "";
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
async function analyzeForReview(analysis, options = {}) {
|
|
5897
|
+
const projectPath = resolve14(analysis.projectPath);
|
|
5898
|
+
const baseBranch = options.baseBranch ?? "main";
|
|
5899
|
+
const depth = options.depth ?? 2;
|
|
5900
|
+
const maxPromptFiles = options.maxPromptFiles ?? 20;
|
|
5901
|
+
const isRepo = await git2(["rev-parse", "--is-inside-work-tree"], projectPath) === "true";
|
|
5902
|
+
if (!isRepo) {
|
|
5903
|
+
return emptyResult3(baseBranch);
|
|
5904
|
+
}
|
|
5905
|
+
const branch = await git2(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
|
|
5906
|
+
const changedFiles = await getChangedFilesWithHunks(projectPath, baseBranch, analysis);
|
|
5907
|
+
if (changedFiles.length === 0) {
|
|
5908
|
+
return {
|
|
5909
|
+
...emptyResult3(baseBranch),
|
|
5910
|
+
branch,
|
|
5911
|
+
isGitRepo: true,
|
|
5912
|
+
renderedSummary: "# Code Review\n\nNo changed files detected."
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
const totalLinesChanged = changedFiles.reduce((s, f) => s + f.linesAdded + f.linesRemoved, 0);
|
|
5916
|
+
const breakingChanges = detectBreakingChanges(changedFiles, analysis);
|
|
5917
|
+
const missingFiles = findMissingFiles(changedFiles, analysis, depth);
|
|
5918
|
+
const impactRadius = computeImpactRadius(changedFiles, analysis, depth);
|
|
5919
|
+
const reviewQuality = calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged);
|
|
5920
|
+
const reviewPrompt = await generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxPromptFiles);
|
|
5921
|
+
const renderedSummary = renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality);
|
|
5922
|
+
return {
|
|
5923
|
+
branch,
|
|
5924
|
+
baseBranch,
|
|
5925
|
+
isGitRepo: true,
|
|
5926
|
+
changedFiles,
|
|
5927
|
+
totalLinesChanged,
|
|
5928
|
+
breakingChanges,
|
|
5929
|
+
missingFiles,
|
|
5930
|
+
impactRadius,
|
|
5931
|
+
reviewQuality,
|
|
5932
|
+
reviewPrompt,
|
|
5933
|
+
renderedSummary
|
|
5934
|
+
};
|
|
5935
|
+
}
|
|
5936
|
+
async function getChangedFilesWithHunks(projectPath, baseBranch, analysis) {
|
|
5937
|
+
const files = [];
|
|
5938
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
5939
|
+
const numstat = await git2(["diff", "--numstat", "HEAD"], projectPath);
|
|
5940
|
+
const branchNumstat = await git2(["diff", "--numstat", `${baseBranch}...HEAD`], projectPath);
|
|
5941
|
+
const nameStatus = await git2(["diff", "--name-status", `${baseBranch}...HEAD`], projectPath);
|
|
5942
|
+
const changeTypes = /* @__PURE__ */ new Map();
|
|
5943
|
+
for (const line of nameStatus.split("\n")) {
|
|
5944
|
+
const parts = line.trim().split(" ");
|
|
5945
|
+
if (parts.length < 2) continue;
|
|
5946
|
+
const status = parts[0];
|
|
5947
|
+
const filePath = parts[parts.length - 1];
|
|
5948
|
+
if (status === "A") changeTypes.set(filePath, "added");
|
|
5949
|
+
else if (status === "D") changeTypes.set(filePath, "deleted");
|
|
5950
|
+
else if (status.startsWith("R")) changeTypes.set(filePath, "renamed");
|
|
5951
|
+
else changeTypes.set(filePath, "modified");
|
|
5952
|
+
}
|
|
5953
|
+
const allNumstat = (numstat + "\n" + branchNumstat).split("\n");
|
|
5954
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5955
|
+
for (const line of allNumstat) {
|
|
5956
|
+
const parts = line.trim().split(" ");
|
|
5957
|
+
if (parts.length < 3) continue;
|
|
5958
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5959
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5960
|
+
const filePath = parts[2];
|
|
5961
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5962
|
+
seen.add(filePath);
|
|
5963
|
+
const af = fileMap.get(filePath);
|
|
5964
|
+
const changeType = changeTypes.get(filePath) ?? "modified";
|
|
5965
|
+
const hunks = await parseDiffHunks(projectPath, baseBranch, filePath);
|
|
5966
|
+
const hasExportChanges = hunks.some(
|
|
5967
|
+
(h) => h.additions.some((l) => /^\s*export\s/.test(l)) || h.deletions.some((l) => /^\s*export\s/.test(l))
|
|
5968
|
+
);
|
|
5969
|
+
const hasTypeChanges = hunks.some(
|
|
5970
|
+
(h) => h.additions.some((l) => /^\s*(interface|type|enum)\s/.test(l)) || h.deletions.some((l) => /^\s*(interface|type|enum)\s/.test(l))
|
|
5971
|
+
);
|
|
5972
|
+
files.push({
|
|
5973
|
+
relativePath: filePath,
|
|
5974
|
+
changeType,
|
|
5975
|
+
linesAdded: added,
|
|
5976
|
+
linesRemoved: removed,
|
|
5977
|
+
riskScore: af?.riskScore ?? 0,
|
|
5978
|
+
kind: af?.kind ?? "unknown",
|
|
5979
|
+
hunks,
|
|
5980
|
+
hasExportChanges,
|
|
5981
|
+
hasTypeChanges
|
|
5982
|
+
});
|
|
5983
|
+
}
|
|
5984
|
+
const workingNumstat = await git2(["diff", "--numstat"], projectPath);
|
|
5985
|
+
for (const line of workingNumstat.split("\n")) {
|
|
5986
|
+
const parts = line.trim().split(" ");
|
|
5987
|
+
if (parts.length < 3) continue;
|
|
5988
|
+
const filePath = parts[2];
|
|
5989
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5990
|
+
seen.add(filePath);
|
|
5991
|
+
const af = fileMap.get(filePath);
|
|
5992
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5993
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5994
|
+
files.push({
|
|
5995
|
+
relativePath: filePath,
|
|
5996
|
+
changeType: "modified",
|
|
5997
|
+
linesAdded: added,
|
|
5998
|
+
linesRemoved: removed,
|
|
5999
|
+
riskScore: af?.riskScore ?? 0,
|
|
6000
|
+
kind: af?.kind ?? "unknown",
|
|
6001
|
+
hunks: [],
|
|
6002
|
+
hasExportChanges: false,
|
|
6003
|
+
hasTypeChanges: false
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
return files.sort((a, b) => b.riskScore - a.riskScore);
|
|
6007
|
+
}
|
|
6008
|
+
async function parseDiffHunks(projectPath, baseBranch, filePath) {
|
|
6009
|
+
const diff = await git2(["diff", "-U3", `${baseBranch}...HEAD`, "--", filePath], projectPath);
|
|
6010
|
+
if (!diff) return [];
|
|
6011
|
+
const hunks = [];
|
|
6012
|
+
const lines = diff.split("\n");
|
|
6013
|
+
let currentHunk = null;
|
|
6014
|
+
for (const line of lines) {
|
|
6015
|
+
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@\s*(.*)/);
|
|
6016
|
+
if (hunkMatch) {
|
|
6017
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
6018
|
+
currentHunk = {
|
|
6019
|
+
startLine: parseInt(hunkMatch[2], 10),
|
|
6020
|
+
endLine: parseInt(hunkMatch[2], 10),
|
|
6021
|
+
header: hunkMatch[3] || "",
|
|
6022
|
+
additions: [],
|
|
6023
|
+
deletions: []
|
|
6024
|
+
};
|
|
6025
|
+
continue;
|
|
6026
|
+
}
|
|
6027
|
+
if (!currentHunk) continue;
|
|
6028
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
6029
|
+
currentHunk.additions.push(line.substring(1));
|
|
6030
|
+
currentHunk.endLine++;
|
|
6031
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
6032
|
+
currentHunk.deletions.push(line.substring(1));
|
|
6033
|
+
} else if (!line.startsWith("\\")) {
|
|
6034
|
+
if (currentHunk) currentHunk.endLine++;
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
6038
|
+
return hunks;
|
|
6039
|
+
}
|
|
6040
|
+
function detectBreakingChanges(changedFiles, analysis) {
|
|
6041
|
+
const breaks = [];
|
|
6042
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
6043
|
+
for (const file of changedFiles) {
|
|
6044
|
+
if (file.changeType === "deleted") {
|
|
6045
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
6046
|
+
if (dependents.length > 0) {
|
|
6047
|
+
breaks.push({
|
|
6048
|
+
file: file.relativePath,
|
|
6049
|
+
type: "export-removed",
|
|
6050
|
+
severity: "critical",
|
|
6051
|
+
description: `File deleted but ${dependents.length} files depend on it`,
|
|
6052
|
+
affectedFiles: dependents
|
|
6053
|
+
});
|
|
6054
|
+
}
|
|
6055
|
+
continue;
|
|
6056
|
+
}
|
|
6057
|
+
for (const hunk of file.hunks) {
|
|
6058
|
+
for (const del of hunk.deletions) {
|
|
6059
|
+
const exportMatch = del.match(/^\s*export\s+(function|const|class|type|interface|enum)\s+(\w+)/);
|
|
6060
|
+
if (exportMatch) {
|
|
6061
|
+
const [, kind, name] = exportMatch;
|
|
6062
|
+
const wasReAdded = hunk.additions.some(
|
|
6063
|
+
(a) => new RegExp(`export\\s+${kind}\\s+${name}\\b`).test(a)
|
|
6064
|
+
);
|
|
6065
|
+
if (!wasReAdded) {
|
|
6066
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
6067
|
+
breaks.push({
|
|
6068
|
+
file: file.relativePath,
|
|
6069
|
+
type: kind === "type" || kind === "interface" ? "type-changed" : "export-removed",
|
|
6070
|
+
severity: dependents.length > 3 ? "critical" : dependents.length > 0 ? "high" : "medium",
|
|
6071
|
+
description: `Removed export ${kind} ${name}`,
|
|
6072
|
+
affectedFiles: dependents
|
|
6073
|
+
});
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
const propMatch = del.match(/^\s+(\w+)\s*[?:].*[;,]?\s*$/);
|
|
6077
|
+
if (propMatch && file.hasTypeChanges) {
|
|
6078
|
+
const propName = propMatch[1];
|
|
6079
|
+
const wasReAdded = hunk.additions.some(
|
|
6080
|
+
(a) => new RegExp(`\\b${propName}\\s*[?:]`).test(a)
|
|
6081
|
+
);
|
|
6082
|
+
if (!wasReAdded) {
|
|
6083
|
+
breaks.push({
|
|
6084
|
+
file: file.relativePath,
|
|
6085
|
+
type: "interface-changed",
|
|
6086
|
+
severity: "high",
|
|
6087
|
+
description: `Removed property "${propName}" from type/interface`,
|
|
6088
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
for (const add of hunk.additions) {
|
|
6094
|
+
const fnMatch = add.match(/^\s*export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
|
|
6095
|
+
if (fnMatch) {
|
|
6096
|
+
const [, , fnName, newParams] = fnMatch;
|
|
6097
|
+
for (const del of hunk.deletions) {
|
|
6098
|
+
const origMatch = del.match(new RegExp(`export\\s+(async\\s+)?function\\s+${fnName}\\s*\\(([^)]*)\\)`));
|
|
6099
|
+
if (origMatch) {
|
|
6100
|
+
const oldParams = origMatch[2];
|
|
6101
|
+
if (oldParams !== newParams) {
|
|
6102
|
+
const oldCount = oldParams.split(",").filter((p) => p.trim()).length;
|
|
6103
|
+
const newCount = newParams.split(",").filter((p) => p.trim()).length;
|
|
6104
|
+
if (oldCount !== newCount || !paramsCompatible(oldParams, newParams)) {
|
|
6105
|
+
breaks.push({
|
|
6106
|
+
file: file.relativePath,
|
|
6107
|
+
type: "function-signature",
|
|
6108
|
+
severity: "high",
|
|
6109
|
+
description: `Function "${fnName}" signature changed: (${oldParams.trim()}) \u2192 (${newParams.trim()})`,
|
|
6110
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
6111
|
+
});
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
return breaks.sort((a, b) => {
|
|
6121
|
+
const sev = { critical: 0, high: 1, medium: 2 };
|
|
6122
|
+
return sev[a.severity] - sev[b.severity];
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
function paramsCompatible(oldParams, newParams) {
|
|
6126
|
+
const oldParts = oldParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
6127
|
+
const newParts = newParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
6128
|
+
let j = 0;
|
|
6129
|
+
for (let i = 0; i < oldParts.length && j < newParts.length; i++) {
|
|
6130
|
+
if (oldParts[i] === newParts[j]) j++;
|
|
6131
|
+
}
|
|
6132
|
+
return j >= oldParts.length;
|
|
6133
|
+
}
|
|
6134
|
+
function findDependents(filePath, analysis) {
|
|
6135
|
+
return analysis.files.filter((f) => f.imports.includes(filePath) || f.imports.some((imp) => imp.endsWith(filePath))).map((f) => f.relativePath);
|
|
6136
|
+
}
|
|
6137
|
+
function findMissingFiles(changedFiles, analysis, depth) {
|
|
6138
|
+
const missing = [];
|
|
6139
|
+
const changedPaths = new Set(changedFiles.map((f) => f.relativePath));
|
|
6140
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
6141
|
+
for (const changed of changedFiles) {
|
|
6142
|
+
if (changed.changeType === "deleted") continue;
|
|
6143
|
+
const af = fileMap.get(changed.relativePath);
|
|
6144
|
+
if (!af) continue;
|
|
6145
|
+
if (changed.hasTypeChanges || changed.hasExportChanges) {
|
|
6146
|
+
const dir2 = dirname5(changed.relativePath);
|
|
6147
|
+
const base = basename6(changed.relativePath).replace(/\.[^.]+$/, "");
|
|
6148
|
+
const typeVariants = [
|
|
6149
|
+
`${dir2}/${base}.types.ts`,
|
|
6150
|
+
`${dir2}/${base}.types.tsx`,
|
|
6151
|
+
`${dir2}/types.ts`,
|
|
6152
|
+
`${dir2}/types/${base}.ts`,
|
|
6153
|
+
`${dir2}/index.d.ts`
|
|
6154
|
+
];
|
|
6155
|
+
for (const variant of typeVariants) {
|
|
6156
|
+
if (fileMap.has(variant) && !changedPaths.has(variant)) {
|
|
6157
|
+
missing.push({
|
|
6158
|
+
file: variant,
|
|
6159
|
+
reason: `Type file for ${changed.relativePath} \u2014 may need updates`,
|
|
6160
|
+
severity: changed.hasExportChanges ? "high" : "medium",
|
|
6161
|
+
relatedChangedFile: changed.relativePath,
|
|
6162
|
+
relationship: "sibling-type"
|
|
6163
|
+
});
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
const testVariants = [
|
|
6168
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".test.$1"),
|
|
6169
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".spec.$1"),
|
|
6170
|
+
changed.relativePath.replace(/^src\//, "tests/").replace(/\.([^.]+)$/, ".test.$1"),
|
|
6171
|
+
changed.relativePath.replace(/^src\//, "__tests__/").replace(/\.([^.]+)$/, ".test.$1")
|
|
6172
|
+
];
|
|
6173
|
+
for (const testPath of testVariants) {
|
|
6174
|
+
if (fileMap.has(testPath) && !changedPaths.has(testPath)) {
|
|
6175
|
+
missing.push({
|
|
6176
|
+
file: testPath,
|
|
6177
|
+
reason: `Test file for ${changed.relativePath} \u2014 should be updated`,
|
|
6178
|
+
severity: "medium",
|
|
6179
|
+
relatedChangedFile: changed.relativePath,
|
|
6180
|
+
relationship: "test"
|
|
6181
|
+
});
|
|
6182
|
+
break;
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
if (changed.hasExportChanges) {
|
|
6186
|
+
const importers = analysis.files.filter(
|
|
6187
|
+
(f) => f.imports.includes(af.relativePath) && !changedPaths.has(f.relativePath)
|
|
6188
|
+
);
|
|
6189
|
+
for (const importer of importers.slice(0, 5)) {
|
|
6190
|
+
missing.push({
|
|
6191
|
+
file: importer.relativePath,
|
|
6192
|
+
reason: `Imports ${changed.relativePath} which has export changes`,
|
|
6193
|
+
severity: "high",
|
|
6194
|
+
relatedChangedFile: changed.relativePath,
|
|
6195
|
+
relationship: "imported-by"
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
const dir = dirname5(changed.relativePath);
|
|
6200
|
+
const indexFile = `${dir}/index.ts`;
|
|
6201
|
+
if (fileMap.has(indexFile) && !changedPaths.has(indexFile) && changed.hasExportChanges) {
|
|
6202
|
+
missing.push({
|
|
6203
|
+
file: indexFile,
|
|
6204
|
+
reason: `Barrel export may need updating after changes to ${changed.relativePath}`,
|
|
6205
|
+
severity: "medium",
|
|
6206
|
+
relatedChangedFile: changed.relativePath,
|
|
6207
|
+
relationship: "co-located"
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6212
|
+
return missing.filter((m) => {
|
|
6213
|
+
if (seen.has(m.file)) return false;
|
|
6214
|
+
seen.add(m.file);
|
|
6215
|
+
return true;
|
|
6216
|
+
}).sort((a, b) => {
|
|
6217
|
+
const sev = { high: 0, medium: 1, low: 2 };
|
|
6218
|
+
return sev[a.severity] - sev[b.severity];
|
|
6219
|
+
});
|
|
6220
|
+
}
|
|
6221
|
+
function computeImpactRadius(changedFiles, analysis, depth) {
|
|
6222
|
+
const changedPaths = changedFiles.map((f) => f.relativePath);
|
|
6223
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
6224
|
+
const direct = /* @__PURE__ */ new Set();
|
|
6225
|
+
for (const path of changedPaths) {
|
|
6226
|
+
const importers = adj.reverse.get(path) ?? [];
|
|
6227
|
+
const imports = adj.forward.get(path) ?? [];
|
|
6228
|
+
for (const n of [...importers, ...imports]) {
|
|
6229
|
+
if (!changedPaths.includes(n)) direct.add(n);
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
const allAffected = bfsBidirectional(changedPaths, adj, depth);
|
|
6233
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
6234
|
+
for (const path of allAffected) {
|
|
6235
|
+
if (!changedPaths.includes(path) && !direct.has(path)) {
|
|
6236
|
+
transitive.add(path);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
const affectedTests = [...allAffected].filter((p) => {
|
|
6240
|
+
const f = analysis.files.find((af) => af.relativePath === p);
|
|
6241
|
+
return f?.kind === "test";
|
|
6242
|
+
}).length;
|
|
6243
|
+
const hotspots = changedFiles.map((f) => ({
|
|
6244
|
+
file: f.relativePath,
|
|
6245
|
+
dependents: adj.reverse.get(f.relativePath)?.length ?? 0,
|
|
6246
|
+
riskScore: f.riskScore
|
|
6247
|
+
})).sort((a, b) => b.dependents * b.riskScore - a.dependents * a.riskScore).slice(0, 5);
|
|
6248
|
+
const totalAffected = direct.size + transitive.size;
|
|
6249
|
+
const maxRisk = Math.max(...changedFiles.map((f) => f.riskScore), 0);
|
|
6250
|
+
const avgRisk = changedFiles.length > 0 ? changedFiles.reduce((s, f) => s + f.riskScore, 0) / changedFiles.length : 0;
|
|
6251
|
+
const riskScore = Math.min(100, Math.round(
|
|
6252
|
+
avgRisk * 0.3 + maxRisk * 0.2 + Math.min(100, totalAffected * 3) * 0.3 + Math.min(100, changedFiles.length * 5) * 0.2
|
|
6253
|
+
));
|
|
6254
|
+
return {
|
|
6255
|
+
directlyAffected: direct.size,
|
|
6256
|
+
transitivelyAffected: transitive.size,
|
|
6257
|
+
totalAffected,
|
|
6258
|
+
affectedTests,
|
|
6259
|
+
riskScore,
|
|
6260
|
+
hotspots
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
function calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged) {
|
|
6264
|
+
const factors = [];
|
|
6265
|
+
const sizeScore = totalLinesChanged <= 50 ? 100 : totalLinesChanged <= 200 ? 85 : totalLinesChanged <= 500 ? 65 : totalLinesChanged <= 1e3 ? 40 : 20;
|
|
6266
|
+
factors.push({
|
|
6267
|
+
name: "PR Size",
|
|
6268
|
+
score: sizeScore,
|
|
6269
|
+
weight: 0.25,
|
|
6270
|
+
detail: `${totalLinesChanged} lines changed \u2014 ${sizeScore >= 80 ? "easy" : sizeScore >= 50 ? "manageable" : "too large"} to review`
|
|
6271
|
+
});
|
|
6272
|
+
const focusScore = changedFiles.length <= 3 ? 100 : changedFiles.length <= 8 ? 80 : changedFiles.length <= 15 ? 55 : 25;
|
|
6273
|
+
factors.push({
|
|
6274
|
+
name: "Focus",
|
|
6275
|
+
score: focusScore,
|
|
6276
|
+
weight: 0.2,
|
|
6277
|
+
detail: `${changedFiles.length} files \u2014 ${focusScore >= 80 ? "focused" : focusScore >= 50 ? "moderate scope" : "unfocused"}`
|
|
6278
|
+
});
|
|
6279
|
+
const criticalBreaks = breakingChanges.filter((b) => b.severity === "critical").length;
|
|
6280
|
+
const highBreaks = breakingChanges.filter((b) => b.severity === "high").length;
|
|
6281
|
+
const breakScore = criticalBreaks > 0 ? 10 : highBreaks > 2 ? 30 : highBreaks > 0 ? 60 : breakingChanges.length > 0 ? 80 : 100;
|
|
6282
|
+
factors.push({
|
|
6283
|
+
name: "Breaking Changes",
|
|
6284
|
+
score: breakScore,
|
|
6285
|
+
weight: 0.25,
|
|
6286
|
+
detail: `${breakingChanges.length} breaking changes (${criticalBreaks} critical, ${highBreaks} high)`
|
|
6287
|
+
});
|
|
6288
|
+
const highMissing = missingFiles.filter((m) => m.severity === "high").length;
|
|
6289
|
+
const completenessScore = highMissing > 3 ? 20 : highMissing > 0 ? 50 : missingFiles.length > 3 ? 65 : missingFiles.length > 0 ? 80 : 100;
|
|
6290
|
+
factors.push({
|
|
6291
|
+
name: "Completeness",
|
|
6292
|
+
score: completenessScore,
|
|
6293
|
+
weight: 0.15,
|
|
6294
|
+
detail: `${missingFiles.length} potentially missing files (${highMissing} high priority)`
|
|
6295
|
+
});
|
|
6296
|
+
const radiusScore = impactRadius.totalAffected <= 5 ? 100 : impactRadius.totalAffected <= 15 ? 75 : impactRadius.totalAffected <= 30 ? 50 : 25;
|
|
6297
|
+
factors.push({
|
|
6298
|
+
name: "Blast Radius",
|
|
6299
|
+
score: radiusScore,
|
|
6300
|
+
weight: 0.15,
|
|
6301
|
+
detail: `${impactRadius.totalAffected} files affected (${impactRadius.directlyAffected} direct, ${impactRadius.transitivelyAffected} transitive)`
|
|
6302
|
+
});
|
|
6303
|
+
const overall = Math.round(factors.reduce((s, f) => s + f.score * f.weight, 0));
|
|
6304
|
+
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";
|
|
6305
|
+
return { score: overall, grade, factors };
|
|
6306
|
+
}
|
|
6307
|
+
async function generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxFiles) {
|
|
6308
|
+
const lines = [];
|
|
6309
|
+
lines.push("# Code Review Context");
|
|
6310
|
+
lines.push("");
|
|
6311
|
+
lines.push("## Project: " + analysis.projectName);
|
|
6312
|
+
lines.push("## Stack: " + analysis.stack.join(", "));
|
|
6313
|
+
lines.push("");
|
|
6314
|
+
if (breakingChanges.length > 0) {
|
|
6315
|
+
lines.push("## \u26A0\uFE0F BREAKING CHANGES DETECTED");
|
|
6316
|
+
lines.push("");
|
|
6317
|
+
for (const bc of breakingChanges) {
|
|
6318
|
+
lines.push("- **" + bc.severity.toUpperCase() + "** " + bc.file + ": " + bc.description);
|
|
6319
|
+
if (bc.affectedFiles.length > 0) {
|
|
6320
|
+
lines.push(" Affected: " + bc.affectedFiles.slice(0, 5).join(", "));
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
lines.push("");
|
|
6324
|
+
}
|
|
6325
|
+
if (missingFiles.length > 0) {
|
|
6326
|
+
lines.push("## \u{1F4CB} Potentially Missing Files");
|
|
6327
|
+
lines.push("");
|
|
6328
|
+
for (const mf of missingFiles.slice(0, 10)) {
|
|
6329
|
+
lines.push("- " + mf.file + " \u2014 " + mf.reason);
|
|
6330
|
+
}
|
|
6331
|
+
lines.push("");
|
|
6332
|
+
}
|
|
6333
|
+
lines.push("## Changed Files");
|
|
6334
|
+
lines.push("");
|
|
6335
|
+
const topFiles = changedFiles.slice(0, maxFiles);
|
|
6336
|
+
for (const file of topFiles) {
|
|
6337
|
+
const icon = file.changeType === "added" ? "\u{1F195}" : file.changeType === "deleted" ? "\u{1F5D1}\uFE0F" : "\u{1F4DD}";
|
|
6338
|
+
lines.push("### " + icon + " " + file.relativePath + " (risk: " + file.riskScore + ", " + file.kind + ")");
|
|
6339
|
+
lines.push("+" + file.linesAdded + "/-" + file.linesRemoved + " lines");
|
|
6340
|
+
lines.push("");
|
|
6341
|
+
if (file.riskScore >= 40 && file.changeType !== "deleted") {
|
|
6342
|
+
try {
|
|
6343
|
+
const content = await readFile13(resolve14(projectPath, file.relativePath), "utf-8");
|
|
6344
|
+
const ext = file.relativePath.split(".").pop() ?? "";
|
|
6345
|
+
const maxChars = 4e3;
|
|
6346
|
+
const truncated = content.length > maxChars;
|
|
6347
|
+
lines.push("```" + ext);
|
|
6348
|
+
lines.push(content.slice(0, maxChars));
|
|
6349
|
+
if (truncated) lines.push("// ... [truncated]");
|
|
6350
|
+
lines.push("```");
|
|
6351
|
+
lines.push("");
|
|
6352
|
+
} catch {
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
}
|
|
6356
|
+
lines.push("## Review Instructions");
|
|
6357
|
+
lines.push("");
|
|
6358
|
+
lines.push("1. Check breaking changes above for correctness");
|
|
6359
|
+
lines.push("2. Verify all affected files have been updated");
|
|
6360
|
+
lines.push("3. Review changed files for bugs, security issues, and code quality");
|
|
6361
|
+
lines.push("4. Ensure tests cover the changes");
|
|
6362
|
+
if (missingFiles.length > 0) {
|
|
6363
|
+
lines.push('5. Consider whether the "potentially missing files" need updates');
|
|
6364
|
+
}
|
|
6365
|
+
return lines.join("\n");
|
|
6366
|
+
}
|
|
6367
|
+
function renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality) {
|
|
6368
|
+
const lines = [];
|
|
6369
|
+
const qIcon = reviewQuality.score >= 80 ? "\u{1F7E2}" : reviewQuality.score >= 60 ? "\u{1F7E1}" : "\u{1F534}";
|
|
6370
|
+
lines.push("");
|
|
6371
|
+
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");
|
|
6372
|
+
lines.push(" " + qIcon + " Code Review: " + reviewQuality.score + "/100 (" + reviewQuality.grade + ")");
|
|
6373
|
+
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");
|
|
6374
|
+
lines.push("");
|
|
6375
|
+
lines.push(" Branch: " + branch + " \u2190 " + baseBranch);
|
|
6376
|
+
lines.push(" Files: " + changedFiles.length + " changed");
|
|
6377
|
+
lines.push(" Lines: +" + changedFiles.reduce((s, f) => s + f.linesAdded, 0) + "/-" + changedFiles.reduce((s, f) => s + f.linesRemoved, 0));
|
|
6378
|
+
lines.push("");
|
|
6379
|
+
for (const f of reviewQuality.factors) {
|
|
6380
|
+
const icon = f.score >= 80 ? "\u2705" : f.score >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
6381
|
+
lines.push(" " + icon + " " + f.name + ": " + f.score + "/100 \u2014 " + f.detail);
|
|
6382
|
+
}
|
|
6383
|
+
if (breakingChanges.length > 0) {
|
|
6384
|
+
lines.push("");
|
|
6385
|
+
lines.push(" \u26A0\uFE0F BREAKING CHANGES (" + breakingChanges.length + "):");
|
|
6386
|
+
for (const bc of breakingChanges.slice(0, 5)) {
|
|
6387
|
+
const icon = bc.severity === "critical" ? "\u{1F534}" : bc.severity === "high" ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
6388
|
+
lines.push(" " + icon + " " + bc.file + ": " + bc.description);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
if (missingFiles.length > 0) {
|
|
6392
|
+
lines.push("");
|
|
6393
|
+
lines.push(" \u{1F4CB} Potentially missing (" + missingFiles.length + "):");
|
|
6394
|
+
for (const mf of missingFiles.slice(0, 5)) {
|
|
6395
|
+
lines.push(" \u2192 " + mf.file + " (" + mf.reason + ")");
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
lines.push("");
|
|
6399
|
+
lines.push(" \u{1F4A5} Impact: " + impactRadius.totalAffected + " files affected (" + impactRadius.directlyAffected + " direct, " + impactRadius.transitivelyAffected + " transitive)");
|
|
6400
|
+
if (impactRadius.affectedTests > 0) {
|
|
6401
|
+
lines.push(" \u{1F9EA} Tests: " + impactRadius.affectedTests + " test files in blast radius");
|
|
6402
|
+
}
|
|
6403
|
+
if (impactRadius.hotspots.length > 0) {
|
|
6404
|
+
lines.push("");
|
|
6405
|
+
lines.push(" \u{1F525} Hotspots:");
|
|
6406
|
+
for (const h of impactRadius.hotspots.slice(0, 3)) {
|
|
6407
|
+
lines.push(" " + h.file + " (risk: " + h.riskScore + ", " + h.dependents + " dependents)");
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
lines.push("");
|
|
6411
|
+
return lines.join("\n");
|
|
6412
|
+
}
|
|
6413
|
+
function emptyResult3(baseBranch) {
|
|
6414
|
+
return {
|
|
6415
|
+
branch: "",
|
|
6416
|
+
baseBranch,
|
|
6417
|
+
isGitRepo: false,
|
|
6418
|
+
changedFiles: [],
|
|
6419
|
+
totalLinesChanged: 0,
|
|
6420
|
+
breakingChanges: [],
|
|
6421
|
+
missingFiles: [],
|
|
6422
|
+
impactRadius: {
|
|
6423
|
+
directlyAffected: 0,
|
|
6424
|
+
transitivelyAffected: 0,
|
|
6425
|
+
totalAffected: 0,
|
|
6426
|
+
affectedTests: 0,
|
|
6427
|
+
riskScore: 0,
|
|
6428
|
+
hotspots: []
|
|
6429
|
+
},
|
|
6430
|
+
reviewQuality: {
|
|
6431
|
+
score: 0,
|
|
6432
|
+
grade: "F",
|
|
6433
|
+
factors: []
|
|
6434
|
+
},
|
|
6435
|
+
reviewPrompt: "",
|
|
6436
|
+
renderedSummary: "# Code Review\n\nNot a git repository."
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
5540
6439
|
export {
|
|
6440
|
+
CtoError,
|
|
5541
6441
|
DEFAULT_GATE_CONFIG,
|
|
5542
6442
|
MODEL_REGISTRY2 as MODEL_REGISTRY,
|
|
5543
6443
|
ProjectWatcher,
|
|
6444
|
+
analyzeForReview,
|
|
5544
6445
|
analyzeMonorepo,
|
|
5545
6446
|
analyzeProject,
|
|
5546
6447
|
analyzeSemantics,
|
|
@@ -5554,11 +6455,13 @@ export {
|
|
|
5554
6455
|
configureCache,
|
|
5555
6456
|
countTokensChars4,
|
|
5556
6457
|
countTokensTiktoken,
|
|
6458
|
+
createLogger,
|
|
5557
6459
|
createProject,
|
|
5558
6460
|
detectMonorepoTool,
|
|
5559
6461
|
detectStack,
|
|
5560
6462
|
estimateFileTokens,
|
|
5561
6463
|
estimateTokens,
|
|
6464
|
+
exportFeedbackForTeam,
|
|
5562
6465
|
freeEncoder,
|
|
5563
6466
|
generatePRContext,
|
|
5564
6467
|
getActiveWatchers,
|
|
@@ -5572,8 +6475,10 @@ export {
|
|
|
5572
6475
|
getPolicyPath,
|
|
5573
6476
|
getPredictorBoosts,
|
|
5574
6477
|
getPruneLevelForRisk,
|
|
6478
|
+
importTeamFeedback,
|
|
5575
6479
|
initProjectConfig,
|
|
5576
6480
|
invalidateCache,
|
|
6481
|
+
isCtoError,
|
|
5577
6482
|
loadBaseline,
|
|
5578
6483
|
loadConfig,
|
|
5579
6484
|
loadFeedbackModel,
|
|
@@ -5594,11 +6499,13 @@ export {
|
|
|
5594
6499
|
renderCompilabilityBenchmark,
|
|
5595
6500
|
renderCompileProof,
|
|
5596
6501
|
renderContextScore,
|
|
6502
|
+
renderCrossRepoReport,
|
|
5597
6503
|
renderFeedbackReport,
|
|
5598
6504
|
renderMonorepoAnalysis,
|
|
5599
6505
|
renderMultiModelResult,
|
|
5600
6506
|
renderPackageContext,
|
|
5601
6507
|
renderQualityBenchmark,
|
|
6508
|
+
renderReviewSummary,
|
|
5602
6509
|
renderSemanticAnalysis,
|
|
5603
6510
|
runBenchmark,
|
|
5604
6511
|
runCompilabilityBenchmark,
|
|
@@ -5612,9 +6519,13 @@ export {
|
|
|
5612
6519
|
selectContext,
|
|
5613
6520
|
selectPackageContext,
|
|
5614
6521
|
semanticBoosts,
|
|
6522
|
+
setJsonLogging,
|
|
6523
|
+
setLogLevel,
|
|
5615
6524
|
unwatchAll,
|
|
5616
6525
|
unwatchProject,
|
|
5617
6526
|
walkProject,
|
|
5618
|
-
watchProject
|
|
6527
|
+
watchProject,
|
|
6528
|
+
wilsonLowerBound,
|
|
6529
|
+
wrapError
|
|
5619
6530
|
};
|
|
5620
6531
|
//# sourceMappingURL=index.js.map
|