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.
@@ -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
- let project;
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 extractSignaturesAST(sf) {
1612
+ function extractSignaturesRegex(content) {
1613
+ const lines = content.split("\n");
1632
1614
  const parts = [];
1633
- for (const imp of sf.getImportDeclarations()) {
1634
- parts.push(imp.getText());
1635
- }
1636
- if (parts.length > 0) parts.push("");
1637
- for (const ta of sf.getTypeAliases()) {
1638
- addJSDoc(ta, parts);
1639
- parts.push(ta.getText());
1640
- }
1641
- for (const iface of sf.getInterfaces()) {
1642
- addJSDoc(iface, parts);
1643
- parts.push(iface.getText());
1644
- }
1645
- for (const en of sf.getEnums()) {
1646
- addJSDoc(en, parts);
1647
- parts.push(en.getText());
1648
- }
1649
- for (const fn of sf.getFunctions()) {
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());
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
- for (const cls of sf.getClasses()) {
1680
- addJSDoc(cls, parts);
1681
- const isExported = cls.isExported();
1682
- const prefix = isExported ? "export " : "";
1683
- const name = cls.getName() ?? "<anonymous>";
1684
- const ext = cls.getExtends()?.getText();
1685
- const impl = cls.getImplements().map((i) => i.getText()).join(", ");
1686
- let header = `${prefix}class ${name}`;
1687
- if (ext) header += ` extends ${ext}`;
1688
- if (impl) header += ` implements ${impl}`;
1689
- header += " {";
1690
- parts.push(header);
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 extractSkeletonAST(sf) {
1703
+ function extractSkeletonRegex(content) {
1704
+ const lines = content.split("\n");
1721
1705
  const parts = [];
1722
- for (const imp of sf.getImportDeclarations()) {
1723
- parts.push(imp.getText());
1724
- }
1725
- if (parts.length > 0) parts.push("");
1726
- for (const ta of sf.getTypeAliases()) {
1727
- if (ta.isExported()) parts.push(ta.getText());
1728
- }
1729
- for (const iface of sf.getInterfaces()) {
1730
- if (!iface.isExported()) continue;
1731
- const ext = iface.getExtends().map((e) => e.getText());
1732
- const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
1733
- parts.push(`export interface ${iface.getName()}${extStr} { /* ${iface.getProperties().length} props */ }`);
1734
- }
1735
- for (const en of sf.getEnums()) {
1736
- if (!en.isExported()) continue;
1737
- const members = en.getMembers().map((m) => m.getName());
1738
- parts.push(`export enum ${en.getName()} { ${members.join(", ")} }`);
1739
- }
1740
- for (const fn of sf.getFunctions()) {
1741
- if (!fn.isExported()) continue;
1742
- const name = fn.getName() ?? "<anonymous>";
1743
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1744
- parts.push(`export function ${name}(${params});`);
1745
- }
1746
- for (const cls of sf.getClasses()) {
1747
- if (!cls.isExported()) continue;
1748
- const methods = cls.getMethods().map((m) => m.getName());
1749
- parts.push(`export class ${cls.getName()} { /* methods: ${methods.join(", ")} */ }`);
1750
- }
1751
- for (const exp of sf.getExportDeclarations()) {
1752
- parts.push(exp.getText());
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 join7 } from "path";
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 = join7(resolve8(projectPath), ".cto");
3380
+ const ctoDir = join6(resolve8(projectPath), ".cto");
3266
3381
  await mkdir2(ctoDir, { recursive: true });
3267
- return join7(ctoDir, "predictor.json");
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 join8, basename as basename3 } from "path";
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 join8(home, ".cto", "global-intelligence.json");
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 = join8(getGlobalModelPath(), "..");
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 join9 } from "path";
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 = join9(resolve10(projectPath), ".cto");
3900
+ const ctoDir = join8(resolve10(projectPath), ".cto");
3786
3901
  await mkdir4(ctoDir, { recursive: true });
3787
- return join9(ctoDir, "feedback.json");
3902
+ return join8(ctoDir, "feedback.json");
3788
3903
  }
3789
3904
  async function getModelPath2(projectPath) {
3790
- const ctoDir = join9(resolve10(projectPath), ".cto");
3905
+ const ctoDir = join8(resolve10(projectPath), ".cto");
3791
3906
  await mkdir4(ctoDir, { recursive: true });
3792
- return join9(ctoDir, "feedback-model.json");
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: 1,
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
- avgTimeToAccept: 0
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.acceptRate >= 0.8).sort((a, b) => b[1].acceptRate - a[1].acceptRate).slice(0, 5);
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 over ${fa.includedCount} selections`,
3909
- impact: Math.round(fa.acceptRate * fa.includedCount)
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].acceptRate - b[1].acceptRate).slice(0, 5);
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 may be adding noise`,
3918
- impact: Math.round((1 - fa.acceptRate) * fa.includedCount * 2)
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].acceptRate - a[1].acceptRate);
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.acceptRate * ta.totalCount * 3)
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 >= 3 && pa.acceptRate >= 0.9).sort((a, b) => b[1].acceptRate * b[1].count - a[1].acceptRate * a[1].count).slice(0, 3);
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 when both included (${pa.count} times)`,
3937
- impact: Math.round(pa.acceptRate * pa.count * 5)
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.acceptRate >= 0.7) {
3950
- boosts.set(file, (boosts.get(file) ?? 0) + fa.acceptRate * 5);
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 >= 3) {
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.acceptRate >= 0.8) {
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 join10, dirname as dirname3, basename as basename4 } from "path";
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 = join10(projectPath, ".cto", `compile-proof-${name.toLowerCase()}`);
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 = join10(projectPath, filePath);
4697
- const dest = join10(tmpDir, filePath);
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(join10(tmpDir, "_compile_test.ts"), consumerCode);
4705
- await writeFile6(join10(tmpDir, "tsconfig.json"), JSON.stringify({
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 join11, relative as relative6, basename as basename5 } from "path";
5014
- import { existsSync as existsSync5 } from "fs";
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 = join11(rootPath, check.file);
5036
- if (existsSync5(filePath)) {
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 (existsSync5(join11(rootPath, "yarn.lock"))) return "yarn-workspaces";
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 = join11(rootPath, cleanGlob);
5058
- if (!existsSync5(searchDir)) continue;
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 = join11(searchDir, entry.name, "package.json");
5064
- if (existsSync5(pkgJsonPath)) {
5065
- packagePaths.push(join11(searchDir, entry.name));
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(join11(rootPath, "package.json"), "utf-8"));
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(join11(rootPath, "pnpm-workspace.yaml"), "utf-8");
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(join11(rootPath, "package.json"), "utf-8"));
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 (existsSync5(join11(rootPath, "pnpm-workspace.yaml"))) {
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) => existsSync5(join11(rootPath, 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(join11(rootPath, "package.json"), "utf-8"));
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(join11(rootPath, "lerna.json"), "utf-8"));
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 = join11(pkgPath, "package.json");
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 = join11(pkg.path, "package.json");
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 existsSync6 } from "fs";
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 (!existsSync6(filePath)) return null;
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 (!existsSync6(dir)) await mkdir6(dir, { recursive: true });
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