weavatrix 0.1.3 → 0.2.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 +168 -63
- package/SECURITY.md +25 -8
- package/package.json +2 -2
- package/skill/SKILL.md +108 -39
- package/src/analysis/architecture-contract.js +343 -0
- package/src/analysis/audit-debt.js +94 -0
- package/src/analysis/change-classification.js +509 -0
- package/src/analysis/cycle-route.js +14 -0
- package/src/analysis/dead-check.js +13 -6
- package/src/analysis/dead-code-review.js +245 -0
- package/src/analysis/dep-check-ecosystems.js +163 -0
- package/src/analysis/dep-check.js +69 -147
- package/src/analysis/dep-rules.js +47 -31
- package/src/analysis/duplicates.compute.js +47 -7
- package/src/analysis/endpoints-java.js +186 -0
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +18 -5
- package/src/analysis/findings.js +8 -1
- package/src/analysis/git-history.js +549 -0
- package/src/analysis/git-ref-graph.js +74 -0
- package/src/analysis/graph-analysis.aggregate.js +29 -28
- package/src/analysis/graph-analysis.edges.js +24 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/http-contracts.js +581 -0
- package/src/analysis/internal-audit.collect.js +43 -2
- package/src/analysis/internal-audit.reach.js +52 -1
- package/src/analysis/internal-audit.run.js +66 -13
- package/src/analysis/java-source.js +36 -0
- package/src/analysis/package-reachability.js +39 -0
- package/src/analysis/static-test-reachability.js +133 -0
- package/src/build-graph.js +72 -13
- package/src/graph/build-worker.js +3 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-js.js +71 -12
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +27 -0
- package/src/graph/file-lock.js +69 -0
- package/src/graph/freshness-probe.js +141 -0
- package/src/graph/graph-filter.js +28 -11
- package/src/graph/incremental-refresh.js +232 -0
- package/src/graph/internal-builder.barrels.js +169 -0
- package/src/graph/internal-builder.build.js +111 -16
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.langs.js +2 -1
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/layout.js +21 -5
- package/src/graph/relations.js +4 -0
- package/src/graph/repo-registry.js +124 -0
- package/src/mcp/catalog.mjs +64 -21
- package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
- package/src/mcp/evidence-snapshot.common.mjs +160 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
- package/src/mcp/evidence-snapshot.health.mjs +95 -0
- package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
- package/src/mcp/evidence-snapshot.mjs +50 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
- package/src/mcp/evidence-snapshot.structure.mjs +81 -0
- package/src/mcp/graph-context.mjs +106 -181
- package/src/mcp/graph-diff.mjs +217 -0
- package/src/mcp/staleness-notice.mjs +20 -0
- package/src/mcp/sync-evidence.mjs +418 -0
- package/src/mcp/sync-payload.mjs +194 -8
- package/src/mcp/tool-result.mjs +51 -0
- package/src/mcp/tools-actions.mjs +114 -33
- package/src/mcp/tools-architecture.mjs +144 -0
- package/src/mcp/tools-company.mjs +273 -0
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +36 -68
- package/src/mcp/tools-health.mjs +354 -20
- package/src/mcp/tools-history.mjs +22 -0
- package/src/mcp/tools-impact-change.mjs +261 -0
- package/src/mcp/tools-impact.mjs +35 -11
- package/src/mcp-server.mjs +100 -17
- package/src/mcp-source-tools.mjs +11 -3
- package/src/path-classification.js +201 -0
- package/src/path-ignore.js +69 -0
- package/src/security/typosquat.js +1 -1
|
@@ -8,10 +8,20 @@ import { specToPkg } from "./builder/spec-pkg.js";
|
|
|
8
8
|
import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
|
|
9
9
|
import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
|
|
10
10
|
import { buildResolvers } from "./internal-builder.resolvers.js";
|
|
11
|
+
import { addJavaReferences } from "./internal-builder.java.js";
|
|
12
|
+
import { assignDeterministicCommunities } from "./community.js";
|
|
13
|
+
import { resolveJsBarrels } from "./internal-builder.barrels.js";
|
|
14
|
+
import { snapshotRepository } from "./incremental-refresh.js";
|
|
11
15
|
|
|
12
16
|
// Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
|
|
13
17
|
export async function buildInternalGraph(repoDir, opts = {}) {
|
|
14
|
-
const
|
|
18
|
+
const rel = (p) => relative(repoDir, p).replace(/\\/g, "/");
|
|
19
|
+
const allFiles = walk(repoDir);
|
|
20
|
+
const snapshot = snapshotRepository(repoDir, allFiles);
|
|
21
|
+
const requestedFiles = Array.isArray(opts.includeFiles)
|
|
22
|
+
? new Set(opts.includeFiles.map((file) => String(file).replace(/\\/g, "/").replace(/^\.\//, "")))
|
|
23
|
+
: null;
|
|
24
|
+
const files = requestedFiles ? allFiles.filter((file) => requestedFiles.has(rel(file))) : allFiles;
|
|
15
25
|
// Lazy grammar loading: compile only the WASMs for languages this repo actually contains.
|
|
16
26
|
const wanted = new Set();
|
|
17
27
|
for (const f of files) { const g = EXT_LANG[extname(f)]; if (g) wanted.add(g); }
|
|
@@ -21,13 +31,31 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
21
31
|
const caps = (grammar, src, root) => { const query = src && q(grammar, src); return query ? query.captures(root) : []; };
|
|
22
32
|
const field = (n, f) => (n && n.childForFieldName ? n.childForFieldName(f) : null);
|
|
23
33
|
|
|
24
|
-
const
|
|
25
|
-
const fileSet = new Set(files.map(rel));
|
|
34
|
+
const fileSet = new Set(allFiles.map(rel));
|
|
26
35
|
const nodes = []; const links = []; const nodeIds = new Set(); const nodeById = new Map();
|
|
27
36
|
const addNode = (n) => { if (!nodeIds.has(n.id)) { nodeIds.add(n.id); nodes.push(n); nodeById.set(n.id, n); } };
|
|
28
37
|
const perFileSymbols = new Map();
|
|
29
38
|
const symByFileName = new Map();
|
|
30
39
|
const importedLocals = new Map();
|
|
40
|
+
const jsExports = new Map();
|
|
41
|
+
if (requestedFiles && opts.baseGraph?.jsExportRecords) {
|
|
42
|
+
for (const [file, records] of Object.entries(opts.baseGraph.jsExportRecords)) {
|
|
43
|
+
if (!requestedFiles.has(file) && Array.isArray(records)) jsExports.set(file, records.map((record) => ({ ...record })));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (requestedFiles && Array.isArray(opts.baseGraph?.nodes)) {
|
|
47
|
+
for (const node of opts.baseGraph.nodes) {
|
|
48
|
+
const id = String(node?.id || "");
|
|
49
|
+
const file = String(node?.source_file || (id.includes("#") ? id.slice(0, id.indexOf("#")) : id)).replace(/\\/g, "/");
|
|
50
|
+
if (!id.includes("#") || requestedFiles.has(file) || !fileSet.has(file)) continue;
|
|
51
|
+
const match = id.match(/#([A-Za-z_$][\w$]*)@\d+/);
|
|
52
|
+
if (!match) continue;
|
|
53
|
+
let names = symByFileName.get(file);
|
|
54
|
+
if (!names) symByFileName.set(file, (names = new Map()));
|
|
55
|
+
if (!names.has(match[1])) names.set(match[1], id);
|
|
56
|
+
nodeById.set(id, node);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
31
59
|
// Bare-package imports (axios, node:fs, @scope/x) — the graph can't resolve them to a repo file, but
|
|
32
60
|
// dependency analysis NEEDS them (unused/missing deps). Additive top-level array; nodes/links untouched.
|
|
33
61
|
const externalImports = [];
|
|
@@ -50,13 +78,15 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
50
78
|
// giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
|
|
51
79
|
if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); continue; }
|
|
52
80
|
if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
|
|
81
|
+
if (typeof opts.onParseFile === "function") opts.onParseFile(fileRel);
|
|
53
82
|
const parser = new Parser(); parser.setLanguage(langs[grammar]);
|
|
54
83
|
let tree; try { tree = parser.parse(code); } catch { continue; }
|
|
55
84
|
|
|
56
85
|
const syms = []; const nameToId = new Map(); const moduleNameToId = new Map();
|
|
57
86
|
const addSym = (name, line, callable, extra) => {
|
|
58
87
|
if (!name || !/^[A-Za-z_$][\w$]*$/.test(name)) return;
|
|
59
|
-
const
|
|
88
|
+
const suffix = /^:[A-Za-z0-9_-]+$/.test(extra?.idSuffix || "") ? extra.idSuffix : "";
|
|
89
|
+
const id = `${fileRel}#${name}@${line}${suffix}`; if (nodeIds.has(id)) return;
|
|
60
90
|
const sourceNode = extra && extra.sourceNode;
|
|
61
91
|
const endLine = sourceNode?.endPosition ? sourceNode.endPosition.row + 1 : 0;
|
|
62
92
|
let complexity = null;
|
|
@@ -82,8 +112,15 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
82
112
|
syms.push({ id, name, start: line, end: endLine >= line ? endLine : 0 });
|
|
83
113
|
if (!nameToId.has(name)) nameToId.set(name, id);
|
|
84
114
|
if (extra?.moduleDeclaration && !moduleNameToId.has(name)) moduleNameToId.set(name, id);
|
|
115
|
+
return id;
|
|
85
116
|
};
|
|
86
117
|
const imports = new Map(); importedLocals.set(fileRel, imports);
|
|
118
|
+
const recordJsExport = (record) => {
|
|
119
|
+
if (!record) return;
|
|
120
|
+
const records = jsExports.get(fileRel) || [];
|
|
121
|
+
records.push(record);
|
|
122
|
+
jsExports.set(fileRel, records);
|
|
123
|
+
};
|
|
87
124
|
const addImportEdge = (tgt, meta = {}) => {
|
|
88
125
|
if (!tgt || tgt === fileRel) return;
|
|
89
126
|
links.push({
|
|
@@ -92,6 +129,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
92
129
|
relation: meta.relation || "imports",
|
|
93
130
|
confidence: "EXTRACTED",
|
|
94
131
|
...(typeof meta.typeOnly === "boolean" ? { typeOnly: meta.typeOnly } : {}),
|
|
132
|
+
...(meta.compileOnly === true ? { compileOnly: true } : {}),
|
|
95
133
|
...(meta.line ? { line: meta.line } : {}),
|
|
96
134
|
...(meta.specifier ? { specifier: meta.specifier } : {}),
|
|
97
135
|
});
|
|
@@ -112,7 +150,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
112
150
|
// Post-hoc export flag (export {a}, export default X, CJS module.exports) — declarations are flagged at addSym time.
|
|
113
151
|
const markExported = (name) => { const id = moduleNameToId.get(name); const n = id && nodeById.get(id); if (n) n.exported = true; };
|
|
114
152
|
|
|
115
|
-
try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, fileSet, ...resolvers }); }
|
|
153
|
+
try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, recordJsExport, fileSet, ...resolvers }); }
|
|
116
154
|
catch (e) { /* one bad file never sinks the whole build */ void e; }
|
|
117
155
|
|
|
118
156
|
syms.sort((a, b) => a.start - b.start);
|
|
@@ -135,6 +173,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
135
173
|
let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
|
|
136
174
|
for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
|
|
137
175
|
}
|
|
176
|
+
const { resolveNamespaceMember } = resolveJsBarrels({ jsExports, importedLocals, links });
|
|
138
177
|
// Exact source ranges can overlap (a named function nested inside another function). Attribute a call
|
|
139
178
|
// to the innermost matching symbol, not whichever outer declaration happened to be added first.
|
|
140
179
|
const enclosing = (fileRel, line) => {
|
|
@@ -149,9 +188,24 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
149
188
|
const local = symByFileName.get(fileRel); if (local && local.has(name)) return local.get(name);
|
|
150
189
|
if (sharesDirScope(fileRel)) { const d = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : ""; const dm = goDirSymbols.get(d); if (dm && dm.has(name)) return dm.get(name); }
|
|
151
190
|
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
|
|
152
|
-
if (imp && imp.targetFile) {
|
|
191
|
+
if (imp && imp.targetFile) {
|
|
192
|
+
const targetFile = imp.originFile || imp.targetFile;
|
|
193
|
+
const importedName = imp.originName || imp.imported;
|
|
194
|
+
const tf = symByFileName.get(targetFile); if (tf && tf.has(importedName)) return tf.get(importedName);
|
|
195
|
+
}
|
|
153
196
|
return null;
|
|
154
197
|
};
|
|
198
|
+
const javaTypeKinds = new Set(["class", "interface", "enum", "record", "annotation"]);
|
|
199
|
+
const resolveJavaType = (name, fileRel) => {
|
|
200
|
+
const imp = importedLocals.get(fileRel)?.get(name);
|
|
201
|
+
if (imp?.targetFile) {
|
|
202
|
+
const symbols = symByFileName.get(imp.targetFile);
|
|
203
|
+
const target = symbols?.get(imp.imported) || symbols?.get(name);
|
|
204
|
+
if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target;
|
|
205
|
+
}
|
|
206
|
+
const target = symByFileName.get(fileRel)?.get(name);
|
|
207
|
+
return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null;
|
|
208
|
+
};
|
|
155
209
|
for (const abs of files) {
|
|
156
210
|
const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
|
|
157
211
|
const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
|
|
@@ -175,14 +229,30 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
175
229
|
const target = dm && dm.get(fld.text);
|
|
176
230
|
if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
|
|
177
231
|
}
|
|
178
|
-
for (const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
232
|
+
for (const heritageSpec of lang.heritage || []) {
|
|
233
|
+
const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
|
|
234
|
+
const relation = typeof heritageSpec === "string" ? "inherits" : (heritageSpec.relation || "inherits");
|
|
235
|
+
for (const cap of caps(grammar, query, tree.rootNode)) {
|
|
236
|
+
const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
|
|
237
|
+
const target = FAMILY[grammar] === "java" ? resolveJavaType(cap.node.text, fileRel) : resolveCall(cap.node.text, fileRel);
|
|
238
|
+
if (target && target !== cls.id) links.push({ source: cls.id, target, relation, confidence: "INFERRED" });
|
|
239
|
+
}
|
|
182
240
|
}
|
|
183
241
|
// JSX is a real symbol use even though it is not a call_expression. Resolve imported components to their
|
|
184
242
|
// declaration so component fan-in and unused-export checks do not claim `<SettingsView />` is unreferenced.
|
|
185
243
|
if (FAMILY[grammar] === "js") {
|
|
244
|
+
// Namespace calls (`ui.run()`) need the member name before an export-star facade can be resolved.
|
|
245
|
+
for (const cap of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
|
|
246
|
+
const object = field(cap.node, "object"), property = field(cap.node, "property");
|
|
247
|
+
if (!object || object.type !== "identifier" || !property) continue;
|
|
248
|
+
const imp = importedLocals.get(fileRel)?.get(object.text);
|
|
249
|
+
if (!imp || imp.imported !== "*" || imp.typeOnly) continue;
|
|
250
|
+
const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
|
|
251
|
+
if (origin.status !== "resolved") continue;
|
|
252
|
+
const target = symByFileName.get(origin.origin.file)?.get(origin.origin.name);
|
|
253
|
+
const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
254
|
+
if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
|
|
255
|
+
}
|
|
186
256
|
for (const cap of caps(grammar, `[
|
|
187
257
|
(jsx_opening_element name: (_) @jsx)
|
|
188
258
|
(jsx_self_closing_element name: (_) @jsx)
|
|
@@ -193,9 +263,14 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
193
263
|
if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue; // undotted lowercase tags are platform/intrinsic elements
|
|
194
264
|
const imp = importedLocals.get(fileRel)?.get(localName);
|
|
195
265
|
if (!imp || !imp.targetFile || imp.typeOnly) continue;
|
|
196
|
-
|
|
266
|
+
let targetFile = imp.originFile || imp.targetFile;
|
|
267
|
+
let importedName = imp.originName || imp.imported;
|
|
268
|
+
if (imp.imported === "*" && parts.length > 1) {
|
|
269
|
+
const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
|
|
270
|
+
if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
|
|
271
|
+
}
|
|
272
|
+
const targetSymbols = symByFileName.get(targetFile);
|
|
197
273
|
if (!targetSymbols) continue;
|
|
198
|
-
const importedName = imp.imported === "*" && parts.length > 1 ? parts[parts.length - 1] : imp.imported;
|
|
199
274
|
const target = targetSymbols.get(importedName) || targetSymbols.get(localName);
|
|
200
275
|
if (!target) continue;
|
|
201
276
|
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
@@ -222,6 +297,9 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
222
297
|
const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
|
|
223
298
|
}
|
|
224
299
|
}
|
|
300
|
+
if (FAMILY[grammar] === "java") {
|
|
301
|
+
addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links });
|
|
302
|
+
}
|
|
225
303
|
tree.delete();
|
|
226
304
|
}
|
|
227
305
|
|
|
@@ -238,17 +316,34 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
238
316
|
|
|
239
317
|
// community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
|
|
240
318
|
// app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
|
|
241
|
-
|
|
242
|
-
const commOf = new Map(); let commSeq = 0;
|
|
243
|
-
for (const n of nodes) { const fo = folderOf(n.source_file); if (!commOf.has(fo)) commOf.set(fo, commSeq++); n.community = commOf.get(fo); }
|
|
319
|
+
assignDeterministicCommunities(nodes);
|
|
244
320
|
|
|
245
321
|
// extImportsV: bump when the externalImports schema/coverage changes (v2 = go/python ecosystems) —
|
|
246
322
|
// deps-engine rebuilds in memory when a saved graph is older than this.
|
|
247
|
-
|
|
323
|
+
// edgeTypesV 2 adds language-neutral compile-only edges (currently Rust mod/use/re-export) on top
|
|
324
|
+
// of v1's TypeScript typeOnly classification.
|
|
325
|
+
return {
|
|
326
|
+
nodes,
|
|
327
|
+
links,
|
|
328
|
+
externalImports,
|
|
329
|
+
extImportsV: 2,
|
|
330
|
+
edgeTypesV: 2,
|
|
331
|
+
complexityV: 1,
|
|
332
|
+
repoBoundaryV: 1,
|
|
333
|
+
barrelResolutionV: 1,
|
|
334
|
+
extractorSchemaV: 1,
|
|
335
|
+
jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
336
|
+
fileHashes: snapshot.fileHashes,
|
|
337
|
+
fileExportSignatures: snapshot.fileExportSignatures,
|
|
338
|
+
controlHashes: snapshot.controlHashes,
|
|
339
|
+
graphRevision: snapshot.revision,
|
|
340
|
+
...(requestedFiles ? { incrementalScope: true } : {}),
|
|
341
|
+
};
|
|
248
342
|
}
|
|
249
343
|
|
|
250
344
|
// Build + write graph.json to outPath (creating the dir). Returns { ok, nodes, links, graphJson }.
|
|
251
345
|
export async function writeInternalGraph(repoDir, outPath, opts = {}) {
|
|
346
|
+
if (Array.isArray(opts.includeFiles)) throw new Error("refusing to write a scoped incremental graph as a complete graph");
|
|
252
347
|
const graph = await buildInternalGraph(repoDir, opts);
|
|
253
348
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
254
349
|
writeFileSync(outPath, JSON.stringify(graph), "utf8");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Resolve Java type usages only when they land on declarations that exist in the graph. Synthetic nodes for
|
|
2
|
+
// String/List/annotations inflate metrics without adding navigation value, so unresolved/external types vanish.
|
|
3
|
+
export function addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links }) {
|
|
4
|
+
const refSeen = new Set();
|
|
5
|
+
const isHeritage = (node) => {
|
|
6
|
+
let current = node?.parent;
|
|
7
|
+
for (let hops = 0; current && hops < 8; hops++, current = current.parent) {
|
|
8
|
+
if (["superclass", "super_interfaces", "extends_interfaces"].includes(current.type)) return true;
|
|
9
|
+
if (["class_declaration", "interface_declaration", "enum_declaration", "record_declaration"].includes(current.type)) return false;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
};
|
|
13
|
+
const emitTypeRef = (name, node) => {
|
|
14
|
+
if (!name || isHeritage(node)) return;
|
|
15
|
+
const target = resolveJavaType(name, fileRel);
|
|
16
|
+
if (!target) return;
|
|
17
|
+
const owner = enclosing(fileRel, node.startPosition.row + 1);
|
|
18
|
+
const source = owner?.id || fileRel;
|
|
19
|
+
if (source === target) return;
|
|
20
|
+
const key = `${source}>${target}`;
|
|
21
|
+
if (refSeen.has(key)) return;
|
|
22
|
+
refSeen.add(key);
|
|
23
|
+
links.push({ source, target, relation: "references", confidence: "INFERRED", usage: "type", line: node.startPosition.row + 1 });
|
|
24
|
+
};
|
|
25
|
+
for (const cap of caps(grammar, `(type_identifier) @type`, tree.rootNode)) {
|
|
26
|
+
// A qualified type is handled once by its outer scoped_type_identifier. Inner package segments could
|
|
27
|
+
// otherwise bind to unrelated same-named project types.
|
|
28
|
+
if (cap.node.parent?.type !== "scoped_type_identifier") emitTypeRef(cap.node.text, cap.node);
|
|
29
|
+
}
|
|
30
|
+
for (const cap of caps(grammar, `(scoped_type_identifier) @type`, tree.rootNode)) {
|
|
31
|
+
if (cap.node.parent?.type !== "scoped_type_identifier") emitTypeRef(cap.node.text.split(".").pop(), cap.node);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -9,6 +9,7 @@ import { execFileSync } from "node:child_process";
|
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { isPathInside } from "../repo-path.js";
|
|
11
11
|
import { childProcessEnv } from "../child-env.js";
|
|
12
|
+
import { filterWeavatrixIgnored } from "../path-ignore.js";
|
|
12
13
|
import LANG_JS from "./builder/lang-js.js";
|
|
13
14
|
import LANG_PY from "./builder/lang-python.js";
|
|
14
15
|
import LANG_GO from "./builder/lang-go.js";
|
|
@@ -132,7 +133,7 @@ function gitFileUniverse(dir) {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
function walk(dir) {
|
|
135
|
-
return gitFileUniverse(dir) ?? walkFallback(dir);
|
|
136
|
+
return filterWeavatrixIgnored(dir, gitFileUniverse(dir) ?? walkFallback(dir));
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
export { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Per-repo resolution context shared by the language modules: JS/TS path-aliases + relative imports, Python
|
|
2
|
-
// dotted/relative modules, Go package dirs, Java class files, and web hrefs /
|
|
2
|
+
// dotted/relative modules, Go package dirs, Rust crate/module paths, Java class files, and web hrefs /
|
|
3
|
+
// the CSS selector index.
|
|
3
4
|
// (Split from internal-builder.js — see its doc comment for the overall architecture.)
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { join, dirname } from "node:path";
|
|
@@ -47,6 +48,112 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
47
48
|
return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
|
|
48
49
|
};
|
|
49
50
|
|
|
51
|
+
// Rust modules are files, but their paths are logical rather than simple source-relative imports:
|
|
52
|
+
// `foo.rs` owns children below `foo/`, while lib.rs/main.rs/mod.rs own siblings. Keep the resolver
|
|
53
|
+
// filesystem-only and crate-local: Cargo/external dependencies belong to dependency analysis, not to
|
|
54
|
+
// the internal module graph.
|
|
55
|
+
const cleanRustRel = (p) => String(p || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "").replace(/^\.$/, "");
|
|
56
|
+
const rustDir = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? "" : p.slice(0, i); };
|
|
57
|
+
const rustBase = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? p : p.slice(i + 1); };
|
|
58
|
+
const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((x) => x != null && x !== "")));
|
|
59
|
+
const rustFiles = new Set([...fileSet].filter((fr) => fr.endsWith(".rs")));
|
|
60
|
+
const rustRoots = new Map();
|
|
61
|
+
for (const fr of rustFiles) {
|
|
62
|
+
const base = rustBase(fr);
|
|
63
|
+
if (base !== "lib.rs" && base !== "main.rs") continue;
|
|
64
|
+
const dir = rustDir(fr);
|
|
65
|
+
let root = rustRoots.get(dir);
|
|
66
|
+
if (!root) rustRoots.set(dir, (root = { base: dir, lib: null, main: null }));
|
|
67
|
+
root[base === "lib.rs" ? "lib" : "main"] = fr;
|
|
68
|
+
}
|
|
69
|
+
const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length);
|
|
70
|
+
const rustContext = (fromRel) => {
|
|
71
|
+
fromRel = cleanRustRel(fromRel);
|
|
72
|
+
const base = rustBase(fromRel);
|
|
73
|
+
const dir = rustDir(fromRel);
|
|
74
|
+
if (base === "lib.rs" || base === "main.rs") return { base: dir, rootFile: fromRel };
|
|
75
|
+
|
|
76
|
+
// Cargo treats direct files in these conventional folders as independent crate roots. This only
|
|
77
|
+
// affects paths originating in the root file itself; nested module ownership still comes from mod/use.
|
|
78
|
+
if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return { base: dir, rootFile: fromRel };
|
|
79
|
+
for (const root of rustRootList) {
|
|
80
|
+
if (!root.base || fromRel.startsWith(root.base + "/")) return { base: root.base, rootFile: root.lib || root.main };
|
|
81
|
+
}
|
|
82
|
+
return { base: dir, rootFile: fromRel };
|
|
83
|
+
};
|
|
84
|
+
const rustModuleBase = (fromRel) => {
|
|
85
|
+
const ctx = rustContext(fromRel);
|
|
86
|
+
if (ctx.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel);
|
|
87
|
+
const name = rustBase(fromRel);
|
|
88
|
+
if (name === "lib.rs" || name === "main.rs" || name === "mod.rs") return rustDir(fromRel);
|
|
89
|
+
return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ""));
|
|
90
|
+
};
|
|
91
|
+
const rustInlineBase = (fromRel, inlineModules = []) => {
|
|
92
|
+
let base = rustModuleBase(fromRel);
|
|
93
|
+
for (let i = 0; i < inlineModules.length; i++) {
|
|
94
|
+
const mod = inlineModules[i] || {};
|
|
95
|
+
if (mod.path) {
|
|
96
|
+
// Rust Reference: a path attribute on the first inline module is relative to the source file's
|
|
97
|
+
// directory; nested attributes are relative to their containing module's search directory.
|
|
98
|
+
const parent = i === 0 ? rustDir(fromRel) : base;
|
|
99
|
+
base = rustJoin(parent, mod.path);
|
|
100
|
+
} else base = rustJoin(base, mod.name);
|
|
101
|
+
}
|
|
102
|
+
return base;
|
|
103
|
+
};
|
|
104
|
+
const rustModuleFile = (moduleBase, ctx) => {
|
|
105
|
+
moduleBase = cleanRustRel(moduleBase);
|
|
106
|
+
if (moduleBase === cleanRustRel(ctx.base) && ctx.rootFile && rustFiles.has(ctx.rootFile)) return ctx.rootFile;
|
|
107
|
+
const flat = moduleBase + ".rs";
|
|
108
|
+
if (rustFiles.has(flat)) return flat;
|
|
109
|
+
const legacy = rustJoin(moduleBase, "mod.rs");
|
|
110
|
+
return rustFiles.has(legacy) ? legacy : null;
|
|
111
|
+
};
|
|
112
|
+
const resolveRustMod = (fromRel, name, { inlineModules = [], explicitPath = "" } = {}) => {
|
|
113
|
+
fromRel = cleanRustRel(fromRel);
|
|
114
|
+
if (explicitPath) {
|
|
115
|
+
const parent = inlineModules.length ? rustInlineBase(fromRel, inlineModules) : rustDir(fromRel);
|
|
116
|
+
const target = rustJoin(parent, explicitPath);
|
|
117
|
+
return rustFiles.has(target) ? target : null;
|
|
118
|
+
}
|
|
119
|
+
const targetBase = rustJoin(rustInlineBase(fromRel, inlineModules), String(name || "").replace(/^r#/, ""));
|
|
120
|
+
return rustModuleFile(targetBase, rustContext(fromRel));
|
|
121
|
+
};
|
|
122
|
+
const resolveRustPath = (fromRel, rawSegments, { inlineModules = [], unqualified = true } = {}) => {
|
|
123
|
+
fromRel = cleanRustRel(fromRel);
|
|
124
|
+
const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || "").split("::"))
|
|
125
|
+
.map((s) => String(s).trim().replace(/^r#/, "")).filter(Boolean);
|
|
126
|
+
if (!segments.length) return null;
|
|
127
|
+
const ctx = rustContext(fromRel);
|
|
128
|
+
const current = rustInlineBase(fromRel, inlineModules);
|
|
129
|
+
let rest = [...segments];
|
|
130
|
+
const starts = [];
|
|
131
|
+
let anchored = false;
|
|
132
|
+
if (rest[0] === "crate") { anchored = true; rest.shift(); starts.push(ctx.base); }
|
|
133
|
+
else if (rest[0] === "self") { anchored = true; rest.shift(); starts.push(current); }
|
|
134
|
+
else if (rest[0] === "super") {
|
|
135
|
+
anchored = true;
|
|
136
|
+
let base = current;
|
|
137
|
+
while (rest[0] === "super") { rest.shift(); base = rustDir(base); }
|
|
138
|
+
if (ctx.base && base !== ctx.base && !base.startsWith(ctx.base + "/")) return null;
|
|
139
|
+
starts.push(base);
|
|
140
|
+
} else if (unqualified) {
|
|
141
|
+
// Rust 2018 resolves a bare use path from the crate root/external prelude. Prefer an internal root
|
|
142
|
+
// module, then allow a lexically-local module for qualified expressions in nested modules.
|
|
143
|
+
starts.push(ctx.base);
|
|
144
|
+
if (current !== ctx.base) starts.push(current);
|
|
145
|
+
} else return null;
|
|
146
|
+
|
|
147
|
+
for (const start of starts) {
|
|
148
|
+
const min = anchored ? 0 : 1; // never reinterpret an unresolved external `serde::X` as the crate root
|
|
149
|
+
for (let used = rest.length; used >= min; used--) {
|
|
150
|
+
const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), ctx);
|
|
151
|
+
if (target) return { targetFile: target, consumed: segments.length - rest.length + used, remaining: rest.slice(used), anchored };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
};
|
|
156
|
+
|
|
50
157
|
// Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
|
|
51
158
|
// Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
|
|
52
159
|
const aliasContexts = new Map();
|
|
@@ -188,5 +295,5 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
188
295
|
return fileSet.has(cand) ? cand : null;
|
|
189
296
|
};
|
|
190
297
|
|
|
191
|
-
return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goRequires };
|
|
298
|
+
return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goRequires };
|
|
192
299
|
}
|
package/src/graph/layout.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// pure filter/analysis helpers so callers get everything from one import:
|
|
3
3
|
// graph-filter.js — pure graph filters (test-mode, path-scope)
|
|
4
4
|
// graph-analysis.js — parse graph.json → file/module/symbol view, hotspots, communities
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { basename, join, resolve } from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { readdirSync, realpathSync } from "node:fs";
|
|
8
10
|
|
|
9
11
|
export * from "./graph-filter.js";
|
|
10
12
|
export * from "../analysis/graph-analysis.js";
|
|
@@ -12,14 +14,28 @@ export * from "../analysis/graph-analysis.js";
|
|
|
12
14
|
// Graphs live in a `weavatrix-graphs/` folder NEXT to the repo (inside the repo's parent folder),
|
|
13
15
|
// never inside the repo itself — the graph is derived data, not source. One folder per repo holds
|
|
14
16
|
// graph.json plus graph.prev.json (saved by rebuild_graph for graph_diff).
|
|
17
|
+
export function graphHomeDir() {
|
|
18
|
+
return resolve(process.env.WEAVATRIX_GRAPH_HOME || join(homedir(), ".weavatrix", "graphs"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function graphStorageKey(repoPath) {
|
|
22
|
+
let absolute;
|
|
23
|
+
try { absolute = realpathSync.native(repoPath); }
|
|
24
|
+
catch { absolute = resolve(repoPath); }
|
|
25
|
+
const normalized = process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
26
|
+
const slug = (basename(absolute) || "repo").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
27
|
+
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
28
|
+
return `${slug}-${digest}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
export function graphOutDirForRepo(repoPath) {
|
|
16
|
-
return join(
|
|
32
|
+
return join(graphHomeDir(), graphStorageKey(repoPath));
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
// A separate dir for a single module's scoped graph, so it never clobbers the repo's graph.
|
|
20
36
|
export function graphOutDirForModule(repoPath, moduleName) {
|
|
21
37
|
const safe = String(moduleName).replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
22
|
-
return join(
|
|
38
|
+
return join(graphOutDirForRepo(repoPath), "modules", safe);
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
// Top-level source folders of a repo (for path-scoped builds).
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Global local registry for repository graphs. Absolute paths never leave the machine; hosted sync
|
|
2
|
+
// uses the opaque UUID. Identity is anchored in each canonical graph folder so simultaneous MCP
|
|
3
|
+
// processes cannot mint different IDs for the same repository.
|
|
4
|
+
import {randomUUID} from 'node:crypto'
|
|
5
|
+
import {existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync} from 'node:fs'
|
|
6
|
+
import {basename, join, relative, resolve} from 'node:path'
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
import {graphStorageKey} from './layout.js'
|
|
9
|
+
import {atomicWriteFileSync, withFileLockSync} from './file-lock.js'
|
|
10
|
+
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
12
|
+
const MARKER = '.repository-id'
|
|
13
|
+
|
|
14
|
+
const normalized = (value) => {
|
|
15
|
+
const path = String(value || '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
16
|
+
return process.platform === 'win32' ? path.toLowerCase() : path
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const realOrResolved = (value) => {
|
|
20
|
+
try { return realpathSync.native(value) } catch { return resolve(value) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const canonicalGraphDir = (repoPath, graphHome) => join(resolve(graphHome), graphStorageKey(repoPath))
|
|
24
|
+
const markerPath = (graphDir) => join(graphDir, MARKER)
|
|
25
|
+
|
|
26
|
+
export function registryPath(graphHome) { return join(graphHome, 'repositories.json') }
|
|
27
|
+
|
|
28
|
+
export function readRepositoryRegistry(graphHome) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(registryPath(graphHome), 'utf8'))
|
|
31
|
+
return parsed?.repositoryRegistryV === 1 && Array.isArray(parsed.repositories)
|
|
32
|
+
? parsed.repositories.filter((item) => item && typeof item === 'object')
|
|
33
|
+
: []
|
|
34
|
+
} catch { return [] }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeRegistry(graphHome, repositories) {
|
|
38
|
+
atomicWriteFileSync(registryPath(graphHome), JSON.stringify({repositoryRegistryV: 1, repositories}, null, 2), 'utf8')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readMarker(graphDir) {
|
|
42
|
+
try {
|
|
43
|
+
const id = readFileSync(markerPath(graphDir), 'utf8').trim()
|
|
44
|
+
return UUID.test(id) ? id : null
|
|
45
|
+
} catch { return null }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createOrReadMarker(graphDir, preferredId) {
|
|
49
|
+
mkdirSync(graphDir, {recursive: true})
|
|
50
|
+
const path = markerPath(graphDir)
|
|
51
|
+
const existing = readMarker(graphDir)
|
|
52
|
+
if (existing) return existing
|
|
53
|
+
if (existsSync(path)) throw new Error(`invalid repository identity marker: ${path}`)
|
|
54
|
+
const candidate = UUID.test(String(preferredId || '')) ? preferredId : randomUUID()
|
|
55
|
+
try { writeFileSync(path, `${candidate}\n`, {encoding: 'utf8', flag: 'wx'}) }
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error?.code !== 'EEXIST') throw error
|
|
58
|
+
const raced = readMarker(graphDir)
|
|
59
|
+
if (!raced) throw new Error(`invalid repository identity marker after concurrent registration: ${path}`)
|
|
60
|
+
return raced
|
|
61
|
+
}
|
|
62
|
+
return candidate
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function registerRepository({repoPath, graphDir, graphHome}) {
|
|
66
|
+
mkdirSync(graphHome, {recursive: true})
|
|
67
|
+
const real = realOrResolved(repoPath)
|
|
68
|
+
const canonical = canonicalGraphDir(real, graphHome)
|
|
69
|
+
if (normalized(realOrResolved(graphDir)) !== normalized(realOrResolved(canonical))) {
|
|
70
|
+
throw new Error(`repository graphs must be registered from the canonical graph directory: ${canonical}`)
|
|
71
|
+
}
|
|
72
|
+
return withFileLockSync(join(graphHome, '.repositories.lock'), () => {
|
|
73
|
+
const key = normalized(real)
|
|
74
|
+
const repositories = readRepositoryRegistry(graphHome)
|
|
75
|
+
let record = repositories.find((item) => normalized(item.repoPath) === key)
|
|
76
|
+
const now = new Date().toISOString()
|
|
77
|
+
const repositoryId = createOrReadMarker(canonical, record?.repositoryId)
|
|
78
|
+
if (!record) {
|
|
79
|
+
record = {repositoryId, repoPath: real, label: basename(real) || 'repo', graphDir: canonical, firstSeenAt: now, lastSeenAt: now}
|
|
80
|
+
repositories.push(record)
|
|
81
|
+
} else {
|
|
82
|
+
record.repositoryId = repositoryId
|
|
83
|
+
record.repoPath = real
|
|
84
|
+
record.graphDir = canonical
|
|
85
|
+
record.label = basename(real) || record.label || 'repo'
|
|
86
|
+
record.lastSeenAt = now
|
|
87
|
+
}
|
|
88
|
+
const deduped = repositories.filter((item, index, all) =>
|
|
89
|
+
normalized(item.repoPath) !== key || all.findIndex((candidate) => normalized(candidate.repoPath) === key) === index)
|
|
90
|
+
deduped.sort((a, b) => String(a.label).localeCompare(String(b.label)) || String(a.repositoryId).localeCompare(String(b.repositoryId)))
|
|
91
|
+
writeRegistry(graphHome, deduped)
|
|
92
|
+
return {...record}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function repositoryRecord(repoPath, graphHome) {
|
|
97
|
+
const real = realOrResolved(repoPath)
|
|
98
|
+
const key = normalized(real)
|
|
99
|
+
return liveRepositoryRecords(graphHome).find((item) => normalized(item.repoPath) === key) || null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validLiveRecord(item, graphHome) {
|
|
103
|
+
if (!UUID.test(String(item?.repositoryId || '')) || typeof item?.repoPath !== 'string' || typeof item?.graphDir !== 'string') return null
|
|
104
|
+
let repoReal
|
|
105
|
+
let graphHomeReal
|
|
106
|
+
let graphReal
|
|
107
|
+
try {
|
|
108
|
+
repoReal = realpathSync.native(item.repoPath)
|
|
109
|
+
graphHomeReal = realpathSync.native(graphHome)
|
|
110
|
+
graphReal = realpathSync.native(item.graphDir)
|
|
111
|
+
if (!statSync(repoReal).isDirectory() || !existsSync(join(repoReal, '.git'))) return null
|
|
112
|
+
if (!statSync(graphReal).isDirectory() || !statSync(join(graphReal, 'graph.json')).isFile()) return null
|
|
113
|
+
} catch { return null }
|
|
114
|
+
if (normalized(graphReal) !== normalized(realOrResolved(canonicalGraphDir(repoReal, graphHomeReal)))) return null
|
|
115
|
+
const rel = relative(graphHomeReal, graphReal)
|
|
116
|
+
if (!rel || rel.startsWith('..') || resolve(graphHomeReal, rel) !== resolve(graphReal)) return null
|
|
117
|
+
if (readMarker(graphReal) !== item.repositoryId) return null
|
|
118
|
+
return {...item, repoPath: repoReal, graphDir: graphReal}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function liveRepositoryRecords(graphHome) {
|
|
122
|
+
if (!existsSync(graphHome)) return []
|
|
123
|
+
return readRepositoryRegistry(graphHome).map((item) => validLiveRecord(item, graphHome)).filter(Boolean)
|
|
124
|
+
}
|