cto-ai-cli 4.0.0 → 5.1.0

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