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.
- package/README.md +25 -3
- package/apps/dashboard/dist/server/index.js +544 -26
- package/apps/dashboard/package.json +2 -2
- package/dist/VectorStore-WDL3H7QT.js +9 -0
- package/dist/chunk-6FGTNOCP.js +397 -0
- package/dist/{chunk-JULFFD7O.js → chunk-7S2ELKNU.js} +123 -3
- package/dist/{chunk-FPMNXF4D.js → chunk-FFCLVZCO.js} +685 -43
- package/dist/{chunk-II2DPYRJ.js → chunk-YHLMQVBV.js} +200 -10
- package/dist/embedder-2JWDJUE2.js +26 -0
- package/dist/index.js +11 -11
- package/dist/setup/postinstall.js +1 -1
- package/dist/{src-DL44T55H.js → src-QAYZWPSL.js} +6 -4
- package/dist/workers/indexerWorker.js +2 -2
- package/package.json +1 -1
- package/dist/VectorStore-2LVECRTY.js +0 -8
- package/dist/chunk-WDX4PJGL.js +0 -214
- package/dist/embedder-3AE4CSR7.js +0 -14
|
@@ -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
|
-
|
|
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:
|
|
940
|
-
source:
|
|
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
|
|
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
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
|
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.
|
|
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.
|
|
2164
|
+
return this.allGoFilesInDir(
|
|
2165
|
+
absTarget,
|
|
2166
|
+
subPath,
|
|
2167
|
+
/* includeTests */
|
|
2168
|
+
false
|
|
2169
|
+
);
|
|
1922
2170
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2580
|
-
|
|
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)
|
|
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
|
|
2824
|
-
|
|
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.
|
|
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
|
|
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
|