ctxloom-pro 1.5.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/apps/dashboard/dist/server/index.js +653 -130
- package/apps/dashboard/package.json +2 -2
- package/dist/VectorStore-WDL3H7QT.js +9 -0
- package/dist/chunk-6FGTNOCP.js +397 -0
- package/dist/{chunk-R56D54Y7.js → chunk-7S2ELKNU.js} +132 -4
- package/dist/{chunk-5R4P7VEE.js → chunk-FFCLVZCO.js} +913 -214
- package/dist/{chunk-II2DPYRJ.js → chunk-YHLMQVBV.js} +200 -10
- package/dist/embedder-2JWDJUE2.js +26 -0
- package/dist/index.js +139 -22
- package/dist/setup/postinstall.js +1 -1
- package/dist/{src-QMDQDATD.js → src-QAYZWPSL.js} +10 -4
- package/dist/workers/indexerWorker.js +2 -2
- package/package.json +1 -1
- package/README.md.bak +0 -832
- package/dist/VectorStore-4VWT2ZMW.js +0 -8
- package/dist/chunk-COH5WYZS.js +0 -214
- package/dist/embedder-7YOG4DFN.js +0 -14
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VectorStore
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7S2ELKNU.js";
|
|
4
4
|
import {
|
|
5
|
+
INDEXER_IGNORED_DIRS,
|
|
5
6
|
collectFiles,
|
|
6
7
|
generateEmbedding
|
|
7
|
-
} from "./chunk-
|
|
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:
|
|
786
|
-
source:
|
|
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
|
|
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
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1960
|
+
return this.allGoFilesInDir(
|
|
1961
|
+
absTarget,
|
|
1962
|
+
subPath,
|
|
1963
|
+
/* includeTests */
|
|
1964
|
+
false
|
|
1965
|
+
);
|
|
1764
1966
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2422
|
-
|
|
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)
|
|
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
|
|
2666
|
-
|
|
3108
|
+
const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
|
|
3109
|
+
for (const resolved of resolvedAll) {
|
|
3110
|
+
this.addEdge(relPath, resolved);
|
|
3111
|
+
}
|
|
2667
3112
|
}
|
|
2668
3113
|
} else {
|
|
2669
3114
|
const importNodes = nodes.filter((n) => n.type === "import");
|
|
@@ -2684,7 +3129,7 @@ var DependencyGraph = class {
|
|
|
2684
3129
|
}
|
|
2685
3130
|
}
|
|
2686
3131
|
for (const node of nodes) {
|
|
2687
|
-
if (node.type === "function" || node.type === "class" || node.type === "interface") {
|
|
3132
|
+
if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
|
|
2688
3133
|
const existing = this.symbolIndex.get(node.name) ?? [];
|
|
2689
3134
|
existing.push({
|
|
2690
3135
|
filePath: relPath,
|
|
@@ -2701,6 +3146,11 @@ var DependencyGraph = class {
|
|
|
2701
3146
|
for (const edge of callEdges) {
|
|
2702
3147
|
this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
|
|
2703
3148
|
}
|
|
3149
|
+
} else if (PY_EXTENSIONS.has(ext)) {
|
|
3150
|
+
const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
|
|
3151
|
+
for (const edge of callEdges) {
|
|
3152
|
+
this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
|
|
3153
|
+
}
|
|
2704
3154
|
}
|
|
2705
3155
|
} else {
|
|
2706
3156
|
const content = fs6.readFileSync(absPath, "utf-8");
|
|
@@ -2757,12 +3207,12 @@ var DependencyGraph = class {
|
|
|
2757
3207
|
symbolIndex: Object.fromEntries(this.symbolIndex.entries())
|
|
2758
3208
|
};
|
|
2759
3209
|
const snapshotPath = this.getSnapshotPath();
|
|
2760
|
-
const tmpPath = snapshotPath
|
|
3210
|
+
const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
2761
3211
|
fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
2762
3212
|
fs6.renameSync(tmpPath, snapshotPath);
|
|
2763
3213
|
const callData = this.callGraphIndex.toJSON();
|
|
2764
3214
|
const callPath = path6.join(this.snapshotDir, "call-graph-snapshot.json");
|
|
2765
|
-
const callTmp = callPath
|
|
3215
|
+
const callTmp = `${callPath}.${process.pid}.tmp`;
|
|
2766
3216
|
fs6.writeFileSync(callTmp, JSON.stringify(callData));
|
|
2767
3217
|
fs6.renameSync(callTmp, callPath);
|
|
2768
3218
|
}
|
|
@@ -3547,8 +3997,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3547
3997
|
if (event.isBulk || event.isMerge) return;
|
|
3548
3998
|
const paths = event.files.map((f) => f.path);
|
|
3549
3999
|
if (paths.length === 0) return;
|
|
3550
|
-
for (const
|
|
3551
|
-
this.nodeCounts.set(
|
|
4000
|
+
for (const path38 of paths) {
|
|
4001
|
+
this.nodeCounts.set(path38, (this.nodeCounts.get(path38) ?? 0) + 1);
|
|
3552
4002
|
}
|
|
3553
4003
|
for (let i = 0; i < paths.length; i++) {
|
|
3554
4004
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3695,8 +4145,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3695
4145
|
*/
|
|
3696
4146
|
snapshot() {
|
|
3697
4147
|
const nodes = {};
|
|
3698
|
-
for (const [
|
|
3699
|
-
nodes[
|
|
4148
|
+
for (const [path38, raw] of this.nodes) {
|
|
4149
|
+
nodes[path38] = {
|
|
3700
4150
|
commits: raw.commits,
|
|
3701
4151
|
churnLines: raw.churnLines,
|
|
3702
4152
|
bugCommits: raw.bugCommits,
|
|
@@ -3711,8 +4161,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3711
4161
|
*/
|
|
3712
4162
|
static load(s) {
|
|
3713
4163
|
const idx = new _ChurnIndex();
|
|
3714
|
-
for (const [
|
|
3715
|
-
idx.nodes.set(
|
|
4164
|
+
for (const [path38, raw] of Object.entries(s.nodes)) {
|
|
4165
|
+
idx.nodes.set(path38, {
|
|
3716
4166
|
commits: raw.commits,
|
|
3717
4167
|
churnLines: raw.churnLines,
|
|
3718
4168
|
bugCommits: raw.bugCommits,
|
|
@@ -3725,8 +4175,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3725
4175
|
// -------------------------------------------------------------------------
|
|
3726
4176
|
// Private helpers
|
|
3727
4177
|
// -------------------------------------------------------------------------
|
|
3728
|
-
getOrCreate(
|
|
3729
|
-
const existing = this.nodes.get(
|
|
4178
|
+
getOrCreate(path38) {
|
|
4179
|
+
const existing = this.nodes.get(path38);
|
|
3730
4180
|
if (existing !== void 0) return existing;
|
|
3731
4181
|
const fresh = {
|
|
3732
4182
|
commits: 0,
|
|
@@ -3735,7 +4185,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3735
4185
|
authorCounts: {},
|
|
3736
4186
|
lastTouch: 0
|
|
3737
4187
|
};
|
|
3738
|
-
this.nodes.set(
|
|
4188
|
+
this.nodes.set(path38, fresh);
|
|
3739
4189
|
return fresh;
|
|
3740
4190
|
}
|
|
3741
4191
|
};
|
|
@@ -3816,12 +4266,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3816
4266
|
*/
|
|
3817
4267
|
snapshot() {
|
|
3818
4268
|
const nodes = {};
|
|
3819
|
-
for (const [
|
|
4269
|
+
for (const [path38, raw] of this.nodes) {
|
|
3820
4270
|
const authorWeights = {};
|
|
3821
4271
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3822
4272
|
authorWeights[email] = { ...entry };
|
|
3823
4273
|
}
|
|
3824
|
-
nodes[
|
|
4274
|
+
nodes[path38] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3825
4275
|
}
|
|
3826
4276
|
return { version: 1, nodes };
|
|
3827
4277
|
}
|
|
@@ -3830,23 +4280,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3830
4280
|
*/
|
|
3831
4281
|
static load(s) {
|
|
3832
4282
|
const idx = new _OwnershipIndex();
|
|
3833
|
-
for (const [
|
|
4283
|
+
for (const [path38, raw] of Object.entries(s.nodes)) {
|
|
3834
4284
|
const authorWeights = {};
|
|
3835
4285
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3836
4286
|
authorWeights[email] = { ...entry };
|
|
3837
4287
|
}
|
|
3838
|
-
idx.nodes.set(
|
|
4288
|
+
idx.nodes.set(path38, { authorWeights, lastTouch: raw.lastTouch });
|
|
3839
4289
|
}
|
|
3840
4290
|
return idx;
|
|
3841
4291
|
}
|
|
3842
4292
|
// -------------------------------------------------------------------------
|
|
3843
4293
|
// Private helpers
|
|
3844
4294
|
// -------------------------------------------------------------------------
|
|
3845
|
-
getOrCreate(
|
|
3846
|
-
const existing = this.nodes.get(
|
|
4295
|
+
getOrCreate(path38) {
|
|
4296
|
+
const existing = this.nodes.get(path38);
|
|
3847
4297
|
if (existing !== void 0) return existing;
|
|
3848
4298
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3849
|
-
this.nodes.set(
|
|
4299
|
+
this.nodes.set(path38, fresh);
|
|
3850
4300
|
return fresh;
|
|
3851
4301
|
}
|
|
3852
4302
|
};
|
|
@@ -4599,6 +5049,61 @@ ${methodLines.join("\n")}
|
|
|
4599
5049
|
}
|
|
4600
5050
|
};
|
|
4601
5051
|
|
|
5052
|
+
// packages/core/src/db/vectorsCleanup.ts
|
|
5053
|
+
import fs14 from "fs";
|
|
5054
|
+
import path14 from "path";
|
|
5055
|
+
var VECTOR_DB_REL = path14.join(".ctxloom", "vectors.lancedb");
|
|
5056
|
+
var TABLE_DIR = "code_embeddings.lance";
|
|
5057
|
+
function inspectVectorsDb(rootDir) {
|
|
5058
|
+
const tablePath = path14.join(rootDir, VECTOR_DB_REL, TABLE_DIR);
|
|
5059
|
+
const counts = {
|
|
5060
|
+
txn: 0,
|
|
5061
|
+
manifest: 0,
|
|
5062
|
+
lance: 0,
|
|
5063
|
+
totalBytes: 0
|
|
5064
|
+
};
|
|
5065
|
+
if (!fs14.existsSync(tablePath)) return counts;
|
|
5066
|
+
for (const sub of ["_transactions", "_versions", "data"]) {
|
|
5067
|
+
const dir = path14.join(tablePath, sub);
|
|
5068
|
+
if (!fs14.existsSync(dir)) continue;
|
|
5069
|
+
for (const name of fs14.readdirSync(dir)) {
|
|
5070
|
+
const full = path14.join(dir, name);
|
|
5071
|
+
try {
|
|
5072
|
+
const st = fs14.statSync(full);
|
|
5073
|
+
if (!st.isFile()) continue;
|
|
5074
|
+
counts.totalBytes += st.size;
|
|
5075
|
+
if (name.endsWith(".txn")) counts.txn += 1;
|
|
5076
|
+
else if (name.endsWith(".manifest")) counts.manifest += 1;
|
|
5077
|
+
else if (name.endsWith(".lance")) counts.lance += 1;
|
|
5078
|
+
} catch {
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
return counts;
|
|
5083
|
+
}
|
|
5084
|
+
function cleanupVectors(options = {}, activePids = []) {
|
|
5085
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
5086
|
+
const dbPath = path14.join(rootDir, VECTOR_DB_REL);
|
|
5087
|
+
if (!fs14.existsSync(dbPath)) {
|
|
5088
|
+
return { cleaned: false, reason: "no-db" };
|
|
5089
|
+
}
|
|
5090
|
+
if (activePids.length > 0) {
|
|
5091
|
+
return {
|
|
5092
|
+
cleaned: false,
|
|
5093
|
+
reason: "in-use",
|
|
5094
|
+
conflictingPids: [...activePids]
|
|
5095
|
+
};
|
|
5096
|
+
}
|
|
5097
|
+
const before = inspectVectorsDb(rootDir);
|
|
5098
|
+
if (options.dryRun) {
|
|
5099
|
+
return { cleaned: true, before };
|
|
5100
|
+
}
|
|
5101
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5102
|
+
const backupPath = `${dbPath}.bak-${stamp}`;
|
|
5103
|
+
fs14.renameSync(dbPath, backupPath);
|
|
5104
|
+
return { cleaned: true, before, backupPath };
|
|
5105
|
+
}
|
|
5106
|
+
|
|
4602
5107
|
// packages/core/src/tools/status.ts
|
|
4603
5108
|
import { z as z2 } from "zod";
|
|
4604
5109
|
|
|
@@ -5624,7 +6129,7 @@ function registerFileTool(registry, ctx) {
|
|
|
5624
6129
|
|
|
5625
6130
|
// packages/core/src/tools/context-packet.ts
|
|
5626
6131
|
import { z as z5 } from "zod";
|
|
5627
|
-
import
|
|
6132
|
+
import path15 from "path";
|
|
5628
6133
|
var DEFAULT_MAX_RESPONSE_TOKENS3 = 6e3;
|
|
5629
6134
|
var Schema3 = z5.object({
|
|
5630
6135
|
target_file: z5.string().describe("Relative path to the primary file"),
|
|
@@ -5693,7 +6198,7 @@ function registerContextPacketTool(registry, ctx) {
|
|
|
5693
6198
|
const skeletons = await Promise.all(
|
|
5694
6199
|
imports.map(async (dep) => {
|
|
5695
6200
|
try {
|
|
5696
|
-
const absDep =
|
|
6201
|
+
const absDep = path15.resolve(ctx.projectRoot, dep);
|
|
5697
6202
|
const sk = await skeletonizer.skeletonize(absDep);
|
|
5698
6203
|
return `
|
|
5699
6204
|
<!-- ${dep} -->
|
|
@@ -5735,7 +6240,7 @@ ${sk}`;
|
|
|
5735
6240
|
import { z as z6 } from "zod";
|
|
5736
6241
|
|
|
5737
6242
|
// packages/core/src/tools/findCallers.ts
|
|
5738
|
-
import
|
|
6243
|
+
import path16 from "path";
|
|
5739
6244
|
function escapeXML4(text) {
|
|
5740
6245
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5741
6246
|
}
|
|
@@ -6078,7 +6583,7 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
|
|
|
6078
6583
|
const now = Math.floor(Date.now() / 1e3);
|
|
6079
6584
|
const coupling = [];
|
|
6080
6585
|
for (const seedFile of changedFiles) {
|
|
6081
|
-
const coupled = overlay.coChange.topFor({ node: seedFile, limit:
|
|
6586
|
+
const coupled = overlay.coChange.topFor({ node: seedFile, limit: 25, minConfidence: 0.1 });
|
|
6082
6587
|
for (const hit of coupled) {
|
|
6083
6588
|
const sibling = hit.nodeA === seedFile ? hit.nodeB : hit.nodeA;
|
|
6084
6589
|
if (!staticSet.has(sibling) && !coupling.some((h) => h.node === sibling)) {
|
|
@@ -6095,25 +6600,143 @@ function buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) {
|
|
|
6095
6600
|
coupling.splice(10);
|
|
6096
6601
|
return coupling;
|
|
6097
6602
|
}
|
|
6603
|
+
function collectImportees(changedFiles, graph) {
|
|
6604
|
+
const changedSet = new Set(changedFiles);
|
|
6605
|
+
const importees = /* @__PURE__ */ new Set();
|
|
6606
|
+
for (const file of changedFiles) {
|
|
6607
|
+
for (const imp of graph.getImports(file)) {
|
|
6608
|
+
if (!changedSet.has(imp)) importees.add(imp);
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
return importees;
|
|
6612
|
+
}
|
|
6613
|
+
var SYMBOL_CALLERS_TOP_K = 25;
|
|
6614
|
+
var PATH_PROXIMITY_BONUS = 1;
|
|
6615
|
+
var SYMBOL_CALLERS_MIN_SCORE = 1;
|
|
6616
|
+
function pathProximityScore(callerFile, seedFile) {
|
|
6617
|
+
const lastSlash = seedFile.lastIndexOf("/");
|
|
6618
|
+
const lastDot = seedFile.lastIndexOf(".");
|
|
6619
|
+
const stem = seedFile.slice(
|
|
6620
|
+
lastSlash + 1,
|
|
6621
|
+
lastDot > lastSlash ? lastDot : seedFile.length
|
|
6622
|
+
);
|
|
6623
|
+
if (stem.length < 3) return 0;
|
|
6624
|
+
const shortPrefix = stem.slice(0, 3);
|
|
6625
|
+
const tokens = stem === shortPrefix ? [stem] : [stem, shortPrefix];
|
|
6626
|
+
const caller = callerFile.toLowerCase();
|
|
6627
|
+
for (const t of tokens) {
|
|
6628
|
+
const token = t.toLowerCase();
|
|
6629
|
+
const re = new RegExp(`(?:^|[/_.\\-])${token}(?:[/_.\\-]|$)`);
|
|
6630
|
+
if (re.test(caller)) return PATH_PROXIMITY_BONUS;
|
|
6631
|
+
}
|
|
6632
|
+
return 0;
|
|
6633
|
+
}
|
|
6634
|
+
function collectSymbolCallers(changedFiles, graph) {
|
|
6635
|
+
const changedSet = new Set(changedFiles);
|
|
6636
|
+
const callGraph = graph.getCallGraphIndex();
|
|
6637
|
+
const callerScores = /* @__PURE__ */ new Map();
|
|
6638
|
+
const callersWithProximityApplied = /* @__PURE__ */ new Set();
|
|
6639
|
+
for (const file of changedFiles) {
|
|
6640
|
+
const symbols = graph.lookupSymbolsByFile(file);
|
|
6641
|
+
for (const sym of symbols) {
|
|
6642
|
+
const defs = graph.lookupSymbol(sym);
|
|
6643
|
+
const definedInSeed = defs.some((d) => changedSet.has(d.filePath));
|
|
6644
|
+
if (!definedInSeed) continue;
|
|
6645
|
+
const specificity = 1 / defs.length;
|
|
6646
|
+
const seenForSym = /* @__PURE__ */ new Set();
|
|
6647
|
+
for (const caller of callGraph.getCallers(sym)) {
|
|
6648
|
+
if (changedSet.has(caller.file)) continue;
|
|
6649
|
+
if (seenForSym.has(caller.file)) continue;
|
|
6650
|
+
seenForSym.add(caller.file);
|
|
6651
|
+
const prev = callerScores.get(caller.file) ?? 0;
|
|
6652
|
+
const next = prev + specificity;
|
|
6653
|
+
callerScores.set(caller.file, next);
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
for (const callerFile of callerScores.keys()) {
|
|
6657
|
+
if (callersWithProximityApplied.has(callerFile)) continue;
|
|
6658
|
+
const bonus = pathProximityScore(callerFile, file);
|
|
6659
|
+
if (bonus > 0) {
|
|
6660
|
+
callerScores.set(callerFile, (callerScores.get(callerFile) ?? 0) + bonus);
|
|
6661
|
+
}
|
|
6662
|
+
callersWithProximityApplied.add(callerFile);
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
const ranked = Array.from(callerScores.entries()).filter(([, score]) => score >= SYMBOL_CALLERS_MIN_SCORE).sort((a, b) => {
|
|
6666
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
6667
|
+
return a[0].localeCompare(b[0]);
|
|
6668
|
+
});
|
|
6669
|
+
return new Set(ranked.slice(0, SYMBOL_CALLERS_TOP_K).map(([file]) => file));
|
|
6670
|
+
}
|
|
6671
|
+
var SEMANTIC_TOP_K = 10;
|
|
6672
|
+
var SEMANTIC_DIST_THRESHOLD = 0.5;
|
|
6673
|
+
async function computeSemanticSimilar(changedFiles, vectorStore, staticSet) {
|
|
6674
|
+
return collectSemanticSimilar(changedFiles, vectorStore, staticSet);
|
|
6675
|
+
}
|
|
6676
|
+
async function collectSemanticSimilar(changedFiles, vectorStore, staticSet) {
|
|
6677
|
+
const changedSet = new Set(changedFiles);
|
|
6678
|
+
const scores = /* @__PURE__ */ new Map();
|
|
6679
|
+
for (const seedFile of changedFiles) {
|
|
6680
|
+
let embedding;
|
|
6681
|
+
try {
|
|
6682
|
+
embedding = await vectorStore.findEmbeddingByPath(seedFile);
|
|
6683
|
+
} catch {
|
|
6684
|
+
continue;
|
|
6685
|
+
}
|
|
6686
|
+
if (embedding === null) continue;
|
|
6687
|
+
let results;
|
|
6688
|
+
try {
|
|
6689
|
+
results = await vectorStore.search(embedding, SEMANTIC_TOP_K + 5);
|
|
6690
|
+
} catch {
|
|
6691
|
+
continue;
|
|
6692
|
+
}
|
|
6693
|
+
for (const r of results) {
|
|
6694
|
+
if (changedSet.has(r.filePath)) continue;
|
|
6695
|
+
if (staticSet.has(r.filePath)) continue;
|
|
6696
|
+
if (r.score >= SEMANTIC_DIST_THRESHOLD) continue;
|
|
6697
|
+
const prev = scores.get(r.filePath);
|
|
6698
|
+
if (prev === void 0 || r.score < prev) {
|
|
6699
|
+
scores.set(r.filePath, r.score);
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6703
|
+
return Array.from(scores.entries()).sort((a, b) => a[1] - b[1]).slice(0, SEMANTIC_TOP_K).map(([node, score]) => ({ node, score }));
|
|
6704
|
+
}
|
|
6098
6705
|
function getImpactRadius(input) {
|
|
6099
|
-
const {
|
|
6706
|
+
const {
|
|
6707
|
+
graph,
|
|
6708
|
+
overlay,
|
|
6709
|
+
changedFiles,
|
|
6710
|
+
depth = 3,
|
|
6711
|
+
includeImportees = false,
|
|
6712
|
+
includeSymbolCallers = false
|
|
6713
|
+
} = input;
|
|
6100
6714
|
const { directImporters, allReachable } = traverseImporters(changedFiles, graph, depth);
|
|
6101
6715
|
const transitiveImporters = [];
|
|
6102
6716
|
for (const file of allReachable) {
|
|
6103
6717
|
if (!directImporters.has(file)) transitiveImporters.push(file);
|
|
6104
6718
|
}
|
|
6719
|
+
const importeesSet = includeImportees ? collectImportees(changedFiles, graph) : /* @__PURE__ */ new Set();
|
|
6720
|
+
const directImportees = Array.from(importeesSet);
|
|
6721
|
+
const symbolCallersSet = includeSymbolCallers ? collectSymbolCallers(changedFiles, graph) : /* @__PURE__ */ new Set();
|
|
6722
|
+
const symbolCallers = Array.from(symbolCallersSet);
|
|
6105
6723
|
const staticSet = /* @__PURE__ */ new Set([
|
|
6106
6724
|
...changedFiles,
|
|
6107
6725
|
...directImporters,
|
|
6108
|
-
...transitiveImporters
|
|
6726
|
+
...transitiveImporters,
|
|
6727
|
+
...directImportees,
|
|
6728
|
+
...symbolCallers
|
|
6109
6729
|
]);
|
|
6110
6730
|
const historicalCoupling = overlay !== void 0 ? buildHistoricalCouplingEntries(changedFiles, staticSet, overlay) : [];
|
|
6111
|
-
const totalImpacted = directImporters.size + transitiveImporters.length;
|
|
6731
|
+
const totalImpacted = directImporters.size + transitiveImporters.length + directImportees.length + symbolCallers.length;
|
|
6112
6732
|
return {
|
|
6113
6733
|
seedFiles: [...changedFiles],
|
|
6114
6734
|
directImporters: Array.from(directImporters),
|
|
6115
6735
|
transitiveImporters,
|
|
6736
|
+
directImportees,
|
|
6737
|
+
symbolCallers,
|
|
6116
6738
|
historicalCoupling,
|
|
6739
|
+
semanticSimilar: [],
|
|
6117
6740
|
totalImpacted
|
|
6118
6741
|
};
|
|
6119
6742
|
}
|
|
@@ -6597,7 +7220,7 @@ function registerArchitectureOverviewTool(registry, ctx) {
|
|
|
6597
7220
|
|
|
6598
7221
|
// packages/core/src/tools/knowledge-gaps.ts
|
|
6599
7222
|
import { z as z15 } from "zod";
|
|
6600
|
-
import
|
|
7223
|
+
import path17 from "path";
|
|
6601
7224
|
var Schema13 = z15.object({
|
|
6602
7225
|
min_importers: z15.number().min(1).max(50).optional().default(3).describe(
|
|
6603
7226
|
"Minimum importers to qualify as an untested hub (default: 3)"
|
|
@@ -6648,7 +7271,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
|
|
|
6648
7271
|
const testFiles = new Set(files.filter((f) => TEST_PATTERN2.test(f)));
|
|
6649
7272
|
const testedBases = /* @__PURE__ */ new Set();
|
|
6650
7273
|
for (const tf of testFiles) {
|
|
6651
|
-
const base =
|
|
7274
|
+
const base = path17.basename(tf).replace(/\.(test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "");
|
|
6652
7275
|
if (base) testedBases.add(base);
|
|
6653
7276
|
}
|
|
6654
7277
|
const isolated = [];
|
|
@@ -6666,7 +7289,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
|
|
|
6666
7289
|
deadCode.push(file);
|
|
6667
7290
|
}
|
|
6668
7291
|
if (importers >= min_importers) {
|
|
6669
|
-
const base =
|
|
7292
|
+
const base = path17.basename(file).replace(/\.[^.]+$/, "");
|
|
6670
7293
|
if (!testedBases.has(base)) {
|
|
6671
7294
|
untestedHubs.push({ file, importers });
|
|
6672
7295
|
}
|
|
@@ -6853,7 +7476,7 @@ function registerSurprisingConnectionsTool(registry, ctx) {
|
|
|
6853
7476
|
|
|
6854
7477
|
// packages/core/src/tools/wiki-generate.ts
|
|
6855
7478
|
import { z as z17 } from "zod";
|
|
6856
|
-
import
|
|
7479
|
+
import fs15 from "fs";
|
|
6857
7480
|
var DEFAULT_MAX_RESPONSE_TOKENS5 = 12e3;
|
|
6858
7481
|
var Schema15 = z17.object({
|
|
6859
7482
|
force: z17.boolean().optional().default(false).describe(
|
|
@@ -6873,7 +7496,7 @@ function escapeXML14(text) {
|
|
|
6873
7496
|
}
|
|
6874
7497
|
function safeFileSize(filePath) {
|
|
6875
7498
|
try {
|
|
6876
|
-
return
|
|
7499
|
+
return fs15.statSync(filePath).size;
|
|
6877
7500
|
} catch {
|
|
6878
7501
|
return 0;
|
|
6879
7502
|
}
|
|
@@ -7163,8 +7786,8 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
7163
7786
|
|
|
7164
7787
|
// packages/core/src/tools/refactor-preview.ts
|
|
7165
7788
|
import { z as z20 } from "zod";
|
|
7166
|
-
import
|
|
7167
|
-
import
|
|
7789
|
+
import fs16 from "fs";
|
|
7790
|
+
import path18 from "path";
|
|
7168
7791
|
var DEFAULT_MAX_RESPONSE_TOKENS7 = 4e3;
|
|
7169
7792
|
var Schema18 = z20.object({
|
|
7170
7793
|
symbol: z20.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
|
|
@@ -7184,7 +7807,7 @@ function escapeXML17(text) {
|
|
|
7184
7807
|
function scanFile(filePath, symbol, newName) {
|
|
7185
7808
|
let content;
|
|
7186
7809
|
try {
|
|
7187
|
-
content =
|
|
7810
|
+
content = fs16.readFileSync(filePath, "utf-8");
|
|
7188
7811
|
} catch {
|
|
7189
7812
|
return [];
|
|
7190
7813
|
}
|
|
@@ -7251,7 +7874,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
7251
7874
|
const fileChanges = [];
|
|
7252
7875
|
let totalOccurrences = 0;
|
|
7253
7876
|
for (const relPath of candidates) {
|
|
7254
|
-
const absPath =
|
|
7877
|
+
const absPath = path18.join(ctx.projectRoot, relPath);
|
|
7255
7878
|
const occurrences = scanFile(absPath, symbol, new_name);
|
|
7256
7879
|
if (occurrences.length > 0) {
|
|
7257
7880
|
fileChanges.push({ filePath: relPath, occurrences });
|
|
@@ -7456,8 +8079,8 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
7456
8079
|
|
|
7457
8080
|
// packages/core/src/tools/cross-repo-search.ts
|
|
7458
8081
|
import { z as z22 } from "zod";
|
|
7459
|
-
import
|
|
7460
|
-
import
|
|
8082
|
+
import fs17 from "fs";
|
|
8083
|
+
import path19 from "path";
|
|
7461
8084
|
var DEFAULT_MAX_RESPONSE_TOKENS9 = 4e3;
|
|
7462
8085
|
var ALIAS_REGEX = /^[a-z0-9-]{1,40}$/;
|
|
7463
8086
|
var RESERVED_ALIASES = /* @__PURE__ */ new Set([
|
|
@@ -7499,16 +8122,16 @@ var RepoRegistry = class {
|
|
|
7499
8122
|
}
|
|
7500
8123
|
load() {
|
|
7501
8124
|
try {
|
|
7502
|
-
if (!
|
|
7503
|
-
return JSON.parse(
|
|
8125
|
+
if (!fs17.existsSync(this.filePath)) return [];
|
|
8126
|
+
return JSON.parse(fs17.readFileSync(this.filePath, "utf-8"));
|
|
7504
8127
|
} catch {
|
|
7505
8128
|
return [];
|
|
7506
8129
|
}
|
|
7507
8130
|
}
|
|
7508
8131
|
save() {
|
|
7509
|
-
const dir =
|
|
7510
|
-
if (!
|
|
7511
|
-
|
|
8132
|
+
const dir = path19.dirname(this.filePath);
|
|
8133
|
+
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
8134
|
+
fs17.writeFileSync(this.filePath, JSON.stringify(this.repos, null, 2), "utf-8");
|
|
7512
8135
|
}
|
|
7513
8136
|
list() {
|
|
7514
8137
|
return [...this.repos];
|
|
@@ -7517,15 +8140,15 @@ var RepoRegistry = class {
|
|
|
7517
8140
|
return this.repos.find((r) => r.alias === alias) ?? null;
|
|
7518
8141
|
}
|
|
7519
8142
|
findByPath(absPath) {
|
|
7520
|
-
const canonical =
|
|
7521
|
-
return this.repos.find((r) =>
|
|
8143
|
+
const canonical = path19.resolve(absPath);
|
|
8144
|
+
return this.repos.find((r) => path19.resolve(r.root) === canonical) ?? null;
|
|
7522
8145
|
}
|
|
7523
8146
|
register(root, dbPath, opts = {}) {
|
|
7524
8147
|
if (opts.alias !== void 0) {
|
|
7525
8148
|
const v = validateAlias(opts.alias);
|
|
7526
8149
|
if (!v.ok) throw new Error(`Invalid alias: ${v.reason}`);
|
|
7527
8150
|
const colliding = this.repos.find(
|
|
7528
|
-
(r) => r.alias === opts.alias &&
|
|
8151
|
+
(r) => r.alias === opts.alias && path19.resolve(r.root) !== path19.resolve(root)
|
|
7529
8152
|
);
|
|
7530
8153
|
if (colliding) {
|
|
7531
8154
|
throw new Error(
|
|
@@ -7533,11 +8156,11 @@ var RepoRegistry = class {
|
|
|
7533
8156
|
);
|
|
7534
8157
|
}
|
|
7535
8158
|
}
|
|
7536
|
-
const existingIdx = this.repos.findIndex((r) =>
|
|
8159
|
+
const existingIdx = this.repos.findIndex((r) => path19.resolve(r.root) === path19.resolve(root));
|
|
7537
8160
|
const entry = {
|
|
7538
8161
|
root,
|
|
7539
8162
|
dbPath,
|
|
7540
|
-
name:
|
|
8163
|
+
name: path19.basename(root),
|
|
7541
8164
|
alias: opts.alias,
|
|
7542
8165
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7543
8166
|
};
|
|
@@ -7549,7 +8172,7 @@ var RepoRegistry = class {
|
|
|
7549
8172
|
this.save();
|
|
7550
8173
|
}
|
|
7551
8174
|
unregister(root) {
|
|
7552
|
-
this.repos = this.repos.filter((r) =>
|
|
8175
|
+
this.repos = this.repos.filter((r) => path19.resolve(r.root) !== path19.resolve(root));
|
|
7553
8176
|
this.save();
|
|
7554
8177
|
}
|
|
7555
8178
|
};
|
|
@@ -7571,7 +8194,7 @@ function escapeXML19(text) {
|
|
|
7571
8194
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7572
8195
|
}
|
|
7573
8196
|
function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
7574
|
-
const repoRegistryPath = registryFilePath ??
|
|
8197
|
+
const repoRegistryPath = registryFilePath ?? path19.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
|
|
7575
8198
|
registry.register(
|
|
7576
8199
|
"ctx_cross_repo_search",
|
|
7577
8200
|
{
|
|
@@ -7692,8 +8315,8 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7692
8315
|
|
|
7693
8316
|
// packages/core/src/tools/apply-refactor.ts
|
|
7694
8317
|
import { z as z23 } from "zod";
|
|
7695
|
-
import
|
|
7696
|
-
import
|
|
8318
|
+
import fs18 from "fs";
|
|
8319
|
+
import path20 from "path";
|
|
7697
8320
|
var DEFAULT_MAX_RESPONSE_TOKENS10 = 2e3;
|
|
7698
8321
|
var Schema21 = z23.object({
|
|
7699
8322
|
symbol: z23.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
|
|
@@ -7716,7 +8339,7 @@ function escapeXML20(text) {
|
|
|
7716
8339
|
function applyToFile(absPath, symbol, newName, dryRun) {
|
|
7717
8340
|
let content;
|
|
7718
8341
|
try {
|
|
7719
|
-
content =
|
|
8342
|
+
content = fs18.readFileSync(absPath, "utf-8");
|
|
7720
8343
|
} catch {
|
|
7721
8344
|
return 0;
|
|
7722
8345
|
}
|
|
@@ -7725,7 +8348,7 @@ function applyToFile(absPath, symbol, newName, dryRun) {
|
|
|
7725
8348
|
const occurrences = (content.match(regex) ?? []).length;
|
|
7726
8349
|
if (occurrences === 0) return 0;
|
|
7727
8350
|
if (!dryRun) {
|
|
7728
|
-
|
|
8351
|
+
fs18.writeFileSync(absPath, content.replace(regex, newName), "utf-8");
|
|
7729
8352
|
}
|
|
7730
8353
|
return occurrences;
|
|
7731
8354
|
}
|
|
@@ -7769,7 +8392,7 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
7769
8392
|
const results = [];
|
|
7770
8393
|
let totalOccurrences = 0;
|
|
7771
8394
|
for (const relPath of candidates) {
|
|
7772
|
-
const absPath =
|
|
8395
|
+
const absPath = path20.join(ctx.projectRoot, relPath);
|
|
7773
8396
|
const count = applyToFile(absPath, symbol, new_name, dry_run);
|
|
7774
8397
|
if (count > 0) {
|
|
7775
8398
|
results.push({ filePath: relPath, occurrences: count, written: !dry_run });
|
|
@@ -7913,8 +8536,8 @@ function registerDetectChangesTool(registry, ctx) {
|
|
|
7913
8536
|
|
|
7914
8537
|
// packages/core/src/tools/full-text-search.ts
|
|
7915
8538
|
import { z as z25 } from "zod";
|
|
7916
|
-
import
|
|
7917
|
-
import
|
|
8539
|
+
import fs19 from "fs";
|
|
8540
|
+
import path21 from "path";
|
|
7918
8541
|
var DEFAULT_MAX_RESPONSE_TOKENS11 = 4e3;
|
|
7919
8542
|
var Schema23 = z25.object({
|
|
7920
8543
|
query: z25.string().min(1).describe("Search term \u2014 literal or /regex/"),
|
|
@@ -7945,7 +8568,7 @@ function buildPattern(query, caseSensitive) {
|
|
|
7945
8568
|
function scanFile2(absPath, pattern, contextLines) {
|
|
7946
8569
|
let content;
|
|
7947
8570
|
try {
|
|
7948
|
-
content =
|
|
8571
|
+
content = fs19.readFileSync(absPath, "utf-8");
|
|
7949
8572
|
} catch {
|
|
7950
8573
|
return null;
|
|
7951
8574
|
}
|
|
@@ -8017,7 +8640,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
8017
8640
|
};
|
|
8018
8641
|
if (mode === "semantic") {
|
|
8019
8642
|
try {
|
|
8020
|
-
const { generateEmbedding: generateEmbedding2 } = await import("./embedder-
|
|
8643
|
+
const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
|
|
8021
8644
|
const store = await ctx.getStore(project_root);
|
|
8022
8645
|
const embedding = await generateEmbedding2(query);
|
|
8023
8646
|
const results = await store.search(embedding, limit);
|
|
@@ -8039,7 +8662,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
8039
8662
|
const files = graph.allFiles();
|
|
8040
8663
|
const keywordResults = [];
|
|
8041
8664
|
for (const relPath of files) {
|
|
8042
|
-
const absPath =
|
|
8665
|
+
const absPath = path21.join(ctx.projectRoot, relPath);
|
|
8043
8666
|
const hit = scanFile2(absPath, pattern, context_lines);
|
|
8044
8667
|
if (hit) {
|
|
8045
8668
|
keywordResults.push({
|
|
@@ -8054,7 +8677,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
8054
8677
|
let merged = keywordResults.slice(0, limit);
|
|
8055
8678
|
if (mode === "hybrid") {
|
|
8056
8679
|
try {
|
|
8057
|
-
const { generateEmbedding: generateEmbedding2 } = await import("./embedder-
|
|
8680
|
+
const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
|
|
8058
8681
|
const store = await ctx.getStore(project_root);
|
|
8059
8682
|
const embedding = await generateEmbedding2(query);
|
|
8060
8683
|
const vectorResults = await store.search(embedding, Math.ceil(limit / 2));
|
|
@@ -8273,8 +8896,8 @@ function registerGetWorkflowTool(registry, _ctx) {
|
|
|
8273
8896
|
}
|
|
8274
8897
|
|
|
8275
8898
|
// packages/core/src/tools/graph-snapshot.ts
|
|
8276
|
-
import
|
|
8277
|
-
import
|
|
8899
|
+
import fs20 from "fs";
|
|
8900
|
+
import path22 from "path";
|
|
8278
8901
|
import { z as z28 } from "zod";
|
|
8279
8902
|
var schema = z28.object({
|
|
8280
8903
|
name: z28.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
|
|
@@ -8286,13 +8909,13 @@ var schema = z28.object({
|
|
|
8286
8909
|
project_root: ProjectRootField
|
|
8287
8910
|
});
|
|
8288
8911
|
function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
|
|
8289
|
-
const snapshotsDir =
|
|
8290
|
-
|
|
8291
|
-
const snapshotPath =
|
|
8292
|
-
if (!snapshotPath.startsWith(snapshotsDir +
|
|
8912
|
+
const snapshotsDir = path22.resolve(rootDir, ".ctxloom", "snapshots");
|
|
8913
|
+
fs20.mkdirSync(snapshotsDir, { recursive: true });
|
|
8914
|
+
const snapshotPath = path22.resolve(snapshotsDir, `${name}.json`);
|
|
8915
|
+
if (!snapshotPath.startsWith(snapshotsDir + path22.sep)) {
|
|
8293
8916
|
throw new Error(`Invalid snapshot name: "${name}"`);
|
|
8294
8917
|
}
|
|
8295
|
-
if (
|
|
8918
|
+
if (fs20.existsSync(snapshotPath) && !overwrite) {
|
|
8296
8919
|
throw new Error(`Snapshot "${name}" already exists. Pass overwrite: true to replace it.`);
|
|
8297
8920
|
}
|
|
8298
8921
|
const files = graph.allFiles();
|
|
@@ -8307,14 +8930,14 @@ function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
|
|
|
8307
8930
|
edgeCount: graph.edgeCount(),
|
|
8308
8931
|
forwardEdges
|
|
8309
8932
|
};
|
|
8310
|
-
const tmp = snapshotPath
|
|
8311
|
-
|
|
8312
|
-
|
|
8933
|
+
const tmp = `${snapshotPath}.${process.pid}.tmp`;
|
|
8934
|
+
fs20.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
8935
|
+
fs20.renameSync(tmp, snapshotPath);
|
|
8313
8936
|
}
|
|
8314
8937
|
function listNamedSnapshots(rootDir) {
|
|
8315
|
-
const snapshotsDir =
|
|
8316
|
-
if (!
|
|
8317
|
-
return
|
|
8938
|
+
const snapshotsDir = path22.join(rootDir, ".ctxloom", "snapshots");
|
|
8939
|
+
if (!fs20.existsSync(snapshotsDir)) return [];
|
|
8940
|
+
return fs20.readdirSync(snapshotsDir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
|
|
8318
8941
|
}
|
|
8319
8942
|
function registerGraphSnapshotTool(registry, ctx) {
|
|
8320
8943
|
registry.register(
|
|
@@ -8363,8 +8986,8 @@ function registerGraphSnapshotTool(registry, ctx) {
|
|
|
8363
8986
|
}
|
|
8364
8987
|
|
|
8365
8988
|
// packages/core/src/tools/graph-diff.ts
|
|
8366
|
-
import
|
|
8367
|
-
import
|
|
8989
|
+
import fs21 from "fs";
|
|
8990
|
+
import path23 from "path";
|
|
8368
8991
|
import { z as z29 } from "zod";
|
|
8369
8992
|
var schema2 = z29.object({
|
|
8370
8993
|
baseline: z29.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
|
|
@@ -8372,16 +8995,16 @@ var schema2 = z29.object({
|
|
|
8372
8995
|
project_root: ProjectRootField
|
|
8373
8996
|
});
|
|
8374
8997
|
function loadSnapshot(name, rootDir) {
|
|
8375
|
-
const snapshotsDir =
|
|
8376
|
-
const snapshotPath =
|
|
8377
|
-
if (!snapshotPath.startsWith(snapshotsDir +
|
|
8998
|
+
const snapshotsDir = path23.resolve(rootDir, ".ctxloom", "snapshots");
|
|
8999
|
+
const snapshotPath = path23.resolve(snapshotsDir, `${name}.json`);
|
|
9000
|
+
if (!snapshotPath.startsWith(snapshotsDir + path23.sep)) {
|
|
8378
9001
|
throw new Error(`Invalid snapshot name: "${name}"`);
|
|
8379
9002
|
}
|
|
8380
|
-
if (!
|
|
9003
|
+
if (!fs21.existsSync(snapshotPath)) {
|
|
8381
9004
|
throw new Error(`Snapshot "${name}" not found. Run ctx_graph_snapshot first.`);
|
|
8382
9005
|
}
|
|
8383
9006
|
try {
|
|
8384
|
-
return JSON.parse(
|
|
9007
|
+
return JSON.parse(fs21.readFileSync(snapshotPath, "utf-8"));
|
|
8385
9008
|
} catch (e) {
|
|
8386
9009
|
throw new Error(`Snapshot "${name}" is corrupted: ${e instanceof Error ? e.message : String(e)}`);
|
|
8387
9010
|
}
|
|
@@ -8775,8 +9398,8 @@ var RulesConfigError = class extends Error {
|
|
|
8775
9398
|
};
|
|
8776
9399
|
|
|
8777
9400
|
// packages/core/src/rules/loadConfig.ts
|
|
8778
|
-
import
|
|
8779
|
-
import
|
|
9401
|
+
import fs22 from "fs/promises";
|
|
9402
|
+
import path24 from "path";
|
|
8780
9403
|
import yaml from "js-yaml";
|
|
8781
9404
|
import { z as z33 } from "zod";
|
|
8782
9405
|
var RuleSchema = z33.object({
|
|
@@ -8791,10 +9414,10 @@ var RulesConfigSchema = z33.object({
|
|
|
8791
9414
|
rules: z33.array(RuleSchema).default([])
|
|
8792
9415
|
});
|
|
8793
9416
|
async function loadRulesConfig(rootDir) {
|
|
8794
|
-
const filePath =
|
|
9417
|
+
const filePath = path24.join(rootDir, ".ctxloom", "rules.yml");
|
|
8795
9418
|
let raw;
|
|
8796
9419
|
try {
|
|
8797
|
-
raw = await
|
|
9420
|
+
raw = await fs22.readFile(filePath, "utf-8");
|
|
8798
9421
|
} catch (err) {
|
|
8799
9422
|
if (err.code === "ENOENT") return null;
|
|
8800
9423
|
throw new RulesConfigError(`Failed to read rules config: ${String(err)}`);
|
|
@@ -9206,11 +9829,11 @@ function readRecentChanges(projectRoot) {
|
|
|
9206
9829
|
return lines.slice(0, 20).map((line) => {
|
|
9207
9830
|
const x = line[0];
|
|
9208
9831
|
const y = line[1];
|
|
9209
|
-
const
|
|
9832
|
+
const path38 = line.slice(3).trim();
|
|
9210
9833
|
let status = "?";
|
|
9211
9834
|
const xy = x === " " ? y : x;
|
|
9212
9835
|
if (xy === "M" || xy === "A" || xy === "D" || xy === "R") status = xy;
|
|
9213
|
-
return { file:
|
|
9836
|
+
return { file: path38, status };
|
|
9214
9837
|
});
|
|
9215
9838
|
} catch {
|
|
9216
9839
|
return [];
|
|
@@ -9437,8 +10060,8 @@ function createToolRegistry(ctx) {
|
|
|
9437
10060
|
}
|
|
9438
10061
|
|
|
9439
10062
|
// packages/core/src/tools/ruleManager.ts
|
|
9440
|
-
import
|
|
9441
|
-
import
|
|
10063
|
+
import fs23 from "fs";
|
|
10064
|
+
import path25 from "path";
|
|
9442
10065
|
var RULE_FILES = [
|
|
9443
10066
|
".cursorrules",
|
|
9444
10067
|
"CLAUDE.md",
|
|
@@ -9462,30 +10085,30 @@ var RuleManager = class {
|
|
|
9462
10085
|
if (this.cachedRules) return this.cachedRules;
|
|
9463
10086
|
const rules = [];
|
|
9464
10087
|
for (const ruleFile of RULE_FILES) {
|
|
9465
|
-
const fullPath =
|
|
10088
|
+
const fullPath = path25.join(this.projectRoot, ruleFile);
|
|
9466
10089
|
try {
|
|
9467
10090
|
this.pathValidator.validate(fullPath);
|
|
9468
|
-
if (
|
|
9469
|
-
const stat =
|
|
10091
|
+
if (fs23.existsSync(fullPath)) {
|
|
10092
|
+
const stat = fs23.statSync(fullPath);
|
|
9470
10093
|
if (stat.isFile()) {
|
|
9471
|
-
const content =
|
|
10094
|
+
const content = fs23.readFileSync(fullPath, "utf-8");
|
|
9472
10095
|
rules.push({
|
|
9473
10096
|
name: ruleFile,
|
|
9474
10097
|
path: ruleFile,
|
|
9475
10098
|
content
|
|
9476
10099
|
});
|
|
9477
10100
|
} else if (stat.isDirectory()) {
|
|
9478
|
-
const dirEntries =
|
|
10101
|
+
const dirEntries = fs23.readdirSync(fullPath);
|
|
9479
10102
|
for (const entry of dirEntries) {
|
|
9480
|
-
const entryPath =
|
|
10103
|
+
const entryPath = path25.join(fullPath, entry);
|
|
9481
10104
|
try {
|
|
9482
10105
|
this.pathValidator.validate(entryPath);
|
|
9483
10106
|
} catch {
|
|
9484
10107
|
continue;
|
|
9485
10108
|
}
|
|
9486
|
-
const entryStat =
|
|
10109
|
+
const entryStat = fs23.statSync(entryPath);
|
|
9487
10110
|
if (entryStat.isFile()) {
|
|
9488
|
-
const content =
|
|
10111
|
+
const content = fs23.readFileSync(entryPath, "utf-8");
|
|
9489
10112
|
rules.push({
|
|
9490
10113
|
name: `${ruleFile}/${entry}`,
|
|
9491
10114
|
path: `${ruleFile}/${entry}`,
|
|
@@ -9528,8 +10151,8 @@ var RuleManager = class {
|
|
|
9528
10151
|
};
|
|
9529
10152
|
|
|
9530
10153
|
// packages/core/src/review/AuthorResolver.ts
|
|
9531
|
-
import
|
|
9532
|
-
import
|
|
10154
|
+
import fs24 from "fs/promises";
|
|
10155
|
+
import path26 from "path";
|
|
9533
10156
|
import yaml2 from "js-yaml";
|
|
9534
10157
|
var AuthorResolver = class {
|
|
9535
10158
|
constructor(ctxloomDir) {
|
|
@@ -9554,8 +10177,8 @@ var AuthorResolver = class {
|
|
|
9554
10177
|
/** Write a new mapping to the cache file. */
|
|
9555
10178
|
async writeCache(email, handle) {
|
|
9556
10179
|
this.cache = { ...this.cache, [email]: handle };
|
|
9557
|
-
await
|
|
9558
|
-
|
|
10180
|
+
await fs24.writeFile(
|
|
10181
|
+
path26.join(this.ctxloomDir, "authors-cache.json"),
|
|
9559
10182
|
JSON.stringify(this.cache, null, 2)
|
|
9560
10183
|
);
|
|
9561
10184
|
}
|
|
@@ -9564,9 +10187,9 @@ var AuthorResolver = class {
|
|
|
9564
10187
|
return emails.filter((e) => this.resolve(e) === void 0);
|
|
9565
10188
|
}
|
|
9566
10189
|
async loadYml() {
|
|
9567
|
-
const file =
|
|
10190
|
+
const file = path26.join(this.ctxloomDir, "authors.yml");
|
|
9568
10191
|
try {
|
|
9569
|
-
const raw = await
|
|
10192
|
+
const raw = await fs24.readFile(file, "utf8");
|
|
9570
10193
|
const parsed = yaml2.load(raw);
|
|
9571
10194
|
if (!parsed) return;
|
|
9572
10195
|
this.mappings = parsed.mappings ?? {};
|
|
@@ -9575,9 +10198,9 @@ var AuthorResolver = class {
|
|
|
9575
10198
|
}
|
|
9576
10199
|
}
|
|
9577
10200
|
async loadCache() {
|
|
9578
|
-
const file =
|
|
10201
|
+
const file = path26.join(this.ctxloomDir, "authors-cache.json");
|
|
9579
10202
|
try {
|
|
9580
|
-
const raw = await
|
|
10203
|
+
const raw = await fs24.readFile(file, "utf8");
|
|
9581
10204
|
this.cache = JSON.parse(raw);
|
|
9582
10205
|
} catch {
|
|
9583
10206
|
}
|
|
@@ -9602,8 +10225,8 @@ async function resolveViaGitHubApi(email, owner, repo, token) {
|
|
|
9602
10225
|
}
|
|
9603
10226
|
|
|
9604
10227
|
// packages/core/src/review/CodeownersWriter.ts
|
|
9605
|
-
import
|
|
9606
|
-
import
|
|
10228
|
+
import fs25 from "fs/promises";
|
|
10229
|
+
import path27 from "path";
|
|
9607
10230
|
var MARKER_START = "# <ctxloom:start> \u2014 managed by ctxloom review-suggest; do not edit between markers";
|
|
9608
10231
|
var MARKER_START_DETECT = "# <ctxloom:start>";
|
|
9609
10232
|
var MARKER_END = "# <ctxloom:end>";
|
|
@@ -9635,15 +10258,15 @@ ${block}
|
|
|
9635
10258
|
async function generateCODEOWNERS(codeownersPath, rules) {
|
|
9636
10259
|
let existing = "";
|
|
9637
10260
|
try {
|
|
9638
|
-
existing = await
|
|
10261
|
+
existing = await fs25.readFile(codeownersPath, "utf8");
|
|
9639
10262
|
} catch {
|
|
9640
10263
|
}
|
|
9641
10264
|
const block = buildCodeownersBlock(rules);
|
|
9642
10265
|
return mergeIntoFile(existing, block);
|
|
9643
10266
|
}
|
|
9644
10267
|
async function writeCODEOWNERS(codeownersPath, content) {
|
|
9645
|
-
await
|
|
9646
|
-
await
|
|
10268
|
+
await fs25.mkdir(path27.dirname(codeownersPath), { recursive: true });
|
|
10269
|
+
await fs25.writeFile(codeownersPath, content, "utf8");
|
|
9647
10270
|
}
|
|
9648
10271
|
|
|
9649
10272
|
// packages/core/src/review/types.ts
|
|
@@ -9828,8 +10451,8 @@ function matchGlob(pattern, value) {
|
|
|
9828
10451
|
}
|
|
9829
10452
|
|
|
9830
10453
|
// packages/core/src/review/loadConfig.ts
|
|
9831
|
-
import
|
|
9832
|
-
import
|
|
10454
|
+
import fs26 from "fs/promises";
|
|
10455
|
+
import path28 from "path";
|
|
9833
10456
|
import yaml3 from "js-yaml";
|
|
9834
10457
|
function freshDefaults() {
|
|
9835
10458
|
return {
|
|
@@ -9840,9 +10463,9 @@ function freshDefaults() {
|
|
|
9840
10463
|
};
|
|
9841
10464
|
}
|
|
9842
10465
|
async function loadReviewConfig(root) {
|
|
9843
|
-
const file =
|
|
10466
|
+
const file = path28.join(root, ".ctxloom", "review.yml");
|
|
9844
10467
|
try {
|
|
9845
|
-
const raw = await
|
|
10468
|
+
const raw = await fs26.readFile(file, "utf8");
|
|
9846
10469
|
const parsed = yaml3.load(raw);
|
|
9847
10470
|
if (!parsed) return freshDefaults();
|
|
9848
10471
|
return {
|
|
@@ -9857,13 +10480,13 @@ async function loadReviewConfig(root) {
|
|
|
9857
10480
|
}
|
|
9858
10481
|
|
|
9859
10482
|
// packages/core/src/security/PathValidator.ts
|
|
9860
|
-
import
|
|
9861
|
-
import
|
|
10483
|
+
import path29 from "path";
|
|
10484
|
+
import fs27 from "fs";
|
|
9862
10485
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
9863
10486
|
var PathValidator = class {
|
|
9864
10487
|
canonicalRoot;
|
|
9865
10488
|
constructor(projectRoot) {
|
|
9866
|
-
this.canonicalRoot =
|
|
10489
|
+
this.canonicalRoot = fs27.realpathSync(path29.resolve(projectRoot));
|
|
9867
10490
|
}
|
|
9868
10491
|
/**
|
|
9869
10492
|
* Validates that the given input path resolves within the project root.
|
|
@@ -9873,14 +10496,14 @@ var PathValidator = class {
|
|
|
9873
10496
|
* @throws Error if the path escapes the project root
|
|
9874
10497
|
*/
|
|
9875
10498
|
validate(inputPath) {
|
|
9876
|
-
const resolved =
|
|
10499
|
+
const resolved = path29.resolve(this.canonicalRoot, inputPath);
|
|
9877
10500
|
let canonical;
|
|
9878
10501
|
try {
|
|
9879
|
-
canonical =
|
|
10502
|
+
canonical = fs27.realpathSync(resolved);
|
|
9880
10503
|
} catch {
|
|
9881
10504
|
canonical = resolved;
|
|
9882
10505
|
}
|
|
9883
|
-
if (!canonical.startsWith(this.canonicalRoot +
|
|
10506
|
+
if (!canonical.startsWith(this.canonicalRoot + path29.sep) && canonical !== this.canonicalRoot) {
|
|
9884
10507
|
throw new Error(
|
|
9885
10508
|
`Path traversal blocked: "${inputPath}" resolves outside of the project root`
|
|
9886
10509
|
);
|
|
@@ -9897,7 +10520,7 @@ var PathValidator = class {
|
|
|
9897
10520
|
* Converts an absolute path to a relative path from the project root.
|
|
9898
10521
|
*/
|
|
9899
10522
|
toRelative(absolutePath) {
|
|
9900
|
-
return
|
|
10523
|
+
return path29.relative(this.canonicalRoot, absolutePath);
|
|
9901
10524
|
}
|
|
9902
10525
|
/**
|
|
9903
10526
|
* Validates and reads a file, returning its content.
|
|
@@ -9905,11 +10528,11 @@ var PathValidator = class {
|
|
|
9905
10528
|
*/
|
|
9906
10529
|
readFile(inputPath) {
|
|
9907
10530
|
const absPath = this.validate(inputPath);
|
|
9908
|
-
const stat =
|
|
10531
|
+
const stat = fs27.statSync(absPath);
|
|
9909
10532
|
if (stat.size > MAX_FILE_SIZE) {
|
|
9910
10533
|
throw new Error(`File too large: ${inputPath} (${Math.round(stat.size / 1024)}KB, max ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
|
|
9911
10534
|
}
|
|
9912
|
-
return
|
|
10535
|
+
return fs27.readFileSync(absPath, "utf-8");
|
|
9913
10536
|
}
|
|
9914
10537
|
/**
|
|
9915
10538
|
* Checks if a path exists and is within the project root.
|
|
@@ -9926,16 +10549,14 @@ var PathValidator = class {
|
|
|
9926
10549
|
|
|
9927
10550
|
// packages/core/src/watcher/FileWatcher.ts
|
|
9928
10551
|
import chokidar from "chokidar";
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
"**/.cache/**"
|
|
9938
|
-
];
|
|
10552
|
+
function isIgnoredPath(absPath) {
|
|
10553
|
+
const segments = absPath.split(/[\\/]/);
|
|
10554
|
+
for (const seg of segments) {
|
|
10555
|
+
if (INDEXER_IGNORED_DIRS.has(seg)) return true;
|
|
10556
|
+
}
|
|
10557
|
+
return false;
|
|
10558
|
+
}
|
|
10559
|
+
var IGNORED = isIgnoredPath;
|
|
9939
10560
|
var FileWatcher = class {
|
|
9940
10561
|
root;
|
|
9941
10562
|
onChange;
|
|
@@ -10062,7 +10683,7 @@ var EmailAlreadyUsedError = class extends Error {
|
|
|
10062
10683
|
|
|
10063
10684
|
// packages/core/src/license/LicenseStore.ts
|
|
10064
10685
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
10065
|
-
import
|
|
10686
|
+
import path30 from "path";
|
|
10066
10687
|
|
|
10067
10688
|
// packages/core/src/license/types.ts
|
|
10068
10689
|
import { z as z37 } from "zod";
|
|
@@ -10083,7 +10704,7 @@ var LicenseFileSchema = z37.object({
|
|
|
10083
10704
|
|
|
10084
10705
|
// packages/core/src/license/LicenseStore.ts
|
|
10085
10706
|
function licenseFilePath(home) {
|
|
10086
|
-
return
|
|
10707
|
+
return path30.join(home, ".ctxloom", "license.json");
|
|
10087
10708
|
}
|
|
10088
10709
|
var LicenseStore = class {
|
|
10089
10710
|
filePath;
|
|
@@ -10106,7 +10727,7 @@ var LicenseStore = class {
|
|
|
10106
10727
|
}
|
|
10107
10728
|
}
|
|
10108
10729
|
async write(license) {
|
|
10109
|
-
mkdirSync(
|
|
10730
|
+
mkdirSync(path30.dirname(this.filePath), { recursive: true });
|
|
10110
10731
|
writeFileSync(this.filePath, JSON.stringify(license, null, 2), {
|
|
10111
10732
|
encoding: "utf8",
|
|
10112
10733
|
mode: 384
|
|
@@ -10237,11 +10858,11 @@ import os5 from "os";
|
|
|
10237
10858
|
|
|
10238
10859
|
// packages/core/src/license/DistinctIdStore.ts
|
|
10239
10860
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
10240
|
-
import
|
|
10861
|
+
import path31 from "path";
|
|
10241
10862
|
import os3 from "os";
|
|
10242
10863
|
var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10243
10864
|
function distinctIdPath(home) {
|
|
10244
|
-
return
|
|
10865
|
+
return path31.join(home ?? os3.homedir(), ".ctxloom", "distinct_id");
|
|
10245
10866
|
}
|
|
10246
10867
|
function isValidV4(id) {
|
|
10247
10868
|
return typeof id === "string" && UUID_V4_REGEX.test(id);
|
|
@@ -10262,7 +10883,7 @@ function getOrCreateDistinctId(home) {
|
|
|
10262
10883
|
id: crypto.randomUUID(),
|
|
10263
10884
|
alias_pending: os3.hostname()
|
|
10264
10885
|
};
|
|
10265
|
-
mkdirSync2(
|
|
10886
|
+
mkdirSync2(path31.dirname(filePath), { recursive: true });
|
|
10266
10887
|
writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
|
|
10267
10888
|
return record;
|
|
10268
10889
|
}
|
|
@@ -10291,10 +10912,10 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
10291
10912
|
function getTelemetryLevel() {
|
|
10292
10913
|
return TELEMETRY_LEVEL;
|
|
10293
10914
|
}
|
|
10294
|
-
var CTXLOOM_VERSION = "1.
|
|
10915
|
+
var CTXLOOM_VERSION = "1.7.0".length > 0 ? "1.7.0" : "dev";
|
|
10295
10916
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
10296
10917
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
10297
|
-
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528
|
|
10918
|
+
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
10298
10919
|
var cachedDistinctId = null;
|
|
10299
10920
|
function resolveDistinctId() {
|
|
10300
10921
|
if (cachedDistinctId) return cachedDistinctId;
|
|
@@ -10424,17 +11045,17 @@ function parseStack(stack) {
|
|
|
10424
11045
|
|
|
10425
11046
|
// packages/core/src/license/FunnelMilestones.ts
|
|
10426
11047
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
10427
|
-
import
|
|
11048
|
+
import path32 from "path";
|
|
10428
11049
|
import os4 from "os";
|
|
10429
11050
|
var INSTALL_MARKER = "installed_at";
|
|
10430
11051
|
var FIRST_REVIEW_MARKER = "first_review_at";
|
|
10431
11052
|
function writeMarker(filePath) {
|
|
10432
|
-
mkdirSync3(
|
|
11053
|
+
mkdirSync3(path32.dirname(filePath), { recursive: true });
|
|
10433
11054
|
writeFileSync3(filePath, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
|
|
10434
11055
|
}
|
|
10435
11056
|
function shouldEmitInstallCompleted(home) {
|
|
10436
11057
|
const root = home ?? os4.homedir();
|
|
10437
|
-
const marker =
|
|
11058
|
+
const marker = path32.join(root, ".ctxloom", INSTALL_MARKER);
|
|
10438
11059
|
if (existsSync3(marker)) return false;
|
|
10439
11060
|
try {
|
|
10440
11061
|
writeMarker(marker);
|
|
@@ -10443,7 +11064,7 @@ function shouldEmitInstallCompleted(home) {
|
|
|
10443
11064
|
return true;
|
|
10444
11065
|
}
|
|
10445
11066
|
function shouldEmitFirstReviewRun(projectRoot) {
|
|
10446
|
-
const marker =
|
|
11067
|
+
const marker = path32.join(projectRoot, ".ctxloom", FIRST_REVIEW_MARKER);
|
|
10447
11068
|
if (existsSync3(marker)) return false;
|
|
10448
11069
|
try {
|
|
10449
11070
|
writeMarker(marker);
|
|
@@ -10561,16 +11182,16 @@ async function startTrial(email, opts = {}) {
|
|
|
10561
11182
|
|
|
10562
11183
|
// packages/core/src/license/TelemetryNotice.ts
|
|
10563
11184
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
10564
|
-
import
|
|
11185
|
+
import path33 from "path";
|
|
10565
11186
|
import os6 from "os";
|
|
10566
11187
|
function noticePath(home) {
|
|
10567
|
-
return
|
|
11188
|
+
return path33.join(home ?? os6.homedir(), ".ctxloom", "telemetry_notice_shown");
|
|
10568
11189
|
}
|
|
10569
11190
|
function shouldShowTelemetryNotice(home) {
|
|
10570
11191
|
const filePath = noticePath(home);
|
|
10571
11192
|
if (existsSync4(filePath)) return false;
|
|
10572
11193
|
try {
|
|
10573
|
-
mkdirSync4(
|
|
11194
|
+
mkdirSync4(path33.dirname(filePath), { recursive: true });
|
|
10574
11195
|
writeFileSync4(filePath, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
|
|
10575
11196
|
} catch {
|
|
10576
11197
|
}
|
|
@@ -10578,13 +11199,13 @@ function shouldShowTelemetryNotice(home) {
|
|
|
10578
11199
|
}
|
|
10579
11200
|
|
|
10580
11201
|
// packages/core/src/server/ProjectState.ts
|
|
10581
|
-
import
|
|
11202
|
+
import path35 from "path";
|
|
10582
11203
|
|
|
10583
11204
|
// packages/core/src/server/projectId.ts
|
|
10584
11205
|
import crypto5 from "crypto";
|
|
10585
|
-
import
|
|
11206
|
+
import path34 from "path";
|
|
10586
11207
|
function hashProjectRoot(absPath) {
|
|
10587
|
-
const canonical =
|
|
11208
|
+
const canonical = path34.resolve(absPath);
|
|
10588
11209
|
return crypto5.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
10589
11210
|
}
|
|
10590
11211
|
|
|
@@ -10592,7 +11213,7 @@ function hashProjectRoot(absPath) {
|
|
|
10592
11213
|
function createProjectState(projectRoot, opts = {}) {
|
|
10593
11214
|
return {
|
|
10594
11215
|
projectRoot,
|
|
10595
|
-
dbPath:
|
|
11216
|
+
dbPath: path35.join(projectRoot, ".ctxloom", "vectors.lancedb"),
|
|
10596
11217
|
pinned: opts.pinned ?? false,
|
|
10597
11218
|
lastTouchedAt: Date.now(),
|
|
10598
11219
|
vectorsInitialized: false,
|
|
@@ -10744,8 +11365,8 @@ var ProjectStateManager = class {
|
|
|
10744
11365
|
};
|
|
10745
11366
|
|
|
10746
11367
|
// packages/core/src/server/resolveProjectRoot.ts
|
|
10747
|
-
import
|
|
10748
|
-
import
|
|
11368
|
+
import fs28 from "fs";
|
|
11369
|
+
import path36 from "path";
|
|
10749
11370
|
var PATH_SEPARATOR_PATTERN = /[/\\~]|^[A-Za-z]:/;
|
|
10750
11371
|
function looksLikePath(value) {
|
|
10751
11372
|
return PATH_SEPARATOR_PATTERN.test(value);
|
|
@@ -10770,13 +11391,13 @@ function resolvePathSafely(p, cwd) {
|
|
|
10770
11391
|
let expanded = p;
|
|
10771
11392
|
if (p === "~" || p.startsWith("~/")) {
|
|
10772
11393
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
10773
|
-
expanded = p === "~" ? home :
|
|
11394
|
+
expanded = p === "~" ? home : path36.join(home, p.slice(2));
|
|
10774
11395
|
}
|
|
10775
|
-
return
|
|
11396
|
+
return path36.isAbsolute(expanded) ? path36.resolve(expanded) : path36.resolve(cwd, expanded);
|
|
10776
11397
|
}
|
|
10777
11398
|
function realpathOrSame(p) {
|
|
10778
11399
|
try {
|
|
10779
|
-
return
|
|
11400
|
+
return fs28.realpathSync(p);
|
|
10780
11401
|
} catch {
|
|
10781
11402
|
return p;
|
|
10782
11403
|
}
|
|
@@ -10801,7 +11422,7 @@ function resolveProjectRoot(input) {
|
|
|
10801
11422
|
};
|
|
10802
11423
|
}
|
|
10803
11424
|
const resolved2 = resolvePathSafely(arg, cwd);
|
|
10804
|
-
if (!
|
|
11425
|
+
if (!fs28.existsSync(resolved2)) {
|
|
10805
11426
|
return {
|
|
10806
11427
|
kind: "project_root_not_found",
|
|
10807
11428
|
attemptedPath: resolved2,
|
|
@@ -10812,7 +11433,7 @@ function resolveProjectRoot(input) {
|
|
|
10812
11433
|
}
|
|
10813
11434
|
if (env !== void 0 && env !== "") {
|
|
10814
11435
|
const resolved2 = resolvePathSafely(env, cwd);
|
|
10815
|
-
if (!
|
|
11436
|
+
if (!fs28.existsSync(resolved2)) {
|
|
10816
11437
|
return {
|
|
10817
11438
|
kind: "project_root_not_found",
|
|
10818
11439
|
attemptedPath: resolved2,
|
|
@@ -10839,12 +11460,12 @@ var FILESYSTEM_ROOTS = /* @__PURE__ */ new Set(["/", "C:\\", "D:\\", "E:\\", "F:
|
|
|
10839
11460
|
function validateDefaultRoot(candidate) {
|
|
10840
11461
|
if (FILESYSTEM_ROOTS.has(candidate)) return false;
|
|
10841
11462
|
try {
|
|
10842
|
-
const stat =
|
|
11463
|
+
const stat = fs28.statSync(candidate);
|
|
10843
11464
|
if (!stat.isDirectory()) return false;
|
|
10844
11465
|
} catch {
|
|
10845
11466
|
return false;
|
|
10846
11467
|
}
|
|
10847
|
-
return PROJECT_MARKERS.some((m) =>
|
|
11468
|
+
return PROJECT_MARKERS.some((m) => fs28.existsSync(path36.join(candidate, m)));
|
|
10848
11469
|
}
|
|
10849
11470
|
|
|
10850
11471
|
// packages/core/src/server/structuredErrors.ts
|
|
@@ -10916,8 +11537,8 @@ var EmittedOnceTracker = class {
|
|
|
10916
11537
|
};
|
|
10917
11538
|
|
|
10918
11539
|
// packages/core/src/install/installer.ts
|
|
10919
|
-
import
|
|
10920
|
-
import
|
|
11540
|
+
import fs29 from "fs";
|
|
11541
|
+
import path37 from "path";
|
|
10921
11542
|
|
|
10922
11543
|
// packages/core/src/install/templates.ts
|
|
10923
11544
|
var RULES_BLOCK_NAME = "CTXLOOM-RULES";
|
|
@@ -10929,6 +11550,27 @@ The graph is faster, cheaper (fewer tokens), and gives you
|
|
|
10929
11550
|
structural context (callers, dependents, test coverage) that file
|
|
10930
11551
|
scanning cannot.
|
|
10931
11552
|
|
|
11553
|
+
### Operating principles
|
|
11554
|
+
|
|
11555
|
+
ctxloom's tools exist to operationalize four principles for working
|
|
11556
|
+
with an AI coding agent. They're the *why* behind every tool below.
|
|
11557
|
+
Adapted from Karpathy's LLM-coding-pitfalls notes
|
|
11558
|
+
(<https://github.com/multica-ai/andrej-karpathy-skills>, MIT).
|
|
11559
|
+
|
|
11560
|
+
1. **Think before coding** \u2014 read the relevant graph slice before
|
|
11561
|
+
editing. \`ctx_blast_radius\`, \`ctx_get_call_graph\`,
|
|
11562
|
+
\`ctx_get_review_context\` are how you do this without re-reading
|
|
11563
|
+
whole files.
|
|
11564
|
+
2. **Simplicity first** \u2014 prefer the smallest viable change. Use
|
|
11565
|
+
\`ctx_refactor_preview\` to see the full diff *before* applying;
|
|
11566
|
+
if the preview is sprawling, the plan is too big.
|
|
11567
|
+
3. **Surgical changes** \u2014 every changed line should trace directly
|
|
11568
|
+
to the user's request. \`ctx_detect_changes\` after each edit
|
|
11569
|
+
confirms scope hasn't drifted.
|
|
11570
|
+
4. **Goal-driven execution** \u2014 stop when the goal is met. Don't
|
|
11571
|
+
"polish" beyond the request. \`ctx_knowledge_gaps\` flags real
|
|
11572
|
+
risk surfaces; everything else is yak-shaving.
|
|
11573
|
+
|
|
10932
11574
|
### Start every workflow with \`ctx_get_minimal_context\`
|
|
10933
11575
|
|
|
10934
11576
|
The first MCP call into ctxloom should always be
|
|
@@ -11174,9 +11816,18 @@ description: Orient yourself to an unfamiliar codebase using ctxloom's structura
|
|
|
11174
11816
|
|
|
11175
11817
|
# Explore Codebase
|
|
11176
11818
|
|
|
11819
|
+
**Principle: Think Before Coding.** Exploration is "think" with a
|
|
11820
|
+
budget. The trap is reading every file in sight; the discipline is
|
|
11821
|
+
to read the *graph*, then the few files the graph nominates. This
|
|
11822
|
+
skill caps you at 5 tool calls and 2000 tokens \u2014 enough to ground
|
|
11823
|
+
the next edit, not enough to procrastinate.
|
|
11824
|
+
|
|
11177
11825
|
Use this when you need to understand a codebase you haven't worked in
|
|
11178
11826
|
before, or when re-orienting after time away.
|
|
11179
11827
|
|
|
11828
|
+
**Skip when:** you already know which files matter \u2014 just open them.
|
|
11829
|
+
Reserve this skill for "I don't know where to start" situations.
|
|
11830
|
+
|
|
11180
11831
|
## Steps
|
|
11181
11832
|
|
|
11182
11833
|
1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
|
|
@@ -11225,9 +11876,17 @@ argument-hint: "<symbol-name | file-path>"
|
|
|
11225
11876
|
|
|
11226
11877
|
# Blast Radius
|
|
11227
11878
|
|
|
11879
|
+
**Principle: Think Before Coding.** You can't make a surgical change
|
|
11880
|
+
if you don't know who depends on the symbol. This skill is the
|
|
11881
|
+
"think" phase \u2014 read the graph slice before editing anything.
|
|
11882
|
+
|
|
11228
11883
|
Use this before any change to a public function, type, or file
|
|
11229
11884
|
where you're not sure who depends on it.
|
|
11230
11885
|
|
|
11886
|
+
**Skip when:** the change is purely internal to a private helper
|
|
11887
|
+
with no callers outside the file, OR you've already run this skill
|
|
11888
|
+
for the same symbol in the current task.
|
|
11889
|
+
|
|
11231
11890
|
## Inputs
|
|
11232
11891
|
|
|
11233
11892
|
- \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
|
|
@@ -11276,9 +11935,18 @@ argument-hint: "<old-name> <new-name>"
|
|
|
11276
11935
|
|
|
11277
11936
|
# Refactor Safely
|
|
11278
11937
|
|
|
11938
|
+
**Principle: Surgical Changes.** A rename touches every call site \u2014
|
|
11939
|
+
done blindly it's the opposite of surgical. This skill enforces
|
|
11940
|
+
"see the full diff before applying" so the change stays scoped to
|
|
11941
|
+
the user's request and doesn't accidentally rewrite tangents.
|
|
11942
|
+
|
|
11279
11943
|
Use this for renames, signature changes, or function moves. The
|
|
11280
11944
|
skill enforces preview-before-apply.
|
|
11281
11945
|
|
|
11946
|
+
**Skip when:** the symbol is local to a single file (just edit it),
|
|
11947
|
+
OR the user wants a behavior change, not a rename (use the regular
|
|
11948
|
+
edit flow with \`ctx_blast_radius\` for impact awareness).
|
|
11949
|
+
|
|
11282
11950
|
## Inputs
|
|
11283
11951
|
|
|
11284
11952
|
- \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
|
|
@@ -11331,9 +11999,18 @@ description: Identify code that lacks test coverage, prioritized by caller frequ
|
|
|
11331
11999
|
|
|
11332
12000
|
# Coverage Gap Analysis
|
|
11333
12001
|
|
|
12002
|
+
**Principle: Goal-Driven Execution.** "Add tests everywhere" is
|
|
12003
|
+
yak-shaving. The goal is to add tests where they pay back the
|
|
12004
|
+
investment: high-caller, high-churn, low-coverage code. This skill
|
|
12005
|
+
ranks gaps so you write tests with intent, not by reflex.
|
|
12006
|
+
|
|
11334
12007
|
Use this to find untested code that genuinely matters \u2014 the
|
|
11335
12008
|
intersection of "no tests" + "many callers" + "high risk score."
|
|
11336
12009
|
|
|
12010
|
+
**Skip when:** the user asked for tests on a specific symbol \u2014
|
|
12011
|
+
write them directly. Reserve this skill for "where should we
|
|
12012
|
+
invest in tests next?" queries.
|
|
12013
|
+
|
|
11337
12014
|
## Steps
|
|
11338
12015
|
|
|
11339
12016
|
1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
|
|
@@ -11381,10 +12058,20 @@ argument-hint: "<PR number | branch name>"
|
|
|
11381
12058
|
|
|
11382
12059
|
# Review PR
|
|
11383
12060
|
|
|
12061
|
+
**Principle: Think Before Coding.** A code review *is* the "think"
|
|
12062
|
+
phase for the author and the reviewer alike. This skill structures
|
|
12063
|
+
that thinking around graph slices rather than naive file-by-file
|
|
12064
|
+
reading, so you spot blast-radius and coverage risks the diff alone
|
|
12065
|
+
hides.
|
|
12066
|
+
|
|
11384
12067
|
Comprehensive PR review using ctxloom's graph. Mirrors the
|
|
11385
12068
|
multi-agent review the ctxloom-bot posts automatically \u2014 useful
|
|
11386
12069
|
when reviewing manually or when the bot isn't wired up.
|
|
11387
12070
|
|
|
12071
|
+
**Skip when:** the PR is one-file, \u226430 lines, with full test
|
|
12072
|
+
coverage \u2014 read the diff directly. Reserve this skill for changes
|
|
12073
|
+
where the structural impact isn't obvious from the diff.
|
|
12074
|
+
|
|
11388
12075
|
## Inputs
|
|
11389
12076
|
|
|
11390
12077
|
- \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
|
|
@@ -11472,6 +12159,11 @@ description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distrib
|
|
|
11472
12159
|
|
|
11473
12160
|
# Budget Stats
|
|
11474
12161
|
|
|
12162
|
+
**Principle: Simplicity First.** Don't guess what \`DEFAULT_MAX_RESPONSE_TOKENS\`
|
|
12163
|
+
should be \u2014 measure. This skill turns real telemetry into the
|
|
12164
|
+
simplest viable constant: the p75 of actual usage. Tune what hurts;
|
|
12165
|
+
leave the rest alone.
|
|
12166
|
+
|
|
11475
12167
|
Wrapper around \`ctxloom budget-stats\` for inline use inside a
|
|
11476
12168
|
Claude Code session. Useful when:
|
|
11477
12169
|
|
|
@@ -11480,6 +12172,10 @@ Claude Code session. Useful when:
|
|
|
11480
12172
|
- Diagnosing why a tool keeps falling back to skeleton mode
|
|
11481
12173
|
- Understanding which tools dominate the user's token budget
|
|
11482
12174
|
|
|
12175
|
+
**Skip when:** there's no budget complaint to investigate \u2014 telemetry
|
|
12176
|
+
exists for tuning, not browsing. Reserve this skill for "this tool
|
|
12177
|
+
keeps hitting the budget" or scheduled tuning passes.
|
|
12178
|
+
|
|
11483
12179
|
## Steps
|
|
11484
12180
|
|
|
11485
12181
|
1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
|
|
@@ -11542,8 +12238,8 @@ function skillFilePath(name) {
|
|
|
11542
12238
|
// packages/core/src/install/installer.ts
|
|
11543
12239
|
function installHarness(opts = {}) {
|
|
11544
12240
|
const cwd = opts.cwd ?? process.cwd();
|
|
11545
|
-
const projectRoot =
|
|
11546
|
-
const stat =
|
|
12241
|
+
const projectRoot = path37.resolve(cwd);
|
|
12242
|
+
const stat = fs29.statSync(projectRoot);
|
|
11547
12243
|
if (!stat.isDirectory()) {
|
|
11548
12244
|
throw new Error(`installHarness: ${projectRoot} is not a directory`);
|
|
11549
12245
|
}
|
|
@@ -11591,20 +12287,20 @@ function resolveExtraHosts(ids, warnings) {
|
|
|
11591
12287
|
return HOST_ADAPTERS.filter((a) => requested.has(a.id));
|
|
11592
12288
|
}
|
|
11593
12289
|
function safeJoin(root, name) {
|
|
11594
|
-
const target =
|
|
11595
|
-
const rootResolved =
|
|
12290
|
+
const target = path37.resolve(root, name);
|
|
12291
|
+
const rootResolved = path37.resolve(root);
|
|
11596
12292
|
const caseFold = process.platform === "darwin" || process.platform === "win32";
|
|
11597
12293
|
const t = caseFold ? target.toLowerCase() : target;
|
|
11598
12294
|
const r = caseFold ? rootResolved.toLowerCase() : rootResolved;
|
|
11599
|
-
if (!t.startsWith(r +
|
|
12295
|
+
if (!t.startsWith(r + path37.sep) && t !== r) {
|
|
11600
12296
|
throw new Error(`installHarness: refusing to write outside project root: ${target}`);
|
|
11601
12297
|
}
|
|
11602
12298
|
return target;
|
|
11603
12299
|
}
|
|
11604
12300
|
function writeRulesBlock(projectRoot, filename, opts) {
|
|
11605
12301
|
const filePath = safeJoin(projectRoot, filename);
|
|
11606
|
-
const existed =
|
|
11607
|
-
const existing = existed ?
|
|
12302
|
+
const existed = fs29.existsSync(filePath);
|
|
12303
|
+
const existing = existed ? fs29.readFileSync(filePath, "utf-8") : "";
|
|
11608
12304
|
if (existed) {
|
|
11609
12305
|
const block = extractBlock(existing, RULES_BLOCK_NAME);
|
|
11610
12306
|
if (block) {
|
|
@@ -11622,7 +12318,7 @@ function writeRulesBlock(projectRoot, filename, opts) {
|
|
|
11622
12318
|
}
|
|
11623
12319
|
const next = upsertBlock(existing, RULES_BLOCK_NAME, RULES_BLOCK_CONTENT);
|
|
11624
12320
|
if (!opts.dryRun) {
|
|
11625
|
-
|
|
12321
|
+
fs29.writeFileSync(filePath, next, "utf-8");
|
|
11626
12322
|
}
|
|
11627
12323
|
return {
|
|
11628
12324
|
path: filePath,
|
|
@@ -11635,15 +12331,15 @@ function writeRulesBlock(projectRoot, filename, opts) {
|
|
|
11635
12331
|
function writeHooksJson(projectRoot, opts) {
|
|
11636
12332
|
const dir = safeJoin(projectRoot, ".claude");
|
|
11637
12333
|
const filePath = safeJoin(projectRoot, ".claude/hooks.json");
|
|
11638
|
-
const existed =
|
|
12334
|
+
const existed = fs29.existsSync(filePath);
|
|
11639
12335
|
let current = {};
|
|
11640
12336
|
if (existed) {
|
|
11641
12337
|
try {
|
|
11642
|
-
const text =
|
|
12338
|
+
const text = fs29.readFileSync(filePath, "utf-8");
|
|
11643
12339
|
current = JSON.parse(text);
|
|
11644
12340
|
} catch (err) {
|
|
11645
12341
|
opts.warnings.push(
|
|
11646
|
-
`Could not parse existing ${
|
|
12342
|
+
`Could not parse existing ${path37.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
|
|
11647
12343
|
);
|
|
11648
12344
|
current = {};
|
|
11649
12345
|
}
|
|
@@ -11660,12 +12356,12 @@ function writeHooksJson(projectRoot, opts) {
|
|
|
11660
12356
|
const nextJson = JSON.stringify(merged, null, 2) + "\n";
|
|
11661
12357
|
let alreadyCorrect = false;
|
|
11662
12358
|
if (existed) {
|
|
11663
|
-
const currentText =
|
|
12359
|
+
const currentText = fs29.readFileSync(filePath, "utf-8");
|
|
11664
12360
|
if (currentText === nextJson) alreadyCorrect = true;
|
|
11665
12361
|
}
|
|
11666
12362
|
if (!opts.dryRun && !alreadyCorrect) {
|
|
11667
|
-
|
|
11668
|
-
|
|
12363
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
12364
|
+
fs29.writeFileSync(filePath, nextJson, "utf-8");
|
|
11669
12365
|
}
|
|
11670
12366
|
return {
|
|
11671
12367
|
path: filePath,
|
|
@@ -11687,19 +12383,19 @@ function isCtxloomEntry(entry, expectedMatcher) {
|
|
|
11687
12383
|
}
|
|
11688
12384
|
function writeHostAdapter(projectRoot, adapter, opts) {
|
|
11689
12385
|
const filePath = safeJoin(projectRoot, adapter.path);
|
|
11690
|
-
const dir =
|
|
11691
|
-
const existed =
|
|
12386
|
+
const dir = path37.dirname(filePath);
|
|
12387
|
+
const existed = fs29.existsSync(filePath);
|
|
11692
12388
|
const rendered = adapter.render();
|
|
11693
12389
|
let alreadyCorrect = false;
|
|
11694
12390
|
if (existed) {
|
|
11695
|
-
const current =
|
|
12391
|
+
const current = fs29.readFileSync(filePath, "utf-8");
|
|
11696
12392
|
if (adapter.isCanonical(current)) {
|
|
11697
12393
|
alreadyCorrect = true;
|
|
11698
12394
|
}
|
|
11699
12395
|
}
|
|
11700
12396
|
if (!opts.dryRun && !alreadyCorrect) {
|
|
11701
|
-
|
|
11702
|
-
|
|
12397
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
12398
|
+
fs29.writeFileSync(filePath, rendered, "utf-8");
|
|
11703
12399
|
}
|
|
11704
12400
|
return {
|
|
11705
12401
|
path: filePath,
|
|
@@ -11712,16 +12408,16 @@ function writeHostAdapter(projectRoot, adapter, opts) {
|
|
|
11712
12408
|
function writeSkill(projectRoot, skill, opts) {
|
|
11713
12409
|
const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
|
|
11714
12410
|
const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
|
|
11715
|
-
const existed =
|
|
12411
|
+
const existed = fs29.existsSync(filePath);
|
|
11716
12412
|
let alreadyCorrect = false;
|
|
11717
12413
|
if (existed) {
|
|
11718
|
-
if (
|
|
12414
|
+
if (fs29.readFileSync(filePath, "utf-8") === skill.content) {
|
|
11719
12415
|
alreadyCorrect = true;
|
|
11720
12416
|
}
|
|
11721
12417
|
}
|
|
11722
12418
|
if (!opts.dryRun && !alreadyCorrect) {
|
|
11723
|
-
|
|
11724
|
-
|
|
12419
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
12420
|
+
fs29.writeFileSync(filePath, skill.content, "utf-8");
|
|
11725
12421
|
}
|
|
11726
12422
|
return {
|
|
11727
12423
|
path: filePath,
|
|
@@ -11734,17 +12430,17 @@ function writeSkill(projectRoot, skill, opts) {
|
|
|
11734
12430
|
function writeSessionStartScript(projectRoot, opts) {
|
|
11735
12431
|
const dir = safeJoin(projectRoot, ".claude/hooks");
|
|
11736
12432
|
const filePath = safeJoin(projectRoot, ".claude/hooks/session-start.sh");
|
|
11737
|
-
const existed =
|
|
12433
|
+
const existed = fs29.existsSync(filePath);
|
|
11738
12434
|
let alreadyCorrect = false;
|
|
11739
12435
|
if (existed) {
|
|
11740
|
-
const current =
|
|
12436
|
+
const current = fs29.readFileSync(filePath, "utf-8");
|
|
11741
12437
|
if (current === SESSION_START_FULL) alreadyCorrect = true;
|
|
11742
12438
|
}
|
|
11743
12439
|
if (!opts.dryRun && !alreadyCorrect) {
|
|
11744
|
-
|
|
11745
|
-
|
|
12440
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
12441
|
+
fs29.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
|
|
11746
12442
|
try {
|
|
11747
|
-
|
|
12443
|
+
fs29.chmodSync(filePath, 493);
|
|
11748
12444
|
} catch {
|
|
11749
12445
|
}
|
|
11750
12446
|
}
|
|
@@ -11793,6 +12489,8 @@ export {
|
|
|
11793
12489
|
loadTrendSeries,
|
|
11794
12490
|
loadFileRiskHistory,
|
|
11795
12491
|
Skeletonizer,
|
|
12492
|
+
inspectVectorsDb,
|
|
12493
|
+
cleanupVectors,
|
|
11796
12494
|
renderStatusXml,
|
|
11797
12495
|
__resetLearnedSuggestionsCacheForTests,
|
|
11798
12496
|
learnSuggestionsFromTelemetry,
|
|
@@ -11805,6 +12503,7 @@ export {
|
|
|
11805
12503
|
emitTaskBudgetBreached,
|
|
11806
12504
|
ToolRegistry,
|
|
11807
12505
|
detectChanges,
|
|
12506
|
+
computeSemanticSimilar,
|
|
11808
12507
|
getImpactRadius,
|
|
11809
12508
|
buildBlastRadiusXml,
|
|
11810
12509
|
validateAlias,
|
|
@@ -11889,4 +12588,4 @@ export {
|
|
|
11889
12588
|
skillFilePath,
|
|
11890
12589
|
installHarness
|
|
11891
12590
|
};
|
|
11892
|
-
//# sourceMappingURL=chunk-
|
|
12591
|
+
//# sourceMappingURL=chunk-FFCLVZCO.js.map
|