weavatrix 0.1.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/weavatrix-mcp.mjs +6 -0
  4. package/package.json +50 -0
  5. package/skill/SKILL.md +56 -0
  6. package/src/analysis/coverage-reports.js +232 -0
  7. package/src/analysis/dead-check.js +136 -0
  8. package/src/analysis/dep-check.js +310 -0
  9. package/src/analysis/dep-rules.js +221 -0
  10. package/src/analysis/duplicates-worker.js +11 -0
  11. package/src/analysis/duplicates.compute.js +215 -0
  12. package/src/analysis/duplicates.js +15 -0
  13. package/src/analysis/duplicates.run.js +51 -0
  14. package/src/analysis/duplicates.tokenize.js +182 -0
  15. package/src/analysis/endpoints.js +156 -0
  16. package/src/analysis/findings.js +52 -0
  17. package/src/analysis/graph-analysis.aggregate.js +280 -0
  18. package/src/analysis/graph-analysis.js +7 -0
  19. package/src/analysis/graph-analysis.refs.js +146 -0
  20. package/src/analysis/graph-analysis.summaries.js +62 -0
  21. package/src/analysis/internal-audit.collect.js +117 -0
  22. package/src/analysis/internal-audit.js +9 -0
  23. package/src/analysis/internal-audit.reach.js +47 -0
  24. package/src/analysis/internal-audit.run.js +189 -0
  25. package/src/analysis/manifests.js +169 -0
  26. package/src/analysis/source-complexity.ast.js +97 -0
  27. package/src/analysis/source-complexity.constants.js +55 -0
  28. package/src/analysis/source-complexity.js +14 -0
  29. package/src/analysis/source-complexity.report.js +76 -0
  30. package/src/analysis/source-complexity.walk.js +174 -0
  31. package/src/build-graph.js +102 -0
  32. package/src/config.js +5 -0
  33. package/src/graph/build-worker.js +30 -0
  34. package/src/graph/builder/lang-csharp.js +40 -0
  35. package/src/graph/builder/lang-css.js +29 -0
  36. package/src/graph/builder/lang-go.js +53 -0
  37. package/src/graph/builder/lang-html.js +29 -0
  38. package/src/graph/builder/lang-java.js +29 -0
  39. package/src/graph/builder/lang-js.js +168 -0
  40. package/src/graph/builder/lang-python.js +78 -0
  41. package/src/graph/builder/lang-rust.js +45 -0
  42. package/src/graph/builder/spec-pkg.js +87 -0
  43. package/src/graph/graph-filter.js +44 -0
  44. package/src/graph/internal-builder.build.js +217 -0
  45. package/src/graph/internal-builder.js +12 -0
  46. package/src/graph/internal-builder.langs.js +86 -0
  47. package/src/graph/internal-builder.resolvers.js +116 -0
  48. package/src/graph/layout.js +35 -0
  49. package/src/infra/infra-items.js +255 -0
  50. package/src/infra/infra-registry.js +184 -0
  51. package/src/infra/infra.detect.js +119 -0
  52. package/src/infra/infra.js +38 -0
  53. package/src/infra/infra.match.js +102 -0
  54. package/src/infra/infra.scan.js +93 -0
  55. package/src/mcp/catalog.mjs +68 -0
  56. package/src/mcp/graph-context.mjs +276 -0
  57. package/src/mcp/tools-actions.mjs +132 -0
  58. package/src/mcp/tools-graph.mjs +274 -0
  59. package/src/mcp/tools-health.mjs +209 -0
  60. package/src/mcp/tools-impact.mjs +189 -0
  61. package/src/mcp-rg.mjs +51 -0
  62. package/src/mcp-server.mjs +171 -0
  63. package/src/mcp-source-tools.mjs +139 -0
  64. package/src/process.js +77 -0
  65. package/src/scan/discover.inventory.js +78 -0
  66. package/src/scan/discover.js +5 -0
  67. package/src/scan/discover.list.js +79 -0
  68. package/src/scan/discover.stack.js +227 -0
  69. package/src/scan/search.core.js +24 -0
  70. package/src/scan/search.git.js +102 -0
  71. package/src/scan/search.js +9 -0
  72. package/src/scan/search.node.js +83 -0
  73. package/src/scan/search.preview.js +49 -0
  74. package/src/scan/search.rg.js +145 -0
  75. package/src/security/advisory-store.js +177 -0
  76. package/src/security/installed.js +247 -0
  77. package/src/security/malware-file-heuristics.js +121 -0
  78. package/src/security/malware-heuristics.exclusions.js +134 -0
  79. package/src/security/malware-heuristics.js +15 -0
  80. package/src/security/malware-heuristics.roots.js +142 -0
  81. package/src/security/malware-heuristics.scan.js +87 -0
  82. package/src/security/malware-heuristics.sweep.js +122 -0
  83. package/src/security/malware-scoring.js +84 -0
  84. package/src/security/match.js +85 -0
  85. package/src/security/registry-sig.classify.js +136 -0
  86. package/src/security/registry-sig.js +18 -0
  87. package/src/security/registry-sig.rules.js +188 -0
  88. package/src/security/typosquat.js +72 -0
  89. package/src/util.js +27 -0
