ctxloom-pro 1.5.6 → 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.
@@ -79,6 +79,27 @@ var init_logger = __esm({
79
79
  // ../../packages/core/src/indexer/embedder.ts
80
80
  import fs3 from "fs";
81
81
  import path3 from "path";
82
+ function resolveEmbeddingModel(env = process.env) {
83
+ const envModel = env.CTXLOOM_EMBEDDING_MODEL?.trim();
84
+ if (!envModel) return MODEL_REGISTRY.minilm;
85
+ const registered = MODEL_REGISTRY[envModel];
86
+ if (registered) return registered;
87
+ const envDim = env.CTXLOOM_EMBEDDING_DIM ? Number.parseInt(env.CTXLOOM_EMBEDDING_DIM, 10) : NaN;
88
+ if (!Number.isFinite(envDim) || envDim <= 0) {
89
+ throw new Error(
90
+ `CTXLOOM_EMBEDDING_MODEL=${envModel} is not a known alias. Either use one of [${Object.keys(MODEL_REGISTRY).join(", ")}] or set CTXLOOM_EMBEDDING_DIM=<vector-length> alongside a raw HF id.`
91
+ );
92
+ }
93
+ return {
94
+ hfId: envModel,
95
+ dim: envDim,
96
+ // Without a known artifact size we can't enforce the truncated-download
97
+ // guard. Use 1 MB as the minimum; the worst case is a redundant retry
98
+ // rather than a hung process.
99
+ minBytes: 1024 * 1024,
100
+ description: `User-supplied model: ${envModel} (${envDim}-dim)`
101
+ };
102
+ }
82
103
  function collectFiles(dir, results = []) {
83
104
  const IGNORED_DIRS = /* @__PURE__ */ new Set([
84
105
  // Build artifacts + dependency caches
@@ -144,12 +165,37 @@ function collectFiles(dir, results = []) {
144
165
  }
145
166
  return results;
146
167
  }
147
- var MIN_MODEL_BYTES;
168
+ var MODEL_REGISTRY, ACTIVE_MODEL, EMBEDDING_DIMENSION, MODEL_ID, MIN_MODEL_BYTES;
148
169
  var init_embedder = __esm({
149
170
  "../../packages/core/src/indexer/embedder.ts"() {
150
171
  "use strict";
151
172
  init_logger();
152
- MIN_MODEL_BYTES = 80 * 1024 * 1024;
173
+ MODEL_REGISTRY = {
174
+ // The historical default. General English, 384-dim, ~90 MB.
175
+ // Kept as the free-tier default so existing users see zero change.
176
+ minilm: {
177
+ hfId: "sentence-transformers/all-MiniLM-L6-v2",
178
+ dim: 384,
179
+ minBytes: 80 * 1024 * 1024,
180
+ description: "General-purpose English sentence embedder (2020). 384-dim. The legacy default."
181
+ },
182
+ // Code-specific embedding model (Jina AI). 768-dim, ~140 MB.
183
+ // Empirically 20-40% better recall on code-similarity queries than
184
+ // MiniLM — the upgrade path recommended in the v1.7.0 analysis.
185
+ // Runs through the same @huggingface/transformers pipeline so the
186
+ // privacy story (fully local, no network at inference time) is
187
+ // preserved.
188
+ "jina-code": {
189
+ hfId: "jinaai/jina-embeddings-v2-base-code",
190
+ dim: 768,
191
+ minBytes: 130 * 1024 * 1024,
192
+ description: "Code-specific embedder (Jina, 2024). 768-dim. Better recall on code-similarity tasks."
193
+ }
194
+ };
195
+ ACTIVE_MODEL = resolveEmbeddingModel();
196
+ EMBEDDING_DIMENSION = ACTIVE_MODEL.dim;
197
+ MODEL_ID = ACTIVE_MODEL.hfId;
198
+ MIN_MODEL_BYTES = ACTIVE_MODEL.minBytes;
153
199
  }
154
200
  });
155
201
 
@@ -160,6 +206,7 @@ var init_VectorStore = __esm({
160
206
  "../../packages/core/src/db/VectorStore.ts"() {
161
207
  "use strict";
162
208
  init_logger();
209
+ init_embedder();
163
210
  }
164
211
  });
165
212
 
@@ -657,6 +704,13 @@ var ASTParser = class {
657
704
  extractTSNodes(rootNode, _filePath, lines) {
658
705
  const nodes = [];
659
706
  const processedIds = /* @__PURE__ */ new Set();
707
+ const hasCallableRight = (n) => {
708
+ const right = n.childForFieldName?.("right") ?? n.children[n.children.length - 1];
709
+ if (!right) return false;
710
+ if (right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration") return true;
711
+ if (right.type === "assignment_expression") return hasCallableRight(right);
712
+ return false;
713
+ };
660
714
  const walk = (node) => {
661
715
  if (processedIds.has(node.id)) return;
662
716
  switch (node.type) {
@@ -828,6 +882,56 @@ var ASTParser = class {
828
882
  processedIds.add(node.id);
829
883
  return;
830
884
  }
885
+ // ─── Prototype / object method assignments ──────────────────────
886
+ // CommonJS libraries (and pre-class-syntax ES) attach their public
887
+ // API via assignment expressions:
888
+ //
889
+ // res.send = function send(body) { ... }
890
+ // res.json = function (obj) { ... }
891
+ // res.contentType = res.type = function (type) { ... } // chained
892
+ // exports.foo = function foo() { ... }
893
+ // MyClass.prototype.bar = function () { ... }
894
+ //
895
+ // Without this case, none of those names enter the symbol index,
896
+ // so `lookupSymbolsByFile()` returns empty for libraries like
897
+ // express, and any downstream tool that wants to attribute callers
898
+ // to a file (blast-radius symbolCallers, ctx_get_definition, etc.)
899
+ // falls flat. The call graph itself already records callers of
900
+ // `send`/`json`/etc. — this case bridges the gap so we can match
901
+ // them back to the file that defines them.
902
+ //
903
+ // Heuristic:
904
+ // - left = member_expression → use the FINAL property as symbol
905
+ // - right = function | arrow_function | function_expression
906
+ // - right = assignment_expression → recurse for chained pattern
907
+ // Anything else (constants, identifiers being aliased) is skipped
908
+ // intentionally — those aren't callable API surface.
909
+ case "assignment_expression": {
910
+ const left = node.childForFieldName?.("left") ?? node.children.find(
911
+ (c) => c?.type === "member_expression" || c?.type === "identifier"
912
+ );
913
+ const right = node.childForFieldName?.("right") ?? node.children[node.children.length - 1];
914
+ if (left?.type === "member_expression" && right) {
915
+ const prop = left.childForFieldName?.("property") ?? left.children[left.children.length - 1];
916
+ const propName = prop?.text;
917
+ if (right.type === "assignment_expression") {
918
+ walk(right);
919
+ }
920
+ const rightIsCallable = right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration" || right.type === "assignment_expression" && hasCallableRight(right);
921
+ if (propName && rightIsCallable && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName)) {
922
+ const sig = lines[node.startPosition.row] ?? "";
923
+ nodes.push({
924
+ type: "method",
925
+ name: propName,
926
+ signature: sig.trim().slice(0, 200),
927
+ startLine: node.startPosition.row + 1,
928
+ endLine: node.endPosition.row + 1
929
+ });
930
+ }
931
+ }
932
+ processedIds.add(node.id);
933
+ return;
934
+ }
831
935
  // ─── Lexical declarations (const fn = () => {}) ────────────────
832
936
  case "lexical_declaration": {
833
937
  for (const child of node.children) {
@@ -934,10 +1038,34 @@ var ASTParser = class {
934
1038
  }
935
1039
  case "import_from_statement": {
936
1040
  const moduleNode = node.children.find((c) => c?.type === "dotted_name" || c?.type === "relative_import");
1041
+ const sourceText = moduleNode?.text ?? "";
1042
+ const importedNames = [];
1043
+ let pastImportKeyword = false;
1044
+ for (const child of node.children) {
1045
+ if (!child) continue;
1046
+ if (child.text === "import" && (child.type === "import" || child.children.length === 0)) {
1047
+ pastImportKeyword = true;
1048
+ continue;
1049
+ }
1050
+ if (!pastImportKeyword) continue;
1051
+ if (child.type === "wildcard_import") continue;
1052
+ if (child.type === "dotted_name") {
1053
+ importedNames.push(child.text);
1054
+ } else if (child.type === "aliased_import") {
1055
+ const alias = child.childForFieldName?.("alias");
1056
+ const aliasName = alias?.text;
1057
+ if (aliasName) importedNames.push(aliasName);
1058
+ else {
1059
+ const name = child.childForFieldName?.("name")?.text;
1060
+ if (name) importedNames.push(name);
1061
+ }
1062
+ }
1063
+ }
937
1064
  nodes.push({
938
1065
  type: "import",
939
- name: moduleNode?.text ?? "",
940
- source: moduleNode?.text ?? "",
1066
+ name: sourceText,
1067
+ source: sourceText,
1068
+ importedNames: importedNames.length > 0 ? importedNames : void 0,
941
1069
  startLine: node.startPosition.row + 1,
942
1070
  endLine: node.endPosition.row + 1
943
1071
  });
@@ -1807,6 +1935,78 @@ var ASTParser = class {
1807
1935
  tree.delete();
1808
1936
  }
1809
1937
  }
1938
+ /**
1939
+ * Extract all call edges in a Python (.py / .ipynb) file. Mirrors
1940
+ * `parseAllCallEdges` but uses tree-sitter-python node types.
1941
+ *
1942
+ * Tree-sitter-python relevant shapes:
1943
+ * - call.function = identifier "foo" → callee = "foo"
1944
+ * - call.function = attribute (obj.method) → callee = "method"
1945
+ * - call.function = call (chained) → recurse into inner
1946
+ *
1947
+ * Enclosing-context tracking follows the same pattern: track the
1948
+ * innermost `function_definition`. Methods inside a class are also
1949
+ * function_definitions, so the same handler covers them.
1950
+ */
1951
+ async parseAllPythonCallEdges(filePath) {
1952
+ if (!this.pyLang) await this.loadPython();
1953
+ if (!this.pyLang) return [];
1954
+ const parser = this.getParser(this.pyLang);
1955
+ let source;
1956
+ try {
1957
+ const raw = fs2.readFileSync(filePath, "utf-8");
1958
+ source = filePath.endsWith(".ipynb") ? extractNotebookPythonSource(raw) : raw;
1959
+ } catch {
1960
+ return [];
1961
+ }
1962
+ if (!source.trim()) return [];
1963
+ const tree = parser.parse(source);
1964
+ if (!tree) return [];
1965
+ const results = [];
1966
+ const extractCalleeName = (fn) => {
1967
+ if (fn.type === "identifier") return fn.text;
1968
+ if (fn.type === "attribute") {
1969
+ const right = fn.childForFieldName?.("attribute") ?? fn.children[fn.children.length - 1];
1970
+ return right?.text ?? "";
1971
+ }
1972
+ if (fn.type === "call") {
1973
+ const innerFn = fn.childForFieldName?.("function");
1974
+ return innerFn ? extractCalleeName(innerFn) : "";
1975
+ }
1976
+ return "";
1977
+ };
1978
+ const walk = (node, contextStack) => {
1979
+ let newStack = contextStack;
1980
+ if (node.type === "function_definition") {
1981
+ const nameNode = node.childForFieldName?.("name");
1982
+ if (nameNode?.text) {
1983
+ newStack = [...contextStack, nameNode.text];
1984
+ }
1985
+ }
1986
+ if (node.type === "call") {
1987
+ const fn = node.childForFieldName?.("function") ?? node.children.find((c) => c?.type === "identifier" || c?.type === "attribute" || c?.type === "call");
1988
+ if (fn) {
1989
+ const name = extractCalleeName(fn);
1990
+ if (name && name.length > 0) {
1991
+ results.push({
1992
+ callerSymbol: newStack[newStack.length - 1] ?? "",
1993
+ calleeSymbol: name,
1994
+ line: node.startPosition.row + 1
1995
+ });
1996
+ }
1997
+ }
1998
+ }
1999
+ for (const child of node.children) {
2000
+ if (child) walk(child, newStack);
2001
+ }
2002
+ };
2003
+ try {
2004
+ walk(tree.rootNode, []);
2005
+ return results;
2006
+ } finally {
2007
+ tree.delete();
2008
+ }
2009
+ }
1810
2010
  /**
1811
2011
  * Extract all call edges in a TypeScript/TSX file.
1812
2012
  * Tracks the enclosing function/method context for each call site.
@@ -1893,48 +2093,100 @@ var GoModuleResolver = class {
1893
2093
  }
1894
2094
  /**
1895
2095
  * Resolve a module-path import (e.g. `github.com/myorg/myapp/internal/auth`)
1896
- * to a relative project path (e.g. `internal/auth/auth.go`).
2096
+ * to the FIRST relative project path (e.g. `internal/auth/auth.go`).
1897
2097
  *
1898
2098
  * Returns null for:
1899
2099
  * - Third-party imports (different module prefix)
1900
2100
  * - Relative imports (use resolveRelative() instead)
1901
2101
  * - Imports where no .go files are found
2102
+ *
2103
+ * NOTE: A Go import imports a PACKAGE (a directory of .go files), not a
2104
+ * single file. Use `resolveAll()` to get every file in the package — that
2105
+ * matches Go's compile-unit semantics and is what the graph wants for
2106
+ * accurate reachability. `resolve()` is kept for back-compat callers
2107
+ * that only need a representative file.
1902
2108
  */
1903
2109
  resolve(importPath) {
1904
- if (!this.modulePath) return null;
1905
- if (importPath.startsWith(".")) return null;
1906
- if (!importPath.startsWith(this.modulePath)) return null;
2110
+ const all = this.resolveAll(importPath);
2111
+ return all[0] ?? null;
2112
+ }
2113
+ /**
2114
+ * Resolve a module-path import to ALL non-test .go files in the target
2115
+ * package directory. This matches Go's compile-unit semantics: a single
2116
+ * `import "github.com/foo/bar/pkg"` statement brings the entire `pkg/`
2117
+ * directory into the dependency graph — every exported symbol from
2118
+ * every .go file in that directory is accessible to the caller.
2119
+ *
2120
+ * Pre-fix the resolver returned only ONE file per import, which made
2121
+ * gin's `binding/` package (~20 files) appear to be a single file from
2122
+ * the graph's perspective. The bench's graphReachability on gin
2123
+ * collapsed to 0.32 because PRs that touched 4 files in `binding/`
2124
+ * had only ONE of them in the graph reach of the entry-point file.
2125
+ * Returning ALL package files fixes the structural model.
2126
+ *
2127
+ * Test files (_test.go) are intentionally excluded — they're not part
2128
+ * of a package's public API and aren't imported by callers. The
2129
+ * test↔source link is handled separately by per-directory sibling
2130
+ * edges (see DependencyGraph's Go intra-package pass).
2131
+ */
2132
+ resolveAll(importPath) {
2133
+ if (!this.modulePath) return [];
2134
+ if (importPath.startsWith(".")) return [];
2135
+ if (!importPath.startsWith(this.modulePath)) return [];
1907
2136
  const suffix = importPath.slice(this.modulePath.length);
1908
- if (!suffix) return null;
2137
+ if (!suffix) return [];
1909
2138
  const subPath = suffix.startsWith("/") ? suffix.slice(1) : suffix;
1910
2139
  const absDir = path4.join(this.rootDir, subPath);
1911
- return this.firstGoFileInDir(absDir, subPath);
2140
+ return this.allGoFilesInDir(
2141
+ absDir,
2142
+ subPath,
2143
+ /* includeTests */
2144
+ false
2145
+ );
1912
2146
  }
1913
2147
  /**
1914
2148
  * Resolve a relative import (`./config`, `../pkg`) from a given Go source file.
1915
2149
  * Returns the relative project path to the first .go file found, or null.
2150
+ * Kept for back-compat — most callers should prefer `resolveRelativeAll()`.
1916
2151
  */
1917
2152
  resolveRelative(fromFile, importSpec) {
2153
+ const all = this.resolveRelativeAll(fromFile, importSpec);
2154
+ return all[0] ?? null;
2155
+ }
2156
+ /**
2157
+ * Resolve a relative import to ALL non-test .go files in the target
2158
+ * package directory. See `resolveAll()` for rationale.
2159
+ */
2160
+ resolveRelativeAll(fromFile, importSpec) {
1918
2161
  const fromDir = path4.dirname(fromFile);
1919
2162
  const absTarget = path4.resolve(fromDir, importSpec);
1920
2163
  const subPath = path4.relative(this.rootDir, absTarget);
1921
- return this.firstGoFileInDir(absTarget, subPath);
2164
+ return this.allGoFilesInDir(
2165
+ absTarget,
2166
+ subPath,
2167
+ /* includeTests */
2168
+ false
2169
+ );
1922
2170
  }
1923
- firstGoFileInDir(absDir, relDir) {
1924
- if (!fs4.existsSync(absDir)) return null;
2171
+ /**
2172
+ * Enumerate every .go file in a directory, returning project-relative
2173
+ * paths sorted with non-test files first. Used by both the single-file
2174
+ * and all-files resolvers.
2175
+ */
2176
+ allGoFilesInDir(absDir, relDir, includeTests) {
2177
+ if (!fs4.existsSync(absDir)) return [];
1925
2178
  let entries;
1926
2179
  try {
1927
2180
  entries = fs4.readdirSync(absDir);
1928
2181
  } catch {
1929
- return null;
2182
+ return [];
1930
2183
  }
1931
- const goFiles = entries.filter((f) => f.endsWith(".go")).sort((a, b) => {
2184
+ const goFiles = entries.filter((f) => f.endsWith(".go")).filter((f) => includeTests || !f.endsWith("_test.go")).sort((a, b) => {
1932
2185
  const aTest = a.endsWith("_test.go") ? 1 : 0;
1933
2186
  const bTest = b.endsWith("_test.go") ? 1 : 0;
1934
2187
  return aTest - bTest || a.localeCompare(b);
1935
2188
  });
1936
- if (goFiles.length === 0) return null;
1937
- return path4.join(relDir, goFiles[0]).replace(/\\/g, "/");
2189
+ return goFiles.map((f) => path4.join(relDir, f).replace(/\\/g, "/"));
1938
2190
  }
1939
2191
  };
1940
2192
 
@@ -1976,6 +2228,24 @@ function extractImports(filePath, content) {
1976
2228
  return extractNotebookImports(filePath, content);
1977
2229
  case ".vue":
1978
2230
  return extractVueImports(content);
2231
+ case ".c":
2232
+ case ".cc":
2233
+ case ".cpp":
2234
+ case ".cxx":
2235
+ case ".h":
2236
+ case ".hh":
2237
+ case ".hpp":
2238
+ case ".hxx":
2239
+ return extractCppImports(content);
2240
+ case ".scala":
2241
+ return extractScalaImports(content);
2242
+ case ".lua":
2243
+ return extractLuaImports(content);
2244
+ case ".ex":
2245
+ case ".exs":
2246
+ return extractElixirImports(content);
2247
+ case ".zig":
2248
+ return extractZigImports(content);
1979
2249
  default:
1980
2250
  return [];
1981
2251
  }
@@ -1995,8 +2265,14 @@ function resolveImport(fromAbs, raw, rootDir) {
1995
2265
  if (ext === ".dart") return resolveDartImport(fromAbs, fromDir, raw, rootDir);
1996
2266
  if (ext === ".ipynb") return resolvePythonImport(fromAbs, fromDir, raw, rootDir);
1997
2267
  if (ext === ".vue") return resolveVueImport(fromAbs, fromDir, raw, rootDir);
2268
+ if (CPP_EXTENSIONS.has(ext)) return resolveCppImport(fromDir, raw, rootDir);
2269
+ if (ext === ".scala") return resolveScalaImport(fromDir, raw, rootDir);
2270
+ if (ext === ".lua") return resolveLuaImport(fromDir, raw, rootDir);
2271
+ if (ext === ".ex" || ext === ".exs") return resolveElixirImport(fromDir, raw, rootDir);
2272
+ if (ext === ".zig") return resolveZigImport(fromAbs, fromDir, raw, rootDir);
1998
2273
  return null;
1999
2274
  }
2275
+ var CPP_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"]);
2000
2276
  function extractPythonImports(content) {
2001
2277
  const results = [];
2002
2278
  let m;
@@ -2283,6 +2559,124 @@ function resolveVueImport(fromAbs, fromDir, raw, rootDir) {
2283
2559
  }
2284
2560
  return null;
2285
2561
  }
2562
+ function extractCppImports(content) {
2563
+ const results = [];
2564
+ const localInclude = /^\s*#\s*include\s+"([^"]+)"/gm;
2565
+ let m;
2566
+ while ((m = localInclude.exec(content)) !== null) {
2567
+ results.push({ specifier: m[1], isRelative: true });
2568
+ }
2569
+ return results;
2570
+ }
2571
+ function resolveCppImport(fromDir, raw, rootDir) {
2572
+ const rootResolved = path5.resolve(rootDir);
2573
+ const candidates = [
2574
+ path5.resolve(fromDir, raw.specifier),
2575
+ path5.resolve(rootDir, raw.specifier)
2576
+ ];
2577
+ for (const c of candidates) {
2578
+ if (!c.startsWith(rootResolved + path5.sep) && c !== rootResolved) continue;
2579
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2580
+ }
2581
+ return null;
2582
+ }
2583
+ function extractScalaImports(content) {
2584
+ const results = [];
2585
+ const importRe = /^\s*import\s+((?:\w+\.)*\w+)(?:\.\{[^}]+\})?/gm;
2586
+ let m;
2587
+ while ((m = importRe.exec(content)) !== null) {
2588
+ results.push({ specifier: m[1], isRelative: false });
2589
+ }
2590
+ return results;
2591
+ }
2592
+ function resolveScalaImport(fromDir, raw, rootDir) {
2593
+ const asPath = raw.specifier.replace(/\./g, path5.sep);
2594
+ const candidates = [
2595
+ path5.join(rootDir, "src", "main", "scala", asPath + ".scala"),
2596
+ path5.join(rootDir, asPath + ".scala")
2597
+ ];
2598
+ for (const c of candidates) {
2599
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2600
+ }
2601
+ const className = raw.specifier.split(".").pop() ?? raw.specifier;
2602
+ const local = path5.join(fromDir, className + ".scala");
2603
+ if (fs5.existsSync(local)) return path5.relative(rootDir, local);
2604
+ return null;
2605
+ }
2606
+ function extractLuaImports(content) {
2607
+ const results = [];
2608
+ const requireRe = /\brequire\s*\(?\s*['"]([^'"]+)['"]/g;
2609
+ let m;
2610
+ while ((m = requireRe.exec(content)) !== null) {
2611
+ results.push({ specifier: m[1], isRelative: false });
2612
+ }
2613
+ return results;
2614
+ }
2615
+ function resolveLuaImport(fromDir, raw, rootDir) {
2616
+ const asPath = raw.specifier.replace(/\./g, path5.sep);
2617
+ const candidates = [
2618
+ path5.join(fromDir, asPath + ".lua"),
2619
+ path5.join(rootDir, asPath + ".lua"),
2620
+ // Lua's package convention also supports init.lua as a directory
2621
+ // entry point — analogous to Python's __init__.py.
2622
+ path5.join(rootDir, asPath, "init.lua")
2623
+ ];
2624
+ for (const c of candidates) {
2625
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2626
+ }
2627
+ return null;
2628
+ }
2629
+ function extractElixirImports(content) {
2630
+ const results = [];
2631
+ const re = /^\s*(?:alias|import|use|require)\s+([A-Z][\w.]*)/gm;
2632
+ let m;
2633
+ while ((m = re.exec(content)) !== null) {
2634
+ results.push({ specifier: m[1], isRelative: false });
2635
+ }
2636
+ return results;
2637
+ }
2638
+ function resolveElixirImport(fromDir, raw, rootDir) {
2639
+ const segments = raw.specifier.split(".").map(
2640
+ (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()
2641
+ );
2642
+ const asPath = segments.join(path5.sep);
2643
+ const candidates = [
2644
+ path5.join(rootDir, "lib", asPath + ".ex"),
2645
+ path5.join(rootDir, "lib", asPath + ".exs"),
2646
+ path5.join(rootDir, asPath + ".ex"),
2647
+ path5.join(rootDir, asPath + ".exs")
2648
+ ];
2649
+ for (const c of candidates) {
2650
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2651
+ }
2652
+ const tail = segments[segments.length - 1];
2653
+ const local = path5.join(fromDir, tail + ".ex");
2654
+ if (fs5.existsSync(local)) return path5.relative(rootDir, local);
2655
+ return null;
2656
+ }
2657
+ function extractZigImports(content) {
2658
+ const results = [];
2659
+ const importRe = /@import\s*\(\s*"([^"]+)"\s*\)/g;
2660
+ let m;
2661
+ while ((m = importRe.exec(content)) !== null) {
2662
+ const spec = m[1];
2663
+ if (spec.endsWith(".zig")) {
2664
+ const isRelative = spec.startsWith(".") || !spec.includes("/");
2665
+ results.push({ specifier: spec, isRelative });
2666
+ }
2667
+ }
2668
+ return results;
2669
+ }
2670
+ function resolveZigImport(fromAbs, fromDir, raw, rootDir) {
2671
+ void fromAbs;
2672
+ const rootResolved = path5.resolve(rootDir);
2673
+ const candidate = path5.resolve(fromDir, raw.specifier);
2674
+ if (!candidate.startsWith(rootResolved + path5.sep) && candidate !== rootResolved) {
2675
+ return null;
2676
+ }
2677
+ if (fs5.existsSync(candidate)) return path5.relative(rootDir, candidate);
2678
+ return null;
2679
+ }
2286
2680
 
2287
2681
  // ../../packages/core/src/utils/TsConfigPathsResolver.ts
2288
2682
  import fs6 from "fs";
@@ -2513,6 +2907,7 @@ var CallGraphIndex = class _CallGraphIndex {
2513
2907
 
2514
2908
  // ../../packages/core/src/graph/DependencyGraph.ts
2515
2909
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
2910
+ var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
2516
2911
  var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
2517
2912
  var DependencyGraph = class {
2518
2913
  /** file → set of files it imports (forward edges) */
@@ -2522,6 +2917,31 @@ var DependencyGraph = class {
2522
2917
  /** Symbol index: symbolName → { filePath, type, signature, startLine?, endLine? } */
2523
2918
  symbolIndex = /* @__PURE__ */ new Map();
2524
2919
  callGraphIndex = new CallGraphIndex();
2920
+ /**
2921
+ * Re-export tracing (v1.6.x):
2922
+ * reExportMap[barrelFile][symbol] = sourceFile
2923
+ *
2924
+ * Built during graph construction from `from .submodule import Name`
2925
+ * statements (Python). When file C imports `from <barrel> import Name`,
2926
+ * the import resolver consults this map and emits a parallel edge
2927
+ * C → sourceFile so blast-radius queries against sourceFile can find
2928
+ * C as a transitive consumer through the barrel.
2929
+ *
2930
+ * Concrete fastapi case:
2931
+ * tests/test_routing.py has `from fastapi import APIRouter`
2932
+ * fastapi/__init__.py has `from .routing import APIRouter`
2933
+ * → reExportMap['fastapi/__init__.py']['APIRouter'] = 'fastapi/routing.py'
2934
+ * → emit edge tests/test_routing.py → fastapi/routing.py
2935
+ */
2936
+ reExportMap = /* @__PURE__ */ new Map();
2937
+ /**
2938
+ * Pending re-export queries. Populated during pass 1 (parse all
2939
+ * files, extract their imports); resolved in pass 2 once the
2940
+ * reExportMap is fully built. Required because file ordering would
2941
+ * otherwise miss re-exports whose source files are parsed AFTER the
2942
+ * consumer file.
2943
+ */
2944
+ pendingReExportQueries = [];
2525
2945
  parser = null;
2526
2946
  rootDir = "";
2527
2947
  snapshotDir = "";
@@ -2576,8 +2996,10 @@ var DependencyGraph = class {
2576
2996
  for (const imp of importNodes) {
2577
2997
  const spec = imp.source ?? imp.name;
2578
2998
  const isRelative = spec.startsWith(".");
2579
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2580
- if (resolved) this.addEdge(relPath, resolved);
2999
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
3000
+ for (const resolved of resolvedAll) {
3001
+ this.addEdge(relPath, resolved);
3002
+ }
2581
3003
  }
2582
3004
  } else {
2583
3005
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2586,7 +3008,26 @@ var DependencyGraph = class {
2586
3008
  const specifier = imp.source ?? imp.name;
2587
3009
  const isRelative = specifier.startsWith(".");
2588
3010
  const resolved = resolveImport(absPath, { specifier, isRelative }, rootDir);
2589
- if (resolved) this.addEdge(relPath, resolved);
3011
+ if (resolved) {
3012
+ this.addEdge(relPath, resolved);
3013
+ if (ext === ".py" && imp.importedNames && imp.importedNames.length > 0) {
3014
+ if (isRelative) {
3015
+ let map2 = this.reExportMap.get(relPath);
3016
+ if (!map2) {
3017
+ map2 = /* @__PURE__ */ new Map();
3018
+ this.reExportMap.set(relPath, map2);
3019
+ }
3020
+ for (const name of imp.importedNames) {
3021
+ map2.set(name, resolved);
3022
+ }
3023
+ }
3024
+ this.pendingReExportQueries.push({
3025
+ caller: relPath,
3026
+ barrel: resolved,
3027
+ symbols: imp.importedNames
3028
+ });
3029
+ }
3030
+ }
2590
3031
  }
2591
3032
  } else {
2592
3033
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -2598,7 +3039,7 @@ var DependencyGraph = class {
2598
3039
  }
2599
3040
  }
2600
3041
  for (const node of nodes) {
2601
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
3042
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2602
3043
  const existing = this.symbolIndex.get(node.name) ?? [];
2603
3044
  existing.push({
2604
3045
  filePath: relPath,
@@ -2615,6 +3056,11 @@ var DependencyGraph = class {
2615
3056
  for (const edge of callEdges) {
2616
3057
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2617
3058
  }
3059
+ } else if (PY_EXTENSIONS.has(ext)) {
3060
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
3061
+ for (const edge of callEdges) {
3062
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
3063
+ }
2618
3064
  }
2619
3065
  } else {
2620
3066
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -2628,6 +3074,49 @@ var DependencyGraph = class {
2628
3074
  logger.error("Failed to parse", { file: relPath, detail: err instanceof Error ? err.message : String(err) });
2629
3075
  }
2630
3076
  }
3077
+ let reExportEdgesAdded = 0;
3078
+ for (const { caller, barrel, symbols } of this.pendingReExportQueries) {
3079
+ const map2 = this.reExportMap.get(barrel);
3080
+ if (!map2) continue;
3081
+ for (const sym of symbols) {
3082
+ const source = map2.get(sym);
3083
+ if (source && source !== caller && source !== barrel) {
3084
+ this.addEdge(caller, source);
3085
+ reExportEdgesAdded += 1;
3086
+ }
3087
+ }
3088
+ }
3089
+ this.pendingReExportQueries = [];
3090
+ if (reExportEdgesAdded > 0) {
3091
+ logger.info("Re-export tracing added parallel edges", { count: reExportEdgesAdded });
3092
+ }
3093
+ let goTestEdgesAdded = 0;
3094
+ for (const relPath of this.forwardEdges.keys()) {
3095
+ if (!relPath.endsWith("_test.go")) continue;
3096
+ const dir = path7.dirname(relPath);
3097
+ const base = path7.basename(relPath, "_test.go");
3098
+ const namesake = path7.join(dir, base + ".go").replace(/\\/g, "/");
3099
+ if (this.forwardEdges.has(namesake)) {
3100
+ this.addEdge(relPath, namesake);
3101
+ this.addEdge(namesake, relPath);
3102
+ goTestEdgesAdded += 2;
3103
+ continue;
3104
+ }
3105
+ const absDir = path7.join(this.rootDir, dir);
3106
+ try {
3107
+ const siblings = fs7.readdirSync(absDir).filter((f) => f.endsWith(".go") && !f.endsWith("_test.go"));
3108
+ for (const sib of siblings) {
3109
+ const sibRel = path7.join(dir, sib).replace(/\\/g, "/");
3110
+ this.addEdge(relPath, sibRel);
3111
+ this.addEdge(sibRel, relPath);
3112
+ goTestEdgesAdded += 2;
3113
+ }
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ if (goTestEdgesAdded > 0) {
3118
+ logger.info("Go test\u2194source linkage added edges", { count: goTestEdgesAdded });
3119
+ }
2631
3120
  await this.saveSnapshot();
2632
3121
  logger.info("Graph built", { files: files.length, edges: this.edgeCount() });
2633
3122
  if (options?.afterReady) {
@@ -2820,8 +3309,10 @@ var DependencyGraph = class {
2820
3309
  for (const imp of importNodes) {
2821
3310
  const spec = imp.source ?? imp.name;
2822
3311
  const isRelative = spec.startsWith(".");
2823
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2824
- if (resolved) this.addEdge(relPath, resolved);
3312
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
3313
+ for (const resolved of resolvedAll) {
3314
+ this.addEdge(relPath, resolved);
3315
+ }
2825
3316
  }
2826
3317
  } else {
2827
3318
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2842,7 +3333,7 @@ var DependencyGraph = class {
2842
3333
  }
2843
3334
  }
2844
3335
  for (const node of nodes) {
2845
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
3336
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2846
3337
  const existing = this.symbolIndex.get(node.name) ?? [];
2847
3338
  existing.push({
2848
3339
  filePath: relPath,
@@ -2859,6 +3350,11 @@ var DependencyGraph = class {
2859
3350
  for (const edge of callEdges) {
2860
3351
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2861
3352
  }
3353
+ } else if (PY_EXTENSIONS.has(ext)) {
3354
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
3355
+ for (const edge of callEdges) {
3356
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
3357
+ }
2862
3358
  }
2863
3359
  } else {
2864
3360
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -11511,6 +12007,7 @@ var MAX_FILE_SIZE = 5 * 1024 * 1024;
11511
12007
  init_logger();
11512
12008
 
11513
12009
  // ../../packages/core/src/watcher/FileWatcher.ts
12010
+ init_embedder();
11514
12011
  init_logger();
11515
12012
 
11516
12013
  // ../../packages/core/src/license/LicenseStore.ts
@@ -11597,10 +12094,10 @@ function resolveTelemetryLevel() {
11597
12094
  }
11598
12095
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11599
12096
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11600
- var CTXLOOM_VERSION = "1.5.6".length > 0 ? "1.5.6" : "dev";
12097
+ var CTXLOOM_VERSION = "1.7.0".length > 0 ? "1.7.0" : "dev";
11601
12098
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11602
12099
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11603
- var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528\u2028" : "");
12100
+ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
11604
12101
  var cachedDistinctId = null;
11605
12102
  function resolveDistinctId() {
11606
12103
  if (cachedDistinctId) return cachedDistinctId;
@@ -11765,6 +12262,27 @@ The graph is faster, cheaper (fewer tokens), and gives you
11765
12262
  structural context (callers, dependents, test coverage) that file
11766
12263
  scanning cannot.
11767
12264
 
12265
+ ### Operating principles
12266
+
12267
+ ctxloom's tools exist to operationalize four principles for working
12268
+ with an AI coding agent. They're the *why* behind every tool below.
12269
+ Adapted from Karpathy's LLM-coding-pitfalls notes
12270
+ (<https://github.com/multica-ai/andrej-karpathy-skills>, MIT).
12271
+
12272
+ 1. **Think before coding** \u2014 read the relevant graph slice before
12273
+ editing. \`ctx_blast_radius\`, \`ctx_get_call_graph\`,
12274
+ \`ctx_get_review_context\` are how you do this without re-reading
12275
+ whole files.
12276
+ 2. **Simplicity first** \u2014 prefer the smallest viable change. Use
12277
+ \`ctx_refactor_preview\` to see the full diff *before* applying;
12278
+ if the preview is sprawling, the plan is too big.
12279
+ 3. **Surgical changes** \u2014 every changed line should trace directly
12280
+ to the user's request. \`ctx_detect_changes\` after each edit
12281
+ confirms scope hasn't drifted.
12282
+ 4. **Goal-driven execution** \u2014 stop when the goal is met. Don't
12283
+ "polish" beyond the request. \`ctx_knowledge_gaps\` flags real
12284
+ risk surfaces; everything else is yak-shaving.
12285
+
11768
12286
  ### Start every workflow with \`ctx_get_minimal_context\`
11769
12287
 
11770
12288
  The first MCP call into ctxloom should always be