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/engine/index.js
CHANGED
|
@@ -1565,10 +1565,7 @@ function deduplicateFindings(findings) {
|
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
1567
|
// src/engine/pruner.ts
|
|
1568
|
-
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1569
1568
|
import { readFile as readFile5 } from "fs/promises";
|
|
1570
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1571
|
-
import { join as join6 } from "path";
|
|
1572
1569
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
1573
1570
|
async function pruneFile(file, level) {
|
|
1574
1571
|
if (level === "excluded") {
|
|
@@ -1600,23 +1597,7 @@ async function pruneTypeScript(file, level) {
|
|
|
1600
1597
|
} catch {
|
|
1601
1598
|
return emptyResult2(file, level);
|
|
1602
1599
|
}
|
|
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);
|
|
1600
|
+
const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
|
|
1620
1601
|
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
1621
1602
|
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
1622
1603
|
return {
|
|
@@ -1628,131 +1609,281 @@ async function pruneTypeScript(file, level) {
|
|
|
1628
1609
|
savingsPercent: Math.max(0, savingsPercent)
|
|
1629
1610
|
};
|
|
1630
1611
|
}
|
|
1631
|
-
function
|
|
1612
|
+
function extractSignaturesRegex(content) {
|
|
1613
|
+
const lines = content.split("\n");
|
|
1632
1614
|
const parts = [];
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1615
|
+
let i = 0;
|
|
1616
|
+
while (i < lines.length) {
|
|
1617
|
+
const line = lines[i];
|
|
1618
|
+
const trimmed = line.trim();
|
|
1619
|
+
if (trimmed === "") {
|
|
1620
|
+
i++;
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (trimmed.startsWith("/**")) {
|
|
1624
|
+
const docLines = [];
|
|
1625
|
+
while (i < lines.length) {
|
|
1626
|
+
docLines.push(lines[i]);
|
|
1627
|
+
if (lines[i].includes("*/")) {
|
|
1628
|
+
i++;
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
i++;
|
|
1632
|
+
}
|
|
1633
|
+
parts.push(docLines.join("\n"));
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (trimmed.startsWith("//")) {
|
|
1637
|
+
parts.push(line);
|
|
1638
|
+
i++;
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
|
|
1642
|
+
const block = collectBracedLine(lines, i);
|
|
1643
|
+
parts.push(block.text);
|
|
1644
|
+
i = block.nextIndex;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
|
|
1648
|
+
const block = collectBracedLine(lines, i);
|
|
1649
|
+
parts.push(block.text);
|
|
1650
|
+
i = block.nextIndex;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
|
|
1654
|
+
const block = collectBalanced(lines, i);
|
|
1655
|
+
parts.push(block.text);
|
|
1656
|
+
i = block.nextIndex;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
|
|
1660
|
+
const block = collectBalanced(lines, i);
|
|
1661
|
+
parts.push(block.text);
|
|
1662
|
+
i = block.nextIndex;
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1666
|
+
const block = collectBalanced(lines, i);
|
|
1667
|
+
parts.push(block.text);
|
|
1668
|
+
i = block.nextIndex;
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
|
|
1672
|
+
if (fnMatch) {
|
|
1673
|
+
const sig = extractFnSignature(lines, i);
|
|
1674
|
+
parts.push(`${sig} { /* ... */ }`);
|
|
1675
|
+
i = skipBlock(lines, i);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
|
|
1679
|
+
if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
|
|
1680
|
+
const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
|
|
1681
|
+
if (prefix) {
|
|
1682
|
+
parts.push(`${prefix} /* ... */;`);
|
|
1676
1683
|
}
|
|
1684
|
+
i = skipBlock(lines, i);
|
|
1685
|
+
continue;
|
|
1677
1686
|
}
|
|
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());
|
|
1687
|
+
if (arrowMatch) {
|
|
1688
|
+
const block = collectStatement(lines, i);
|
|
1689
|
+
parts.push(block.text);
|
|
1690
|
+
i = block.nextIndex;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
|
|
1694
|
+
const classOutline = extractClassOutline(lines, i);
|
|
1695
|
+
parts.push(classOutline.text);
|
|
1696
|
+
i = classOutline.nextIndex;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
i++;
|
|
1717
1700
|
}
|
|
1718
1701
|
return parts.join("\n");
|
|
1719
1702
|
}
|
|
1720
|
-
function
|
|
1703
|
+
function extractSkeletonRegex(content) {
|
|
1704
|
+
const lines = content.split("\n");
|
|
1721
1705
|
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
|
-
|
|
1706
|
+
let i = 0;
|
|
1707
|
+
while (i < lines.length) {
|
|
1708
|
+
const trimmed = lines[i].trim();
|
|
1709
|
+
if (/^import\s/.test(trimmed)) {
|
|
1710
|
+
const block = collectBracedLine(lines, i);
|
|
1711
|
+
parts.push(block.text);
|
|
1712
|
+
i = block.nextIndex;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
|
|
1716
|
+
const block = collectBalanced(lines, i);
|
|
1717
|
+
parts.push(block.text);
|
|
1718
|
+
i = block.nextIndex;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1722
|
+
const block = collectBalanced(lines, i);
|
|
1723
|
+
parts.push(block.text);
|
|
1724
|
+
i = block.nextIndex;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
|
|
1728
|
+
const sig = extractFnSignature(lines, i);
|
|
1729
|
+
parts.push(`${sig};`);
|
|
1730
|
+
i = skipBlock(lines, i);
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
|
|
1734
|
+
const nameMatch = trimmed.match(/class\s+(\w+)/);
|
|
1735
|
+
const name = nameMatch?.[1] ?? "Unknown";
|
|
1736
|
+
const end = skipBlock(lines, i);
|
|
1737
|
+
const methods = [];
|
|
1738
|
+
for (let j = i + 1; j < end; j++) {
|
|
1739
|
+
const mt = lines[j].trim();
|
|
1740
|
+
const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
|
|
1741
|
+
if (mm && mm[1] !== "constructor") methods.push(mm[1]);
|
|
1742
|
+
}
|
|
1743
|
+
parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
|
|
1744
|
+
i = end;
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (/^export\s*(\{|\*)/.test(trimmed)) {
|
|
1748
|
+
const block = collectBracedLine(lines, i);
|
|
1749
|
+
parts.push(block.text);
|
|
1750
|
+
i = block.nextIndex;
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
i++;
|
|
1753
1754
|
}
|
|
1754
1755
|
return parts.join("\n");
|
|
1755
1756
|
}
|
|
1757
|
+
function collectBracedLine(lines, start) {
|
|
1758
|
+
let text = lines[start];
|
|
1759
|
+
let i = start + 1;
|
|
1760
|
+
while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
|
|
1761
|
+
text += "\n" + lines[i];
|
|
1762
|
+
i++;
|
|
1763
|
+
}
|
|
1764
|
+
return { text, nextIndex: i };
|
|
1765
|
+
}
|
|
1766
|
+
function collectBalanced(lines, start) {
|
|
1767
|
+
let depth = 0;
|
|
1768
|
+
let text = "";
|
|
1769
|
+
let i = start;
|
|
1770
|
+
let started = false;
|
|
1771
|
+
while (i < lines.length) {
|
|
1772
|
+
const line = lines[i];
|
|
1773
|
+
text += (text ? "\n" : "") + line;
|
|
1774
|
+
for (const ch of line) {
|
|
1775
|
+
if (ch === "{" || ch === "(") {
|
|
1776
|
+
depth++;
|
|
1777
|
+
started = true;
|
|
1778
|
+
}
|
|
1779
|
+
if (ch === "}" || ch === ")") depth--;
|
|
1780
|
+
}
|
|
1781
|
+
i++;
|
|
1782
|
+
if (started && depth <= 0) break;
|
|
1783
|
+
if (!started && line.includes(";")) break;
|
|
1784
|
+
}
|
|
1785
|
+
return { text, nextIndex: i };
|
|
1786
|
+
}
|
|
1787
|
+
function collectStatement(lines, start) {
|
|
1788
|
+
let text = lines[start];
|
|
1789
|
+
let i = start + 1;
|
|
1790
|
+
if (text.includes(";")) return { text, nextIndex: i };
|
|
1791
|
+
let depth = 0;
|
|
1792
|
+
for (const ch of text) {
|
|
1793
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1794
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1795
|
+
}
|
|
1796
|
+
while (i < lines.length && depth > 0) {
|
|
1797
|
+
text += "\n" + lines[i];
|
|
1798
|
+
for (const ch of lines[i]) {
|
|
1799
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1800
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1801
|
+
}
|
|
1802
|
+
i++;
|
|
1803
|
+
}
|
|
1804
|
+
return { text, nextIndex: i };
|
|
1805
|
+
}
|
|
1806
|
+
function extractFnSignature(lines, start) {
|
|
1807
|
+
let sig = "";
|
|
1808
|
+
let i = start;
|
|
1809
|
+
while (i < lines.length) {
|
|
1810
|
+
const line = lines[i].trim();
|
|
1811
|
+
sig += (sig ? " " : "") + line;
|
|
1812
|
+
if (line.includes("{")) {
|
|
1813
|
+
sig = sig.replace(/\s*\{[^]*$/, "").trim();
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
i++;
|
|
1817
|
+
}
|
|
1818
|
+
return sig;
|
|
1819
|
+
}
|
|
1820
|
+
function skipBlock(lines, start) {
|
|
1821
|
+
let depth = 0;
|
|
1822
|
+
let i = start;
|
|
1823
|
+
let foundBrace = false;
|
|
1824
|
+
while (i < lines.length) {
|
|
1825
|
+
for (const ch of lines[i]) {
|
|
1826
|
+
if (ch === "{") {
|
|
1827
|
+
depth++;
|
|
1828
|
+
foundBrace = true;
|
|
1829
|
+
}
|
|
1830
|
+
if (ch === "}") depth--;
|
|
1831
|
+
}
|
|
1832
|
+
i++;
|
|
1833
|
+
if (foundBrace && depth <= 0) break;
|
|
1834
|
+
if (!foundBrace && lines[i - 1].includes(";")) break;
|
|
1835
|
+
}
|
|
1836
|
+
return i;
|
|
1837
|
+
}
|
|
1838
|
+
function looksLikeFunctionDecl(lines, start) {
|
|
1839
|
+
const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
|
|
1840
|
+
return /=>/.test(chunk) || /=\s*function/.test(chunk);
|
|
1841
|
+
}
|
|
1842
|
+
function extractClassOutline(lines, start) {
|
|
1843
|
+
const header = lines[start].trim();
|
|
1844
|
+
let headerText = header;
|
|
1845
|
+
let i = start + 1;
|
|
1846
|
+
if (!header.includes("{")) {
|
|
1847
|
+
while (i < lines.length) {
|
|
1848
|
+
headerText += " " + lines[i].trim();
|
|
1849
|
+
if (lines[i].includes("{")) {
|
|
1850
|
+
i++;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
i++;
|
|
1854
|
+
}
|
|
1855
|
+
} else {
|
|
1856
|
+
i = start + 1;
|
|
1857
|
+
}
|
|
1858
|
+
const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
|
|
1859
|
+
let depth = 1;
|
|
1860
|
+
while (i < lines.length && depth > 0) {
|
|
1861
|
+
const line = lines[i];
|
|
1862
|
+
const trimmed = line.trim();
|
|
1863
|
+
for (const ch of line) {
|
|
1864
|
+
if (ch === "{") depth++;
|
|
1865
|
+
if (ch === "}") depth--;
|
|
1866
|
+
}
|
|
1867
|
+
if (depth <= 0) {
|
|
1868
|
+
i++;
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
if (depth === 1) {
|
|
1872
|
+
if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
|
|
1873
|
+
bodyParts.push(` ${trimmed}`);
|
|
1874
|
+
} else if (/^constructor\s*\(/.test(trimmed)) {
|
|
1875
|
+
const sig = extractFnSignature(lines, i);
|
|
1876
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1877
|
+
} else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
1878
|
+
const sig = extractFnSignature(lines, i);
|
|
1879
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
i++;
|
|
1883
|
+
}
|
|
1884
|
+
bodyParts.push("}");
|
|
1885
|
+
return { text: bodyParts.join("\n"), nextIndex: i };
|
|
1886
|
+
}
|
|
1756
1887
|
async function pruneGeneric(file, level) {
|
|
1757
1888
|
let content;
|
|
1758
1889
|
try {
|
|
@@ -1813,22 +1944,6 @@ function emptyResult2(file, level) {
|
|
|
1813
1944
|
savingsPercent: 100
|
|
1814
1945
|
};
|
|
1815
1946
|
}
|
|
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
1947
|
|
|
1833
1948
|
// src/engine/coverage.ts
|
|
1834
1949
|
function calculateCoverage(targetPaths, includedPaths, allFiles, graph, depth = 2) {
|
|
@@ -3252,7 +3367,7 @@ function fmt2(n) {
|
|
|
3252
3367
|
}
|
|
3253
3368
|
|
|
3254
3369
|
// src/engine/predictor.ts
|
|
3255
|
-
import { resolve as resolve8, join as
|
|
3370
|
+
import { resolve as resolve8, join as join6 } from "path";
|
|
3256
3371
|
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3257
3372
|
var DEFAULT_PREDICTOR_CONFIG = {
|
|
3258
3373
|
maxCoSelectionPairs: 500,
|
|
@@ -3262,9 +3377,9 @@ var DEFAULT_PREDICTOR_CONFIG = {
|
|
|
3262
3377
|
// need at least 2 observations before predicting
|
|
3263
3378
|
};
|
|
3264
3379
|
async function getModelPath(projectPath) {
|
|
3265
|
-
const ctoDir =
|
|
3380
|
+
const ctoDir = join6(resolve8(projectPath), ".cto");
|
|
3266
3381
|
await mkdir2(ctoDir, { recursive: true });
|
|
3267
|
-
return
|
|
3382
|
+
return join6(ctoDir, "predictor.json");
|
|
3268
3383
|
}
|
|
3269
3384
|
async function loadModel(projectPath) {
|
|
3270
3385
|
try {
|
|
@@ -3514,7 +3629,7 @@ function pruneCoSelection(model, maxPairs) {
|
|
|
3514
3629
|
}
|
|
3515
3630
|
|
|
3516
3631
|
// src/engine/cross-repo.ts
|
|
3517
|
-
import { join as
|
|
3632
|
+
import { join as join7, basename as basename3 } from "path";
|
|
3518
3633
|
import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
3519
3634
|
function computeFingerprint2(analysis) {
|
|
3520
3635
|
const totalFiles = analysis.totalFiles;
|
|
@@ -3547,7 +3662,7 @@ function fingerprintHash(fp) {
|
|
|
3547
3662
|
}
|
|
3548
3663
|
function getGlobalModelPath() {
|
|
3549
3664
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
3550
|
-
return
|
|
3665
|
+
return join7(home, ".cto", "global-intelligence.json");
|
|
3551
3666
|
}
|
|
3552
3667
|
async function loadGlobalModel() {
|
|
3553
3668
|
try {
|
|
@@ -3558,7 +3673,7 @@ async function loadGlobalModel() {
|
|
|
3558
3673
|
}
|
|
3559
3674
|
}
|
|
3560
3675
|
async function saveGlobalModel(model) {
|
|
3561
|
-
const dir =
|
|
3676
|
+
const dir = join7(getGlobalModelPath(), "..");
|
|
3562
3677
|
await mkdir3(dir, { recursive: true });
|
|
3563
3678
|
await writeFile4(getGlobalModelPath(), JSON.stringify(model, null, 2));
|
|
3564
3679
|
}
|
|
@@ -3779,17 +3894,17 @@ function getCrossRepoStats(model) {
|
|
|
3779
3894
|
}
|
|
3780
3895
|
|
|
3781
3896
|
// src/engine/feedback.ts
|
|
3782
|
-
import { resolve as resolve10, join as
|
|
3897
|
+
import { resolve as resolve10, join as join8 } from "path";
|
|
3783
3898
|
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
3784
3899
|
async function getFeedbackPath(projectPath) {
|
|
3785
|
-
const ctoDir =
|
|
3900
|
+
const ctoDir = join8(resolve10(projectPath), ".cto");
|
|
3786
3901
|
await mkdir4(ctoDir, { recursive: true });
|
|
3787
|
-
return
|
|
3902
|
+
return join8(ctoDir, "feedback.json");
|
|
3788
3903
|
}
|
|
3789
3904
|
async function getModelPath2(projectPath) {
|
|
3790
|
-
const ctoDir =
|
|
3905
|
+
const ctoDir = join8(resolve10(projectPath), ".cto");
|
|
3791
3906
|
await mkdir4(ctoDir, { recursive: true });
|
|
3792
|
-
return
|
|
3907
|
+
return join8(ctoDir, "feedback-model.json");
|
|
3793
3908
|
}
|
|
3794
3909
|
async function loadFeedback(projectPath) {
|
|
3795
3910
|
try {
|
|
@@ -3815,16 +3930,30 @@ async function saveFeedbackModel(projectPath, model) {
|
|
|
3815
3930
|
}
|
|
3816
3931
|
function createEmptyModel3() {
|
|
3817
3932
|
return {
|
|
3818
|
-
version:
|
|
3933
|
+
version: 2,
|
|
3819
3934
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3820
3935
|
totalFeedback: 0,
|
|
3821
3936
|
acceptRate: 0,
|
|
3822
3937
|
fileAcceptance: {},
|
|
3823
3938
|
taskTypeAcceptance: {},
|
|
3824
3939
|
pairAcceptance: {},
|
|
3940
|
+
sessions: {},
|
|
3941
|
+
strategyComparison: {},
|
|
3825
3942
|
insights: []
|
|
3826
3943
|
};
|
|
3827
3944
|
}
|
|
3945
|
+
var EWMA_ALPHA = 0.15;
|
|
3946
|
+
function ewmaUpdate(prev, newValue, alpha = EWMA_ALPHA) {
|
|
3947
|
+
return alpha * newValue + (1 - alpha) * prev;
|
|
3948
|
+
}
|
|
3949
|
+
function wilsonLowerBound(successes, total, z = 1.96) {
|
|
3950
|
+
if (total === 0) return 0;
|
|
3951
|
+
const phat = successes / total;
|
|
3952
|
+
const denom = 1 + z * z / total;
|
|
3953
|
+
const center = phat + z * z / (2 * total);
|
|
3954
|
+
const spread = z * Math.sqrt((phat * (1 - phat) + z * z / (4 * total)) / total);
|
|
3955
|
+
return Math.max(0, (center - spread) / denom);
|
|
3956
|
+
}
|
|
3828
3957
|
async function recordFeedback(projectPath, entry) {
|
|
3829
3958
|
const entries = await loadFeedback(projectPath);
|
|
3830
3959
|
const fullEntry = {
|
|
@@ -3853,13 +3982,20 @@ function rebuildModel(entries) {
|
|
|
3853
3982
|
includedCount: 0,
|
|
3854
3983
|
acceptedCount: 0,
|
|
3855
3984
|
acceptRate: 0,
|
|
3856
|
-
|
|
3985
|
+
ewmaAcceptRate: 0.5,
|
|
3986
|
+
// prior: 50%
|
|
3987
|
+
avgTimeToAccept: 0,
|
|
3988
|
+
lastSeen: entry.timestamp,
|
|
3989
|
+
bayesianLower: 0
|
|
3857
3990
|
};
|
|
3858
3991
|
}
|
|
3859
3992
|
const fa = model.fileAcceptance[file];
|
|
3860
3993
|
fa.includedCount++;
|
|
3861
3994
|
if (accepted) fa.acceptedCount++;
|
|
3862
3995
|
fa.acceptRate = fa.acceptedCount / fa.includedCount;
|
|
3996
|
+
fa.ewmaAcceptRate = ewmaUpdate(fa.ewmaAcceptRate, accepted ? 1 : 0);
|
|
3997
|
+
fa.bayesianLower = wilsonLowerBound(fa.acceptedCount, fa.includedCount);
|
|
3998
|
+
fa.lastSeen = entry.timestamp;
|
|
3863
3999
|
if (entry.outcome.timeToAcceptMs) {
|
|
3864
4000
|
fa.avgTimeToAccept = (fa.avgTimeToAccept * (fa.includedCount - 1) + entry.outcome.timeToAcceptMs) / fa.includedCount;
|
|
3865
4001
|
}
|
|
@@ -3870,13 +4006,15 @@ function rebuildModel(entries) {
|
|
|
3870
4006
|
totalCount: 0,
|
|
3871
4007
|
acceptedCount: 0,
|
|
3872
4008
|
acceptRate: 0,
|
|
3873
|
-
avgCompilable: 0
|
|
4009
|
+
avgCompilable: 0,
|
|
4010
|
+
ewmaAcceptRate: 0.5
|
|
3874
4011
|
};
|
|
3875
4012
|
}
|
|
3876
4013
|
const tta = model.taskTypeAcceptance[tt];
|
|
3877
4014
|
tta.totalCount++;
|
|
3878
4015
|
if (accepted) tta.acceptedCount++;
|
|
3879
4016
|
tta.acceptRate = tta.acceptedCount / tta.totalCount;
|
|
4017
|
+
tta.ewmaAcceptRate = ewmaUpdate(tta.ewmaAcceptRate, accepted ? 1 : 0);
|
|
3880
4018
|
if (entry.outcome.compilable !== void 0) {
|
|
3881
4019
|
tta.avgCompilable = (tta.avgCompilable * (tta.totalCount - 1) + (entry.outcome.compilable ? 1 : 0)) / tta.totalCount;
|
|
3882
4020
|
}
|
|
@@ -3893,6 +4031,36 @@ function rebuildModel(entries) {
|
|
|
3893
4031
|
pa.acceptRate = pa.acceptedCount / pa.count;
|
|
3894
4032
|
}
|
|
3895
4033
|
}
|
|
4034
|
+
if (entry.sessionId) {
|
|
4035
|
+
if (!model.sessions[entry.sessionId]) {
|
|
4036
|
+
model.sessions[entry.sessionId] = {
|
|
4037
|
+
strategy: entry.strategy ?? "default",
|
|
4038
|
+
entries: 0,
|
|
4039
|
+
acceptRate: 0
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
const sess = model.sessions[entry.sessionId];
|
|
4043
|
+
sess.entries++;
|
|
4044
|
+
const sessionEntries = entries.filter((e) => e.sessionId === entry.sessionId);
|
|
4045
|
+
const sessionAccepted = sessionEntries.filter((e) => e.outcome.accepted).length;
|
|
4046
|
+
sess.acceptRate = sessionEntries.length > 0 ? sessionAccepted / sessionEntries.length : 0;
|
|
4047
|
+
}
|
|
4048
|
+
const strat = entry.strategy ?? "default";
|
|
4049
|
+
if (!model.strategyComparison[strat]) {
|
|
4050
|
+
model.strategyComparison[strat] = {
|
|
4051
|
+
totalCount: 0,
|
|
4052
|
+
acceptedCount: 0,
|
|
4053
|
+
acceptRate: 0,
|
|
4054
|
+
avgTimeToAccept: 0
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
const sc = model.strategyComparison[strat];
|
|
4058
|
+
sc.totalCount++;
|
|
4059
|
+
if (accepted) sc.acceptedCount++;
|
|
4060
|
+
sc.acceptRate = sc.acceptedCount / sc.totalCount;
|
|
4061
|
+
if (entry.outcome.timeToAcceptMs) {
|
|
4062
|
+
sc.avgTimeToAccept = (sc.avgTimeToAccept * (sc.totalCount - 1) + entry.outcome.timeToAcceptMs) / sc.totalCount;
|
|
4063
|
+
}
|
|
3896
4064
|
}
|
|
3897
4065
|
model.acceptRate = entries.length > 0 ? acceptedTotal / entries.length : 0;
|
|
3898
4066
|
model.insights = generateInsights(model);
|
|
@@ -3900,43 +4068,64 @@ function rebuildModel(entries) {
|
|
|
3900
4068
|
}
|
|
3901
4069
|
function generateInsights(model) {
|
|
3902
4070
|
const insights = [];
|
|
3903
|
-
const highAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.
|
|
4071
|
+
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
4072
|
for (const [file, fa] of highAccept) {
|
|
3905
4073
|
insights.push({
|
|
3906
4074
|
type: "positive",
|
|
3907
4075
|
title: `${file} consistently leads to accepted output`,
|
|
3908
|
-
detail: `${Math.round(fa.acceptRate * 100)}% accept rate
|
|
3909
|
-
impact: Math.round(fa.
|
|
4076
|
+
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)}%`,
|
|
4077
|
+
impact: Math.round(fa.bayesianLower * fa.includedCount * 10)
|
|
3910
4078
|
});
|
|
3911
4079
|
}
|
|
3912
|
-
const lowAccept = Object.entries(model.fileAcceptance).filter(([_, fa]) => fa.includedCount >= 3 && fa.acceptRate < 0.3).sort((a, b) => a[1].
|
|
4080
|
+
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
4081
|
for (const [file, fa] of lowAccept) {
|
|
3914
4082
|
insights.push({
|
|
3915
4083
|
type: "negative",
|
|
3916
4084
|
title: `${file} often included but output rejected`,
|
|
3917
|
-
detail: `Only ${Math.round(fa.acceptRate * 100)}% accept rate \u2014
|
|
3918
|
-
impact: Math.round((1 - fa.
|
|
4085
|
+
detail: `Only ${Math.round(fa.acceptRate * 100)}% accept rate \u2014 EWMA trending ${Math.round(fa.ewmaAcceptRate * 100)}%`,
|
|
4086
|
+
impact: Math.round((1 - fa.bayesianLower) * fa.includedCount * 5)
|
|
3919
4087
|
});
|
|
3920
4088
|
}
|
|
3921
|
-
const bestTasks = Object.entries(model.taskTypeAcceptance).filter(([_, ta]) => ta.totalCount >= 2).sort((a, b) => b[1].
|
|
4089
|
+
const bestTasks = Object.entries(model.taskTypeAcceptance).filter(([_, ta]) => ta.totalCount >= 2).sort((a, b) => b[1].ewmaAcceptRate - a[1].ewmaAcceptRate);
|
|
3922
4090
|
for (const [taskType, ta] of bestTasks.slice(0, 3)) {
|
|
4091
|
+
const trending = ta.ewmaAcceptRate > ta.acceptRate ? "improving" : ta.ewmaAcceptRate < ta.acceptRate - 0.05 ? "declining" : "stable";
|
|
3923
4092
|
insights.push({
|
|
3924
4093
|
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.
|
|
4094
|
+
title: `${taskType} tasks: ${Math.round(ta.acceptRate * 100)}% acceptance (${trending})`,
|
|
4095
|
+
detail: `${ta.acceptedCount}/${ta.totalCount} accepted, ${Math.round(ta.avgCompilable * 100)}% compilable, EWMA: ${Math.round(ta.ewmaAcceptRate * 100)}%`,
|
|
4096
|
+
impact: Math.round(ta.ewmaAcceptRate * ta.totalCount * 3)
|
|
3928
4097
|
});
|
|
3929
4098
|
}
|
|
3930
|
-
const powerPairs = Object.entries(model.pairAcceptance).filter(([_, pa]) => pa.count >=
|
|
4099
|
+
const powerPairs = Object.entries(model.pairAcceptance).filter(([_, pa]) => pa.count >= 4 && pa.acceptRate >= 0.85).sort((a, b) => {
|
|
4100
|
+
const aWilson = wilsonLowerBound(a[1].acceptedCount, a[1].count);
|
|
4101
|
+
const bWilson = wilsonLowerBound(b[1].acceptedCount, b[1].count);
|
|
4102
|
+
return bWilson - aWilson;
|
|
4103
|
+
}).slice(0, 3);
|
|
3931
4104
|
for (const [pair, pa] of powerPairs) {
|
|
3932
4105
|
const [a, b] = pair.split("|");
|
|
4106
|
+
const wilson = wilsonLowerBound(pa.acceptedCount, pa.count);
|
|
3933
4107
|
insights.push({
|
|
3934
4108
|
type: "positive",
|
|
3935
4109
|
title: `Power pair: ${a} + ${b}`,
|
|
3936
|
-
detail: `${Math.round(pa.acceptRate * 100)}% acceptance
|
|
3937
|
-
impact: Math.round(
|
|
4110
|
+
detail: `${Math.round(pa.acceptRate * 100)}% acceptance (${pa.count}x), confidence: ${Math.round(wilson * 100)}%`,
|
|
4111
|
+
impact: Math.round(wilson * pa.count * 5)
|
|
3938
4112
|
});
|
|
3939
4113
|
}
|
|
4114
|
+
const strategies = Object.entries(model.strategyComparison).filter(([_, sc]) => sc.totalCount >= 3);
|
|
4115
|
+
if (strategies.length >= 2) {
|
|
4116
|
+
strategies.sort((a, b) => b[1].acceptRate - a[1].acceptRate);
|
|
4117
|
+
const best = strategies[0];
|
|
4118
|
+
const worst = strategies[strategies.length - 1];
|
|
4119
|
+
const diff = best[1].acceptRate - worst[1].acceptRate;
|
|
4120
|
+
if (diff > 0.1) {
|
|
4121
|
+
insights.push({
|
|
4122
|
+
type: "opportunity",
|
|
4123
|
+
title: `Strategy "${best[0]}" outperforms "${worst[0]}" by ${Math.round(diff * 100)}%`,
|
|
4124
|
+
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})`,
|
|
4125
|
+
impact: Math.round(diff * 50)
|
|
4126
|
+
});
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
3940
4129
|
return insights.sort((a, b) => b.impact - a.impact);
|
|
3941
4130
|
}
|
|
3942
4131
|
async function getFeedbackBoosts(projectPath, task) {
|
|
@@ -3946,18 +4135,19 @@ async function getFeedbackBoosts(projectPath, task) {
|
|
|
3946
4135
|
const taskType = classifyTask(task);
|
|
3947
4136
|
for (const [file, fa] of Object.entries(model.fileAcceptance)) {
|
|
3948
4137
|
if (fa.includedCount < 2) continue;
|
|
3949
|
-
if (fa.
|
|
3950
|
-
|
|
4138
|
+
if (fa.bayesianLower >= 0.5) {
|
|
4139
|
+
const boost = fa.ewmaAcceptRate * 5 * Math.min(1, fa.includedCount / 5);
|
|
4140
|
+
boosts.set(file, (boosts.get(file) ?? 0) + boost);
|
|
3951
4141
|
}
|
|
3952
|
-
if (fa.acceptRate < 0.2 && fa.includedCount >=
|
|
3953
|
-
boosts.set(file, (boosts.get(file) ?? 0) - 3);
|
|
4142
|
+
if (fa.acceptRate < 0.2 && fa.includedCount >= 4) {
|
|
4143
|
+
boosts.set(file, (boosts.get(file) ?? 0) - 3 * (1 - fa.bayesianLower));
|
|
3954
4144
|
}
|
|
3955
4145
|
}
|
|
3956
4146
|
const tta = model.taskTypeAcceptance[taskType];
|
|
3957
4147
|
if (tta && tta.totalCount >= 2) {
|
|
3958
4148
|
if (tta.avgCompilable >= 0.8) {
|
|
3959
4149
|
for (const [file, fa] of Object.entries(model.fileAcceptance)) {
|
|
3960
|
-
if (fa.
|
|
4150
|
+
if (fa.bayesianLower >= 0.6) {
|
|
3961
4151
|
boosts.set(file, (boosts.get(file) ?? 0) + 1);
|
|
3962
4152
|
}
|
|
3963
4153
|
}
|
|
@@ -3965,6 +4155,53 @@ async function getFeedbackBoosts(projectPath, task) {
|
|
|
3965
4155
|
}
|
|
3966
4156
|
return boosts;
|
|
3967
4157
|
}
|
|
4158
|
+
async function exportFeedbackForTeam(projectPath, projectName) {
|
|
4159
|
+
const model = await loadFeedbackModel(projectPath);
|
|
4160
|
+
const entries = await loadFeedback(projectPath);
|
|
4161
|
+
return {
|
|
4162
|
+
version: 2,
|
|
4163
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4164
|
+
projectName,
|
|
4165
|
+
model,
|
|
4166
|
+
entrySummary: {
|
|
4167
|
+
total: entries.length,
|
|
4168
|
+
accepted: entries.filter((e) => e.outcome.accepted).length,
|
|
4169
|
+
sessions: new Set(entries.map((e) => e.sessionId).filter(Boolean)).size
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
async function importTeamFeedback(projectPath, teamExport) {
|
|
4174
|
+
const localModel = await loadFeedbackModel(projectPath);
|
|
4175
|
+
for (const [file, fa] of Object.entries(teamExport.model.fileAcceptance)) {
|
|
4176
|
+
const local = localModel.fileAcceptance[file];
|
|
4177
|
+
if (!local) {
|
|
4178
|
+
localModel.fileAcceptance[file] = {
|
|
4179
|
+
...fa,
|
|
4180
|
+
includedCount: Math.max(1, Math.round(fa.includedCount * 0.5)),
|
|
4181
|
+
acceptedCount: Math.max(0, Math.round(fa.acceptedCount * 0.5)),
|
|
4182
|
+
ewmaAcceptRate: fa.ewmaAcceptRate * 0.7 + 0.5 * 0.3,
|
|
4183
|
+
// blend with prior
|
|
4184
|
+
bayesianLower: wilsonLowerBound(
|
|
4185
|
+
Math.round(fa.acceptedCount * 0.5),
|
|
4186
|
+
Math.max(1, Math.round(fa.includedCount * 0.5))
|
|
4187
|
+
)
|
|
4188
|
+
};
|
|
4189
|
+
} else {
|
|
4190
|
+
const totalLocal = local.includedCount;
|
|
4191
|
+
const totalTeam = fa.includedCount;
|
|
4192
|
+
const combinedCount = totalLocal + Math.round(totalTeam * 0.3);
|
|
4193
|
+
const combinedAccepted = local.acceptedCount + Math.round(fa.acceptedCount * 0.3);
|
|
4194
|
+
local.includedCount = combinedCount;
|
|
4195
|
+
local.acceptedCount = combinedAccepted;
|
|
4196
|
+
local.acceptRate = combinedCount > 0 ? combinedAccepted / combinedCount : 0;
|
|
4197
|
+
local.ewmaAcceptRate = local.ewmaAcceptRate * 0.7 + fa.ewmaAcceptRate * 0.3;
|
|
4198
|
+
local.bayesianLower = wilsonLowerBound(combinedAccepted, combinedCount);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
localModel.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4202
|
+
await saveFeedbackModel(projectPath, localModel);
|
|
4203
|
+
return localModel;
|
|
4204
|
+
}
|
|
3968
4205
|
function renderFeedbackReport(model) {
|
|
3969
4206
|
const lines = [];
|
|
3970
4207
|
lines.push("");
|
|
@@ -3991,6 +4228,28 @@ function renderFeedbackReport(model) {
|
|
|
3991
4228
|
function pad3(s, w) {
|
|
3992
4229
|
return s.padEnd(w).substring(0, w);
|
|
3993
4230
|
}
|
|
4231
|
+
function renderCrossRepoReport(stats) {
|
|
4232
|
+
const lines = [];
|
|
4233
|
+
lines.push("");
|
|
4234
|
+
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");
|
|
4235
|
+
lines.push(" \u2551 \u{1F310} Cross-Repo Intelligence \u2551");
|
|
4236
|
+
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");
|
|
4237
|
+
lines.push(" \u2551 \u2551");
|
|
4238
|
+
lines.push(` \u2551 Projects learned: ${pad3(stats.totalProjects.toString(), 8)} \u2551`);
|
|
4239
|
+
lines.push(` \u2551 Total observations: ${pad3(stats.totalObservations.toString(), 8)} \u2551`);
|
|
4240
|
+
lines.push(` \u2551 Archetypes: ${pad3(stats.archetypes.length.toString(), 8)} \u2551`);
|
|
4241
|
+
lines.push(` \u2551 Universal patterns: ${pad3(stats.universalPatterns.toString(), 8)} \u2551`);
|
|
4242
|
+
lines.push(" \u2551 \u2551");
|
|
4243
|
+
if (stats.archetypes.length > 0) {
|
|
4244
|
+
lines.push(" \u2551 Archetypes: \u2551");
|
|
4245
|
+
for (const a of stats.archetypes.slice(0, 5)) {
|
|
4246
|
+
lines.push(` \u2551 ${pad3(a.name, 24)} ${pad3(a.observations + " obs", 12)} \u2551`);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
lines.push(" \u2551 \u2551");
|
|
4250
|
+
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");
|
|
4251
|
+
return lines.join("\n");
|
|
4252
|
+
}
|
|
3994
4253
|
|
|
3995
4254
|
// src/engine/semantic.ts
|
|
3996
4255
|
var DOMAIN_SIGNALS = {
|
|
@@ -4486,7 +4745,7 @@ function fmt3(n) {
|
|
|
4486
4745
|
}
|
|
4487
4746
|
|
|
4488
4747
|
// src/engine/compile-proof.ts
|
|
4489
|
-
import { resolve as resolve11, join as
|
|
4748
|
+
import { resolve as resolve11, join as join9, dirname as dirname3, basename as basename4 } from "path";
|
|
4490
4749
|
import { writeFile as writeFile6, mkdir as mkdir5, rm, cp } from "fs/promises";
|
|
4491
4750
|
import { execSync } from "child_process";
|
|
4492
4751
|
async function runCompileProof(analysis, task, budget = 5e4) {
|
|
@@ -4688,21 +4947,21 @@ function extractKnownTypes(analysis, typePaths) {
|
|
|
4688
4947
|
return types;
|
|
4689
4948
|
}
|
|
4690
4949
|
async function runTscWithContext(name, projectPath, selectedPaths, tokensUsed, typePaths, consumerCode) {
|
|
4691
|
-
const tmpDir =
|
|
4950
|
+
const tmpDir = join9(projectPath, ".cto", `compile-proof-${name.toLowerCase()}`);
|
|
4692
4951
|
try {
|
|
4693
4952
|
await rm(tmpDir, { recursive: true, force: true });
|
|
4694
4953
|
await mkdir5(tmpDir, { recursive: true });
|
|
4695
4954
|
for (const filePath of selectedPaths) {
|
|
4696
|
-
const src =
|
|
4697
|
-
const dest =
|
|
4955
|
+
const src = join9(projectPath, filePath);
|
|
4956
|
+
const dest = join9(tmpDir, filePath);
|
|
4698
4957
|
try {
|
|
4699
4958
|
await mkdir5(dirname3(dest), { recursive: true });
|
|
4700
4959
|
await cp(src, dest);
|
|
4701
4960
|
} catch {
|
|
4702
4961
|
}
|
|
4703
4962
|
}
|
|
4704
|
-
await writeFile6(
|
|
4705
|
-
await writeFile6(
|
|
4963
|
+
await writeFile6(join9(tmpDir, "_compile_test.ts"), consumerCode);
|
|
4964
|
+
await writeFile6(join9(tmpDir, "tsconfig.json"), JSON.stringify({
|
|
4706
4965
|
compilerOptions: {
|
|
4707
4966
|
target: "ES2022",
|
|
4708
4967
|
module: "nodenext",
|
|
@@ -5010,8 +5269,8 @@ function fmt5(n) {
|
|
|
5010
5269
|
|
|
5011
5270
|
// src/engine/monorepo.ts
|
|
5012
5271
|
import { readFile as readFile11, readdir as readdir4 } from "fs/promises";
|
|
5013
|
-
import { join as
|
|
5014
|
-
import { existsSync as
|
|
5272
|
+
import { join as join10, relative as relative6, basename as basename5 } from "path";
|
|
5273
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5015
5274
|
async function detectMonorepoTool(rootPath) {
|
|
5016
5275
|
const checks = [
|
|
5017
5276
|
{ file: "nx.json", tool: "nx" },
|
|
@@ -5032,14 +5291,14 @@ async function detectMonorepoTool(rootPath) {
|
|
|
5032
5291
|
}
|
|
5033
5292
|
];
|
|
5034
5293
|
for (const check of checks) {
|
|
5035
|
-
const filePath =
|
|
5036
|
-
if (
|
|
5294
|
+
const filePath = join10(rootPath, check.file);
|
|
5295
|
+
if (existsSync4(filePath)) {
|
|
5037
5296
|
if (!check.validate) return check.tool;
|
|
5038
5297
|
try {
|
|
5039
5298
|
const content = await readFile11(filePath, "utf-8");
|
|
5040
5299
|
if (check.validate(content)) {
|
|
5041
5300
|
if (check.tool === "npm-workspaces") {
|
|
5042
|
-
if (
|
|
5301
|
+
if (existsSync4(join10(rootPath, "yarn.lock"))) return "yarn-workspaces";
|
|
5043
5302
|
return "npm-workspaces";
|
|
5044
5303
|
}
|
|
5045
5304
|
return check.tool;
|
|
@@ -5054,15 +5313,15 @@ async function resolveWorkspaceGlobs(rootPath, globs) {
|
|
|
5054
5313
|
const packagePaths = [];
|
|
5055
5314
|
for (const glob of globs) {
|
|
5056
5315
|
const cleanGlob = glob.replace(/\/?\*\*?$/, "");
|
|
5057
|
-
const searchDir =
|
|
5058
|
-
if (!
|
|
5316
|
+
const searchDir = join10(rootPath, cleanGlob);
|
|
5317
|
+
if (!existsSync4(searchDir)) continue;
|
|
5059
5318
|
try {
|
|
5060
5319
|
const entries = await readdir4(searchDir, { withFileTypes: true });
|
|
5061
5320
|
for (const entry of entries) {
|
|
5062
5321
|
if (!entry.isDirectory()) continue;
|
|
5063
|
-
const pkgJsonPath =
|
|
5064
|
-
if (
|
|
5065
|
-
packagePaths.push(
|
|
5322
|
+
const pkgJsonPath = join10(searchDir, entry.name, "package.json");
|
|
5323
|
+
if (existsSync4(pkgJsonPath)) {
|
|
5324
|
+
packagePaths.push(join10(searchDir, entry.name));
|
|
5066
5325
|
}
|
|
5067
5326
|
}
|
|
5068
5327
|
} catch {
|
|
@@ -5074,12 +5333,12 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5074
5333
|
switch (tool) {
|
|
5075
5334
|
case "npm-workspaces":
|
|
5076
5335
|
case "yarn-workspaces": {
|
|
5077
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5336
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5078
5337
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5079
5338
|
return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5080
5339
|
}
|
|
5081
5340
|
case "pnpm-workspaces": {
|
|
5082
|
-
const content = await readFile11(
|
|
5341
|
+
const content = await readFile11(join10(rootPath, "pnpm-workspace.yaml"), "utf-8");
|
|
5083
5342
|
const packages = [];
|
|
5084
5343
|
let inPackages = false;
|
|
5085
5344
|
for (const line of content.split("\n")) {
|
|
@@ -5097,20 +5356,20 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5097
5356
|
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5098
5357
|
}
|
|
5099
5358
|
case "turborepo": {
|
|
5100
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5359
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5101
5360
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
5102
5361
|
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5103
|
-
if (
|
|
5362
|
+
if (existsSync4(join10(rootPath, "pnpm-workspace.yaml"))) {
|
|
5104
5363
|
return discoverPackages(rootPath, "pnpm-workspaces");
|
|
5105
5364
|
}
|
|
5106
5365
|
return [];
|
|
5107
5366
|
}
|
|
5108
5367
|
case "nx": {
|
|
5109
5368
|
const standardDirs = ["packages", "apps", "libs"];
|
|
5110
|
-
const globs = standardDirs.filter((d) =>
|
|
5369
|
+
const globs = standardDirs.filter((d) => existsSync4(join10(rootPath, d)));
|
|
5111
5370
|
if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
|
|
5112
5371
|
try {
|
|
5113
|
-
const pkgJson = JSON.parse(await readFile11(
|
|
5372
|
+
const pkgJson = JSON.parse(await readFile11(join10(rootPath, "package.json"), "utf-8"));
|
|
5114
5373
|
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
|
|
5115
5374
|
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
5116
5375
|
} catch {
|
|
@@ -5118,7 +5377,7 @@ async function discoverPackages(rootPath, tool) {
|
|
|
5118
5377
|
return [];
|
|
5119
5378
|
}
|
|
5120
5379
|
case "lerna": {
|
|
5121
|
-
const lernaJson = JSON.parse(await readFile11(
|
|
5380
|
+
const lernaJson = JSON.parse(await readFile11(join10(rootPath, "lerna.json"), "utf-8"));
|
|
5122
5381
|
const packages = lernaJson.packages || ["packages/*"];
|
|
5123
5382
|
return resolveWorkspaceGlobs(rootPath, packages);
|
|
5124
5383
|
}
|
|
@@ -5172,7 +5431,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
5172
5431
|
const packages = [];
|
|
5173
5432
|
const packageTokenMap = {};
|
|
5174
5433
|
for (const pkgPath of packagePaths) {
|
|
5175
|
-
const pkgJsonPath =
|
|
5434
|
+
const pkgJsonPath = join10(pkgPath, "package.json");
|
|
5176
5435
|
let name = basename5(pkgPath);
|
|
5177
5436
|
let pkgDeps = [];
|
|
5178
5437
|
try {
|
|
@@ -5215,7 +5474,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
5215
5474
|
}
|
|
5216
5475
|
const pkgNames = new Set(packages.map((p) => p.name));
|
|
5217
5476
|
for (const pkg of packages) {
|
|
5218
|
-
const pkgJsonPath =
|
|
5477
|
+
const pkgJsonPath = join10(pkg.path, "package.json");
|
|
5219
5478
|
try {
|
|
5220
5479
|
const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
|
|
5221
5480
|
const allDeps = {
|
|
@@ -5369,7 +5628,7 @@ function renderPackageContext(result) {
|
|
|
5369
5628
|
// src/engine/quality-gate.ts
|
|
5370
5629
|
import { readFile as readFile12, writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
|
|
5371
5630
|
import { resolve as resolve13 } from "path";
|
|
5372
|
-
import { existsSync as
|
|
5631
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5373
5632
|
var DEFAULT_GATE_CONFIG = {
|
|
5374
5633
|
threshold: 70,
|
|
5375
5634
|
failOnSecrets: true,
|
|
@@ -5380,7 +5639,7 @@ var DEFAULT_GATE_CONFIG = {
|
|
|
5380
5639
|
};
|
|
5381
5640
|
async function loadBaseline(projectPath, baselinePath) {
|
|
5382
5641
|
const filePath = resolve13(projectPath, baselinePath || ".cto/baseline.json");
|
|
5383
|
-
if (!
|
|
5642
|
+
if (!existsSync5(filePath)) return null;
|
|
5384
5643
|
try {
|
|
5385
5644
|
const content = await readFile12(filePath, "utf-8");
|
|
5386
5645
|
return JSON.parse(content);
|
|
@@ -5390,7 +5649,7 @@ async function loadBaseline(projectPath, baselinePath) {
|
|
|
5390
5649
|
}
|
|
5391
5650
|
async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
|
|
5392
5651
|
const dir = resolve13(projectPath, ".cto");
|
|
5393
|
-
if (!
|
|
5652
|
+
if (!existsSync5(dir)) await mkdir6(dir, { recursive: true });
|
|
5394
5653
|
const baseline = {
|
|
5395
5654
|
score: score.overall,
|
|
5396
5655
|
grade: score.grade,
|
|
@@ -5537,10 +5796,569 @@ Warnings: ${warnings.map((c) => c.name).join(", ")}`;
|
|
|
5537
5796
|
}
|
|
5538
5797
|
return summary;
|
|
5539
5798
|
}
|
|
5799
|
+
|
|
5800
|
+
// src/engine/code-review.ts
|
|
5801
|
+
import { resolve as resolve14, basename as basename6, dirname as dirname5 } from "path";
|
|
5802
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
5803
|
+
import { execFile as execFile2 } from "child_process";
|
|
5804
|
+
import { promisify as promisify2 } from "util";
|
|
5805
|
+
var exec2 = promisify2(execFile2);
|
|
5806
|
+
async function git2(args, cwd) {
|
|
5807
|
+
try {
|
|
5808
|
+
const { stdout } = await exec2("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
5809
|
+
return stdout.trim();
|
|
5810
|
+
} catch {
|
|
5811
|
+
return "";
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
async function analyzeForReview(analysis, options = {}) {
|
|
5815
|
+
const projectPath = resolve14(analysis.projectPath);
|
|
5816
|
+
const baseBranch = options.baseBranch ?? "main";
|
|
5817
|
+
const depth = options.depth ?? 2;
|
|
5818
|
+
const maxPromptFiles = options.maxPromptFiles ?? 20;
|
|
5819
|
+
const isRepo = await git2(["rev-parse", "--is-inside-work-tree"], projectPath) === "true";
|
|
5820
|
+
if (!isRepo) {
|
|
5821
|
+
return emptyResult3(baseBranch);
|
|
5822
|
+
}
|
|
5823
|
+
const branch = await git2(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
|
|
5824
|
+
const changedFiles = await getChangedFilesWithHunks(projectPath, baseBranch, analysis);
|
|
5825
|
+
if (changedFiles.length === 0) {
|
|
5826
|
+
return {
|
|
5827
|
+
...emptyResult3(baseBranch),
|
|
5828
|
+
branch,
|
|
5829
|
+
isGitRepo: true,
|
|
5830
|
+
renderedSummary: "# Code Review\n\nNo changed files detected."
|
|
5831
|
+
};
|
|
5832
|
+
}
|
|
5833
|
+
const totalLinesChanged = changedFiles.reduce((s, f) => s + f.linesAdded + f.linesRemoved, 0);
|
|
5834
|
+
const breakingChanges = detectBreakingChanges(changedFiles, analysis);
|
|
5835
|
+
const missingFiles = findMissingFiles(changedFiles, analysis, depth);
|
|
5836
|
+
const impactRadius = computeImpactRadius(changedFiles, analysis, depth);
|
|
5837
|
+
const reviewQuality = calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged);
|
|
5838
|
+
const reviewPrompt = await generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxPromptFiles);
|
|
5839
|
+
const renderedSummary = renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality);
|
|
5840
|
+
return {
|
|
5841
|
+
branch,
|
|
5842
|
+
baseBranch,
|
|
5843
|
+
isGitRepo: true,
|
|
5844
|
+
changedFiles,
|
|
5845
|
+
totalLinesChanged,
|
|
5846
|
+
breakingChanges,
|
|
5847
|
+
missingFiles,
|
|
5848
|
+
impactRadius,
|
|
5849
|
+
reviewQuality,
|
|
5850
|
+
reviewPrompt,
|
|
5851
|
+
renderedSummary
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
5854
|
+
async function getChangedFilesWithHunks(projectPath, baseBranch, analysis) {
|
|
5855
|
+
const files = [];
|
|
5856
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
5857
|
+
const numstat = await git2(["diff", "--numstat", "HEAD"], projectPath);
|
|
5858
|
+
const branchNumstat = await git2(["diff", "--numstat", `${baseBranch}...HEAD`], projectPath);
|
|
5859
|
+
const nameStatus = await git2(["diff", "--name-status", `${baseBranch}...HEAD`], projectPath);
|
|
5860
|
+
const changeTypes = /* @__PURE__ */ new Map();
|
|
5861
|
+
for (const line of nameStatus.split("\n")) {
|
|
5862
|
+
const parts = line.trim().split(" ");
|
|
5863
|
+
if (parts.length < 2) continue;
|
|
5864
|
+
const status = parts[0];
|
|
5865
|
+
const filePath = parts[parts.length - 1];
|
|
5866
|
+
if (status === "A") changeTypes.set(filePath, "added");
|
|
5867
|
+
else if (status === "D") changeTypes.set(filePath, "deleted");
|
|
5868
|
+
else if (status.startsWith("R")) changeTypes.set(filePath, "renamed");
|
|
5869
|
+
else changeTypes.set(filePath, "modified");
|
|
5870
|
+
}
|
|
5871
|
+
const allNumstat = (numstat + "\n" + branchNumstat).split("\n");
|
|
5872
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5873
|
+
for (const line of allNumstat) {
|
|
5874
|
+
const parts = line.trim().split(" ");
|
|
5875
|
+
if (parts.length < 3) continue;
|
|
5876
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5877
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5878
|
+
const filePath = parts[2];
|
|
5879
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5880
|
+
seen.add(filePath);
|
|
5881
|
+
const af = fileMap.get(filePath);
|
|
5882
|
+
const changeType = changeTypes.get(filePath) ?? "modified";
|
|
5883
|
+
const hunks = await parseDiffHunks(projectPath, baseBranch, filePath);
|
|
5884
|
+
const hasExportChanges = hunks.some(
|
|
5885
|
+
(h) => h.additions.some((l) => /^\s*export\s/.test(l)) || h.deletions.some((l) => /^\s*export\s/.test(l))
|
|
5886
|
+
);
|
|
5887
|
+
const hasTypeChanges = hunks.some(
|
|
5888
|
+
(h) => h.additions.some((l) => /^\s*(interface|type|enum)\s/.test(l)) || h.deletions.some((l) => /^\s*(interface|type|enum)\s/.test(l))
|
|
5889
|
+
);
|
|
5890
|
+
files.push({
|
|
5891
|
+
relativePath: filePath,
|
|
5892
|
+
changeType,
|
|
5893
|
+
linesAdded: added,
|
|
5894
|
+
linesRemoved: removed,
|
|
5895
|
+
riskScore: af?.riskScore ?? 0,
|
|
5896
|
+
kind: af?.kind ?? "unknown",
|
|
5897
|
+
hunks,
|
|
5898
|
+
hasExportChanges,
|
|
5899
|
+
hasTypeChanges
|
|
5900
|
+
});
|
|
5901
|
+
}
|
|
5902
|
+
const workingNumstat = await git2(["diff", "--numstat"], projectPath);
|
|
5903
|
+
for (const line of workingNumstat.split("\n")) {
|
|
5904
|
+
const parts = line.trim().split(" ");
|
|
5905
|
+
if (parts.length < 3) continue;
|
|
5906
|
+
const filePath = parts[2];
|
|
5907
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5908
|
+
seen.add(filePath);
|
|
5909
|
+
const af = fileMap.get(filePath);
|
|
5910
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5911
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5912
|
+
files.push({
|
|
5913
|
+
relativePath: filePath,
|
|
5914
|
+
changeType: "modified",
|
|
5915
|
+
linesAdded: added,
|
|
5916
|
+
linesRemoved: removed,
|
|
5917
|
+
riskScore: af?.riskScore ?? 0,
|
|
5918
|
+
kind: af?.kind ?? "unknown",
|
|
5919
|
+
hunks: [],
|
|
5920
|
+
hasExportChanges: false,
|
|
5921
|
+
hasTypeChanges: false
|
|
5922
|
+
});
|
|
5923
|
+
}
|
|
5924
|
+
return files.sort((a, b) => b.riskScore - a.riskScore);
|
|
5925
|
+
}
|
|
5926
|
+
async function parseDiffHunks(projectPath, baseBranch, filePath) {
|
|
5927
|
+
const diff = await git2(["diff", "-U3", `${baseBranch}...HEAD`, "--", filePath], projectPath);
|
|
5928
|
+
if (!diff) return [];
|
|
5929
|
+
const hunks = [];
|
|
5930
|
+
const lines = diff.split("\n");
|
|
5931
|
+
let currentHunk = null;
|
|
5932
|
+
for (const line of lines) {
|
|
5933
|
+
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@\s*(.*)/);
|
|
5934
|
+
if (hunkMatch) {
|
|
5935
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
5936
|
+
currentHunk = {
|
|
5937
|
+
startLine: parseInt(hunkMatch[2], 10),
|
|
5938
|
+
endLine: parseInt(hunkMatch[2], 10),
|
|
5939
|
+
header: hunkMatch[3] || "",
|
|
5940
|
+
additions: [],
|
|
5941
|
+
deletions: []
|
|
5942
|
+
};
|
|
5943
|
+
continue;
|
|
5944
|
+
}
|
|
5945
|
+
if (!currentHunk) continue;
|
|
5946
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
5947
|
+
currentHunk.additions.push(line.substring(1));
|
|
5948
|
+
currentHunk.endLine++;
|
|
5949
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
5950
|
+
currentHunk.deletions.push(line.substring(1));
|
|
5951
|
+
} else if (!line.startsWith("\\")) {
|
|
5952
|
+
if (currentHunk) currentHunk.endLine++;
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
5956
|
+
return hunks;
|
|
5957
|
+
}
|
|
5958
|
+
function detectBreakingChanges(changedFiles, analysis) {
|
|
5959
|
+
const breaks = [];
|
|
5960
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
5961
|
+
for (const file of changedFiles) {
|
|
5962
|
+
if (file.changeType === "deleted") {
|
|
5963
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
5964
|
+
if (dependents.length > 0) {
|
|
5965
|
+
breaks.push({
|
|
5966
|
+
file: file.relativePath,
|
|
5967
|
+
type: "export-removed",
|
|
5968
|
+
severity: "critical",
|
|
5969
|
+
description: `File deleted but ${dependents.length} files depend on it`,
|
|
5970
|
+
affectedFiles: dependents
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
continue;
|
|
5974
|
+
}
|
|
5975
|
+
for (const hunk of file.hunks) {
|
|
5976
|
+
for (const del of hunk.deletions) {
|
|
5977
|
+
const exportMatch = del.match(/^\s*export\s+(function|const|class|type|interface|enum)\s+(\w+)/);
|
|
5978
|
+
if (exportMatch) {
|
|
5979
|
+
const [, kind, name] = exportMatch;
|
|
5980
|
+
const wasReAdded = hunk.additions.some(
|
|
5981
|
+
(a) => new RegExp(`export\\s+${kind}\\s+${name}\\b`).test(a)
|
|
5982
|
+
);
|
|
5983
|
+
if (!wasReAdded) {
|
|
5984
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
5985
|
+
breaks.push({
|
|
5986
|
+
file: file.relativePath,
|
|
5987
|
+
type: kind === "type" || kind === "interface" ? "type-changed" : "export-removed",
|
|
5988
|
+
severity: dependents.length > 3 ? "critical" : dependents.length > 0 ? "high" : "medium",
|
|
5989
|
+
description: `Removed export ${kind} ${name}`,
|
|
5990
|
+
affectedFiles: dependents
|
|
5991
|
+
});
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
const propMatch = del.match(/^\s+(\w+)\s*[?:].*[;,]?\s*$/);
|
|
5995
|
+
if (propMatch && file.hasTypeChanges) {
|
|
5996
|
+
const propName = propMatch[1];
|
|
5997
|
+
const wasReAdded = hunk.additions.some(
|
|
5998
|
+
(a) => new RegExp(`\\b${propName}\\s*[?:]`).test(a)
|
|
5999
|
+
);
|
|
6000
|
+
if (!wasReAdded) {
|
|
6001
|
+
breaks.push({
|
|
6002
|
+
file: file.relativePath,
|
|
6003
|
+
type: "interface-changed",
|
|
6004
|
+
severity: "high",
|
|
6005
|
+
description: `Removed property "${propName}" from type/interface`,
|
|
6006
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
6007
|
+
});
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
for (const add of hunk.additions) {
|
|
6012
|
+
const fnMatch = add.match(/^\s*export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
|
|
6013
|
+
if (fnMatch) {
|
|
6014
|
+
const [, , fnName, newParams] = fnMatch;
|
|
6015
|
+
for (const del of hunk.deletions) {
|
|
6016
|
+
const origMatch = del.match(new RegExp(`export\\s+(async\\s+)?function\\s+${fnName}\\s*\\(([^)]*)\\)`));
|
|
6017
|
+
if (origMatch) {
|
|
6018
|
+
const oldParams = origMatch[2];
|
|
6019
|
+
if (oldParams !== newParams) {
|
|
6020
|
+
const oldCount = oldParams.split(",").filter((p) => p.trim()).length;
|
|
6021
|
+
const newCount = newParams.split(",").filter((p) => p.trim()).length;
|
|
6022
|
+
if (oldCount !== newCount || !paramsCompatible(oldParams, newParams)) {
|
|
6023
|
+
breaks.push({
|
|
6024
|
+
file: file.relativePath,
|
|
6025
|
+
type: "function-signature",
|
|
6026
|
+
severity: "high",
|
|
6027
|
+
description: `Function "${fnName}" signature changed: (${oldParams.trim()}) \u2192 (${newParams.trim()})`,
|
|
6028
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
return breaks.sort((a, b) => {
|
|
6039
|
+
const sev = { critical: 0, high: 1, medium: 2 };
|
|
6040
|
+
return sev[a.severity] - sev[b.severity];
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
function paramsCompatible(oldParams, newParams) {
|
|
6044
|
+
const oldParts = oldParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
6045
|
+
const newParts = newParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
6046
|
+
let j = 0;
|
|
6047
|
+
for (let i = 0; i < oldParts.length && j < newParts.length; i++) {
|
|
6048
|
+
if (oldParts[i] === newParts[j]) j++;
|
|
6049
|
+
}
|
|
6050
|
+
return j >= oldParts.length;
|
|
6051
|
+
}
|
|
6052
|
+
function findDependents(filePath, analysis) {
|
|
6053
|
+
return analysis.files.filter((f) => f.imports.includes(filePath) || f.imports.some((imp) => imp.endsWith(filePath))).map((f) => f.relativePath);
|
|
6054
|
+
}
|
|
6055
|
+
function findMissingFiles(changedFiles, analysis, depth) {
|
|
6056
|
+
const missing = [];
|
|
6057
|
+
const changedPaths = new Set(changedFiles.map((f) => f.relativePath));
|
|
6058
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
6059
|
+
for (const changed of changedFiles) {
|
|
6060
|
+
if (changed.changeType === "deleted") continue;
|
|
6061
|
+
const af = fileMap.get(changed.relativePath);
|
|
6062
|
+
if (!af) continue;
|
|
6063
|
+
if (changed.hasTypeChanges || changed.hasExportChanges) {
|
|
6064
|
+
const dir2 = dirname5(changed.relativePath);
|
|
6065
|
+
const base = basename6(changed.relativePath).replace(/\.[^.]+$/, "");
|
|
6066
|
+
const typeVariants = [
|
|
6067
|
+
`${dir2}/${base}.types.ts`,
|
|
6068
|
+
`${dir2}/${base}.types.tsx`,
|
|
6069
|
+
`${dir2}/types.ts`,
|
|
6070
|
+
`${dir2}/types/${base}.ts`,
|
|
6071
|
+
`${dir2}/index.d.ts`
|
|
6072
|
+
];
|
|
6073
|
+
for (const variant of typeVariants) {
|
|
6074
|
+
if (fileMap.has(variant) && !changedPaths.has(variant)) {
|
|
6075
|
+
missing.push({
|
|
6076
|
+
file: variant,
|
|
6077
|
+
reason: `Type file for ${changed.relativePath} \u2014 may need updates`,
|
|
6078
|
+
severity: changed.hasExportChanges ? "high" : "medium",
|
|
6079
|
+
relatedChangedFile: changed.relativePath,
|
|
6080
|
+
relationship: "sibling-type"
|
|
6081
|
+
});
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
const testVariants = [
|
|
6086
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".test.$1"),
|
|
6087
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".spec.$1"),
|
|
6088
|
+
changed.relativePath.replace(/^src\//, "tests/").replace(/\.([^.]+)$/, ".test.$1"),
|
|
6089
|
+
changed.relativePath.replace(/^src\//, "__tests__/").replace(/\.([^.]+)$/, ".test.$1")
|
|
6090
|
+
];
|
|
6091
|
+
for (const testPath of testVariants) {
|
|
6092
|
+
if (fileMap.has(testPath) && !changedPaths.has(testPath)) {
|
|
6093
|
+
missing.push({
|
|
6094
|
+
file: testPath,
|
|
6095
|
+
reason: `Test file for ${changed.relativePath} \u2014 should be updated`,
|
|
6096
|
+
severity: "medium",
|
|
6097
|
+
relatedChangedFile: changed.relativePath,
|
|
6098
|
+
relationship: "test"
|
|
6099
|
+
});
|
|
6100
|
+
break;
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
if (changed.hasExportChanges) {
|
|
6104
|
+
const importers = analysis.files.filter(
|
|
6105
|
+
(f) => f.imports.includes(af.relativePath) && !changedPaths.has(f.relativePath)
|
|
6106
|
+
);
|
|
6107
|
+
for (const importer of importers.slice(0, 5)) {
|
|
6108
|
+
missing.push({
|
|
6109
|
+
file: importer.relativePath,
|
|
6110
|
+
reason: `Imports ${changed.relativePath} which has export changes`,
|
|
6111
|
+
severity: "high",
|
|
6112
|
+
relatedChangedFile: changed.relativePath,
|
|
6113
|
+
relationship: "imported-by"
|
|
6114
|
+
});
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
const dir = dirname5(changed.relativePath);
|
|
6118
|
+
const indexFile = `${dir}/index.ts`;
|
|
6119
|
+
if (fileMap.has(indexFile) && !changedPaths.has(indexFile) && changed.hasExportChanges) {
|
|
6120
|
+
missing.push({
|
|
6121
|
+
file: indexFile,
|
|
6122
|
+
reason: `Barrel export may need updating after changes to ${changed.relativePath}`,
|
|
6123
|
+
severity: "medium",
|
|
6124
|
+
relatedChangedFile: changed.relativePath,
|
|
6125
|
+
relationship: "co-located"
|
|
6126
|
+
});
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6130
|
+
return missing.filter((m) => {
|
|
6131
|
+
if (seen.has(m.file)) return false;
|
|
6132
|
+
seen.add(m.file);
|
|
6133
|
+
return true;
|
|
6134
|
+
}).sort((a, b) => {
|
|
6135
|
+
const sev = { high: 0, medium: 1, low: 2 };
|
|
6136
|
+
return sev[a.severity] - sev[b.severity];
|
|
6137
|
+
});
|
|
6138
|
+
}
|
|
6139
|
+
function computeImpactRadius(changedFiles, analysis, depth) {
|
|
6140
|
+
const changedPaths = changedFiles.map((f) => f.relativePath);
|
|
6141
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
6142
|
+
const direct = /* @__PURE__ */ new Set();
|
|
6143
|
+
for (const path of changedPaths) {
|
|
6144
|
+
const importers = adj.reverse.get(path) ?? [];
|
|
6145
|
+
const imports = adj.forward.get(path) ?? [];
|
|
6146
|
+
for (const n of [...importers, ...imports]) {
|
|
6147
|
+
if (!changedPaths.includes(n)) direct.add(n);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
const allAffected = bfsBidirectional(changedPaths, adj, depth);
|
|
6151
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
6152
|
+
for (const path of allAffected) {
|
|
6153
|
+
if (!changedPaths.includes(path) && !direct.has(path)) {
|
|
6154
|
+
transitive.add(path);
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
const affectedTests = [...allAffected].filter((p) => {
|
|
6158
|
+
const f = analysis.files.find((af) => af.relativePath === p);
|
|
6159
|
+
return f?.kind === "test";
|
|
6160
|
+
}).length;
|
|
6161
|
+
const hotspots = changedFiles.map((f) => ({
|
|
6162
|
+
file: f.relativePath,
|
|
6163
|
+
dependents: adj.reverse.get(f.relativePath)?.length ?? 0,
|
|
6164
|
+
riskScore: f.riskScore
|
|
6165
|
+
})).sort((a, b) => b.dependents * b.riskScore - a.dependents * a.riskScore).slice(0, 5);
|
|
6166
|
+
const totalAffected = direct.size + transitive.size;
|
|
6167
|
+
const maxRisk = Math.max(...changedFiles.map((f) => f.riskScore), 0);
|
|
6168
|
+
const avgRisk = changedFiles.length > 0 ? changedFiles.reduce((s, f) => s + f.riskScore, 0) / changedFiles.length : 0;
|
|
6169
|
+
const riskScore = Math.min(100, Math.round(
|
|
6170
|
+
avgRisk * 0.3 + maxRisk * 0.2 + Math.min(100, totalAffected * 3) * 0.3 + Math.min(100, changedFiles.length * 5) * 0.2
|
|
6171
|
+
));
|
|
6172
|
+
return {
|
|
6173
|
+
directlyAffected: direct.size,
|
|
6174
|
+
transitivelyAffected: transitive.size,
|
|
6175
|
+
totalAffected,
|
|
6176
|
+
affectedTests,
|
|
6177
|
+
riskScore,
|
|
6178
|
+
hotspots
|
|
6179
|
+
};
|
|
6180
|
+
}
|
|
6181
|
+
function calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged) {
|
|
6182
|
+
const factors = [];
|
|
6183
|
+
const sizeScore = totalLinesChanged <= 50 ? 100 : totalLinesChanged <= 200 ? 85 : totalLinesChanged <= 500 ? 65 : totalLinesChanged <= 1e3 ? 40 : 20;
|
|
6184
|
+
factors.push({
|
|
6185
|
+
name: "PR Size",
|
|
6186
|
+
score: sizeScore,
|
|
6187
|
+
weight: 0.25,
|
|
6188
|
+
detail: `${totalLinesChanged} lines changed \u2014 ${sizeScore >= 80 ? "easy" : sizeScore >= 50 ? "manageable" : "too large"} to review`
|
|
6189
|
+
});
|
|
6190
|
+
const focusScore = changedFiles.length <= 3 ? 100 : changedFiles.length <= 8 ? 80 : changedFiles.length <= 15 ? 55 : 25;
|
|
6191
|
+
factors.push({
|
|
6192
|
+
name: "Focus",
|
|
6193
|
+
score: focusScore,
|
|
6194
|
+
weight: 0.2,
|
|
6195
|
+
detail: `${changedFiles.length} files \u2014 ${focusScore >= 80 ? "focused" : focusScore >= 50 ? "moderate scope" : "unfocused"}`
|
|
6196
|
+
});
|
|
6197
|
+
const criticalBreaks = breakingChanges.filter((b) => b.severity === "critical").length;
|
|
6198
|
+
const highBreaks = breakingChanges.filter((b) => b.severity === "high").length;
|
|
6199
|
+
const breakScore = criticalBreaks > 0 ? 10 : highBreaks > 2 ? 30 : highBreaks > 0 ? 60 : breakingChanges.length > 0 ? 80 : 100;
|
|
6200
|
+
factors.push({
|
|
6201
|
+
name: "Breaking Changes",
|
|
6202
|
+
score: breakScore,
|
|
6203
|
+
weight: 0.25,
|
|
6204
|
+
detail: `${breakingChanges.length} breaking changes (${criticalBreaks} critical, ${highBreaks} high)`
|
|
6205
|
+
});
|
|
6206
|
+
const highMissing = missingFiles.filter((m) => m.severity === "high").length;
|
|
6207
|
+
const completenessScore = highMissing > 3 ? 20 : highMissing > 0 ? 50 : missingFiles.length > 3 ? 65 : missingFiles.length > 0 ? 80 : 100;
|
|
6208
|
+
factors.push({
|
|
6209
|
+
name: "Completeness",
|
|
6210
|
+
score: completenessScore,
|
|
6211
|
+
weight: 0.15,
|
|
6212
|
+
detail: `${missingFiles.length} potentially missing files (${highMissing} high priority)`
|
|
6213
|
+
});
|
|
6214
|
+
const radiusScore = impactRadius.totalAffected <= 5 ? 100 : impactRadius.totalAffected <= 15 ? 75 : impactRadius.totalAffected <= 30 ? 50 : 25;
|
|
6215
|
+
factors.push({
|
|
6216
|
+
name: "Blast Radius",
|
|
6217
|
+
score: radiusScore,
|
|
6218
|
+
weight: 0.15,
|
|
6219
|
+
detail: `${impactRadius.totalAffected} files affected (${impactRadius.directlyAffected} direct, ${impactRadius.transitivelyAffected} transitive)`
|
|
6220
|
+
});
|
|
6221
|
+
const overall = Math.round(factors.reduce((s, f) => s + f.score * f.weight, 0));
|
|
6222
|
+
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";
|
|
6223
|
+
return { score: overall, grade, factors };
|
|
6224
|
+
}
|
|
6225
|
+
async function generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxFiles) {
|
|
6226
|
+
const lines = [];
|
|
6227
|
+
lines.push("# Code Review Context");
|
|
6228
|
+
lines.push("");
|
|
6229
|
+
lines.push("## Project: " + analysis.projectName);
|
|
6230
|
+
lines.push("## Stack: " + analysis.stack.join(", "));
|
|
6231
|
+
lines.push("");
|
|
6232
|
+
if (breakingChanges.length > 0) {
|
|
6233
|
+
lines.push("## \u26A0\uFE0F BREAKING CHANGES DETECTED");
|
|
6234
|
+
lines.push("");
|
|
6235
|
+
for (const bc of breakingChanges) {
|
|
6236
|
+
lines.push("- **" + bc.severity.toUpperCase() + "** " + bc.file + ": " + bc.description);
|
|
6237
|
+
if (bc.affectedFiles.length > 0) {
|
|
6238
|
+
lines.push(" Affected: " + bc.affectedFiles.slice(0, 5).join(", "));
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
lines.push("");
|
|
6242
|
+
}
|
|
6243
|
+
if (missingFiles.length > 0) {
|
|
6244
|
+
lines.push("## \u{1F4CB} Potentially Missing Files");
|
|
6245
|
+
lines.push("");
|
|
6246
|
+
for (const mf of missingFiles.slice(0, 10)) {
|
|
6247
|
+
lines.push("- " + mf.file + " \u2014 " + mf.reason);
|
|
6248
|
+
}
|
|
6249
|
+
lines.push("");
|
|
6250
|
+
}
|
|
6251
|
+
lines.push("## Changed Files");
|
|
6252
|
+
lines.push("");
|
|
6253
|
+
const topFiles = changedFiles.slice(0, maxFiles);
|
|
6254
|
+
for (const file of topFiles) {
|
|
6255
|
+
const icon = file.changeType === "added" ? "\u{1F195}" : file.changeType === "deleted" ? "\u{1F5D1}\uFE0F" : "\u{1F4DD}";
|
|
6256
|
+
lines.push("### " + icon + " " + file.relativePath + " (risk: " + file.riskScore + ", " + file.kind + ")");
|
|
6257
|
+
lines.push("+" + file.linesAdded + "/-" + file.linesRemoved + " lines");
|
|
6258
|
+
lines.push("");
|
|
6259
|
+
if (file.riskScore >= 40 && file.changeType !== "deleted") {
|
|
6260
|
+
try {
|
|
6261
|
+
const content = await readFile13(resolve14(projectPath, file.relativePath), "utf-8");
|
|
6262
|
+
const ext = file.relativePath.split(".").pop() ?? "";
|
|
6263
|
+
const maxChars = 4e3;
|
|
6264
|
+
const truncated = content.length > maxChars;
|
|
6265
|
+
lines.push("```" + ext);
|
|
6266
|
+
lines.push(content.slice(0, maxChars));
|
|
6267
|
+
if (truncated) lines.push("// ... [truncated]");
|
|
6268
|
+
lines.push("```");
|
|
6269
|
+
lines.push("");
|
|
6270
|
+
} catch {
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
lines.push("## Review Instructions");
|
|
6275
|
+
lines.push("");
|
|
6276
|
+
lines.push("1. Check breaking changes above for correctness");
|
|
6277
|
+
lines.push("2. Verify all affected files have been updated");
|
|
6278
|
+
lines.push("3. Review changed files for bugs, security issues, and code quality");
|
|
6279
|
+
lines.push("4. Ensure tests cover the changes");
|
|
6280
|
+
if (missingFiles.length > 0) {
|
|
6281
|
+
lines.push('5. Consider whether the "potentially missing files" need updates');
|
|
6282
|
+
}
|
|
6283
|
+
return lines.join("\n");
|
|
6284
|
+
}
|
|
6285
|
+
function renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality) {
|
|
6286
|
+
const lines = [];
|
|
6287
|
+
const qIcon = reviewQuality.score >= 80 ? "\u{1F7E2}" : reviewQuality.score >= 60 ? "\u{1F7E1}" : "\u{1F534}";
|
|
6288
|
+
lines.push("");
|
|
6289
|
+
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");
|
|
6290
|
+
lines.push(" " + qIcon + " Code Review: " + reviewQuality.score + "/100 (" + reviewQuality.grade + ")");
|
|
6291
|
+
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");
|
|
6292
|
+
lines.push("");
|
|
6293
|
+
lines.push(" Branch: " + branch + " \u2190 " + baseBranch);
|
|
6294
|
+
lines.push(" Files: " + changedFiles.length + " changed");
|
|
6295
|
+
lines.push(" Lines: +" + changedFiles.reduce((s, f) => s + f.linesAdded, 0) + "/-" + changedFiles.reduce((s, f) => s + f.linesRemoved, 0));
|
|
6296
|
+
lines.push("");
|
|
6297
|
+
for (const f of reviewQuality.factors) {
|
|
6298
|
+
const icon = f.score >= 80 ? "\u2705" : f.score >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
6299
|
+
lines.push(" " + icon + " " + f.name + ": " + f.score + "/100 \u2014 " + f.detail);
|
|
6300
|
+
}
|
|
6301
|
+
if (breakingChanges.length > 0) {
|
|
6302
|
+
lines.push("");
|
|
6303
|
+
lines.push(" \u26A0\uFE0F BREAKING CHANGES (" + breakingChanges.length + "):");
|
|
6304
|
+
for (const bc of breakingChanges.slice(0, 5)) {
|
|
6305
|
+
const icon = bc.severity === "critical" ? "\u{1F534}" : bc.severity === "high" ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
6306
|
+
lines.push(" " + icon + " " + bc.file + ": " + bc.description);
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
if (missingFiles.length > 0) {
|
|
6310
|
+
lines.push("");
|
|
6311
|
+
lines.push(" \u{1F4CB} Potentially missing (" + missingFiles.length + "):");
|
|
6312
|
+
for (const mf of missingFiles.slice(0, 5)) {
|
|
6313
|
+
lines.push(" \u2192 " + mf.file + " (" + mf.reason + ")");
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
lines.push("");
|
|
6317
|
+
lines.push(" \u{1F4A5} Impact: " + impactRadius.totalAffected + " files affected (" + impactRadius.directlyAffected + " direct, " + impactRadius.transitivelyAffected + " transitive)");
|
|
6318
|
+
if (impactRadius.affectedTests > 0) {
|
|
6319
|
+
lines.push(" \u{1F9EA} Tests: " + impactRadius.affectedTests + " test files in blast radius");
|
|
6320
|
+
}
|
|
6321
|
+
if (impactRadius.hotspots.length > 0) {
|
|
6322
|
+
lines.push("");
|
|
6323
|
+
lines.push(" \u{1F525} Hotspots:");
|
|
6324
|
+
for (const h of impactRadius.hotspots.slice(0, 3)) {
|
|
6325
|
+
lines.push(" " + h.file + " (risk: " + h.riskScore + ", " + h.dependents + " dependents)");
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
lines.push("");
|
|
6329
|
+
return lines.join("\n");
|
|
6330
|
+
}
|
|
6331
|
+
function emptyResult3(baseBranch) {
|
|
6332
|
+
return {
|
|
6333
|
+
branch: "",
|
|
6334
|
+
baseBranch,
|
|
6335
|
+
isGitRepo: false,
|
|
6336
|
+
changedFiles: [],
|
|
6337
|
+
totalLinesChanged: 0,
|
|
6338
|
+
breakingChanges: [],
|
|
6339
|
+
missingFiles: [],
|
|
6340
|
+
impactRadius: {
|
|
6341
|
+
directlyAffected: 0,
|
|
6342
|
+
transitivelyAffected: 0,
|
|
6343
|
+
totalAffected: 0,
|
|
6344
|
+
affectedTests: 0,
|
|
6345
|
+
riskScore: 0,
|
|
6346
|
+
hotspots: []
|
|
6347
|
+
},
|
|
6348
|
+
reviewQuality: {
|
|
6349
|
+
score: 0,
|
|
6350
|
+
grade: "F",
|
|
6351
|
+
factors: []
|
|
6352
|
+
},
|
|
6353
|
+
reviewPrompt: "",
|
|
6354
|
+
renderedSummary: "# Code Review\n\nNot a git repository."
|
|
6355
|
+
};
|
|
6356
|
+
}
|
|
5540
6357
|
export {
|
|
5541
6358
|
DEFAULT_GATE_CONFIG,
|
|
5542
6359
|
MODEL_REGISTRY2 as MODEL_REGISTRY,
|
|
5543
6360
|
ProjectWatcher,
|
|
6361
|
+
analyzeForReview,
|
|
5544
6362
|
analyzeMonorepo,
|
|
5545
6363
|
analyzeProject,
|
|
5546
6364
|
analyzeSemantics,
|
|
@@ -5559,6 +6377,7 @@ export {
|
|
|
5559
6377
|
detectStack,
|
|
5560
6378
|
estimateFileTokens,
|
|
5561
6379
|
estimateTokens,
|
|
6380
|
+
exportFeedbackForTeam,
|
|
5562
6381
|
freeEncoder,
|
|
5563
6382
|
generatePRContext,
|
|
5564
6383
|
getActiveWatchers,
|
|
@@ -5572,6 +6391,7 @@ export {
|
|
|
5572
6391
|
getPolicyPath,
|
|
5573
6392
|
getPredictorBoosts,
|
|
5574
6393
|
getPruneLevelForRisk,
|
|
6394
|
+
importTeamFeedback,
|
|
5575
6395
|
initProjectConfig,
|
|
5576
6396
|
invalidateCache,
|
|
5577
6397
|
loadBaseline,
|
|
@@ -5594,11 +6414,13 @@ export {
|
|
|
5594
6414
|
renderCompilabilityBenchmark,
|
|
5595
6415
|
renderCompileProof,
|
|
5596
6416
|
renderContextScore,
|
|
6417
|
+
renderCrossRepoReport,
|
|
5597
6418
|
renderFeedbackReport,
|
|
5598
6419
|
renderMonorepoAnalysis,
|
|
5599
6420
|
renderMultiModelResult,
|
|
5600
6421
|
renderPackageContext,
|
|
5601
6422
|
renderQualityBenchmark,
|
|
6423
|
+
renderReviewSummary,
|
|
5602
6424
|
renderSemanticAnalysis,
|
|
5603
6425
|
runBenchmark,
|
|
5604
6426
|
runCompilabilityBenchmark,
|
|
@@ -5615,6 +6437,7 @@ export {
|
|
|
5615
6437
|
unwatchAll,
|
|
5616
6438
|
unwatchProject,
|
|
5617
6439
|
walkProject,
|
|
5618
|
-
watchProject
|
|
6440
|
+
watchProject,
|
|
6441
|
+
wilsonLowerBound
|
|
5619
6442
|
};
|
|
5620
6443
|
//# sourceMappingURL=index.js.map
|