ctxloom-pro 1.5.6 → 1.7.1

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-JULFFD7O.js";
3
+ } from "./chunk-7S2ELKNU.js";
4
4
  import {
5
+ INDEXER_IGNORED_DIRS,
5
6
  collectFiles,
6
7
  generateEmbedding
7
- } from "./chunk-WDX4PJGL.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");
@@ -6133,7 +6583,7 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
6133
6583
  const now = Math.floor(Date.now() / 1e3);
6134
6584
  const coupling = [];
6135
6585
  for (const seedFile of changedFiles) {
6136
- 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 });
6137
6587
  for (const hit of coupled) {
6138
6588
  const sibling = hit.nodeA === seedFile ? hit.nodeB : hit.nodeA;
6139
6589
  if (!staticSet.has(sibling) && !coupling.some((h) => h.node === sibling)) {
@@ -6150,25 +6600,143 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
6150
6600
  coupling.splice(10);
6151
6601
  return coupling;
6152
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
+ }
6153
6705
  function getImpactRadius(input) {
6154
- 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;
6155
6714
  const { directImporters, allReachable } = traverseImporters(changedFiles, graph, depth);
6156
6715
  const transitiveImporters = [];
6157
6716
  for (const file of allReachable) {
6158
6717
  if (!directImporters.has(file)) transitiveImporters.push(file);
6159
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);
6160
6723
  const staticSet = /* @__PURE__ */ new Set([
6161
6724
  ...changedFiles,
6162
6725
  ...directImporters,
6163
- ...transitiveImporters
6726
+ ...transitiveImporters,
6727
+ ...directImportees,
6728
+ ...symbolCallers
6164
6729
  ]);
6165
6730
  const historicalCoupling = overlay !== void 0 ? buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) : [];
6166
- const totalImpacted = directImporters.size + transitiveImporters.length;
6731
+ const totalImpacted = directImporters.size + transitiveImporters.length + directImportees.length + symbolCallers.length;
6167
6732
  return {
6168
6733
  seedFiles: [...changedFiles],
6169
6734
  directImporters: Array.from(directImporters),
6170
6735
  transitiveImporters,
6736
+ directImportees,
6737
+ symbolCallers,
6171
6738
  historicalCoupling,
6739
+ semanticSimilar: [],
6172
6740
  totalImpacted
6173
6741
  };
6174
6742
  }
@@ -8072,7 +8640,7 @@ function registerFullTextSearchTool(registry, ctx) {
8072
8640
  };