@@ -0,0 +1,217 @@
1
+ // Orchestrator for the internal graph builder: the two-pass parse loop, community bucketing, and the
2
+ // graph.json writer. (Split from internal-builder.js — see its doc comment for the overall architecture;
3
+ // the language registry / parser lifecycle / walk live in ./internal-builder.langs.js and the per-repo
4
+ // resolvers in ./internal-builder.resolvers.js.)
5
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
6
+ import { extname, relative, dirname } from "node:path";
7
+ import { specToPkg } from "./builder/spec-pkg.js";
8
+ import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
9
+ import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
10
+ import { buildResolvers } from "./internal-builder.resolvers.js";
11
+
12
+ // Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
13
+ export async function buildInternalGraph(repoDir, opts = {}) {
14
+ const langs = await ensureParser(opts);
15
+ const qc = new Map();
16
+ const q = (grammar, src) => { const k = grammar + ":" + src; if (qc.has(k)) return qc.get(k); let x = null; try { x = new Query(langs[grammar], src); } catch { x = null; } qc.set(k, x); return x; };
17
+ const caps = (grammar, src, root) => { const query = src && q(grammar, src); return query ? query.captures(root) : []; };
18
+ const field = (n, f) => (n && n.childForFieldName ? n.childForFieldName(f) : null);
19
+
20
+ const files = walk(repoDir);
21
+ const rel = (p) => relative(repoDir, p).replace(/\\/g, "/");
22
+ const fileSet = new Set(files.map(rel));
23
+ const nodes = []; const links = []; const nodeIds = new Set(); const nodeById = new Map();
24
+ const addNode = (n) => { if (!nodeIds.has(n.id)) { nodeIds.add(n.id); nodes.push(n); nodeById.set(n.id, n); } };
25
+ const perFileSymbols = new Map();
26
+ const symByFileName = new Map();
27
+ const importedLocals = new Map();
28
+ // Bare-package imports (axios, node:fs, @scope/x) — the graph can't resolve them to a repo file, but
29
+ // dependency analysis NEEDS them (unused/missing deps). Additive top-level array; nodes/links untouched.
30
+ const externalImports = [];
31
+
32
+ const resolvers = buildResolvers(repoDir, fileSet); // aliases, go.mod, java index, href, selectors
33
+ const { selectorIndex, htmlUsages } = resolvers;
34
+
35
+ // ---- pass 1: files + symbols + imports (dispatched to each language module) ----
36
+ // This runs on Electron's MAIN thread (the worker path hung web-tree-sitter's WASM in an Electron worker
37
+ // thread → the infinite "BUILDING GRAPH…"). Parsing is synchronous CPU, so we YIELD the event loop every
38
+ // few dozen files to keep the window responsive during a big-repo build, and SKIP giant/minified files
39
+ // (a multi-MB single-line bundle can wedge the tree-sitter parse).
40
+ let _parsed = 0;
41
+ for (const abs of files) {
42
+ const fileRel = rel(abs); const ext = extname(abs);
43
+ addNode({ id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" });
44
+ if (isDataFile(fileRel) || isDocFile(fileRel)) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); continue; } // config/infra/docs file-only node
45
+ const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
46
+ let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
47
+ // giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
48
+ if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); continue; }
49
+ if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
50
+ const parser = new Parser(); parser.setLanguage(langs[grammar]);
51
+ let tree; try { tree = parser.parse(code); } catch { continue; }
52
+
53
+ const syms = []; const nameToId = new Map();
54
+ const addSym = (name, line, callable, extra) => {
55
+ if (!name || !/^[A-Za-z_$][\w$]*$/.test(name)) return;
56
+ const id = `${fileRel}#${name}@${line}`; if (nodeIds.has(id)) return;
57
+ const sourceNode = extra && extra.sourceNode;
58
+ const endLine = sourceNode?.endPosition ? sourceNode.endPosition.row + 1 : 0;
59
+ let complexity = null;
60
+ if (callable && sourceNode) {
61
+ try { complexity = analyzeSyntaxComplexity(sourceNode, { family: lang.family, name }); }
62
+ catch { complexity = null; }
63
+ }
64
+ addNode({
65
+ id,
66
+ label: callable ? `${name}()` : name,
67
+ file_type: "code",
68
+ source_file: fileRel,
69
+ source_location: `L${line}`,
70
+ ...(endLine >= line ? { source_end: `L${endLine}` } : {}),
71
+ ...(complexity ? { complexity } : {}),
72
+ ...(extra && extra.exported ? { exported: true } : {}),
73
+ ...(extra && extra.decorated ? { decorated: true } : {})
74
+ });
75
+ links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
76
+ syms.push({ id, name, start: line, end: endLine >= line ? endLine : 0 }); if (!nameToId.has(name)) nameToId.set(name, id);
77
+ };
78
+ const imports = new Map(); importedLocals.set(fileRel, imports);
79
+ const addImportEdge = (tgt) => { if (tgt && tgt !== fileRel) links.push({ source: fileRel, target: tgt, relation: "imports", confidence: "EXTRACTED" }); };
80
+ // rec: {spec, kind, line} bare-pkg import · {dynamic:true, spec?, target?} dynamic import marker
81
+ // (target = internally-resolved dynamic import; suppresses false "unused file" in dep analysis) ·
82
+ // {unresolved:true, spec} broken local import (relative or alias path that resolves to no file).
83
+ const addExternalImport = (rec) => {
84
+ if (!rec) return;
85
+ if (rec.dynamic) { externalImports.push({ file: fileRel, spec: rec.spec || null, kind: rec.kind || "dynamic", dynamic: true, line: rec.line || 0, ...(rec.target ? { target: rec.target } : {}) }); return; }
86
+ if (rec.unresolved) { externalImports.push({ file: fileRel, spec: rec.spec, kind: rec.kind || "esm", unresolved: true, line: rec.line || 0 }); return; }
87
+ // non-npm extractors (go/python) classify their own specs and pass pkg/builtin/ecosystem precomputed
88
+ if (rec.pkg) { externalImports.push({ file: fileRel, spec: rec.spec, pkg: rec.pkg, builtin: !!rec.builtin, kind: rec.kind || "import", line: rec.line || 0, ...(rec.ecosystem ? { ecosystem: rec.ecosystem } : {}) }); return; }
89
+ const r = specToPkg(rec.spec);
90
+ if (!r) return;
91
+ externalImports.push({ file: fileRel, spec: rec.spec, pkg: r.pkg, builtin: !!r.builtin, kind: rec.kind || "esm", line: rec.line || 0 });
92
+ };
93
+ // Post-hoc export flag (export {a}, export default X, CJS module.exports) — declarations are flagged at addSym time.
94
+ const markExported = (name) => { const id = nameToId.get(name); const n = id && nodeById.get(id); if (n) n.exported = true; };
95
+
96
+ try { lang.pass1({ grammar, tree, fileRel, code, caps, field, addSym, addNode, links, nodeIds, syms, nameToId, imports, addImportEdge, addExternalImport, markExported, fileSet, ...resolvers }); }
97
+ catch (e) { /* one bad file never sinks the whole build */ void e; }
98
+
99
+ syms.sort((a, b) => a.start - b.start);
100
+ const eof = code.split("\n").length;
101
+ for (let i = 0; i < syms.length; i++) {
102
+ if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
103
+ }
104
+ perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId);
105
+ tree.delete();
106
+ }
107
+
108
+ // ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
109
+ // C# gets the same treatment: one folder ≈ one namespace by convention, and `using` names namespaces,
110
+ // not files, so the folder map is the only reliable cross-file resolver) ----
111
+ const goDirSymbols = new Map();
112
+ const sharesDirScope = (fr) => fr.endsWith(".go") || fr.endsWith(".cs") || fr.endsWith(".rs");
113
+ for (const [fr, m] of symByFileName) {
114
+ if (!sharesDirScope(fr)) continue;
115
+ const d = fr.includes("/") ? fr.slice(0, fr.lastIndexOf("/")) : "";
116
+ let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
117
+ for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
118
+ }
119
+ // Exact source ranges can overlap (a named function nested inside another function). Attribute a call
120
+ // to the innermost matching symbol, not whichever outer declaration happened to be added first.
121
+ const enclosing = (fileRel, line) => {
122
+ let best = null;
123
+ for (const s of perFileSymbols.get(fileRel) || []) {
124
+ if (line < s.start || line > s.end) continue;
125
+ if (!best || s.start > best.start || (s.start === best.start && s.end < best.end)) best = s;
126
+ }
127
+ return best;
128
+ };
129
+ const resolveCall = (name, fileRel) => {
130
+ const local = symByFileName.get(fileRel); if (local && local.has(name)) return local.get(name);
131
+ 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); }
132
+ const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
133
+ if (imp && imp.targetFile) { const tf = symByFileName.get(imp.targetFile); if (tf && tf.has(imp.imported)) return tf.get(imp.imported); }
134
+ return null;
135
+ };
136
+ for (const abs of files) {
137
+ const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
138
+ const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
139
+ let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
140
+ const parser = new Parser(); parser.setLanguage(langs[grammar]);
141
+ let tree; try { tree = parser.parse(code); } catch { continue; }
142
+ for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
143
+ const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
144
+ const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
145
+ links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
146
+ }
147
+ // qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
148
+ // SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
149
+ if (lang.selectorCall) for (const cap of caps(grammar, lang.selectorCall, tree.rootNode)) {
150
+ const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
151
+ if (!operand || operand.type !== "identifier" || !fld) continue;
152
+ const caller = enclosing(fileRel, sel.startPosition.row + 1); if (!caller) continue;
153
+ const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text);
154
+ const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
155
+ const dm = goDirSymbols.get(imp && imp.targetDir ? imp.targetDir : dir);
156
+ const target = dm && dm.get(fld.text);
157
+ if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
158
+ }
159
+ for (const heritageSrc of lang.heritage || []) for (const cap of caps(grammar, heritageSrc, tree.rootNode)) {
160
+ const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
161
+ const target = resolveCall(cap.node.text, fileRel);
162
+ if (target && target !== cls.id) links.push({ source: cls.id, target, relation: "inherits", confidence: "INFERRED" });
163
+ }
164
+ // Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
165
+ // another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
166
+ // not falsely DEAD. (Same-file usage is already covered by graph-builder-analysis localRefs.) Go-only for now.
167
+ if (FAMILY[grammar] === "go") {
168
+ const refSeen = new Set();
169
+ const emitRef = (src, target) => { if (!target || target === src) return; const k = src + ">" + target; if (refSeen.has(k)) return; refSeen.add(k); links.push({ source: src, target, relation: "references", confidence: "INFERRED" }); };
170
+ const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
171
+ const dm = goDirSymbols.get(dir);
172
+ for (const cap of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) { // type_identifier → struct/type usage
173
+ const target = dm && dm.get(cap.node.text); if (!target || target.slice(0, target.indexOf("#")) === fileRel) continue;
174
+ const caller = enclosing(fileRel, cap.node.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
175
+ }
176
+ for (const cap of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
177
+ const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
178
+ if (!operand || operand.type !== "identifier" || !fld) continue;
179
+ const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text); if (!imp || !imp.targetDir) continue;
180
+ const tdm = goDirSymbols.get(imp.targetDir); const target = tdm && tdm.get(fld.text); if (!target) continue;
181
+ const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
182
+ }
183
+ }
184
+ tree.delete();
185
+ }
186
+
187
+ // HTML class/id usage → the CSS file(s) defining that selector: file-level reference edges (deduped per pair).
188
+ const htmlRefSeen = new Set();
189
+ for (const u of htmlUsages) {
190
+ const defs = selectorIndex.get(u.label); if (!defs) continue;
191
+ for (const cssFile of defs) {
192
+ if (cssFile === u.htmlFile) continue;
193
+ const key = u.htmlFile + ">" + cssFile; if (htmlRefSeen.has(key)) continue; htmlRefSeen.add(key);
194
+ links.push({ source: u.htmlFile, target: cssFile, relation: "references", confidence: "INFERRED" });
195
+ }
196
+ }
197
+
198
+ // community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
199
+ // app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
200
+ const folderOf = (f) => { const d = String(f || "").split("/").filter(Boolean).slice(0, -1); return d.length ? d.slice(0, 2).join("/") : "(root)"; };
201
+ const commOf = new Map(); let commSeq = 0;
202
+ for (const n of nodes) { const fo = folderOf(n.source_file); if (!commOf.has(fo)) commOf.set(fo, commSeq++); n.community = commOf.get(fo); }
203
+
204
+ // extImportsV: bump when the externalImports schema/coverage changes (v2 = go/python ecosystems) —
205
+ // deps-engine rebuilds in memory when a saved graph is older than this.
206
+ return { nodes, links, externalImports, extImportsV: 2, complexityV: 1 };
207
+ }
208
+
209
+ // Build + write graph.json to outPath (creating the dir). Returns { ok, nodes, links, graphJson }.
210
+ export async function writeInternalGraph(repoDir, outPath, opts = {}) {
211
+ const graph = await buildInternalGraph(repoDir, opts);
212
+ mkdirSync(dirname(outPath), { recursive: true });
213
+ writeFileSync(outPath, JSON.stringify(graph), "utf8");
214
+ return { ok: true, nodes: graph.nodes.length, links: graph.links.length, graphJson: outPath };
215
+ }
216
+
217
+ export const INTERNAL_BUILDER_LANGS = GRAMMARS;
@@ -0,0 +1,12 @@
1
+ // Built-in, dependency-free code-graph builder: parses a repo with web-tree-sitter (WASM grammars,
2
+ // no Python/native tooling) and emits graph.json ({nodes: files+symbols, links:
3
+ // contains/imports/calls/inherits}) for the analysis pipeline.
4
+ //
5
+ // ARCHITECTURE: this file is a slim FACADE so external import paths stay unchanged. The orchestrator
6
+ // (file walk dispatch, the two-pass loop, community, graph.json writer) lives in ./internal-builder.build.js;
7
+ // the language registry, web-tree-sitter parser lifecycle, and the cycle-safe file walk live in
8
+ // ./internal-builder.langs.js; the per-repo resolvers (JS/TS aliases, go.mod, java index, href, CSS
9
+ // selector index) live in ./internal-builder.resolvers.js. Each language lives in its OWN module under
10
+ // ./builder/lang-*.js and declares its grammars, file extensions, tree-sitter queries, and a pass1(ctx)
11
+ // extractor. To add/fix a language, edit only its module (or add one to LANG_MODULES).
12
+ export { buildInternalGraph, writeInternalGraph, INTERNAL_BUILDER_LANGS } from "./internal-builder.build.js";
@@ -0,0 +1,86 @@
1
+ // Language registry, parser lifecycle (web-tree-sitter init + grammar loading), and the cycle-safe
2
+ // repo file walk for the internal graph builder (split from internal-builder.js — see its doc comment).
3
+ //
4
+ // Loaded via createRequire: web-tree-sitter's ESM build throws on fs/promises in pure-ESM Node, but its CJS
5
+ // build works — and Electron main runs Node, so this needs no external runtime.
6
+ import { readdirSync, statSync, realpathSync } from "node:fs";
7
+ import { join, extname, dirname } from "node:path";
8
+ import { createRequire } from "node:module";
9
+ import LANG_JS from "./builder/lang-js.js";
10
+ import LANG_PY from "./builder/lang-python.js";
11
+ import LANG_GO from "./builder/lang-go.js";
12
+ import LANG_JAVA from "./builder/lang-java.js";
13
+ import LANG_CSHARP from "./builder/lang-csharp.js";
14
+ import LANG_RUST from "./builder/lang-rust.js";
15
+ import LANG_HTML from "./builder/lang-html.js";
16
+ import LANG_CSS from "./builder/lang-css.js";
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const { Parser, Language, Query } = require("web-tree-sitter");
20
+
21
+ const WTS_DIR = dirname(require.resolve("web-tree-sitter"));
22
+ const NODE_MODULES = dirname(WTS_DIR);
23
+ const DEFAULT_RUNTIME_WASM = join(WTS_DIR, "tree-sitter.wasm");
24
+ const DEFAULT_WASM_DIR = join(NODE_MODULES, "tree-sitter-wasms", "out");
25
+
26
+ // ---- language registry (derived from the per-language modules) ----
27
+ const LANG_MODULES = [LANG_JS, LANG_PY, LANG_GO, LANG_JAVA, LANG_CSHARP, LANG_RUST, LANG_HTML, LANG_CSS];
28
+ const LANGS = {}; // family -> module
29
+ const EXT_LANG = {}; // ext -> grammar
30
+ const FAMILY = {}; // grammar -> family
31
+ const GRAMMARS_SET = new Set();
32
+ for (const L of LANG_MODULES) {
33
+ LANGS[L.family] = L;
34
+ for (const g of L.grammars) GRAMMARS_SET.add(g);
35
+ for (const [ext, g] of Object.entries(L.exts)) { EXT_LANG[ext] = g; FAMILY[g] = L.family; }
36
+ }
37
+ const GRAMMARS = [...GRAMMARS_SET];
38
+
39
+ // non-code files graph-builder also indexes as nodes (config/data/scripts) — added as file-only nodes (no symbols),
40
+ // so file counts + import targets (e.g. import cfg from "./x.json") match graph-builder.
41
+ const DATA_EXT = new Set([".json", ".sh", ".ps1", ".yaml", ".yml"]); // config/data/scripts + k8s/skaffold/CI yaml
42
+ const INFRA_NAME = /(^|[\\/])(Dockerfile|Containerfile)(\.[\w.-]+)?$|\.dockerfile$/i; // Dockerfile[.prod], *.dockerfile (no ext)
43
+ const isDataFile = (p) => DATA_EXT.has(extname(p)) || INFRA_NAME.test(String(p));
44
+ // Docs/prose (README, CLAUDE.md, AGENTS.md, docs/*.md, …) are indexed as file-only nodes so the GUI board can
45
+ // render them as NEUTRAL pillars (never dead-code scored) and wire the agent-instruction ones UP to the
46
+ // Claude Code / Codex node. AGENT_DOTFILE lets a few AI-agent instruction dotfiles past the dotfile skip below.
47
+ const DOC_EXT = new Set([".md", ".mdx", ".markdown", ".mdown", ".mkd", ".mkdn", ".rst", ".adoc", ".asciidoc"]);
48
+ const AGENT_DOTFILE = /^\.(cursorrules|windsurfrules|clinerules)$/i;
49
+ const isDocFile = (p) => DOC_EXT.has(extname(p)) || AGENT_DOTFILE.test(String(p).split(/[\\/]/).pop() || "");
50
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", "coverage", "vendor", "weavatrix-graphs", "weavatrix-graphs", ".next", "out", "__pycache__", ".venv", "venv", "env", ".tox", "site-packages", ".mypy_cache", ".pytest_cache"]);
51
+ const MAX_PARSE_BYTES = 1_500_000; // skip parsing files above this (minified bundles / generated blobs wedge tree-sitter)
52
+
53
+ let _ready = null;
54
+ const _langs = {};
55
+ async function ensureParser(opts = {}) {
56
+ if (!_ready) _ready = Parser.init({ locateFile: () => opts.runtimeWasm || DEFAULT_RUNTIME_WASM });
57
+ await _ready;
58
+ const wasmDir = opts.wasmDir || DEFAULT_WASM_DIR;
59
+ for (const g of GRAMMARS) if (!_langs[g]) { try { _langs[g] = await Language.load(join(wasmDir, `tree-sitter-${g}.wasm`)); } catch { _langs[g] = null; } }
60
+ return _langs;
61
+ }
62
+
63
+ // Cycle-safe directory walk. statSync FOLLOWS symlinks/junctions, so a link pointing at an ancestor would
64
+ // otherwise recurse forever (a/b/link/b/link/…). We dedupe by REAL path (a visited dir is never re-entered)
65
+ // and cap depth as a backstop, so a symlink loop can't wedge the build.
66
+ function walk(dir, acc = [], seen = new Set(), depth = 0) {
67
+ if (depth > 40) return acc;
68
+ let real; try { real = realpathSync.native(dir); } catch { real = dir; }
69
+ if (seen.has(real)) return acc;
70
+ seen.add(real);
71
+ let entries;
72
+ try { entries = readdirSync(dir); } catch { return acc; }
73
+ for (const name of entries) {
74
+ if (SKIP.has(name)) continue;
75
+ // dotfiles/dot-dirs are skipped EXCEPT a few AI-agent instruction dotfiles (.cursorrules etc.); dot-DIRS
76
+ // never match AGENT_DOTFILE, so we still never recurse into them (.git/.github/.cursor stay out).
77
+ if (name.startsWith(".") && !AGENT_DOTFILE.test(name)) continue;
78
+ const full = join(dir, name);
79
+ let st; try { st = statSync(full); } catch { continue; }
80
+ if (st.isDirectory()) walk(full, acc, seen, depth + 1);
81
+ else { const e = extname(name); if ((EXT_LANG[e] && _langs[EXT_LANG[e]]) || isDataFile(name) || isDocFile(name)) acc.push(full); }
82
+ }
83
+ return acc;
84
+ }
85
+
86
+ export { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk };
@@ -0,0 +1,116 @@
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 / the CSS selector index.
3
+ // (Split from internal-builder.js — see its doc comment for the overall architecture.)
4
+ import { readFileSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { parseGoMod } from "../analysis/manifests.js";
7
+
8
+ export function buildResolvers(repoDir, fileSet) {
9
+ // Go package = directory (resolved via go.mod module prefix); Java class = file (basename index).
10
+ // go.mod requires also feed goSpecToPkg so external Go imports map to their declared module.
11
+ let goModule = "";
12
+ let goRequires = [];
13
+ try {
14
+ const gomod = parseGoMod(readFileSync(join(repoDir, "go.mod"), "utf8"));
15
+ goModule = gomod.module;
16
+ goRequires = gomod.requires.map((r) => r.path);
17
+ } catch { /* no go.mod */ }
18
+ const dirFiles = new Map();
19
+ const filesByBase = new Map();
20
+ for (const fr of fileSet) {
21
+ const base = fr.split("/").pop();
22
+ (filesByBase.get(base) || filesByBase.set(base, []).get(base)).push(fr);
23
+ if (fr.endsWith(".go")) { const d = fr.includes("/") ? fr.slice(0, fr.lastIndexOf("/")) : ""; (dirFiles.get(d) || dirFiles.set(d, []).get(d)).push(fr); }
24
+ }
25
+ const resolveGoImport = (importPath) => {
26
+ if (goModule && (importPath === goModule || importPath.startsWith(goModule + "/"))) {
27
+ const d = importPath === goModule ? "" : importPath.slice(goModule.length + 1);
28
+ if (dirFiles.has(d)) return d;
29
+ }
30
+ // a module DECLARED in go.mod is external by definition — never let the suffix fallback hijack it
31
+ // into a same-named internal dir (pkg/errors/ vs github.com/pkg/errors → false "unused module")
32
+ if (goRequires.some((r) => importPath === r || importPath.startsWith(r + "/"))) return null;
33
+ const segs = importPath.split("/");
34
+ for (const n of [Math.min(2, segs.length), 1]) { const suf = segs.slice(-n).join("/"); for (const d of dirFiles.keys()) if (d === suf || d.endsWith("/" + suf)) return d; }
35
+ return null;
36
+ };
37
+ const resolveJavaImport = (parts) => {
38
+ const full = parts.join("/") + ".java", base = parts[parts.length - 1] + ".java";
39
+ const cands = filesByBase.get(base) || [];
40
+ return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
41
+ };
42
+
43
+ // path aliases (tsconfig compilerOptions.paths + vite/webpack alias) — without these, @components/@/etc
44
+ // imports are missed and their targets look falsely DEAD.
45
+ const aliasList = [];
46
+ const addAlias = (a, t) => {
47
+ a = String(a).replace(/\/\*$/, "").replace(/\/$/, "");
48
+ t = String(t).replace(/\/\*$/, "").replace(/^\.\//, "").replace(/\/$/, "");
49
+ if (a && t && !aliasList.some((x) => x.alias === a)) aliasList.push({ alias: a, target: t });
50
+ };
51
+ const jsBaseUrls = []; // tsconfig/jsconfig baseUrl roots — bare "components/Button" may be baseUrl-rooted, not an npm package
52
+ for (const cfg of ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json", "jsconfig.json"]) {
53
+ try {
54
+ const raw = readFileSync(join(repoDir, cfg), "utf8").replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
55
+ const tj = JSON.parse(raw); const co = tj.compilerOptions || {}; const paths = co.paths || {};
56
+ const baseUrl = String(co.baseUrl || ".").replace(/^\.\/?/, "").replace(/\/$/, "");
57
+ if (co.baseUrl != null && !jsBaseUrls.includes(baseUrl)) jsBaseUrls.push(baseUrl);
58
+ for (const [k, v] of Object.entries(paths)) { const t = Array.isArray(v) ? v[0] : v; if (t) addAlias(k, (baseUrl && !String(t).startsWith("./") ? baseUrl + "/" : "") + t); }
59
+ } catch { /* no/invalid tsconfig */ }
60
+ }
61
+ for (const vc of ["vite.config.ts", "vite.config.js", "vite.config.mjs", "webpack.config.js"]) {
62
+ try { const src = readFileSync(join(repoDir, vc), "utf8"); for (const m of src.matchAll(/['"`]([^'"`]+)['"`]\s*:\s*path\.resolve\([^,]+,\s*['"`]([^'"`]+)['"`]\s*\)/g)) addAlias(m[1], m[2]); } catch { /* no bundler config */ }
63
+ }
64
+ aliasList.sort((a, b) => b.alias.length - a.alias.length);
65
+ const resolveAlias = (spec) => { for (const { alias, target } of aliasList) { if (spec === alias) return target; if (spec.startsWith(alias + "/")) return target + spec.slice(alias.length); } return null; };
66
+
67
+ const JS_EXTS = ["", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs", ".json", "/index.js", "/index.ts", "/index.jsx", "/index.tsx"];
68
+ const resolveJsImport = (fromRel, spec) => {
69
+ if (!spec) return null;
70
+ let base;
71
+ if (spec.startsWith(".")) base = join(dirname(fromRel), spec).replace(/\\/g, "/").replace(/^\.\//, "");
72
+ else {
73
+ base = resolveAlias(spec);
74
+ if (base == null) {
75
+ // baseUrl-rooted internal import ("components/Button" with baseUrl:"src") — try before calling it an npm package
76
+ for (const b of jsBaseUrls) {
77
+ const root = (b ? b + "/" : "") + spec;
78
+ for (const e of JS_EXTS) { const cand = (root + e).replace(/\/+/g, "/"); if (fileSet.has(cand)) return cand; }
79
+ }
80
+ return null; // genuinely bare → npm package (stays unresolved here)
81
+ }
82
+ }
83
+ for (const e of JS_EXTS) { const cand = (base + e).replace(/\/+/g, "/"); if (fileSet.has(cand)) return cand; }
84
+ return null;
85
+ };
86
+
87
+ const resolvePyPath = (baseDir, parts) => {
88
+ const p = [baseDir, ...parts].filter(Boolean).join("/").replace(/\/+/g, "/").replace(/^\.\//, "");
89
+ // src-layout: absolute imports of the repo's own package live under src/ (PEP 517 convention)
90
+ const cands = baseDir ? [p + ".py", p + "/__init__.py"] : [p + ".py", p + "/__init__.py", "src/" + p + ".py", "src/" + p + "/__init__.py"];
91
+ for (const cand of cands) if (fileSet.has(cand)) return cand;
92
+ return null;
93
+ };
94
+ const pyBaseDir = (fromRel, dots) => { let d = dots > 0 ? dirname(fromRel) : ""; for (let i = 1; i < dots; i++) d = dirname(d); return d === "." ? "" : d; };
95
+ // top-level dirs holding .py files (incl. under src/) — PEP 420 namespace packages have no __init__.py,
96
+ // so an absolute import of one resolves to no FILE; knowing the dir exists stops a false "external dep".
97
+ const pyTopDirs = new Set();
98
+ for (const fr of fileSet) {
99
+ if (!fr.endsWith(".py") || !fr.includes("/")) continue;
100
+ const seg = fr.split("/");
101
+ pyTopDirs.add(seg[0]);
102
+ if (seg[0] === "src" && seg.length > 2) pyTopDirs.add(seg[1]);
103
+ }
104
+
105
+ const selectorIndex = new Map();
106
+ const htmlUsages = [];
107
+ const resolveHref = (fromRel, href) => {
108
+ if (!href) return null;
109
+ const h = href.split(/[?#]/)[0].replace(/^\.\//, "");
110
+ if (/^(https?:)?\/\//.test(h) || h.startsWith("data:") || h.startsWith("#") || h.startsWith("mailto:")) return null;
111
+ const cand = h.startsWith("/") ? h.slice(1) : join(dirname(fromRel), h).replace(/\\/g, "/").replace(/^\.\//, "");
112
+ return fileSet.has(cand) ? cand : null;
113
+ };
114
+
115
+ return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goRequires };
116
+ }
@@ -0,0 +1,35 @@
1
+ // Graph paths + analysis re-exports. This module owns the on-disk graph locations and re-exports the
2
+ // pure filter/analysis helpers so callers get everything from one import:
3
+ // graph-filter.js — pure graph filters (test-mode, path-scope)
4
+ // graph-analysis.js — parse graph.json → file/module/symbol view, hotspots, communities
5
+ import { readdirSync } from "node:fs";
6
+ import { join, dirname } from "node:path";
7
+ import { repoBaseName } from "../scan/discover.js";
8
+
9
+ export * from "./graph-filter.js";
10
+ export * from "../analysis/graph-analysis.js";
11
+
12
+ // Graphs live in a `weavatrix-graphs/` folder NEXT to the repo (inside the repo's parent folder),
13
+ // never inside the repo itself — the graph is derived data, not source. One folder per repo holds
14
+ // graph.json plus graph.prev.json (saved by rebuild_graph for graph_diff).
15
+ export function graphOutDirForRepo(repoPath) {
16
+ return join(dirname(repoPath), "weavatrix-graphs", repoBaseName(repoPath));
17
+ }
18
+
19
+ // A separate dir for a single module's scoped graph, so it never clobbers the repo's graph.
20
+ export function graphOutDirForModule(repoPath, moduleName) {
21
+ const safe = String(moduleName).replace(/[^A-Za-z0-9_.-]/g, "_");
22
+ return join(dirname(repoPath), "weavatrix-graphs", repoBaseName(repoPath), "modules", safe);
23
+ }
24
+
25
+ // Top-level source folders of a repo (for path-scoped builds).
26
+ export function repoTopFolders(repoPath) {
27
+ try {
28
+ return readdirSync(repoPath, { withFileTypes: true })
29
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !["node_modules", "weavatrix-graphs", "dist", "build", "coverage", "vendor"].includes(entry.name))
30
+ .map((entry) => entry.name)
31
+ .sort((left, right) => left.localeCompare(right));
32
+ } catch {
33
+ return [];
34
+ }
35
+ }