ctxloom-pro 1.5.5 → 1.7.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.
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  VectorStore
3
- } from "./chunk-R56D54Y7.js";
3
+ } from "./chunk-7S2ELKNU.js";
4
4
  import {
5
+ INDEXER_IGNORED_DIRS,
5
6
  collectFiles,
6
7
  generateEmbedding
7
- } from "./chunk-COH5WYZS.js";
8
+ } from "./chunk-6FGTNOCP.js";
8
9
  import {
9
10
  diskSink,
10
11
  readEvents
@@ -503,6 +504,13 @@ var ASTParser = class {
503
504
  extractTSNodes(rootNode, _filePath, lines) {
504
505
  const nodes = [];
505
506
  const processedIds = /* @__PURE__ */ new Set();
507
+ const hasCallableRight = (n) => {
508
+ const right = n.childForFieldName?.("right") ?? n.children[n.children.length - 1];
509
+ if (!right) return false;
510
+ if (right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration") return true;
511
+ if (right.type === "assignment_expression") return hasCallableRight(right);
512
+ return false;
513
+ };
506
514
  const walk = (node) => {
507
515
  if (processedIds.has(node.id)) return;
508
516
  switch (node.type) {
@@ -674,6 +682,56 @@ var ASTParser = class {
674
682
  processedIds.add(node.id);
675
683
  return;
676
684
  }
685
+ // ─── Prototype / object method assignments ──────────────────────
686
+ // CommonJS libraries (and pre-class-syntax ES) attach their public
687
+ // API via assignment expressions:
688
+ //
689
+ // res.send = function send(body) { ... }
690
+ // res.json = function (obj) { ... }
691
+ // res.contentType = res.type = function (type) { ... } // chained
692
+ // exports.foo = function foo() { ... }
693
+ // MyClass.prototype.bar = function () { ... }
694
+ //
695
+ // Without this case, none of those names enter the symbol index,
696
+ // so `lookupSymbolsByFile()` returns empty for libraries like
697
+ // express, and any downstream tool that wants to attribute callers
698
+ // to a file (blast-radius symbolCallers, ctx_get_definition, etc.)
699
+ // falls flat. The call graph itself already records callers of
700
+ // `send`/`json`/etc. — this case bridges the gap so we can match
701
+ // them back to the file that defines them.
702
+ //
703
+ // Heuristic:
704
+ // - left = member_expression → use the FINAL property as symbol
705
+ // - right = function | arrow_function | function_expression
706
+ // - right = assignment_expression → recurse for chained pattern
707
+ // Anything else (constants, identifiers being aliased) is skipped
708
+ // intentionally — those aren't callable API surface.
709
+ case "assignment_expression": {
710
+ const left = node.childForFieldName?.("left") ?? node.children.find(
711
+ (c) => c?.type === "member_expression" || c?.type === "identifier"
712
+ );
713
+ const right = node.childForFieldName?.("right") ?? node.children[node.children.length - 1];
714
+ if (left?.type === "member_expression" && right) {
715
+ const prop = left.childForFieldName?.("property") ?? left.children[left.children.length - 1];
716
+ const propName = prop?.text;
717
+ if (right.type === "assignment_expression") {
718
+ walk(right);
719
+ }
720
+ const rightIsCallable = right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration" || right.type === "assignment_expression" && hasCallableRight(right);
721
+ if (propName && rightIsCallable && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName)) {
722
+ const sig = lines[node.startPosition.row] ?? "";
723
+ nodes.push({
724
+ type: "method",
725
+ name: propName,
726
+ signature: sig.trim().slice(0, 200),
727
+ startLine: node.startPosition.row + 1,
728
+ endLine: node.endPosition.row + 1
729
+ });
730
+ }
731
+ }
732
+ processedIds.add(node.id);
733
+ return;
734
+ }
677
735
  // ─── Lexical declarations (const fn = () => {}) ────────────────
678
736
  case "lexical_declaration": {
679
737
  for (const child of node.children) {
@@ -780,10 +838,34 @@ var ASTParser = class {
780
838
  }
781
839
  case "import_from_statement": {
782
840
  const moduleNode = node.children.find((c) => c?.type === "dotted_name" || c?.type === "relative_import");
841
+ const sourceText = moduleNode?.text ?? "";
842
+ const importedNames = [];
843
+ let pastImportKeyword = false;
844
+ for (const child of node.children) {
845
+ if (!child) continue;
846
+ if (child.text === "import" && (child.type === "import" || child.children.length === 0)) {
847
+ pastImportKeyword = true;
848
+ continue;
849
+ }
850
+ if (!pastImportKeyword) continue;
851
+ if (child.type === "wildcard_import") continue;
852
+ if (child.type === "dotted_name") {
853
+ importedNames.push(child.text);
854
+ } else if (child.type === "aliased_import") {
855
+ const alias = child.childForFieldName?.("alias");
856
+ const aliasName = alias?.text;
857
+ if (aliasName) importedNames.push(aliasName);
858
+ else {
859
+ const name = child.childForFieldName?.("name")?.text;
860
+ if (name) importedNames.push(name);
861
+ }
862
+ }
863
+ }
783
864
  nodes.push({
784
865
  type: "import",
785
- name: moduleNode?.text ?? "",
786
- source: moduleNode?.text ?? "",
866
+ name: sourceText,
867
+ source: sourceText,
868
+ importedNames: importedNames.length > 0 ? importedNames : void 0,
787
869
  startLine: node.startPosition.row + 1,
788
870
  endLine: node.endPosition.row + 1
789
871
  });
@@ -1653,6 +1735,78 @@ var ASTParser = class {
1653
1735
  tree.delete();
1654
1736
  }
1655
1737
  }
1738
+ /**
1739
+ * Extract all call edges in a Python (.py / .ipynb) file. Mirrors
1740
+ * `parseAllCallEdges` but uses tree-sitter-python node types.
1741
+ *
1742
+ * Tree-sitter-python relevant shapes:
1743
+ * - call.function = identifier "foo" → callee = "foo"
1744
+ * - call.function = attribute (obj.method) → callee = "method"
1745
+ * - call.function = call (chained) → recurse into inner
1746
+ *
1747
+ * Enclosing-context tracking follows the same pattern: track the
1748
+ * innermost `function_definition`. Methods inside a class are also
1749
+ * function_definitions, so the same handler covers them.
1750
+ */
1751
+ async parseAllPythonCallEdges(filePath) {
1752
+ if (!this.pyLang) await this.loadPython();
1753
+ if (!this.pyLang) return [];
1754
+ const parser = this.getParser(this.pyLang);
1755
+ let source;
1756
+ try {
1757
+ const raw = fs2.readFileSync(filePath, "utf-8");
1758
+ source = filePath.endsWith(".ipynb") ? extractNotebookPythonSource(raw) : raw;
1759
+ } catch {
1760
+ return [];
1761
+ }
1762
+ if (!source.trim()) return [];
1763
+ const tree = parser.parse(source);
1764
+ if (!tree) return [];
1765
+ const results = [];
1766
+ const extractCalleeName = (fn) => {
1767
+ if (fn.type === "identifier") return fn.text;
1768
+ if (fn.type === "attribute") {
1769
+ const right = fn.childForFieldName?.("attribute") ?? fn.children[fn.children.length - 1];
1770
+ return right?.text ?? "";
1771
+ }
1772
+ if (fn.type === "call") {
1773
+ const innerFn = fn.childForFieldName?.("function");
1774
+ return innerFn ? extractCalleeName(innerFn) : "";
1775
+ }
1776
+ return "";
1777
+ };
1778
+ const walk = (node, contextStack) => {
1779
+ let newStack = contextStack;
1780
+ if (node.type === "function_definition") {
1781
+ const nameNode = node.childForFieldName?.("name");
1782
+ if (nameNode?.text) {
1783
+ newStack = [...contextStack, nameNode.text];
1784
+ }
1785
+ }
1786
+ if (node.type === "call") {
1787
+ const fn = node.childForFieldName?.("function") ?? node.children.find((c) => c?.type === "identifier" || c?.type === "attribute" || c?.type === "call");
1788
+ if (fn) {
1789
+ const name = extractCalleeName(fn);
1790
+ if (name && name.length > 0) {
1791
+ results.push({
1792
+ callerSymbol: newStack[newStack.length - 1] ?? "",
1793
+ calleeSymbol: name,
1794
+ line: node.startPosition.row + 1
1795
+ });
1796
+ }
1797
+ }
1798
+ }
1799
+ for (const child of node.children) {
1800
+ if (child) walk(child, newStack);
1801
+ }
1802
+ };
1803
+ try {
1804
+ walk(tree.rootNode, []);
1805
+ return results;
1806
+ } finally {
1807
+ tree.delete();
1808
+ }
1809
+ }
1656
1810
  /**
1657
1811
  * Extract all call edges in a TypeScript/TSX file.
1658
1812
  * Tracks the enclosing function/method context for each call site.
@@ -1735,48 +1889,100 @@ var GoModuleResolver = class {
1735
1889
  }
1736
1890
  /**
1737
1891
  * Resolve a module-path import (e.g. `github.com/myorg/myapp/internal/auth`)
1738
- * to a relative project path (e.g. `internal/auth/auth.go`).
1892
+ * to the FIRST relative project path (e.g. `internal/auth/auth.go`).
1739
1893
  *
1740
1894
  * Returns null for:
1741
1895
  * - Third-party imports (different module prefix)
1742
1896
  * - Relative imports (use resolveRelative() instead)
1743
1897
  * - Imports where no .go files are found
1898
+ *
1899
+ * NOTE: A Go import imports a PACKAGE (a directory of .go files), not a
1900
+ * single file. Use `resolveAll()` to get every file in the package — that
1901
+ * matches Go's compile-unit semantics and is what the graph wants for
1902
+ * accurate reachability. `resolve()` is kept for back-compat callers
1903
+ * that only need a representative file.
1744
1904
  */
1745
1905
  resolve(importPath) {
1746
- if (!this.modulePath) return null;
1747
- if (importPath.startsWith(".")) return null;
1748
- if (!importPath.startsWith(this.modulePath)) return null;
1906
+ const all = this.resolveAll(importPath);
1907
+ return all[0] ?? null;
1908
+ }
1909
+ /**
1910
+ * Resolve a module-path import to ALL non-test .go files in the target
1911
+ * package directory. This matches Go's compile-unit semantics: a single
1912
+ * `import "github.com/foo/bar/pkg"` statement brings the entire `pkg/`
1913
+ * directory into the dependency graph — every exported symbol from
1914
+ * every .go file in that directory is accessible to the caller.
1915
+ *
1916
+ * Pre-fix the resolver returned only ONE file per import, which made
1917
+ * gin's `binding/` package (~20 files) appear to be a single file from
1918
+ * the graph's perspective. The bench's graphReachability on gin
1919
+ * collapsed to 0.32 because PRs that touched 4 files in `binding/`
1920
+ * had only ONE of them in the graph reach of the entry-point file.
1921
+ * Returning ALL package files fixes the structural model.
1922
+ *
1923
+ * Test files (_test.go) are intentionally excluded — they're not part
1924
+ * of a package's public API and aren't imported by callers. The
1925
+ * test↔source link is handled separately by per-directory sibling
1926
+ * edges (see DependencyGraph's Go intra-package pass).
1927
+ */
1928
+ resolveAll(importPath) {
1929
+ if (!this.modulePath) return [];
1930
+ if (importPath.startsWith(".")) return [];
1931
+ if (!importPath.startsWith(this.modulePath)) return [];
1749
1932
  const suffix = importPath.slice(this.modulePath.length);
1750
- if (!suffix) return null;
1933
+ if (!suffix) return [];
1751
1934
  const subPath = suffix.startsWith("/") ? suffix.slice(1) : suffix;
1752
1935
  const absDir = path3.join(this.rootDir, subPath);
1753
- return this.firstGoFileInDir(absDir, subPath);
1936
+ return this.allGoFilesInDir(
1937
+ absDir,
1938
+ subPath,
1939
+ /* includeTests */
1940
+ false
1941
+ );
1754
1942
  }
1755
1943
  /**
1756
1944
  * Resolve a relative import (`./config`, `../pkg`) from a given Go source file.
1757
1945
  * Returns the relative project path to the first .go file found, or null.
1946
+ * Kept for back-compat — most callers should prefer `resolveRelativeAll()`.
1758
1947
  */
1759
1948
  resolveRelative(fromFile, importSpec) {
1949
+ const all = this.resolveRelativeAll(fromFile, importSpec);
1950
+ return all[0] ?? null;
1951
+ }
1952
+ /**
1953
+ * Resolve a relative import to ALL non-test .go files in the target
1954
+ * package directory. See `resolveAll()` for rationale.
1955
+ */
1956
+ resolveRelativeAll(fromFile, importSpec) {
1760
1957
  const fromDir = path3.dirname(fromFile);
1761
1958
  const absTarget = path3.resolve(fromDir, importSpec);
1762
1959
  const subPath = path3.relative(this.rootDir, absTarget);
1763
- return this.firstGoFileInDir(absTarget, subPath);
1960
+ return this.allGoFilesInDir(
1961
+ absTarget,
1962
+ subPath,
1963
+ /* includeTests */
1964
+ false
1965
+ );
1764
1966
  }
1765
- firstGoFileInDir(absDir, relDir) {
1766
- if (!fs3.existsSync(absDir)) return null;
1967
+ /**
1968
+ * Enumerate every .go file in a directory, returning project-relative
1969
+ * paths sorted with non-test files first. Used by both the single-file
1970
+ * and all-files resolvers.
1971
+ */
1972
+ allGoFilesInDir(absDir, relDir, includeTests) {
1973
+ if (!fs3.existsSync(absDir)) return [];
1767
1974
  let entries;
1768
1975
  try {
1769
1976
  entries = fs3.readdirSync(absDir);
1770
1977
  } catch {
1771
- return null;
1978
+ return [];
1772
1979
  }
1773
- const goFiles = entries.filter((f) => f.endsWith(".go")).sort((a, b) => {
1980
+ const goFiles = entries.filter((f) => f.endsWith(".go")).filter((f) => includeTests || !f.endsWith("_test.go")).sort((a, b) => {
1774
1981
  const aTest = a.endsWith("_test.go") ? 1 : 0;
1775
1982
  const bTest = b.endsWith("_test.go") ? 1 : 0;
1776
1983
  return aTest - bTest || a.localeCompare(b);
1777
1984
  });
1778
- if (goFiles.length === 0) return null;
1779
- return path3.join(relDir, goFiles[0]).replace(/\\/g, "/");
1985
+ return goFiles.map((f) => path3.join(relDir, f).replace(/\\/g, "/"));
1780
1986
  }
1781
1987
  };
1782
1988
 
@@ -1818,6 +2024,24 @@ function extractImports(filePath, content) {
1818
2024
  return extractNotebookImports(filePath, content);
1819
2025
  case ".vue":
1820
2026
  return extractVueImports(content);
2027
+ case ".c":
2028
+ case ".cc":
2029
+ case ".cpp":
2030
+ case ".cxx":
2031
+ case ".h":
2032
+ case ".hh":
2033
+ case ".hpp":
2034
+ case ".hxx":
2035
+ return extractCppImports(content);
2036
+ case ".scala":
2037
+ return extractScalaImports(content);
2038
+ case ".lua":
2039
+ return extractLuaImports(content);
2040
+ case ".ex":
2041
+ case ".exs":
2042
+ return extractElixirImports(content);
2043
+ case ".zig":
2044
+ return extractZigImports(content);
1821
2045
  default:
1822
2046
  return [];
1823
2047
  }
@@ -1837,8 +2061,14 @@ function resolveImport(fromAbs, raw, rootDir) {
1837
2061
  if (ext === ".dart") return resolveDartImport(fromAbs, fromDir, raw, rootDir);
1838
2062
  if (ext === ".ipynb") return resolvePythonImport(fromAbs, fromDir, raw, rootDir);
1839
2063
  if (ext === ".vue") return resolveVueImport(fromAbs, fromDir, raw, rootDir);
2064
+ if (CPP_EXTENSIONS.has(ext)) return resolveCppImport(fromDir, raw, rootDir);
2065
+ if (ext === ".scala") return resolveScalaImport(fromDir, raw, rootDir);
2066
+ if (ext === ".lua") return resolveLuaImport(fromDir, raw, rootDir);
2067
+ if (ext === ".ex" || ext === ".exs") return resolveElixirImport(fromDir, raw, rootDir);
2068
+ if (ext === ".zig") return resolveZigImport(fromAbs, fromDir, raw, rootDir);
1840
2069
  return null;
1841
2070
  }
2071
+ var CPP_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"]);
1842
2072
  function extractPythonImports(content) {
1843
2073
  const results = [];
1844
2074
  let m;
@@ -2125,6 +2355,124 @@ function resolveVueImport(fromAbs, fromDir, raw, rootDir) {
2125
2355
  }
2126
2356
  return null;
2127
2357
  }
2358
+ function extractCppImports(content) {
2359
+ const results = [];
2360
+ const localInclude = /^\s*#\s*include\s+"([^"]+)"/gm;
2361
+ let m;
2362
+ while ((m = localInclude.exec(content)) !== null) {
2363
+ results.push({ specifier: m[1], isRelative: true });
2364
+ }
2365
+ return results;
2366
+ }
2367
+ function resolveCppImport(fromDir, raw, rootDir) {
2368
+ const rootResolved = path4.resolve(rootDir);
2369
+ const candidates = [
2370
+ path4.resolve(fromDir, raw.specifier),
2371
+ path4.resolve(rootDir, raw.specifier)
2372
+ ];
2373
+ for (const c of candidates) {
2374
+ if (!c.startsWith(rootResolved + path4.sep) && c !== rootResolved) continue;
2375
+ if (fs4.existsSync(c)) return path4.relative(rootDir, c);
2376
+ }
2377
+ return null;
2378
+ }
2379
+ function extractScalaImports(content) {
2380
+ const results = [];
2381
+ const importRe = /^\s*import\s+((?:\w+\.)*\w+)(?:\.\{[^}]+\})?/gm;
2382
+ let m;
2383
+ while ((m = importRe.exec(content)) !== null) {
2384
+ results.push({ specifier: m[1], isRelative: false });
2385
+ }
2386
+ return results;
2387
+ }
2388
+ function resolveScalaImport(fromDir, raw, rootDir) {
2389
+ const asPath = raw.specifier.replace(/\./g, path4.sep);
2390
+ const candidates = [
2391
+ path4.join(rootDir, "src", "main", "scala", asPath + ".scala"),
2392
+ path4.join(rootDir, asPath + ".scala")
2393
+ ];
2394
+ for (const c of candidates) {
2395
+ if (fs4.existsSync(c)) return path4.relative(rootDir, c);
2396
+ }
2397
+ const className = raw.specifier.split(".").pop() ?? raw.specifier;
2398
+ const local = path4.join(fromDir, className + ".scala");
2399
+ if (fs4.existsSync(local)) return path4.relative(rootDir, local);
2400
+ return null;
2401
+ }
2402
+ function extractLuaImports(content) {
2403
+ const results = [];
2404
+ const requireRe = /\brequire\s*\(?\s*['"]([^'"]+)['"]/g;
2405
+ let m;
2406
+ while ((m = requireRe.exec(content)) !== null) {
2407
+ results.push({ specifier: m[1], isRelative: false });
2408
+ }
2409
+ return results;
2410
+ }
2411
+ function resolveLuaImport(fromDir, raw, rootDir) {
2412
+ const asPath = raw.specifier.replace(/\./g, path4.sep);
2413
+ const candidates = [
2414
+ path4.join(fromDir, asPath + ".lua"),
2415
+ path4.join(rootDir, asPath + ".lua"),
2416
+ // Lua's package convention also supports init.lua as a directory
2417
+ // entry point — analogous to Python's __init__.py.
2418
+ path4.join(rootDir, asPath, "init.lua")
2419
+ ];
2420
+ for (const c of candidates) {
2421
+ if (fs4.existsSync(c)) return path4.relative(rootDir, c);
2422
+ }
2423
+ return null;
2424
+ }
2425
+ function extractElixirImports(content) {
2426
+ const results = [];
2427
+ const re = /^\s*(?:alias|import|use|require)\s+([A-Z][\w.]*)/gm;
2428
+ let m;
2429
+ while ((m = re.exec(content)) !== null) {
2430
+ results.push({ specifier: m[1], isRelative: false });
2431
+ }
2432
+ return results;
2433
+ }
2434
+ function resolveElixirImport(fromDir, raw, rootDir) {
2435
+ const segments = raw.specifier.split(".").map(
2436
+ (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()
2437
+ );
2438
+ const asPath = segments.join(path4.sep);
2439
+ const candidates = [
2440
+ path4.join(rootDir, "lib", asPath + ".ex"),
2441
+ path4.join(rootDir, "lib", asPath + ".exs"),
2442
+ path4.join(rootDir, asPath + ".ex"),
2443
+ path4.join(rootDir, asPath + ".exs")
2444
+ ];
2445
+ for (const c of candidates) {
2446
+ if (fs4.existsSync(c)) return path4.relative(rootDir, c);
2447
+ }
2448
+ const tail = segments[segments.length - 1];
2449
+ const local = path4.join(fromDir, tail + ".ex");
2450
+ if (fs4.existsSync(local)) return path4.relative(rootDir, local);
2451
+ return null;
2452
+ }
2453
+ function extractZigImports(content) {
2454
+ const results = [];
2455
+ const importRe = /@import\s*\(\s*"([^"]+)"\s*\)/g;
2456
+ let m;
2457
+ while ((m = importRe.exec(content)) !== null) {
2458
+ const spec = m[1];
2459
+ if (spec.endsWith(".zig")) {
2460
+ const isRelative = spec.startsWith(".") || !spec.includes("/");
2461
+ results.push({ specifier: spec, isRelative });
2462
+ }
2463
+ }
2464
+ return results;
2465
+ }
2466
+ function resolveZigImport(fromAbs, fromDir, raw, rootDir) {
2467
+ void fromAbs;
2468
+ const rootResolved = path4.resolve(rootDir);
2469
+ const candidate = path4.resolve(fromDir, raw.specifier);
2470
+ if (!candidate.startsWith(rootResolved + path4.sep) && candidate !== rootResolved) {
2471
+ return null;
2472
+ }
2473
+ if (fs4.existsSync(candidate)) return path4.relative(rootDir, candidate);
2474
+ return null;
2475
+ }
2128
2476
 
2129
2477
  // packages/core/src/utils/TsConfigPathsResolver.ts
2130
2478
  import fs5 from "fs";
@@ -2355,6 +2703,7 @@ var CallGraphIndex = class _CallGraphIndex {
2355
2703
 
2356
2704
  // packages/core/src/graph/DependencyGraph.ts
2357
2705
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
2706
+ var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
2358
2707
  var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
2359
2708
  var DependencyGraph = class {
2360
2709
  /** file → set of files it imports (forward edges) */
@@ -2364,6 +2713,31 @@ var DependencyGraph = class {
2364
2713
  /** Symbol index: symbolName → { filePath, type, signature, startLine?, endLine? } */
2365
2714
  symbolIndex = /* @__PURE__ */ new Map();
2366
2715
  callGraphIndex = new CallGraphIndex();
2716
+ /**
2717
+ * Re-export tracing (v1.6.x):
2718
+ * reExportMap[barrelFile][symbol] = sourceFile
2719
+ *
2720
+ * Built during graph construction from `from .submodule import Name`
2721
+ * statements (Python). When file C imports `from <barrel> import Name`,
2722
+ * the import resolver consults this map and emits a parallel edge
2723
+ * C → sourceFile so blast-radius queries against sourceFile can find
2724
+ * C as a transitive consumer through the barrel.
2725
+ *
2726
+ * Concrete fastapi case:
2727
+ * tests/test_routing.py has `from fastapi import APIRouter`
2728
+ * fastapi/__init__.py has `from .routing import APIRouter`
2729
+ * → reExportMap['fastapi/__init__.py']['APIRouter'] = 'fastapi/routing.py'
2730
+ * → emit edge tests/test_routing.py → fastapi/routing.py
2731
+ */
2732
+ reExportMap = /* @__PURE__ */ new Map();
2733
+ /**
2734
+ * Pending re-export queries. Populated during pass 1 (parse all
2735
+ * files, extract their imports); resolved in pass 2 once the
2736
+ * reExportMap is fully built. Required because file ordering would
2737
+ * otherwise miss re-exports whose source files are parsed AFTER the
2738
+ * consumer file.
2739
+ */
2740
+ pendingReExportQueries = [];
2367
2741
  parser = null;
2368
2742
  rootDir = "";
2369
2743
  snapshotDir = "";
@@ -2418,8 +2792,10 @@ var DependencyGraph = class {
2418
2792
  for (const imp of importNodes) {
2419
2793
  const spec = imp.source ?? imp.name;
2420
2794
  const isRelative = spec.startsWith(".");
2421
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2422
- if (resolved) this.addEdge(relPath, resolved);
2795
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
2796
+ for (const resolved of resolvedAll) {
2797
+ this.addEdge(relPath, resolved);
2798
+ }
2423
2799
  }
2424
2800
  } else {
2425
2801
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2428,7 +2804,26 @@ var DependencyGraph = class {
2428
2804
  const specifier = imp.source ?? imp.name;
2429
2805
  const isRelative = specifier.startsWith(".");
2430
2806
  const resolved = resolveImport(absPath, { specifier, isRelative }, rootDir);
2431
- if (resolved) this.addEdge(relPath, resolved);
2807
+ if (resolved) {
2808
+ this.addEdge(relPath, resolved);
2809
+ if (ext === ".py" && imp.importedNames && imp.importedNames.length > 0) {
2810
+ if (isRelative) {
2811
+ let map = this.reExportMap.get(relPath);
2812
+ if (!map) {
2813
+ map = /* @__PURE__ */ new Map();
2814
+ this.reExportMap.set(relPath, map);
2815
+ }
2816
+ for (const name of imp.importedNames) {
2817
+ map.set(name, resolved);
2818
+ }
2819
+ }
2820
+ this.pendingReExportQueries.push({
2821
+ caller: relPath,
2822
+ barrel: resolved,
2823
+ symbols: imp.importedNames
2824
+ });
2825
+ }
2826
+ }
2432
2827
  }
2433
2828
  } else {
2434
2829
  const content = fs6.readFileSync(absPath, "utf-8");
@@ -2440,7 +2835,7 @@ var DependencyGraph = class {
2440
2835
  }
2441
2836
  }
2442
2837
  for (const node of nodes) {
2443
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
2838
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2444
2839
  const existing = this.symbolIndex.get(node.name) ?? [];
2445
2840
  existing.push({
2446
2841
  filePath: relPath,
@@ -2457,6 +2852,11 @@ var DependencyGraph = class {
2457
2852
  for (const edge of callEdges) {
2458
2853
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2459
2854
  }
2855
+ } else if (PY_EXTENSIONS.has(ext)) {
2856
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
2857
+ for (const edge of callEdges) {
2858
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2859
+ }
2460
2860
  }
2461
2861
  } else {
2462
2862
  const content = fs6.readFileSync(absPath, "utf-8");
@@ -2470,6 +2870,49 @@ var DependencyGraph = class {
2470
2870
  logger.error("Failed to parse", { file: relPath, detail: err instanceof Error ? err.message : String(err) });
2471
2871
  }
2472
2872
  }
2873
+ let reExportEdgesAdded = 0;
2874
+ for (const { caller, barrel, symbols } of this.pendingReExportQueries) {
2875
+ const map = this.reExportMap.get(barrel);
2876
+ if (!map) continue;
2877
+ for (const sym of symbols) {
2878
+ const source = map.get(sym);
2879
+ if (source && source !== caller && source !== barrel) {
2880
+ this.addEdge(caller, source);
2881
+ reExportEdgesAdded += 1;
2882
+ }
2883
+ }
2884
+ }
2885
+ this.pendingReExportQueries = [];
2886
+ if (reExportEdgesAdded > 0) {
2887
+ logger.info("Re-export tracing added parallel edges", { count: reExportEdgesAdded });
2888
+ }
2889
+ let goTestEdgesAdded = 0;
2890
+ for (const relPath of this.forwardEdges.keys()) {
2891
+ if (!relPath.endsWith("_test.go")) continue;
2892
+ const dir = path6.dirname(relPath);
2893
+ const base = path6.basename(relPath, "_test.go");
2894
+ const namesake = path6.join(dir, base + ".go").replace(/\\/g, "/");
2895
+ if (this.forwardEdges.has(namesake)) {
2896
+ this.addEdge(relPath, namesake);
2897
+ this.addEdge(namesake, relPath);
2898
+ goTestEdgesAdded += 2;
2899
+ continue;
2900
+ }
2901
+ const absDir = path6.join(this.rootDir, dir);
2902
+ try {
2903
+ const siblings = fs6.readdirSync(absDir).filter((f) => f.endsWith(".go") && !f.endsWith("_test.go"));
2904
+ for (const sib of siblings) {
2905
+ const sibRel = path6.join(dir, sib).replace(/\\/g, "/");
2906
+ this.addEdge(relPath, sibRel);
2907
+ this.addEdge(sibRel, relPath);
2908
+ goTestEdgesAdded += 2;
2909
+ }
2910
+ } catch {
2911
+ }
2912
+ }
2913
+ if (goTestEdgesAdded > 0) {
2914
+ logger.info("Go test\u2194source linkage added edges", { count: goTestEdgesAdded });
2915
+ }
2473
2916
  await this.saveSnapshot();
2474
2917
  logger.info("Graph built", { files: files.length, edges: this.edgeCount() });
2475
2918
  if (options?.afterReady) {
@@ -2662,8 +3105,10 @@ var DependencyGraph = class {
2662
3105
  for (const imp of importNodes) {
2663
3106
  const spec = imp.source ?? imp.name;
2664
3107
  const isRelative = spec.startsWith(".");
2665
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2666
- if (resolved) this.addEdge(relPath, resolved);
3108
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
3109
+ for (const resolved of resolvedAll) {
3110
+ this.addEdge(relPath, resolved);
3111
+ }
2667
3112
  }
2668
3113
  } else {
2669
3114
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2684,7 +3129,7 @@ var DependencyGraph = class {
2684
3129
  }
2685
3130
  }
2686
3131
  for (const node of nodes) {
2687
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
3132
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2688
3133
  const existing = this.symbolIndex.get(node.name) ?? [];
2689
3134
  existing.push({
2690
3135
  filePath: relPath,
@@ -2701,6 +3146,11 @@ var DependencyGraph = class {
2701
3146
  for (const edge of callEdges) {
2702
3147
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2703
3148
  }
3149
+ } else if (PY_EXTENSIONS.has(ext)) {
3150
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
3151
+ for (const edge of callEdges) {
3152
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
3153
+ }
2704
3154
  }
2705
3155
  } else {
2706
3156
  const content = fs6.readFileSync(absPath, "utf-8");
@@ -2757,12 +3207,12 @@ var DependencyGraph = class {
2757
3207
  symbolIndex: Object.fromEntries(this.symbolIndex.entries())
2758
3208
  };
2759
3209
  const snapshotPath = this.getSnapshotPath();
2760
- const tmpPath = snapshotPath + ".tmp";
3210
+ const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
2761
3211
  fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
2762
3212
  fs6.renameSync(tmpPath, snapshotPath);
2763
3213
  const callData = this.callGraphIndex.toJSON();
2764
3214
  const callPath = path6.join(this.snapshotDir, "call-graph-snapshot.json");
2765
- const callTmp = callPath + ".tmp";
3215
+ const callTmp = `${callPath}.${process.pid}.tmp`;
2766
3216
  fs6.writeFileSync(callTmp, JSON.stringify(callData));
2767
3217
  fs6.renameSync(callTmp, callPath);
2768
3218
  }
@@ -3547,8 +3997,8 @@ var CoChangeIndex = class _CoChangeIndex {
3547
3997
  if (event.isBulk || event.isMerge) return;
3548
3998
  const paths = event.files.map((f) => f.path);
3549
3999
  if (paths.length === 0) return;
3550
- for (const path37 of paths) {
3551
- this.nodeCounts.set(path37, (this.nodeCounts.get(path37) ?? 0) + 1);
4000
+ for (const path38 of paths) {
4001
+ this.nodeCounts.set(path38, (this.nodeCounts.get(path38) ?? 0) + 1);
3552
4002
  }
3553
4003
  for (let i = 0; i < paths.length; i++) {
3554
4004
  for (let j = i + 1; j < paths.length; j++) {
@@ -3695,8 +4145,8 @@ var ChurnIndex = class _ChurnIndex {
3695
4145
  */
3696
4146
  snapshot() {
3697
4147
  const nodes = {};
3698
- for (const [path37, raw] of this.nodes) {
3699
- nodes[path37] = {
4148
+ for (const [path38, raw] of this.nodes) {
4149
+ nodes[path38] = {
3700
4150
  commits: raw.commits,
3701
4151
  churnLines: raw.churnLines,
3702
4152
  bugCommits: raw.bugCommits,
@@ -3711,8 +4161,8 @@ var ChurnIndex = class _ChurnIndex {
3711
4161
  */
3712
4162
  static load(s) {
3713
4163
  const idx = new _ChurnIndex();
3714
- for (const [path37, raw] of Object.entries(s.nodes)) {
3715
- idx.nodes.set(path37, {
4164
+ for (const [path38, raw] of Object.entries(s.nodes)) {
4165
+ idx.nodes.set(path38, {
3716
4166
  commits: raw.commits,
3717
4167
  churnLines: raw.churnLines,
3718
4168
  bugCommits: raw.bugCommits,
@@ -3725,8 +4175,8 @@ var ChurnIndex = class _ChurnIndex {
3725
4175
  // -------------------------------------------------------------------------
3726
4176
  // Private helpers
3727
4177
  // -------------------------------------------------------------------------
3728
- getOrCreate(path37) {
3729
- const existing = this.nodes.get(path37);
4178
+ getOrCreate(path38) {
4179
+ const existing = this.nodes.get(path38);
3730
4180
  if (existing !== void 0) return existing;
3731
4181
  const fresh = {
3732
4182
  commits: 0,
@@ -3735,7 +4185,7 @@ var ChurnIndex = class _ChurnIndex {
3735
4185
  authorCounts: {},
3736
4186
  lastTouch: 0
3737
4187
  };
3738
- this.nodes.set(path37, fresh);
4188
+ this.nodes.set(path38, fresh);
3739
4189
  return fresh;
3740
4190
  }
3741
4191
  };
@@ -3816,12 +4266,12 @@ var OwnershipIndex = class _OwnershipIndex {
3816
4266
  */
3817
4267
  snapshot() {
3818
4268
  const nodes = {};
3819
- for (const [path37, raw] of this.nodes) {
4269
+ for (const [path38, raw] of this.nodes) {
3820
4270
  const authorWeights = {};
3821
4271
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3822
4272
  authorWeights[email] = { ...entry };
3823
4273
  }
3824
- nodes[path37] = { authorWeights, lastTouch: raw.lastTouch };
4274
+ nodes[path38] = { authorWeights, lastTouch: raw.lastTouch };
3825
4275
  }
3826
4276
  return { version: 1, nodes };
3827
4277
  }
@@ -3830,23 +4280,23 @@ var OwnershipIndex = class _OwnershipIndex {
3830
4280
  */
3831
4281
  static load(s) {
3832
4282
  const idx = new _OwnershipIndex();
3833
- for (const [path37, raw] of Object.entries(s.nodes)) {
4283
+ for (const [path38, raw] of Object.entries(s.nodes)) {
3834
4284
  const authorWeights = {};
3835
4285
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3836
4286
  authorWeights[email] = { ...entry };
3837
4287
  }
3838
- idx.nodes.set(path37, { authorWeights, lastTouch: raw.lastTouch });
4288
+ idx.nodes.set(path38, { authorWeights, lastTouch: raw.lastTouch });
3839
4289
  }
3840
4290
  return idx;
3841
4291
  }
3842
4292
  // -------------------------------------------------------------------------
3843
4293
  // Private helpers
3844
4294
  // -------------------------------------------------------------------------
3845
- getOrCreate(path37) {
3846
- const existing = this.nodes.get(path37);
4295
+ getOrCreate(path38) {
4296
+ const existing = this.nodes.get(path38);
3847
4297
  if (existing !== void 0) return existing;
3848
4298
  const fresh = { authorWeights: {}, lastTouch: 0 };
3849
- this.nodes.set(path37, fresh);
4299
+ this.nodes.set(path38, fresh);
3850
4300
  return fresh;
3851
4301
  }
3852
4302
  };
@@ -4599,6 +5049,61 @@ ${methodLines.join("\n")}
4599
5049
  }
4600
5050
  };
4601
5051
 
5052
+ // packages/core/src/db/vectorsCleanup.ts
5053
+ import fs14 from "fs";
5054
+ import path14 from "path";
5055
+ var VECTOR_DB_REL = path14.join(".ctxloom", "vectors.lancedb");
5056
+ var TABLE_DIR = "code_embeddings.lance";
5057
+ function inspectVectorsDb(rootDir) {
5058
+ const tablePath = path14.join(rootDir, VECTOR_DB_REL, TABLE_DIR);
5059
+ const counts = {
5060
+ txn: 0,
5061
+ manifest: 0,
5062
+ lance: 0,
5063
+ totalBytes: 0
5064
+ };
5065
+ if (!fs14.existsSync(tablePath)) return counts;
5066
+ for (const sub of ["_transactions", "_versions", "data"]) {
5067
+ const dir = path14.join(tablePath, sub);
5068
+ if (!fs14.existsSync(dir)) continue;
5069
+ for (const name of fs14.readdirSync(dir)) {
5070
+ const full = path14.join(dir, name);
5071
+ try {
5072
+ const st = fs14.statSync(full);
5073
+ if (!st.isFile()) continue;
5074
+ counts.totalBytes += st.size;
5075
+ if (name.endsWith(".txn")) counts.txn += 1;
5076
+ else if (name.endsWith(".manifest")) counts.manifest += 1;
5077
+ else if (name.endsWith(".lance")) counts.lance += 1;
5078
+ } catch {
5079
+ }
5080
+ }
5081
+ }
5082
+ return counts;
5083
+ }
5084
+ function cleanupVectors(options = {}, activePids = []) {
5085
+ const rootDir = options.rootDir ?? process.cwd();
5086
+ const dbPath = path14.join(rootDir, VECTOR_DB_REL);
5087
+ if (!fs14.existsSync(dbPath)) {
5088
+ return { cleaned: false, reason: "no-db" };
5089
+ }
5090
+ if (activePids.length > 0) {
5091
+ return {
5092
+ cleaned: false,
5093
+ reason: "in-use",
5094
+ conflictingPids: [...activePids]
5095
+ };
5096
+ }
5097
+ const before = inspectVectorsDb(rootDir);
5098
+ if (options.dryRun) {
5099
+ return { cleaned: true, before };
5100
+ }
5101
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5102
+ const backupPath = `${dbPath}.bak-${stamp}`;
5103
+ fs14.renameSync(dbPath, backupPath);
5104
+ return { cleaned: true, before, backupPath };
5105
+ }
5106
+
4602
5107
  // packages/core/src/tools/status.ts
4603
5108
  import { z as z2 } from "zod";
4604
5109
 
@@ -5624,7 +6129,7 @@ function registerFileTool(registry, ctx) {
5624
6129
 
5625
6130
  // packages/core/src/tools/context-packet.ts
5626
6131
  import { z as z5 } from "zod";
5627
- import path14 from "path";
6132
+ import path15 from "path";
5628
6133
  var DEFAULT_MAX_RESPONSE_TOKENS3 = 6e3;
5629
6134
  var Schema3 = z5.object({
5630
6135
  target_file: z5.string().describe("Relative path to the primary file"),
@@ -5693,7 +6198,7 @@ function registerContextPacketTool(registry, ctx) {
5693
6198
  const skeletons = await Promise.all(
5694
6199
  imports.map(async (dep) => {
5695
6200
  try {
5696
- const absDep = path14.resolve(ctx.projectRoot, dep);
6201
+ const absDep = path15.resolve(ctx.projectRoot, dep);
5697
6202
  const sk = await skeletonizer.skeletonize(absDep);
5698
6203
  return `
5699
6204
  <!-- ${dep} -->
@@ -5735,7 +6240,7 @@ ${sk}`;
5735
6240
  import { z as z6 } from "zod";
5736
6241
 
5737
6242
  // packages/core/src/tools/findCallers.ts
5738
- import path15 from "path";
6243
+ import path16 from "path";
5739
6244
  function escapeXML4(text) {
5740
6245
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5741
6246
  }
@@ -6078,7 +6583,7 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
6078
6583
  const now = Math.floor(Date.now() / 1e3);
6079
6584
  const coupling = [];
6080
6585
  for (const seedFile of changedFiles) {
6081
- const coupled = overlay.coChange.topFor({ node: seedFile, limit: 10, minConfidence: 0.2 });
6586
+ const coupled = overlay.coChange.topFor({ node: seedFile, limit: 25, minConfidence: 0.1 });
6082
6587
  for (const hit of coupled) {
6083
6588
  const sibling = hit.nodeA === seedFile ? hit.nodeB : hit.nodeA;
6084
6589
  if (!staticSet.has(sibling) && !coupling.some((h) => h.node === sibling)) {
@@ -6095,25 +6600,143 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
6095
6600
  coupling.splice(10);
6096
6601
  return coupling;
6097
6602
  }
6603
+ function collectImportees(changedFiles, graph) {
6604
+ const changedSet = new Set(changedFiles);
6605
+ const importees = /* @__PURE__ */ new Set();
6606
+ for (const file of changedFiles) {
6607
+ for (const imp of graph.getImports(file)) {
6608
+ if (!changedSet.has(imp)) importees.add(imp);
6609
+ }
6610
+ }
6611
+ return importees;
6612
+ }
6613
+ var SYMBOL_CALLERS_TOP_K = 25;
6614
+ var PATH_PROXIMITY_BONUS = 1;
6615
+ var SYMBOL_CALLERS_MIN_SCORE = 1;
6616
+ function pathProximityScore(callerFile, seedFile) {
6617
+ const lastSlash = seedFile.lastIndexOf("/");
6618
+ const lastDot = seedFile.lastIndexOf(".");
6619
+ const stem = seedFile.slice(
6620
+ lastSlash + 1,
6621
+ lastDot > lastSlash ? lastDot : seedFile.length
6622
+ );
6623
+ if (stem.length < 3) return 0;
6624
+ const shortPrefix = stem.slice(0, 3);
6625
+ const tokens = stem === shortPrefix ? [stem] : [stem, shortPrefix];
6626
+ const caller = callerFile.toLowerCase();
6627
+ for (const t of tokens) {
6628
+ const token = t.toLowerCase();
6629
+ const re = new RegExp(`(?:^|[/_.\\-])${token}(?:[/_.\\-]|$)`);
6630
+ if (re.test(caller)) return PATH_PROXIMITY_BONUS;
6631
+ }
6632
+ return 0;
6633
+ }
6634
+ function collectSymbolCallers(changedFiles, graph) {
6635
+ const changedSet = new Set(changedFiles);
6636
+ const callGraph = graph.getCallGraphIndex();
6637
+ const callerScores = /* @__PURE__ */ new Map();
6638
+ const callersWithProximityApplied = /* @__PURE__ */ new Set();
6639
+ for (const file of changedFiles) {
6640
+ const symbols = graph.lookupSymbolsByFile(file);
6641
+ for (const sym of symbols) {
6642
+ const defs = graph.lookupSymbol(sym);
6643
+ const definedInSeed = defs.some((d) => changedSet.has(d.filePath));
6644
+ if (!definedInSeed) continue;
6645
+ const specificity = 1 / defs.length;
6646
+ const seenForSym = /* @__PURE__ */ new Set();
6647
+ for (const caller of callGraph.getCallers(sym)) {
6648
+ if (changedSet.has(caller.file)) continue;
6649
+ if (seenForSym.has(caller.file)) continue;
6650
+ seenForSym.add(caller.file);
6651
+ const prev = callerScores.get(caller.file) ?? 0;
6652
+ const next = prev + specificity;
6653
+ callerScores.set(caller.file, next);
6654
+ }
6655
+ }
6656
+ for (const callerFile of callerScores.keys()) {
6657
+ if (callersWithProximityApplied.has(callerFile)) continue;
6658
+ const bonus = pathProximityScore(callerFile, file);
6659
+ if (bonus > 0) {
6660
+ callerScores.set(callerFile, (callerScores.get(callerFile) ?? 0) + bonus);
6661
+ }
6662
+ callersWithProximityApplied.add(callerFile);
6663
+ }
6664
+ }
6665
+ const ranked = Array.from(callerScores.entries()).filter(([, score]) => score >= SYMBOL_CALLERS_MIN_SCORE).sort((a, b) => {
6666
+ if (b[1] !== a[1]) return b[1] - a[1];
6667
+ return a[0].localeCompare(b[0]);
6668
+ });
6669
+ return new Set(ranked.slice(0, SYMBOL_CALLERS_TOP_K).map(([file]) => file));
6670
+ }
6671
+ var SEMANTIC_TOP_K = 10;
6672
+ var SEMANTIC_DIST_THRESHOLD = 0.5;
6673
+ async function computeSemanticSimilar(changedFiles, vectorStore, staticSet) {
6674
+ return collectSemanticSimilar(changedFiles, vectorStore, staticSet);
6675
+ }
6676
+ async function collectSemanticSimilar(changedFiles, vectorStore, staticSet) {
6677
+ const changedSet = new Set(changedFiles);
6678
+ const scores = /* @__PURE__ */ new Map();
6679
+ for (const seedFile of changedFiles) {
6680
+ let embedding;
6681
+ try {
6682
+ embedding = await vectorStore.findEmbeddingByPath(seedFile);
6683
+ } catch {
6684
+ continue;
6685
+ }
6686
+ if (embedding === null) continue;
6687
+ let results;
6688
+ try {
6689
+ results = await vectorStore.search(embedding, SEMANTIC_TOP_K + 5);
6690
+ } catch {
6691
+ continue;
6692
+ }
6693
+ for (const r of results) {
6694
+ if (changedSet.has(r.filePath)) continue;
6695
+ if (staticSet.has(r.filePath)) continue;
6696
+ if (r.score >= SEMANTIC_DIST_THRESHOLD) continue;
6697
+ const prev = scores.get(r.filePath);
6698
+ if (prev === void 0 || r.score < prev) {
6699
+ scores.set(r.filePath, r.score);
6700
+ }
6701
+ }
6702
+ }
6703
+ return Array.from(scores.entries()).sort((a, b) => a[1] - b[1]).slice(0, SEMANTIC_TOP_K).map(([node, score]) => ({ node, score }));
6704
+ }
6098
6705
  function getImpactRadius(input) {
6099
- const { graph, overlay, changedFiles, depth = 3 } = input;
6706
+ const {
6707
+ graph,
6708
+ overlay,
6709
+ changedFiles,
6710
+ depth = 3,
6711
+ includeImportees = false,
6712
+ includeSymbolCallers = false
6713
+ } = input;
6100
6714
  const { directImporters, allReachable } = traverseImporters(changedFiles, graph, depth);
6101
6715
  const transitiveImporters = [];
6102
6716
  for (const file of allReachable) {
6103
6717
  if (!directImporters.has(file)) transitiveImporters.push(file);
6104
6718
  }
6719
+ const importeesSet = includeImportees ? collectImportees(changedFiles, graph) : /* @__PURE__ */ new Set();
6720
+ const directImportees = Array.from(importeesSet);
6721
+ const symbolCallersSet = includeSymbolCallers ? collectSymbolCallers(changedFiles, graph) : /* @__PURE__ */ new Set();
6722
+ const symbolCallers = Array.from(symbolCallersSet);
6105
6723
  const staticSet = /* @__PURE__ */ new Set([
6106
6724
  ...changedFiles,
6107
6725
  ...directImporters,
6108
- ...transitiveImporters
6726
+ ...transitiveImporters,
6727
+ ...directImportees,
6728
+ ...symbolCallers
6109
6729
  ]);
6110
6730
  const historicalCoupling = overlay !== void 0 ? buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) : [];
6111
- const totalImpacted = directImporters.size + transitiveImporters.length;
6731
+ const totalImpacted = directImporters.size + transitiveImporters.length + directImportees.length + symbolCallers.length;
6112
6732
  return {
6113
6733
  seedFiles: [...changedFiles],
6114
6734
  directImporters: Array.from(directImporters),
6115
6735
  transitiveImporters,
6736
+ directImportees,
6737
+ symbolCallers,
6116
6738
  historicalCoupling,
6739
+ semanticSimilar: [],
6117
6740
  totalImpacted
6118
6741
  };
6119
6742
  }
@@ -6597,7 +7220,7 @@ function registerArchitectureOverviewTool(registry, ctx) {
6597
7220
 
6598
7221
  // packages/core/src/tools/knowledge-gaps.ts
6599
7222
  import { z as z15 } from "zod";
6600
- import path16 from "path";
7223
+ import path17 from "path";
6601
7224
  var Schema13 = z15.object({
6602
7225
  min_importers: z15.number().min(1).max(50).optional().default(3).describe(
6603
7226
  "Minimum importers to qualify as an untested hub (default: 3)"
@@ -6648,7 +7271,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
6648
7271
  const testFiles = new Set(files.filter((f) => TEST_PATTERN2.test(f)));
6649
7272
  const testedBases = /* @__PURE__ */ new Set();
6650
7273
  for (const tf of testFiles) {
6651
- const base = path16.basename(tf).replace(/\.(test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "");
7274
+ const base = path17.basename(tf).replace(/\.(test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "");
6652
7275
  if (base) testedBases.add(base);
6653
7276
  }
6654
7277
  const isolated = [];
@@ -6666,7 +7289,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
6666
7289
  deadCode.push(file);
6667
7290
  }
6668
7291
  if (importers >= min_importers) {
6669
- const base = path16.basename(file).replace(/\.[^.]+$/, "");
7292
+ const base = path17.basename(file).replace(/\.[^.]+$/, "");
6670
7293
  if (!testedBases.has(base)) {
6671
7294
  untestedHubs.push({ file, importers });
6672
7295
  }
@@ -6853,7 +7476,7 @@ function registerSurprisingConnectionsTool(registry, ctx) {
6853
7476
 
6854
7477
  // packages/core/src/tools/wiki-generate.ts
6855
7478
  import { z as z17 } from "zod";
6856
- import fs14 from "fs";
7479
+ import fs15 from "fs";
6857
7480
  var DEFAULT_MAX_RESPONSE_TOKENS5 = 12e3;
6858
7481
  var Schema15 = z17.object({
6859
7482
  force: z17.boolean().optional().default(false).describe(
@@ -6873,7 +7496,7 @@ function escapeXML14(text) {
6873
7496
  }
6874
7497
  function safeFileSize(filePath) {
6875
7498
  try {
6876
- return fs14.statSync(filePath).size;
7499
+ return fs15.statSync(filePath).size;
6877
7500
  } catch {
6878
7501
  return 0;
6879
7502
  }
@@ -7163,8 +7786,8 @@ function registerGitDiffReviewTool(registry, ctx) {
7163
7786
 
7164
7787
  // packages/core/src/tools/refactor-preview.ts
7165
7788
  import { z as z20 } from "zod";
7166
- import fs15 from "fs";
7167
- import path17 from "path";
7789
+ import fs16 from "fs";
7790
+ import path18 from "path";
7168
7791
  var DEFAULT_MAX_RESPONSE_TOKENS7 = 4e3;
7169
7792
  var Schema18 = z20.object({
7170
7793
  symbol: z20.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
@@ -7184,7 +7807,7 @@ function escapeXML17(text) {
7184
7807
  function scanFile(filePath, symbol, newName) {
7185
7808
  let content;
7186
7809
  try {
7187
- content = fs15.readFileSync(filePath, "utf-8");
7810
+ content = fs16.readFileSync(filePath, "utf-8");
7188
7811
  } catch {
7189
7812
  return [];
7190
7813
  }
@@ -7251,7 +7874,7 @@ function registerRefactorPreviewTool(registry, ctx) {
7251
7874
  const fileChanges = [];
7252
7875
  let totalOccurrences = 0;
7253
7876
  for (const relPath of candidates) {
7254
- const absPath = path17.join(ctx.projectRoot, relPath);
7877
+ const absPath = path18.join(ctx.projectRoot, relPath);
7255
7878
  const occurrences = scanFile(absPath, symbol, new_name);
7256
7879
  if (occurrences.length > 0) {
7257
7880
  fileChanges.push({ filePath: relPath, occurrences });
@@ -7456,8 +8079,8 @@ function registerExecutionFlowTool(registry, ctx) {
7456
8079
 
7457
8080
  // packages/core/src/tools/cross-repo-search.ts
7458
8081
  import { z as z22 } from "zod";
7459
- import fs16 from "fs";
7460
- import path18 from "path";
8082
+ import fs17 from "fs";
8083
+ import path19 from "path";
7461
8084
  var DEFAULT_MAX_RESPONSE_TOKENS9 = 4e3;
7462
8085
  var ALIAS_REGEX = /^[a-z0-9-]{1,40}$/;
7463
8086
  var RESERVED_ALIASES = /* @__PURE__ */ new Set([
@@ -7499,16 +8122,16 @@ var RepoRegistry = class {
7499
8122
  }
7500
8123
  load() {
7501
8124
  try {
7502
- if (!fs16.existsSync(this.filePath)) return [];
7503
- return JSON.parse(fs16.readFileSync(this.filePath, "utf-8"));
8125
+ if (!fs17.existsSync(this.filePath)) return [];
8126
+ return JSON.parse(fs17.readFileSync(this.filePath, "utf-8"));
7504
8127
  } catch {
7505
8128
  return [];
7506
8129
  }
7507
8130
  }
7508
8131
  save() {
7509
- const dir = path18.dirname(this.filePath);
7510
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
7511
- fs16.writeFileSync(this.filePath, JSON.stringify(this.repos, null, 2), "utf-8");
8132
+ const dir = path19.dirname(this.filePath);
8133
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
8134
+ fs17.writeFileSync(this.filePath, JSON.stringify(this.repos, null, 2), "utf-8");
7512
8135
  }
7513
8136
  list() {
7514
8137
  return [...this.repos];
@@ -7517,15 +8140,15 @@ var RepoRegistry = class {
7517
8140
  return this.repos.find((r) => r.alias === alias) ?? null;
7518
8141
  }
7519
8142
  findByPath(absPath) {
7520
- const canonical = path18.resolve(absPath);
7521
- return this.repos.find((r) => path18.resolve(r.root) === canonical) ?? null;
8143
+ const canonical = path19.resolve(absPath);
8144
+ return this.repos.find((r) => path19.resolve(r.root) === canonical) ?? null;
7522
8145
  }
7523
8146
  register(root, dbPath, opts = {}) {
7524
8147
  if (opts.alias !== void 0) {
7525
8148
  const v = validateAlias(opts.alias);
7526
8149
  if (!v.ok) throw new Error(`Invalid alias: ${v.reason}`);
7527
8150
  const colliding = this.repos.find(
7528
- (r) => r.alias === opts.alias && path18.resolve(r.root) !== path18.resolve(root)
8151
+ (r) => r.alias === opts.alias && path19.resolve(r.root) !== path19.resolve(root)
7529
8152
  );
7530
8153
  if (colliding) {
7531
8154
  throw new Error(
@@ -7533,11 +8156,11 @@ var RepoRegistry = class {
7533
8156
  );
7534
8157
  }
7535
8158
  }
7536
- const existingIdx = this.repos.findIndex((r) => path18.resolve(r.root) === path18.resolve(root));
8159
+ const existingIdx = this.repos.findIndex((r) => path19.resolve(r.root) === path19.resolve(root));
7537
8160
  const entry = {
7538
8161
  root,
7539
8162
  dbPath,
7540
- name: path18.basename(root),
8163
+ name: path19.basename(root),
7541
8164
  alias: opts.alias,
7542
8165
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
7543
8166
  };
@@ -7549,7 +8172,7 @@ var RepoRegistry = class {
7549
8172
  this.save();
7550
8173
  }
7551
8174
  unregister(root) {
7552
- this.repos = this.repos.filter((r) => path18.resolve(r.root) !== path18.resolve(root));
8175
+ this.repos = this.repos.filter((r) => path19.resolve(r.root) !== path19.resolve(root));
7553
8176
  this.save();
7554
8177
  }
7555
8178
  };
@@ -7571,7 +8194,7 @@ function escapeXML19(text) {
7571
8194
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7572
8195
  }
7573
8196
  function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
7574
- const repoRegistryPath = registryFilePath ?? path18.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
8197
+ const repoRegistryPath = registryFilePath ?? path19.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
7575
8198
  registry.register(
7576
8199
  "ctx_cross_repo_search",
7577
8200
  {
@@ -7692,8 +8315,8 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
7692
8315
 
7693
8316
  // packages/core/src/tools/apply-refactor.ts
7694
8317
  import { z as z23 } from "zod";
7695
- import fs17 from "fs";
7696
- import path19 from "path";
8318
+ import fs18 from "fs";
8319
+ import path20 from "path";
7697
8320
  var DEFAULT_MAX_RESPONSE_TOKENS10 = 2e3;
7698
8321
  var Schema21 = z23.object({
7699
8322
  symbol: z23.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
@@ -7716,7 +8339,7 @@ function escapeXML20(text) {
7716
8339
  function applyToFile(absPath, symbol, newName, dryRun) {
7717
8340
  let content;
7718
8341
  try {
7719
- content = fs17.readFileSync(absPath, "utf-8");
8342
+ content = fs18.readFileSync(absPath, "utf-8");
7720
8343
  } catch {
7721
8344
  return 0;
7722
8345
  }
@@ -7725,7 +8348,7 @@ function applyToFile(absPath, symbol, newName, dryRun) {
7725
8348
  const occurrences = (content.match(regex) ?? []).length;
7726
8349
  if (occurrences === 0) return 0;
7727
8350
  if (!dryRun) {
7728
- fs17.writeFileSync(absPath, content.replace(regex, newName), "utf-8");
8351
+ fs18.writeFileSync(absPath, content.replace(regex, newName), "utf-8");
7729
8352
  }
7730
8353
  return occurrences;
7731
8354
  }
@@ -7769,7 +8392,7 @@ function registerApplyRefactorTool(registry, ctx) {
7769
8392
  const results = [];
7770
8393
  let totalOccurrences = 0;
7771
8394
  for (const relPath of candidates) {
7772
- const absPath = path19.join(ctx.projectRoot, relPath);
8395
+ const absPath = path20.join(ctx.projectRoot, relPath);
7773
8396
  const count = applyToFile(absPath, symbol, new_name, dry_run);
7774
8397
  if (count > 0) {
7775
8398
  results.push({ filePath: relPath, occurrences: count, written: !dry_run });
@@ -7913,8 +8536,8 @@ function registerDetectChangesTool(registry, ctx) {
7913
8536
 
7914
8537
  // packages/core/src/tools/full-text-search.ts
7915
8538
  import { z as z25 } from "zod";
7916
- import fs18 from "fs";
7917
- import path20 from "path";
8539
+ import fs19 from "fs";
8540
+ import path21 from "path";
7918
8541
  var DEFAULT_MAX_RESPONSE_TOKENS11 = 4e3;
7919
8542
  var Schema23 = z25.object({
7920
8543
  query: z25.string().min(1).describe("Search term \u2014 literal or /regex/"),
@@ -7945,7 +8568,7 @@ function buildPattern(query, caseSensitive) {
7945
8568
  function scanFile2(absPath, pattern, contextLines) {
7946
8569
  let content;
7947
8570
  try {
7948
- content = fs18.readFileSync(absPath, "utf-8");
8571
+ content = fs19.readFileSync(absPath, "utf-8");
7949
8572
  } catch {
7950
8573
  return null;
7951
8574
  }
@@ -8017,7 +8640,7 @@ function registerFullTextSearchTool(registry, ctx) {
8017
8640
  };
8018
8641
  if (mode === "semantic") {
8019
8642
  try {
8020
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-7YOG4DFN.js");
8643
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8021
8644
  const store = await ctx.getStore(project_root);
8022
8645
  const embedding = await generateEmbedding2(query);
8023
8646
  const results = await store.search(embedding, limit);
@@ -8039,7 +8662,7 @@ function registerFullTextSearchTool(registry, ctx) {
8039
8662
  const files = graph.allFiles();
8040
8663
  const keywordResults = [];
8041
8664
  for (const relPath of files) {
8042
- const absPath = path20.join(ctx.projectRoot, relPath);
8665
+ const absPath = path21.join(ctx.projectRoot, relPath);
8043
8666
  const hit = scanFile2(absPath, pattern, context_lines);
8044
8667
  if (hit) {
8045
8668
  keywordResults.push({
@@ -8054,7 +8677,7 @@ function registerFullTextSearchTool(registry, ctx) {
8054
8677
  let merged = keywordResults.slice(0, limit);
8055
8678
  if (mode === "hybrid") {
8056
8679
  try {
8057
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-7YOG4DFN.js");
8680
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8058
8681
  const store = await ctx.getStore(project_root);
8059
8682
  const embedding = await generateEmbedding2(query);
8060
8683
  const vectorResults = await store.search(embedding, Math.ceil(limit / 2));
@@ -8273,8 +8896,8 @@ function registerGetWorkflowTool(registry, _ctx) {
8273
8896
  }
8274
8897
 
8275
8898
  // packages/core/src/tools/graph-snapshot.ts
8276
- import fs19 from "fs";
8277
- import path21 from "path";
8899
+ import fs20 from "fs";
8900
+ import path22 from "path";
8278
8901
  import { z as z28 } from "zod";
8279
8902
  var schema = z28.object({
8280
8903
  name: z28.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
@@ -8286,13 +8909,13 @@ var schema = z28.object({
8286
8909
  project_root: ProjectRootField
8287
8910
  });
8288
8911
  function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
8289
- const snapshotsDir = path21.resolve(rootDir, ".ctxloom", "snapshots");
8290
- fs19.mkdirSync(snapshotsDir, { recursive: true });
8291
- const snapshotPath = path21.resolve(snapshotsDir, `${name}.json`);
8292
- if (!snapshotPath.startsWith(snapshotsDir + path21.sep)) {
8912
+ const snapshotsDir = path22.resolve(rootDir, ".ctxloom", "snapshots");
8913
+ fs20.mkdirSync(snapshotsDir, { recursive: true });
8914
+ const snapshotPath = path22.resolve(snapshotsDir, `${name}.json`);
8915
+ if (!snapshotPath.startsWith(snapshotsDir + path22.sep)) {
8293
8916
  throw new Error(`Invalid snapshot name: "${name}"`);
8294
8917
  }
8295
- if (fs19.existsSync(snapshotPath) && !overwrite) {
8918
+ if (fs20.existsSync(snapshotPath) && !overwrite) {
8296
8919
  throw new Error(`Snapshot "${name}" already exists. Pass overwrite: true to replace it.`);
8297
8920
  }
8298
8921
  const files = graph.allFiles();
@@ -8307,14 +8930,14 @@ function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
8307
8930
  edgeCount: graph.edgeCount(),
8308
8931
  forwardEdges
8309
8932
  };
8310
- const tmp = snapshotPath + ".tmp";
8311
- fs19.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
8312
- fs19.renameSync(tmp, snapshotPath);
8933
+ const tmp = `${snapshotPath}.${process.pid}.tmp`;
8934
+ fs20.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
8935
+ fs20.renameSync(tmp, snapshotPath);
8313
8936
  }
8314
8937
  function listNamedSnapshots(rootDir) {
8315
- const snapshotsDir = path21.join(rootDir, ".ctxloom", "snapshots");
8316
- if (!fs19.existsSync(snapshotsDir)) return [];
8317
- return fs19.readdirSync(snapshotsDir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
8938
+ const snapshotsDir = path22.join(rootDir, ".ctxloom", "snapshots");
8939
+ if (!fs20.existsSync(snapshotsDir)) return [];
8940
+ return fs20.readdirSync(snapshotsDir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
8318
8941
  }
8319
8942
  function registerGraphSnapshotTool(registry, ctx) {
8320
8943
  registry.register(
@@ -8363,8 +8986,8 @@ function registerGraphSnapshotTool(registry, ctx) {
8363
8986
  }
8364
8987
 
8365
8988
  // packages/core/src/tools/graph-diff.ts
8366
- import fs20 from "fs";
8367
- import path22 from "path";
8989
+ import fs21 from "fs";
8990
+ import path23 from "path";
8368
8991
  import { z as z29 } from "zod";
8369
8992
  var schema2 = z29.object({
8370
8993
  baseline: z29.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
@@ -8372,16 +8995,16 @@ var schema2 = z29.object({
8372
8995
  project_root: ProjectRootField
8373
8996
  });
8374
8997
  function loadSnapshot(name, rootDir) {
8375
- const snapshotsDir = path22.resolve(rootDir, ".ctxloom", "snapshots");
8376
- const snapshotPath = path22.resolve(snapshotsDir, `${name}.json`);
8377
- if (!snapshotPath.startsWith(snapshotsDir + path22.sep)) {
8998
+ const snapshotsDir = path23.resolve(rootDir, ".ctxloom", "snapshots");
8999
+ const snapshotPath = path23.resolve(snapshotsDir, `${name}.json`);
9000
+ if (!snapshotPath.startsWith(snapshotsDir + path23.sep)) {
8378
9001
  throw new Error(`Invalid snapshot name: "${name}"`);
8379
9002
  }
8380
- if (!fs20.existsSync(snapshotPath)) {
9003
+ if (!fs21.existsSync(snapshotPath)) {
8381
9004
  throw new Error(`Snapshot "${name}" not found. Run ctx_graph_snapshot first.`);
8382
9005
  }
8383
9006
  try {
8384
- return JSON.parse(fs20.readFileSync(snapshotPath, "utf-8"));
9007
+ return JSON.parse(fs21.readFileSync(snapshotPath, "utf-8"));
8385
9008
  } catch (e) {
8386
9009
  throw new Error(`Snapshot "${name}" is corrupted: ${e instanceof Error ? e.message : String(e)}`);
8387
9010
  }
@@ -8775,8 +9398,8 @@ var RulesConfigError = class extends Error {
8775
9398
  };
8776
9399
 
8777
9400
  // packages/core/src/rules/loadConfig.ts
8778
- import fs21 from "fs/promises";
8779
- import path23 from "path";
9401
+ import fs22 from "fs/promises";
9402
+ import path24 from "path";
8780
9403
  import yaml from "js-yaml";
8781
9404
  import { z as z33 } from "zod";
8782
9405
  var RuleSchema = z33.object({
@@ -8791,10 +9414,10 @@ var RulesConfigSchema = z33.object({
8791
9414
  rules: z33.array(RuleSchema).default([])
8792
9415
  });
8793
9416
  async function loadRulesConfig(rootDir) {
8794
- const filePath = path23.join(rootDir, ".ctxloom", "rules.yml");
9417
+ const filePath = path24.join(rootDir, ".ctxloom", "rules.yml");
8795
9418
  let raw;
8796
9419
  try {
8797
- raw = await fs21.readFile(filePath, "utf-8");
9420
+ raw = await fs22.readFile(filePath, "utf-8");
8798
9421
  } catch (err) {
8799
9422
  if (err.code === "ENOENT") return null;
8800
9423
  throw new RulesConfigError(`Failed to read rules config: ${String(err)}`);
@@ -9206,11 +9829,11 @@ function readRecentChanges(projectRoot) {
9206
9829
  return lines.slice(0, 20).map((line) => {
9207
9830
  const x = line[0];
9208
9831
  const y = line[1];
9209
- const path37 = line.slice(3).trim();
9832
+ const path38 = line.slice(3).trim();
9210
9833
  let status = "?";
9211
9834
  const xy = x === " " ? y : x;
9212
9835
  if (xy === "M" || xy === "A" || xy === "D" || xy === "R") status = xy;
9213
- return { file: path37, status };
9836
+ return { file: path38, status };
9214
9837
  });
9215
9838
  } catch {
9216
9839
  return [];
@@ -9437,8 +10060,8 @@ function createToolRegistry(ctx) {
9437
10060
  }
9438
10061
 
9439
10062
  // packages/core/src/tools/ruleManager.ts
9440
- import fs22 from "fs";
9441
- import path24 from "path";
10063
+ import fs23 from "fs";
10064
+ import path25 from "path";
9442
10065
  var RULE_FILES = [
9443
10066
  ".cursorrules",
9444
10067
  "CLAUDE.md",
@@ -9462,30 +10085,30 @@ var RuleManager = class {
9462
10085
  if (this.cachedRules) return this.cachedRules;
9463
10086
  const rules = [];
9464
10087
  for (const ruleFile of RULE_FILES) {
9465
- const fullPath = path24.join(this.projectRoot, ruleFile);
10088
+ const fullPath = path25.join(this.projectRoot, ruleFile);
9466
10089
  try {
9467
10090
  this.pathValidator.validate(fullPath);
9468
- if (fs22.existsSync(fullPath)) {
9469
- const stat = fs22.statSync(fullPath);
10091
+ if (fs23.existsSync(fullPath)) {
10092
+ const stat = fs23.statSync(fullPath);
9470
10093
  if (stat.isFile()) {
9471
- const content = fs22.readFileSync(fullPath, "utf-8");
10094
+ const content = fs23.readFileSync(fullPath, "utf-8");
9472
10095
  rules.push({
9473
10096
  name: ruleFile,
9474
10097
  path: ruleFile,
9475
10098
  content
9476
10099
  });
9477
10100
  } else if (stat.isDirectory()) {
9478
- const dirEntries = fs22.readdirSync(fullPath);
10101
+ const dirEntries = fs23.readdirSync(fullPath);
9479
10102
  for (const entry of dirEntries) {
9480
- const entryPath = path24.join(fullPath, entry);
10103
+ const entryPath = path25.join(fullPath, entry);
9481
10104
  try {
9482
10105
  this.pathValidator.validate(entryPath);
9483
10106
  } catch {
9484
10107
  continue;
9485
10108
  }
9486
- const entryStat = fs22.statSync(entryPath);
10109
+ const entryStat = fs23.statSync(entryPath);
9487
10110
  if (entryStat.isFile()) {
9488
- const content = fs22.readFileSync(entryPath, "utf-8");
10111
+ const content = fs23.readFileSync(entryPath, "utf-8");
9489
10112
  rules.push({
9490
10113
  name: `${ruleFile}/${entry}`,
9491
10114
  path: `${ruleFile}/${entry}`,
@@ -9528,8 +10151,8 @@ var RuleManager = class {
9528
10151
  };
9529
10152
 
9530
10153
  // packages/core/src/review/AuthorResolver.ts
9531
- import fs23 from "fs/promises";
9532
- import path25 from "path";
10154
+ import fs24 from "fs/promises";
10155
+ import path26 from "path";
9533
10156
  import yaml2 from "js-yaml";
9534
10157
  var AuthorResolver = class {
9535
10158
  constructor(ctxloomDir) {
@@ -9554,8 +10177,8 @@ var AuthorResolver = class {
9554
10177
  /** Write a new mapping to the cache file. */
9555
10178
  async writeCache(email, handle) {
9556
10179
  this.cache = { ...this.cache, [email]: handle };
9557
- await fs23.writeFile(
9558
- path25.join(this.ctxloomDir, "authors-cache.json"),
10180
+ await fs24.writeFile(
10181
+ path26.join(this.ctxloomDir, "authors-cache.json"),
9559
10182
  JSON.stringify(this.cache, null, 2)
9560
10183
  );
9561
10184
  }
@@ -9564,9 +10187,9 @@ var AuthorResolver = class {
9564
10187
  return emails.filter((e) => this.resolve(e) === void 0);
9565
10188
  }
9566
10189
  async loadYml() {
9567
- const file = path25.join(this.ctxloomDir, "authors.yml");
10190
+ const file = path26.join(this.ctxloomDir, "authors.yml");
9568
10191
  try {
9569
- const raw = await fs23.readFile(file, "utf8");
10192
+ const raw = await fs24.readFile(file, "utf8");
9570
10193
  const parsed = yaml2.load(raw);
9571
10194
  if (!parsed) return;
9572
10195
  this.mappings = parsed.mappings ?? {};
@@ -9575,9 +10198,9 @@ var AuthorResolver = class {
9575
10198
  }
9576
10199
  }
9577
10200
  async loadCache() {
9578
- const file = path25.join(this.ctxloomDir, "authors-cache.json");
10201
+ const file = path26.join(this.ctxloomDir, "authors-cache.json");
9579
10202
  try {
9580
- const raw = await fs23.readFile(file, "utf8");
10203
+ const raw = await fs24.readFile(file, "utf8");
9581
10204
  this.cache = JSON.parse(raw);
9582
10205
  } catch {
9583
10206
  }
@@ -9602,8 +10225,8 @@ async function resolveViaGitHubApi(email, owner, repo, token) {
9602
10225
  }
9603
10226
 
9604
10227
  // packages/core/src/review/CodeownersWriter.ts
9605
- import fs24 from "fs/promises";
9606
- import path26 from "path";
10228
+ import fs25 from "fs/promises";
10229
+ import path27 from "path";
9607
10230
  var MARKER_START = "# <ctxloom:start> \u2014 managed by ctxloom review-suggest; do not edit between markers";
9608
10231
  var MARKER_START_DETECT = "# <ctxloom:start>";
9609
10232
  var MARKER_END = "# <ctxloom:end>";
@@ -9635,15 +10258,15 @@ ${block}
9635
10258
  async function generateCODEOWNERS(codeownersPath, rules) {
9636
10259
  let existing = "";
9637
10260
  try {
9638
- existing = await fs24.readFile(codeownersPath, "utf8");
10261
+ existing = await fs25.readFile(codeownersPath, "utf8");
9639
10262
  } catch {
9640
10263
  }
9641
10264
  const block = buildCodeownersBlock(rules);
9642
10265
  return mergeIntoFile(existing, block);
9643
10266
  }
9644
10267
  async function writeCODEOWNERS(codeownersPath, content) {
9645
- await fs24.mkdir(path26.dirname(codeownersPath), { recursive: true });
9646
- await fs24.writeFile(codeownersPath, content, "utf8");
10268
+ await fs25.mkdir(path27.dirname(codeownersPath), { recursive: true });
10269
+ await fs25.writeFile(codeownersPath, content, "utf8");
9647
10270
  }
9648
10271
 
9649
10272
  // packages/core/src/review/types.ts
@@ -9828,8 +10451,8 @@ function matchGlob(pattern, value) {
9828
10451
  }
9829
10452
 
9830
10453
  // packages/core/src/review/loadConfig.ts
9831
- import fs25 from "fs/promises";
9832
- import path27 from "path";
10454
+ import fs26 from "fs/promises";
10455
+ import path28 from "path";
9833
10456
  import yaml3 from "js-yaml";
9834
10457
  function freshDefaults() {
9835
10458
  return {
@@ -9840,9 +10463,9 @@ function freshDefaults() {
9840
10463
  };
9841
10464
  }
9842
10465
  async function loadReviewConfig(root) {
9843
- const file = path27.join(root, ".ctxloom", "review.yml");
10466
+ const file = path28.join(root, ".ctxloom", "review.yml");
9844
10467
  try {
9845
- const raw = await fs25.readFile(file, "utf8");
10468
+ const raw = await fs26.readFile(file, "utf8");
9846
10469
  const parsed = yaml3.load(raw);
9847
10470
  if (!parsed) return freshDefaults();
9848
10471
  return {
@@ -9857,13 +10480,13 @@ async function loadReviewConfig(root) {
9857
10480
  }
9858
10481
 
9859
10482
  // packages/core/src/security/PathValidator.ts
9860
- import path28 from "path";
9861
- import fs26 from "fs";
10483
+ import path29 from "path";
10484
+ import fs27 from "fs";
9862
10485
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
9863
10486
  var PathValidator = class {
9864
10487
  canonicalRoot;
9865
10488
  constructor(projectRoot) {
9866
- this.canonicalRoot = fs26.realpathSync(path28.resolve(projectRoot));
10489
+ this.canonicalRoot = fs27.realpathSync(path29.resolve(projectRoot));
9867
10490
  }
9868
10491
  /**
9869
10492
  * Validates that the given input path resolves within the project root.
@@ -9873,14 +10496,14 @@ var PathValidator = class {
9873
10496
  * @throws Error if the path escapes the project root
9874
10497
  */
9875
10498
  validate(inputPath) {
9876
- const resolved = path28.resolve(this.canonicalRoot, inputPath);
10499
+ const resolved = path29.resolve(this.canonicalRoot, inputPath);
9877
10500
  let canonical;
9878
10501
  try {
9879
- canonical = fs26.realpathSync(resolved);
10502
+ canonical = fs27.realpathSync(resolved);
9880
10503
  } catch {
9881
10504
  canonical = resolved;
9882
10505
  }
9883
- if (!canonical.startsWith(this.canonicalRoot + path28.sep) && canonical !== this.canonicalRoot) {
10506
+ if (!canonical.startsWith(this.canonicalRoot + path29.sep) && canonical !== this.canonicalRoot) {
9884
10507
  throw new Error(
9885
10508
  `Path traversal blocked: "${inputPath}" resolves outside of the project root`
9886
10509
  );
@@ -9897,7 +10520,7 @@ var PathValidator = class {
9897
10520
  * Converts an absolute path to a relative path from the project root.
9898
10521
  */
9899
10522
  toRelative(absolutePath) {
9900
- return path28.relative(this.canonicalRoot, absolutePath);
10523
+ return path29.relative(this.canonicalRoot, absolutePath);
9901
10524
  }
9902
10525
  /**
9903
10526
  * Validates and reads a file, returning its content.
@@ -9905,11 +10528,11 @@ var PathValidator = class {
9905
10528
  */
9906
10529
  readFile(inputPath) {
9907
10530
  const absPath = this.validate(inputPath);
9908
- const stat = fs26.statSync(absPath);
10531
+ const stat = fs27.statSync(absPath);
9909
10532
  if (stat.size > MAX_FILE_SIZE) {
9910
10533
  throw new Error(`File too large: ${inputPath} (${Math.round(stat.size / 1024)}KB, max ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
9911
10534
  }
9912
- return fs26.readFileSync(absPath, "utf-8");
10535
+ return fs27.readFileSync(absPath, "utf-8");
9913
10536
  }
9914
10537
  /**
9915
10538
  * Checks if a path exists and is within the project root.
@@ -9926,16 +10549,14 @@ var PathValidator = class {
9926
10549
 
9927
10550
  // packages/core/src/watcher/FileWatcher.ts
9928
10551
  import chokidar from "chokidar";
9929
- var IGNORED = [
9930
- "**/node_modules/**",
9931
- "**/.git/**",
9932
- "**/dist/**",
9933
- "**/build/**",
9934
- "**/.ctxloom/**",
9935
- "**/coverage/**",
9936
- "**/.next/**",
9937
- "**/.cache/**"
9938
- ];
10552
+ function isIgnoredPath(absPath) {
10553
+ const segments = absPath.split(/[\\/]/);
10554
+ for (const seg of segments) {
10555
+ if (INDEXER_IGNORED_DIRS.has(seg)) return true;
10556
+ }
10557
+ return false;
10558
+ }
10559
+ var IGNORED = isIgnoredPath;
9939
10560
  var FileWatcher = class {
9940
10561
  root;
9941
10562
  onChange;
@@ -10062,7 +10683,7 @@ var EmailAlreadyUsedError = class extends Error {
10062
10683
 
10063
10684
  // packages/core/src/license/LicenseStore.ts
10064
10685
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
10065
- import path29 from "path";
10686
+ import path30 from "path";
10066
10687
 
10067
10688
  // packages/core/src/license/types.ts
10068
10689
  import { z as z37 } from "zod";
@@ -10083,7 +10704,7 @@ var LicenseFileSchema = z37.object({
10083
10704
 
10084
10705
  // packages/core/src/license/LicenseStore.ts
10085
10706
  function licenseFilePath(home) {
10086
- return path29.join(home, ".ctxloom", "license.json");
10707
+ return path30.join(home, ".ctxloom", "license.json");
10087
10708
  }
10088
10709
  var LicenseStore = class {
10089
10710
  filePath;
@@ -10106,7 +10727,7 @@ var LicenseStore = class {
10106
10727
  }
10107
10728
  }
10108
10729
  async write(license) {
10109
- mkdirSync(path29.dirname(this.filePath), { recursive: true });
10730
+ mkdirSync(path30.dirname(this.filePath), { recursive: true });
10110
10731
  writeFileSync(this.filePath, JSON.stringify(license, null, 2), {
10111
10732
  encoding: "utf8",
10112
10733
  mode: 384
@@ -10237,11 +10858,11 @@ import os5 from "os";
10237
10858
 
10238
10859
  // packages/core/src/license/DistinctIdStore.ts
10239
10860
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
10240
- import path30 from "path";
10861
+ import path31 from "path";
10241
10862
  import os3 from "os";
10242
10863
  var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10243
10864
  function distinctIdPath(home) {
10244
- return path30.join(home ?? os3.homedir(), ".ctxloom", "distinct_id");
10865
+ return path31.join(home ?? os3.homedir(), ".ctxloom", "distinct_id");
10245
10866
  }
10246
10867
  function isValidV4(id) {
10247
10868
  return typeof id === "string" && UUID_V4_REGEX.test(id);
@@ -10262,7 +10883,7 @@ function getOrCreateDistinctId(home) {
10262
10883
  id: crypto.randomUUID(),
10263
10884
  alias_pending: os3.hostname()
10264
10885
  };
10265
- mkdirSync2(path30.dirname(filePath), { recursive: true });
10886
+ mkdirSync2(path31.dirname(filePath), { recursive: true });
10266
10887
  writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
10267
10888
  return record;
10268
10889
  }
@@ -10291,10 +10912,10 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
10291
10912
  function getTelemetryLevel() {
10292
10913
  return TELEMETRY_LEVEL;
10293
10914
  }
10294
- var CTXLOOM_VERSION = "1.5.5".length > 0 ? "1.5.5" : "dev";
10915
+ var CTXLOOM_VERSION = "1.7.0".length > 0 ? "1.7.0" : "dev";
10295
10916
  var POSTHOG_HOST = "https://eu.i.posthog.com";
10296
10917
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
10297
- var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528\u2028" : "");
10918
+ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
10298
10919
  var cachedDistinctId = null;
10299
10920
  function resolveDistinctId() {
10300
10921
  if (cachedDistinctId) return cachedDistinctId;
@@ -10424,17 +11045,17 @@ function parseStack(stack) {
10424
11045
 
10425
11046
  // packages/core/src/license/FunnelMilestones.ts
10426
11047
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
10427
- import path31 from "path";
11048
+ import path32 from "path";
10428
11049
  import os4 from "os";
10429
11050
  var INSTALL_MARKER = "installed_at";
10430
11051
  var FIRST_REVIEW_MARKER = "first_review_at";
10431
11052
  function writeMarker(filePath) {
10432
- mkdirSync3(path31.dirname(filePath), { recursive: true });
11053
+ mkdirSync3(path32.dirname(filePath), { recursive: true });
10433
11054
  writeFileSync3(filePath, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
10434
11055
  }
10435
11056
  function shouldEmitInstallCompleted(home) {
10436
11057
  const root = home ?? os4.homedir();
10437
- const marker = path31.join(root, ".ctxloom", INSTALL_MARKER);
11058
+ const marker = path32.join(root, ".ctxloom", INSTALL_MARKER);
10438
11059
  if (existsSync3(marker)) return false;
10439
11060
  try {
10440
11061
  writeMarker(marker);
@@ -10443,7 +11064,7 @@ function shouldEmitInstallCompleted(home) {
10443
11064
  return true;
10444
11065
  }
10445
11066
  function shouldEmitFirstReviewRun(projectRoot) {
10446
- const marker = path31.join(projectRoot, ".ctxloom", FIRST_REVIEW_MARKER);
11067
+ const marker = path32.join(projectRoot, ".ctxloom", FIRST_REVIEW_MARKER);
10447
11068
  if (existsSync3(marker)) return false;
10448
11069
  try {
10449
11070
  writeMarker(marker);
@@ -10561,16 +11182,16 @@ async function startTrial(email, opts = {}) {
10561
11182
 
10562
11183
  // packages/core/src/license/TelemetryNotice.ts
10563
11184
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
10564
- import path32 from "path";
11185
+ import path33 from "path";
10565
11186
  import os6 from "os";
10566
11187
  function noticePath(home) {
10567
- return path32.join(home ?? os6.homedir(), ".ctxloom", "telemetry_notice_shown");
11188
+ return path33.join(home ?? os6.homedir(), ".ctxloom", "telemetry_notice_shown");
10568
11189
  }
10569
11190
  function shouldShowTelemetryNotice(home) {
10570
11191
  const filePath = noticePath(home);
10571
11192
  if (existsSync4(filePath)) return false;
10572
11193
  try {
10573
- mkdirSync4(path32.dirname(filePath), { recursive: true });
11194
+ mkdirSync4(path33.dirname(filePath), { recursive: true });
10574
11195
  writeFileSync4(filePath, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
10575
11196
  } catch {
10576
11197
  }
@@ -10578,13 +11199,13 @@ function shouldShowTelemetryNotice(home) {
10578
11199
  }
10579
11200
 
10580
11201
  // packages/core/src/server/ProjectState.ts
10581
- import path34 from "path";
11202
+ import path35 from "path";
10582
11203
 
10583
11204
  // packages/core/src/server/projectId.ts
10584
11205
  import crypto5 from "crypto";
10585
- import path33 from "path";
11206
+ import path34 from "path";
10586
11207
  function hashProjectRoot(absPath) {
10587
- const canonical = path33.resolve(absPath);
11208
+ const canonical = path34.resolve(absPath);
10588
11209
  return crypto5.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
10589
11210
  }
10590
11211
 
@@ -10592,7 +11213,7 @@ function hashProjectRoot(absPath) {
10592
11213
  function createProjectState(projectRoot, opts = {}) {
10593
11214
  return {
10594
11215
  projectRoot,
10595
- dbPath: path34.join(projectRoot, ".ctxloom", "vectors.lancedb"),
11216
+ dbPath: path35.join(projectRoot, ".ctxloom", "vectors.lancedb"),
10596
11217
  pinned: opts.pinned ?? false,
10597
11218
  lastTouchedAt: Date.now(),
10598
11219
  vectorsInitialized: false,
@@ -10744,8 +11365,8 @@ var ProjectStateManager = class {
10744
11365
  };
10745
11366
 
10746
11367
  // packages/core/src/server/resolveProjectRoot.ts
10747
- import fs27 from "fs";
10748
- import path35 from "path";
11368
+ import fs28 from "fs";
11369
+ import path36 from "path";
10749
11370
  var PATH_SEPARATOR_PATTERN = /[/\\~]|^[A-Za-z]:/;
10750
11371
  function looksLikePath(value) {
10751
11372
  return PATH_SEPARATOR_PATTERN.test(value);
@@ -10770,13 +11391,13 @@ function resolvePathSafely(p, cwd) {
10770
11391
  let expanded = p;
10771
11392
  if (p === "~" || p.startsWith("~/")) {
10772
11393
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
10773
- expanded = p === "~" ? home : path35.join(home, p.slice(2));
11394
+ expanded = p === "~" ? home : path36.join(home, p.slice(2));
10774
11395
  }
10775
- return path35.isAbsolute(expanded) ? path35.resolve(expanded) : path35.resolve(cwd, expanded);
11396
+ return path36.isAbsolute(expanded) ? path36.resolve(expanded) : path36.resolve(cwd, expanded);
10776
11397
  }
10777
11398
  function realpathOrSame(p) {
10778
11399
  try {
10779
- return fs27.realpathSync(p);
11400
+ return fs28.realpathSync(p);
10780
11401
  } catch {
10781
11402
  return p;
10782
11403
  }
@@ -10801,7 +11422,7 @@ function resolveProjectRoot(input) {
10801
11422
  };
10802
11423
  }
10803
11424
  const resolved2 = resolvePathSafely(arg, cwd);
10804
- if (!fs27.existsSync(resolved2)) {
11425
+ if (!fs28.existsSync(resolved2)) {
10805
11426
  return {
10806
11427
  kind: "project_root_not_found",
10807
11428
  attemptedPath: resolved2,
@@ -10812,7 +11433,7 @@ function resolveProjectRoot(input) {
10812
11433
  }
10813
11434
  if (env !== void 0 && env !== "") {
10814
11435
  const resolved2 = resolvePathSafely(env, cwd);
10815
- if (!fs27.existsSync(resolved2)) {
11436
+ if (!fs28.existsSync(resolved2)) {
10816
11437
  return {
10817
11438
  kind: "project_root_not_found",
10818
11439
  attemptedPath: resolved2,
@@ -10839,12 +11460,12 @@ var FILESYSTEM_ROOTS = /* @__PURE__ */ new Set(["/", "C:\\", "D:\\", "E:\\", "F:
10839
11460
  function validateDefaultRoot(candidate) {
10840
11461
  if (FILESYSTEM_ROOTS.has(candidate)) return false;
10841
11462
  try {
10842
- const stat = fs27.statSync(candidate);
11463
+ const stat = fs28.statSync(candidate);
10843
11464
  if (!stat.isDirectory()) return false;
10844
11465
  } catch {
10845
11466
  return false;
10846
11467
  }
10847
- return PROJECT_MARKERS.some((m) => fs27.existsSync(path35.join(candidate, m)));
11468
+ return PROJECT_MARKERS.some((m) => fs28.existsSync(path36.join(candidate, m)));
10848
11469
  }
10849
11470
 
10850
11471
  // packages/core/src/server/structuredErrors.ts
@@ -10916,8 +11537,8 @@ var EmittedOnceTracker = class {
10916
11537
  };
10917
11538
 
10918
11539
  // packages/core/src/install/installer.ts
10919
- import fs28 from "fs";
10920
- import path36 from "path";
11540
+ import fs29 from "fs";
11541
+ import path37 from "path";
10921
11542
 
10922
11543
  // packages/core/src/install/templates.ts
10923
11544
  var RULES_BLOCK_NAME = "CTXLOOM-RULES";
@@ -10929,6 +11550,27 @@ The graph is faster, cheaper (fewer tokens), and gives you
10929
11550
  structural context (callers, dependents, test coverage) that file
10930
11551
  scanning cannot.
10931
11552
 
11553
+ ### Operating principles
11554
+
11555
+ ctxloom's tools exist to operationalize four principles for working
11556
+ with an AI coding agent. They're the *why* behind every tool below.
11557
+ Adapted from Karpathy's LLM-coding-pitfalls notes
11558
+ (<https://github.com/multica-ai/andrej-karpathy-skills>, MIT).
11559
+
11560
+ 1. **Think before coding** \u2014 read the relevant graph slice before
11561
+ editing. \`ctx_blast_radius\`, \`ctx_get_call_graph\`,
11562
+ \`ctx_get_review_context\` are how you do this without re-reading
11563
+ whole files.
11564
+ 2. **Simplicity first** \u2014 prefer the smallest viable change. Use
11565
+ \`ctx_refactor_preview\` to see the full diff *before* applying;
11566
+ if the preview is sprawling, the plan is too big.
11567
+ 3. **Surgical changes** \u2014 every changed line should trace directly
11568
+ to the user's request. \`ctx_detect_changes\` after each edit
11569
+ confirms scope hasn't drifted.
11570
+ 4. **Goal-driven execution** \u2014 stop when the goal is met. Don't
11571
+ "polish" beyond the request. \`ctx_knowledge_gaps\` flags real
11572
+ risk surfaces; everything else is yak-shaving.
11573
+
10932
11574
  ### Start every workflow with \`ctx_get_minimal_context\`
10933
11575
 
10934
11576
  The first MCP call into ctxloom should always be
@@ -11174,9 +11816,18 @@ description: Orient yourself to an unfamiliar codebase using ctxloom's structura
11174
11816
 
11175
11817
  # Explore Codebase
11176
11818
 
11819
+ **Principle: Think Before Coding.** Exploration is "think" with a
11820
+ budget. The trap is reading every file in sight; the discipline is
11821
+ to read the *graph*, then the few files the graph nominates. This
11822
+ skill caps you at 5 tool calls and 2000 tokens \u2014 enough to ground
11823
+ the next edit, not enough to procrastinate.
11824
+
11177
11825
  Use this when you need to understand a codebase you haven't worked in
11178
11826
  before, or when re-orienting after time away.
11179
11827
 
11828
+ **Skip when:** you already know which files matter \u2014 just open them.
11829
+ Reserve this skill for "I don't know where to start" situations.
11830
+
11180
11831
  ## Steps
11181
11832
 
11182
11833
  1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
@@ -11225,9 +11876,17 @@ argument-hint: "<symbol-name | file-path>"
11225
11876
 
11226
11877
  # Blast Radius
11227
11878
 
11879
+ **Principle: Think Before Coding.** You can't make a surgical change
11880
+ if you don't know who depends on the symbol. This skill is the
11881
+ "think" phase \u2014 read the graph slice before editing anything.
11882
+
11228
11883
  Use this before any change to a public function, type, or file
11229
11884
  where you're not sure who depends on it.
11230
11885
 
11886
+ **Skip when:** the change is purely internal to a private helper
11887
+ with no callers outside the file, OR you've already run this skill
11888
+ for the same symbol in the current task.
11889
+
11231
11890
  ## Inputs
11232
11891
 
11233
11892
  - \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
@@ -11276,9 +11935,18 @@ argument-hint: "<old-name> <new-name>"
11276
11935
 
11277
11936
  # Refactor Safely
11278
11937
 
11938
+ **Principle: Surgical Changes.** A rename touches every call site \u2014
11939
+ done blindly it's the opposite of surgical. This skill enforces
11940
+ "see the full diff before applying" so the change stays scoped to
11941
+ the user's request and doesn't accidentally rewrite tangents.
11942
+
11279
11943
  Use this for renames, signature changes, or function moves. The
11280
11944
  skill enforces preview-before-apply.
11281
11945
 
11946
+ **Skip when:** the symbol is local to a single file (just edit it),
11947
+ OR the user wants a behavior change, not a rename (use the regular
11948
+ edit flow with \`ctx_blast_radius\` for impact awareness).
11949
+
11282
11950
  ## Inputs
11283
11951
 
11284
11952
  - \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
@@ -11331,9 +11999,18 @@ description: Identify code that lacks test coverage, prioritized by caller frequ
11331
11999
 
11332
12000
  # Coverage Gap Analysis
11333
12001
 
12002
+ **Principle: Goal-Driven Execution.** "Add tests everywhere" is
12003
+ yak-shaving. The goal is to add tests where they pay back the
12004
+ investment: high-caller, high-churn, low-coverage code. This skill
12005
+ ranks gaps so you write tests with intent, not by reflex.
12006
+
11334
12007
  Use this to find untested code that genuinely matters \u2014 the
11335
12008
  intersection of "no tests" + "many callers" + "high risk score."
11336
12009
 
12010
+ **Skip when:** the user asked for tests on a specific symbol \u2014
12011
+ write them directly. Reserve this skill for "where should we
12012
+ invest in tests next?" queries.
12013
+
11337
12014
  ## Steps
11338
12015
 
11339
12016
  1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
@@ -11381,10 +12058,20 @@ argument-hint: "<PR number | branch name>"
11381
12058
 
11382
12059
  # Review PR
11383
12060
 
12061
+ **Principle: Think Before Coding.** A code review *is* the "think"
12062
+ phase for the author and the reviewer alike. This skill structures
12063
+ that thinking around graph slices rather than naive file-by-file
12064
+ reading, so you spot blast-radius and coverage risks the diff alone
12065
+ hides.
12066
+
11384
12067
  Comprehensive PR review using ctxloom's graph. Mirrors the
11385
12068
  multi-agent review the ctxloom-bot posts automatically \u2014 useful
11386
12069
  when reviewing manually or when the bot isn't wired up.
11387
12070
 
12071
+ **Skip when:** the PR is one-file, \u226430 lines, with full test
12072
+ coverage \u2014 read the diff directly. Reserve this skill for changes
12073
+ where the structural impact isn't obvious from the diff.
12074
+
11388
12075
  ## Inputs
11389
12076
 
11390
12077
  - \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
@@ -11472,6 +12159,11 @@ description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distrib
11472
12159
 
11473
12160
  # Budget Stats
11474
12161
 
12162
+ **Principle: Simplicity First.** Don't guess what \`DEFAULT_MAX_RESPONSE_TOKENS\`
12163
+ should be \u2014 measure. This skill turns real telemetry into the
12164
+ simplest viable constant: the p75 of actual usage. Tune what hurts;
12165
+ leave the rest alone.
12166
+
11475
12167
  Wrapper around \`ctxloom budget-stats\` for inline use inside a
11476
12168
  Claude Code session. Useful when:
11477
12169
 
@@ -11480,6 +12172,10 @@ Claude Code session. Useful when:
11480
12172
  - Diagnosing why a tool keeps falling back to skeleton mode
11481
12173
  - Understanding which tools dominate the user's token budget
11482
12174
 
12175
+ **Skip when:** there's no budget complaint to investigate \u2014 telemetry
12176
+ exists for tuning, not browsing. Reserve this skill for "this tool
12177
+ keeps hitting the budget" or scheduled tuning passes.
12178
+
11483
12179
  ## Steps
11484
12180
 
11485
12181
  1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
@@ -11542,8 +12238,8 @@ function skillFilePath(name) {
11542
12238
  // packages/core/src/install/installer.ts
11543
12239
  function installHarness(opts = {}) {
11544
12240
  const cwd = opts.cwd ?? process.cwd();
11545
- const projectRoot = path36.resolve(cwd);
11546
- const stat = fs28.statSync(projectRoot);
12241
+ const projectRoot = path37.resolve(cwd);
12242
+ const stat = fs29.statSync(projectRoot);
11547
12243
  if (!stat.isDirectory()) {
11548
12244
  throw new Error(`installHarness: ${projectRoot} is not a directory`);
11549
12245
  }
@@ -11591,20 +12287,20 @@ function resolveExtraHosts(ids, warnings) {
11591
12287
  return HOST_ADAPTERS.filter((a) => requested.has(a.id));
11592
12288
  }
11593
12289
  function safeJoin(root, name) {
11594
- const target = path36.resolve(root, name);
11595
- const rootResolved = path36.resolve(root);
12290
+ const target = path37.resolve(root, name);
12291
+ const rootResolved = path37.resolve(root);
11596
12292
  const caseFold = process.platform === "darwin" || process.platform === "win32";
11597
12293
  const t = caseFold ? target.toLowerCase() : target;
11598
12294
  const r = caseFold ? rootResolved.toLowerCase() : rootResolved;
11599
- if (!t.startsWith(r + path36.sep) && t !== r) {
12295
+ if (!t.startsWith(r + path37.sep) && t !== r) {
11600
12296
  throw new Error(`installHarness: refusing to write outside project root: ${target}`);
11601
12297
  }
11602
12298
  return target;
11603
12299
  }
11604
12300
  function writeRulesBlock(projectRoot, filename, opts) {
11605
12301
  const filePath = safeJoin(projectRoot, filename);
11606
- const existed = fs28.existsSync(filePath);
11607
- const existing = existed ? fs28.readFileSync(filePath, "utf-8") : "";
12302
+ const existed = fs29.existsSync(filePath);
12303
+ const existing = existed ? fs29.readFileSync(filePath, "utf-8") : "";
11608
12304
  if (existed) {
11609
12305
  const block = extractBlock(existing, RULES_BLOCK_NAME);
11610
12306
  if (block) {
@@ -11622,7 +12318,7 @@ function writeRulesBlock(projectRoot, filename, opts) {
11622
12318
  }
11623
12319
  const next = upsertBlock(existing, RULES_BLOCK_NAME, RULES_BLOCK_CONTENT);
11624
12320
  if (!opts.dryRun) {
11625
- fs28.writeFileSync(filePath, next, "utf-8");
12321
+ fs29.writeFileSync(filePath, next, "utf-8");
11626
12322
  }
11627
12323
  return {
11628
12324
  path: filePath,
@@ -11635,15 +12331,15 @@ function writeRulesBlock(projectRoot, filename, opts) {
11635
12331
  function writeHooksJson(projectRoot, opts) {
11636
12332
  const dir = safeJoin(projectRoot, ".claude");
11637
12333
  const filePath = safeJoin(projectRoot, ".claude/hooks.json");
11638
- const existed = fs28.existsSync(filePath);
12334
+ const existed = fs29.existsSync(filePath);
11639
12335
  let current = {};
11640
12336
  if (existed) {
11641
12337
  try {
11642
- const text = fs28.readFileSync(filePath, "utf-8");
12338
+ const text = fs29.readFileSync(filePath, "utf-8");
11643
12339
  current = JSON.parse(text);
11644
12340
  } catch (err) {
11645
12341
  opts.warnings.push(
11646
- `Could not parse existing ${path36.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
12342
+ `Could not parse existing ${path37.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
11647
12343
  );
11648
12344
  current = {};
11649
12345
  }
@@ -11660,12 +12356,12 @@ function writeHooksJson(projectRoot, opts) {
11660
12356
  const nextJson = JSON.stringify(merged, null, 2) + "\n";
11661
12357
  let alreadyCorrect = false;
11662
12358
  if (existed) {
11663
- const currentText = fs28.readFileSync(filePath, "utf-8");
12359
+ const currentText = fs29.readFileSync(filePath, "utf-8");
11664
12360
  if (currentText === nextJson) alreadyCorrect = true;
11665
12361
  }
11666
12362
  if (!opts.dryRun && !alreadyCorrect) {
11667
- fs28.mkdirSync(dir, { recursive: true });
11668
- fs28.writeFileSync(filePath, nextJson, "utf-8");
12363
+ fs29.mkdirSync(dir, { recursive: true });
12364
+ fs29.writeFileSync(filePath, nextJson, "utf-8");
11669
12365
  }
11670
12366
  return {
11671
12367
  path: filePath,
@@ -11687,19 +12383,19 @@ function isCtxloomEntry(entry, expectedMatcher) {
11687
12383
  }
11688
12384
  function writeHostAdapter(projectRoot, adapter, opts) {
11689
12385
  const filePath = safeJoin(projectRoot, adapter.path);
11690
- const dir = path36.dirname(filePath);
11691
- const existed = fs28.existsSync(filePath);
12386
+ const dir = path37.dirname(filePath);
12387
+ const existed = fs29.existsSync(filePath);
11692
12388
  const rendered = adapter.render();
11693
12389
  let alreadyCorrect = false;
11694
12390
  if (existed) {
11695
- const current = fs28.readFileSync(filePath, "utf-8");
12391
+ const current = fs29.readFileSync(filePath, "utf-8");
11696
12392
  if (adapter.isCanonical(current)) {
11697
12393
  alreadyCorrect = true;
11698
12394
  }
11699
12395
  }
11700
12396
  if (!opts.dryRun && !alreadyCorrect) {
11701
- fs28.mkdirSync(dir, { recursive: true });
11702
- fs28.writeFileSync(filePath, rendered, "utf-8");
12397
+ fs29.mkdirSync(dir, { recursive: true });
12398
+ fs29.writeFileSync(filePath, rendered, "utf-8");
11703
12399
  }
11704
12400
  return {
11705
12401
  path: filePath,
@@ -11712,16 +12408,16 @@ function writeHostAdapter(projectRoot, adapter, opts) {
11712
12408
  function writeSkill(projectRoot, skill, opts) {
11713
12409
  const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
11714
12410
  const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
11715
- const existed = fs28.existsSync(filePath);
12411
+ const existed = fs29.existsSync(filePath);
11716
12412
  let alreadyCorrect = false;
11717
12413
  if (existed) {
11718
- if (fs28.readFileSync(filePath, "utf-8") === skill.content) {
12414
+ if (fs29.readFileSync(filePath, "utf-8") === skill.content) {
11719
12415
  alreadyCorrect = true;
11720
12416
  }
11721
12417
  }
11722
12418
  if (!opts.dryRun && !alreadyCorrect) {
11723
- fs28.mkdirSync(dir, { recursive: true });
11724
- fs28.writeFileSync(filePath, skill.content, "utf-8");
12419
+ fs29.mkdirSync(dir, { recursive: true });
12420
+ fs29.writeFileSync(filePath, skill.content, "utf-8");
11725
12421
  }
11726
12422
  return {
11727
12423
  path: filePath,
@@ -11734,17 +12430,17 @@ function writeSkill(projectRoot, skill, opts) {
11734
12430
  function writeSessionStartScript(projectRoot, opts) {
11735
12431
  const dir = safeJoin(projectRoot, ".claude/hooks");
11736
12432
  const filePath = safeJoin(projectRoot, ".claude/hooks/session-start.sh");
11737
- const existed = fs28.existsSync(filePath);
12433
+ const existed = fs29.existsSync(filePath);
11738
12434
  let alreadyCorrect = false;
11739
12435
  if (existed) {
11740
- const current = fs28.readFileSync(filePath, "utf-8");
12436
+ const current = fs29.readFileSync(filePath, "utf-8");
11741
12437
  if (current === SESSION_START_FULL) alreadyCorrect = true;
11742
12438
  }
11743
12439
  if (!opts.dryRun && !alreadyCorrect) {
11744
- fs28.mkdirSync(dir, { recursive: true });
11745
- fs28.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
12440
+ fs29.mkdirSync(dir, { recursive: true });
12441
+ fs29.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
11746
12442
  try {
11747
- fs28.chmodSync(filePath, 493);
12443
+ fs29.chmodSync(filePath, 493);
11748
12444
  } catch {
11749
12445
  }
11750
12446
  }
@@ -11793,6 +12489,8 @@ export {
11793
12489
  loadTrendSeries,
11794
12490
  loadFileRiskHistory,
11795
12491
  Skeletonizer,
12492
+ inspectVectorsDb,
12493
+ cleanupVectors,
11796
12494
  renderStatusXml,
11797
12495
  __resetLearnedSuggestionsCacheForTests,
11798
12496
  learnSuggestionsFromTelemetry,
@@ -11805,6 +12503,7 @@ export {
11805
12503
  emitTaskBudgetBreached,
11806
12504
  ToolRegistry,
11807
12505
  detectChanges,
12506
+ computeSemanticSimilar,
11808
12507
  getImpactRadius,
11809
12508
  buildBlastRadiusXml,
11810
12509
  validateAlias,
@@ -11889,4 +12588,4 @@ export {
11889
12588
  skillFilePath,
11890
12589
  installHarness
11891
12590
  };
11892
- //# sourceMappingURL=chunk-5R4P7VEE.js.map
12591
+ //# sourceMappingURL=chunk-FFCLVZCO.js.map