8073
8641
  if (mode === "semantic") {
8074
8642
  try {
8075
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-3AE4CSR7.js");
8643
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8076
8644
  const store = await ctx.getStore(project_root);
8077
8645
  const embedding = await generateEmbedding2(query);
8078
8646
  const results = await store.search(embedding, limit);
@@ -8109,7 +8677,7 @@ function registerFullTextSearchTool(registry, ctx) {
8109
8677
  let merged = keywordResults.slice(0, limit);
8110
8678
  if (mode === "hybrid") {
8111
8679
  try {
8112
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-3AE4CSR7.js");
8680
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8113
8681
  const store = await ctx.getStore(project_root);
8114
8682
  const embedding = await generateEmbedding2(query);
8115
8683
  const vectorResults = await store.search(embedding, Math.ceil(limit / 2));
@@ -9981,16 +10549,14 @@ var PathValidator = class {
9981
10549
 
9982
10550
  // packages/core/src/watcher/FileWatcher.ts
9983
10551
  import chokidar from "chokidar";
9984
- var IGNORED = [
9985
- "**/node_modules/**",
9986
- "**/.git/**",
9987
- "**/dist/**",
9988
- "**/build/**",
9989
- "**/.ctxloom/**",
9990
- "**/coverage/**",
9991
- "**/.next/**",
9992
- "**/.cache/**"
9993
- ];
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;
9994
10560
  var FileWatcher = class {
9995
10561
  root;
9996
10562
  onChange;
@@ -10346,10 +10912,10 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
10346
10912
  function getTelemetryLevel() {
10347
10913
  return TELEMETRY_LEVEL;
10348
10914
  }
10349
- var CTXLOOM_VERSION = "1.5.6".length > 0 ? "1.5.6" : "dev";
10915
+ var CTXLOOM_VERSION = "1.7.1".length > 0 ? "1.7.1" : "dev";
10350
10916
  var POSTHOG_HOST = "https://eu.i.posthog.com";
10351
10917
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
10352
- 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" : "");
10353
10919
  var cachedDistinctId = null;
10354
10920
  function resolveDistinctId() {
10355
10921
  if (cachedDistinctId) return cachedDistinctId;
@@ -10984,6 +11550,27 @@ The graph is faster, cheaper (fewer tokens), and gives you
10984
11550
  structural context (callers, dependents, test coverage) that file
10985
11551
  scanning cannot.
10986
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
+
10987
11574
  ### Start every workflow with \`ctx_get_minimal_context\`
10988
11575
 
10989
11576
  The first MCP call into ctxloom should always be
@@ -11229,9 +11816,18 @@ description: Orient yourself to an unfamiliar codebase using ctxloom's structura
11229
11816
 
11230
11817
  # Explore Codebase
11231
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
+
11232
11825
  Use this when you need to understand a codebase you haven't worked in
11233
11826
  before, or when re-orienting after time away.
11234
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
+
11235
11831
  ## Steps
11236
11832
 
11237
11833
  1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
@@ -11280,9 +11876,17 @@ argument-hint: "<symbol-name | file-path>"
11280
11876
 
11281
11877
  # Blast Radius
11282
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
+
11283
11883
  Use this before any change to a public function, type, or file
11284
11884
  where you're not sure who depends on it.
11285
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
+
11286
11890
  ## Inputs
11287
11891
 
11288
11892
  - \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
@@ -11331,9 +11935,18 @@ argument-hint: "<old-name> <new-name>"
11331
11935
 
11332
11936
  # Refactor Safely
11333
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
+
11334
11943
  Use this for renames, signature changes, or function moves. The
11335
11944
  skill enforces preview-before-apply.
11336
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
+
11337
11950
  ## Inputs
11338
11951
 
11339
11952
  - \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
@@ -11386,9 +11999,18 @@ description: Identify code that lacks test coverage, prioritized by caller frequ
11386
11999
 
11387
12000
  # Coverage Gap Analysis
11388
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
+
11389
12007
  Use this to find untested code that genuinely matters \u2014 the
11390
12008
  intersection of "no tests" + "many callers" + "high risk score."
11391
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
+
11392
12014
  ## Steps
11393
12015
 
11394
12016
  1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
@@ -11436,10 +12058,20 @@ argument-hint: "<PR number | branch name>"
11436
12058
 
11437
12059
  # Review PR
11438
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
+
11439
12067
  Comprehensive PR review using ctxloom's graph. Mirrors the
11440
12068
  multi-agent review the ctxloom-bot posts automatically \u2014 useful
11441
12069
  when reviewing manually or when the bot isn't wired up.
11442
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
+
11443
12075
  ## Inputs
11444
12076
 
11445
12077
  - \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
@@ -11527,6 +12159,11 @@ description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distrib
11527
12159
 
11528
12160
  # Budget Stats
11529
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
+
11530
12167
  Wrapper around \`ctxloom budget-stats\` for inline use inside a
11531
12168
  Claude Code session. Useful when:
11532
12169
 
@@ -11535,6 +12172,10 @@ Claude Code session. Useful when:
11535
12172
  - Diagnosing why a tool keeps falling back to skeleton mode
11536
12173
  - Understanding which tools dominate the user's token budget
11537
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
+
11538
12179
  ## Steps
11539
12180
 
11540
12181
  1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
@@ -11862,6 +12503,7 @@ export {
11862
12503
  emitTaskBudgetBreached,
11863
12504
  ToolRegistry,
11864
12505
  detectChanges,
12506
+ computeSemanticSimilar,
11865
12507
  getImpactRadius,
11866
12508
  buildBlastRadiusXml,
11867
12509
  validateAlias,
@@ -11946,4 +12588,4 @@ export {
11946
12588
  skillFilePath,
11947
12589
  installHarness
11948
12590
  };
11949
- //# sourceMappingURL=chunk-FPMNXF4D.js.map
12591
+ //# sourceMappingURL=chunk-6W4DFPP2.js.map