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,76 @@
1
+ // Rank, score, label, and evidence builders for the source-complexity report.
2
+
3
+ function timeScore(rank) {
4
+ return [0.2, 0.4, 0.65, 0.95, 1.3, 1.7][Math.max(0, Math.min(5, rank))] || 0.2;
5
+ }
6
+
7
+ function memoryScore(rank) {
8
+ return [0.15, 0.35, 0.85, 1.05, 1.4, 1.7][Math.max(0, Math.min(5, rank))] || 0.15;
9
+ }
10
+
11
+ function plural(n, one, many) {
12
+ return `${n} ${n === 1 ? one : many}`;
13
+ }
14
+
15
+ function deriveTimeRank(stats) {
16
+ if (stats.maxLoopDepth >= 3) return 5;
17
+ if (stats.maxLoopDepth === 2 || (stats.sorts && stats.maxSortLoopDepth > 0)) return 4;
18
+ if (stats.sorts) return 3;
19
+ if (stats.loops || stats.linearOps || stats.spreadCopies) return 2;
20
+ return 0;
21
+ }
22
+
23
+ function timeLabelForRank(stats, rank) {
24
+ if (stats.recursion) return "recursive local work — bound unknown";
25
+ if (rank >= 5) return "O(n^3+) local — deeply nested iteration";
26
+ if (rank === 4 && stats.sorts && stats.maxSortLoopDepth > 0) return "O(n^2 log n) local — sort inside iteration";
27
+ if (rank === 4) return "O(n^2) local — nested iteration";
28
+ if (rank === 3) return "O(n log n) local — sort";
29
+ if (rank === 2 && stats.spreadCopies) return `O(n) local — ${plural(stats.spreadCopies, "shallow copy", "shallow copies")}`;
30
+ if (rank === 2 && stats.loops) return `O(n) local — ${plural(stats.loops, "iteration", "iterations")}`;
31
+ return rank === 2 ? "O(n) local — collection traversal" : "O(1) local";
32
+ }
33
+
34
+ function buildTimeSummary(stats) {
35
+ const timeRank = stats.recursion ? Math.max(2, deriveTimeRank(stats)) : deriveTimeRank(stats);
36
+ let timeLabel = timeLabelForRank(stats, timeRank);
37
+ if (stats.asyncBoundaries || stats.externalCalls) timeLabel += " · I/O/callee-bound";
38
+ else if (stats.callCount && !stats.recursion) timeLabel += " · callee-bound";
39
+ if (stats.branches >= 8 && timeRank === 0) timeLabel += " · branch-heavy";
40
+ return { timeRank, timeScore: timeScore(timeRank), timeLabel };
41
+ }
42
+
43
+ function buildMemorySummary(stats) {
44
+ let memoryRank = 0;
45
+ let memoryLabel = "O(1) auxiliary";
46
+ const variableAllocation = stats.spreadCopies || stats.producerCalls;
47
+ if (variableAllocation) {
48
+ memoryRank = stats.maxVariableAllocationDepth >= 2 ? 4 : 2;
49
+ memoryLabel = memoryRank >= 4 ? "O(n^2) — nested collection copies" : stats.spreadCopies
50
+ ? `O(n) — ${plural(stats.spreadCopies, "shallow copy", "shallow copies")}`
51
+ : "O(n) — produced collection";
52
+ } else if (stats.recursion) {
53
+ memoryRank = 1;
54
+ memoryLabel = "O(depth) stack — recursive";
55
+ } else if (stats.allocations) {
56
+ memoryLabel = "O(1) auxiliary — fixed allocations";
57
+ }
58
+ return { memoryRank, memoryScore: memoryScore(memoryRank), memoryLabel };
59
+ }
60
+
61
+ export function buildLabels(stats) {
62
+ return { ...buildTimeSummary(stats), ...buildMemorySummary(stats) };
63
+ }
64
+
65
+ export function buildEvidence(stats) {
66
+ const out = [];
67
+ if (stats.loops) out.push(plural(stats.loops, "iteration", "iterations"));
68
+ if (stats.maxLoopDepth > 1) out.push(`loop depth ${stats.maxLoopDepth}`);
69
+ if (stats.sorts) out.push(plural(stats.sorts, "sort", "sorts"));
70
+ if (stats.spreadCopies) out.push(plural(stats.spreadCopies, "shallow copy", "shallow copies"));
71
+ if (stats.branches) out.push(plural(stats.branches, "branch point", "branch points"));
72
+ if (stats.awaits) out.push(plural(stats.awaits, "await boundary", "await boundaries"));
73
+ if (stats.callCount) out.push(plural(stats.callCount, "call", "calls"));
74
+ if (stats.recursion) out.push("direct recursion");
75
+ return out;
76
+ }
@@ -0,0 +1,174 @@
1
+ // Syntax-tree walk that gathers complexity stats and assembles the summary report.
2
+
3
+ import {
4
+ LOOP_NODES, CALL_NODES, RETURN_NODES, AWAIT_NODES, OBJECT_NODES,
5
+ FIXED_ALLOCATION_NODES, VARIABLE_ALLOCATION_NODES, SPREAD_NODES,
6
+ DECLARATION_BOUNDARIES, EXPRESSION_CALLABLES, ARGUMENT_NODES,
7
+ ITERATOR_CALLS, PRODUCER_CALLS, LINEAR_CALLS, SORT_CALLS, BRANCH_NODES
8
+ } from "./source-complexity.constants.js";
9
+ import {
10
+ field, children, sameNode, normalizedName, looksLikeIoCall, callName,
11
+ logicalBranch, isDefaultCase, directParameterCount, countObjectPatternFields, sourceRange
12
+ } from "./source-complexity.ast.js";
13
+ import { buildLabels, buildEvidence } from "./source-complexity.report.js";
14
+
15
+ function createStats(parameters, family) {
16
+ return {
17
+ params: directParameterCount(parameters, family),
18
+ objectFields: countObjectPatternFields(parameters),
19
+ branches: 0,
20
+ loops: 0,
21
+ maxLoopDepth: 0,
22
+ returns: 0,
23
+ awaits: 0,
24
+ callCount: 0,
25
+ externalCalls: 0,
26
+ asyncBoundaries: 0,
27
+ allocations: 0,
28
+ objectLiterals: 0,
29
+ spreadCopies: 0,
30
+ sorts: 0,
31
+ linearOps: 0,
32
+ producerCalls: 0,
33
+ recursion: false,
34
+ maxSortLoopDepth: 0,
35
+ maxVariableAllocationDepth: 0
36
+ };
37
+ }
38
+
39
+ function shouldSkipNode(root, node, state) {
40
+ if (sameNode(node, root)) return false;
41
+ const type = String(node.type || "");
42
+ if (DECLARATION_BOUNDARIES.has(type)) return true;
43
+ if (!EXPRESSION_CALLABLES.has(type) || state.callbackContext) return false;
44
+ const parent = state.parent;
45
+ return !(CALL_NODES.has(String(parent?.type || "")) && sameNode(field(parent, "function"), node));
46
+ }
47
+
48
+ function nodeFacts(node, state) {
49
+ const type = String(node.type || "");
50
+ const isCall = CALL_NODES.has(type);
51
+ const nameAtCall = isCall ? callName(node) : "";
52
+ const normalizedCall = normalizedName(nameAtCall);
53
+ return {
54
+ type,
55
+ isCall,
56
+ isLoop: LOOP_NODES.has(type),
57
+ nameAtCall,
58
+ normalizedCall,
59
+ iteratorCall: isCall && ITERATOR_CALLS.has(normalizedCall),
60
+ sortCall: isCall && SORT_CALLS.has(normalizedCall),
61
+ producerCall: isCall && PRODUCER_CALLS.has(normalizedCall),
62
+ currentDepth: Math.max(0, Number(state.loopDepth) || 0)
63
+ };
64
+ }
65
+
66
+ function recordBasicSignals(stats, node, facts) {
67
+ const { type, currentDepth } = facts;
68
+ if (BRANCH_NODES.has(type) && !(type.includes("case") && isDefaultCase(node))) stats.branches++;
69
+ else if (logicalBranch(node)) stats.branches++;
70
+ if (RETURN_NODES.has(type)) stats.returns++;
71
+ if (AWAIT_NODES.has(type)) { stats.awaits++; stats.asyncBoundaries++; }
72
+ if (OBJECT_NODES.has(type)) stats.objectLiterals++;
73
+ if (FIXED_ALLOCATION_NODES.has(type)) stats.allocations++;
74
+ if (!VARIABLE_ALLOCATION_NODES.has(type)) return;
75
+ stats.producerCalls++;
76
+ stats.maxVariableAllocationDepth = Math.max(stats.maxVariableAllocationDepth, currentDepth + 1);
77
+ }
78
+
79
+ function recordSpread(stats, facts, parent) {
80
+ if (!SPREAD_NODES.has(facts.type) || /parameters|parameter_list/.test(String(parent?.type || ""))) return;
81
+ stats.spreadCopies++;
82
+ stats.linearOps++;
83
+ stats.maxVariableAllocationDepth = Math.max(stats.maxVariableAllocationDepth, facts.currentDepth + 1);
84
+ }
85
+
86
+ function recordCall(stats, facts, state, targetName) {
87
+ if (!facts.isCall) return;
88
+ stats.callCount++;
89
+ if (state.awaited || looksLikeIoCall(facts.nameAtCall)) stats.externalCalls++;
90
+ if (targetName && facts.normalizedCall === targetName) stats.recursion = true;
91
+ if (facts.sortCall) {
92
+ stats.sorts++;
93
+ stats.maxSortLoopDepth = Math.max(stats.maxSortLoopDepth, facts.currentDepth);
94
+ } else if (LINEAR_CALLS.has(facts.normalizedCall) && !facts.iteratorCall) stats.linearOps++;
95
+ if (!facts.producerCall) return;
96
+ stats.producerCalls++;
97
+ stats.allocations++;
98
+ stats.maxVariableAllocationDepth = Math.max(stats.maxVariableAllocationDepth, facts.currentDepth + 1);
99
+ }
100
+
101
+ function recordLoop(stats, facts) {
102
+ if (!facts.isLoop && !facts.iteratorCall) return facts.currentDepth;
103
+ const nextDepth = facts.currentDepth + 1;
104
+ stats.loops++;
105
+ stats.maxLoopDepth = Math.max(stats.maxLoopDepth, nextDepth);
106
+ return nextDepth;
107
+ }
108
+
109
+ function depthForChild(node, child, facts, nextDepth) {
110
+ const childIsArgs = ARGUMENT_NODES.has(String(child.type || ""));
111
+ if (facts.iteratorCall && childIsArgs) return nextDepth;
112
+ if (!facts.isLoop) return facts.currentDepth;
113
+ const loopBody = field(node, "body");
114
+ if (loopBody) return sameNode(child, loopBody) ? nextDepth : facts.currentDepth;
115
+ return facts.type === "for_in_clause" ? facts.currentDepth : nextDepth;
116
+ }
117
+
118
+ function walkSyntax(root, node, state, context) {
119
+ if (!node || shouldSkipNode(root, node, state)) return;
120
+ const parent = state.parent || null;
121
+ const callbackContext = state.callbackContext || ARGUMENT_NODES.has(String(parent?.type || ""));
122
+ const facts = nodeFacts(node, state);
123
+ recordBasicSignals(context.stats, node, facts);
124
+ recordSpread(context.stats, facts, parent);
125
+ recordCall(context.stats, facts, state, context.targetName);
126
+ const nextDepth = recordLoop(context.stats, facts);
127
+ for (const child of children(node)) {
128
+ const childIsArgs = ARGUMENT_NODES.has(String(child.type || ""));
129
+ walkSyntax(root, child, {
130
+ parent: node,
131
+ loopDepth: depthForChild(node, child, facts, nextDepth),
132
+ callbackContext: callbackContext || childIsArgs,
133
+ awaited: AWAIT_NODES.has(facts.type) && !facts.isCall
134
+ }, context);
135
+ }
136
+ }
137
+
138
+ export function analyzeSyntaxComplexity(root, { family = "", name = "" } = {}) {
139
+ const parameters = field(root, "parameters") || field(root, "parameter");
140
+ const stats = createStats(parameters, family);
141
+ walkSyntax(root, root, {}, { stats, targetName: normalizedName(name) });
142
+
143
+ const labels = buildLabels(stats);
144
+ const range = sourceRange(root);
145
+ const evidence = buildEvidence(stats);
146
+ const structuralWork = stats.loops || stats.sorts || stats.spreadCopies || stats.linearOps;
147
+ const confidence = stats.recursion ? "low" : structuralWork ? "medium" : stats.callCount ? "low" : "high";
148
+ return {
149
+ ...range,
150
+ family,
151
+ params: stats.params,
152
+ objectFields: stats.objectFields,
153
+ branches: stats.branches,
154
+ cyclomatic: stats.branches + 1,
155
+ loops: stats.loops,
156
+ maxLoopDepth: stats.maxLoopDepth,
157
+ returns: stats.returns,
158
+ awaits: stats.awaits,
159
+ callCount: stats.callCount,
160
+ externalCalls: stats.externalCalls,
161
+ asyncBoundaries: stats.asyncBoundaries,
162
+ allocations: stats.allocations,
163
+ objectLiterals: stats.objectLiterals,
164
+ spreadCopies: stats.spreadCopies,
165
+ sorts: stats.sorts,
166
+ linearOps: stats.linearOps,
167
+ recursion: stats.recursion,
168
+ ...labels,
169
+ scope: "local",
170
+ complexityScope: "local",
171
+ confidence,
172
+ evidence
173
+ };
174
+ }
@@ -0,0 +1,102 @@
1
+ // Graph build — weavatrix's own web-tree-sitter builder.
2
+ // Emits <weavatrix-graphs/<repo>>/graph.json for the rest of the pipeline.
3
+ //
4
+ // The parse itself runs in a WORKER THREAD (build-worker.js): in-process it pinned Electron's main
5
+ // event loop for the whole repo walk, and since Windows routes window input through the main process
6
+ // the ENTIRE app froze on big repos. The worker writes graph.json itself and returns only counts +
7
+ // summaries. If the worker can't even start (exotic packaging), we fall back to the in-process build
8
+ // rather than lose the feature.
9
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { Worker } from "node:worker_threads";
12
+ import { graphOutDirForRepo, repoTopFolders, summarizeCommunities, summarizeHotspots, filterGraphForMode, filterGraphByScope } from "./graph/layout.js";
13
+
14
+ // The worker path deadlocks web-tree-sitter's WASM in Electron's worker threads (fine in plain Node) — off
15
+ // until that's Electron-safe. In-process + event-loop yielding keeps the window responsive without it.
16
+ const USE_BUILD_WORKER = false;
17
+
18
+ // Hard ceiling so a wedged parse (WASM stuck, pathological file, symlink loop) can NEVER leave the UI on
19
+ // an eternal "BUILDING GRAPH…". On timeout we terminate the worker and reject as a REAL failure (no
20
+ // workerStartFailed → we do NOT fall back to the in-process build, which would hang the same way and
21
+ // freeze the whole app on the main thread). 4 min is generous for very large repos.
22
+ const BUILD_WORKER_TIMEOUT_MS = 4 * 60 * 1000;
23
+
24
+ function buildGraphInWorker(payload) {
25
+ return new Promise((resolve, reject) => {
26
+ let worker;
27
+ try {
28
+ worker = new Worker(new URL("./graph/build-worker.js", import.meta.url), { workerData: payload });
29
+ } catch (e) {
30
+ reject(Object.assign(e, { workerStartFailed: true }));
31
+ return;
32
+ }
33
+ let settled = false;
34
+ let timer = null;
35
+ const done = (fn, v) => {
36
+ if (settled) return;
37
+ settled = true;
38
+ if (timer) { clearTimeout(timer); timer = null; }
39
+ fn(v);
40
+ };
41
+ timer = setTimeout(() => {
42
+ try { worker.terminate(); } catch { /* already gone */ }
43
+ done(reject, new Error(`graph build timed out after ${Math.round(BUILD_WORKER_TIMEOUT_MS / 1000)}s (the parser is wedged on this repo — try again, or report it)`));
44
+ }, BUILD_WORKER_TIMEOUT_MS);
45
+ worker.once("message", (msg) => {
46
+ if (msg && msg.ok) done(resolve, msg);
47
+ else done(reject, new Error((msg && msg.error) || "graph worker failed"));
48
+ });
49
+ worker.once("error", (e) => done(reject, Object.assign(e, { workerStartFailed: true })));
50
+ worker.once("exit", (code) => { if (code !== 0) done(reject, Object.assign(new Error(`graph worker exited with code ${code}`), { workerStartFailed: true })); });
51
+ });
52
+ }
53
+
54
+ async function buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central }) {
55
+ const { buildInternalGraph } = await import("./graph/internal-builder.js");
56
+ let graph = await buildInternalGraph(repoPath);
57
+ // mode (no-tests/tests-only) + path-scope reuse the same pure filters graph-builder used.
58
+ if (mode === "no-tests" || mode === "tests-only") graph = filterGraphForMode(graph, mode);
59
+ if (scope) graph = filterGraphByScope(graph, scope);
60
+ mkdirSync(central, { recursive: true });
61
+ writeFileSync(graphJson, JSON.stringify(graph), "utf8");
62
+ return {
63
+ nodes: graph.nodes.length,
64
+ links: graph.links.length,
65
+ communities: summarizeCommunities(graphJson),
66
+ hotspots: summarizeHotspots(graphJson),
67
+ };
68
+ }
69
+
70
+ export async function buildGraphForRepo(repoPath, { mode = "full", scope = "", outDir } = {}) {
71
+ if (!existsSync(repoPath)) return { ok: false, error: "Repo path not found", builder: "internal" };
72
+ const central = outDir || graphOutDirForRepo(repoPath);
73
+ const graphJson = join(central, "graph.json");
74
+ try {
75
+ // In-process is the ONLY path now. The worker (buildGraphInWorker) hung web-tree-sitter's WASM inside an
76
+ // Electron worker thread → an eternal "BUILDING GRAPH…" for JS repos, even though the same code finishes
77
+ // in ~3s on the main thread (and in a plain-Node worker). buildInternalGraph now YIELDS the event loop
78
+ // between file chunks, so the main-thread parse no longer freezes the window — the reason the worker was
79
+ // introduced. buildGraphInWorker/USE_BUILD_WORKER are kept for a future Electron-safe re-enable.
80
+ const built = USE_BUILD_WORKER
81
+ ? await buildGraphInWorker({ repoPath, mode, scope, graphJson, central }).catch((e) => {
82
+ if (e && e.workerStartFailed) return buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central });
83
+ throw e;
84
+ })
85
+ : await buildAndWriteInProcess(repoPath, { mode, scope, graphJson, central });
86
+ return {
87
+ ok: true,
88
+ builder: "internal",
89
+ mode,
90
+ scope,
91
+ topFolders: repoTopFolders(repoPath),
92
+ report: "",
93
+ graphFile: "", // no graph.html — weavatrix renders its own GUI board/relations views
94
+ graphDir: central,
95
+ communities: built.communities,
96
+ hotspots: built.hotspots,
97
+ log: `built-in builder: ${built.nodes} nodes, ${built.links} links`
98
+ };
99
+ } catch (error) {
100
+ return { ok: false, error: `graph build failed: ${error.message}`, builder: "internal" };
101
+ }
102
+ }
package/src/config.js ADDED
@@ -0,0 +1,5 @@
1
+ // weavatrix config (Node).
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const MAIN_DIR = fileURLToPath(new URL(".", import.meta.url));
5
+ export const ROOT_DIR = MAIN_DIR; // fallback cwd for repo-list helpers when a call passes none
@@ -0,0 +1,30 @@
1
+ // build-worker.js — runs the web-tree-sitter graph build OFF Electron's main thread. In-process the
2
+ // parse blocks the main event loop for the whole repo walk (big repos = many seconds), and on Windows
3
+ // window input routing lives on the main process — so the ENTIRE app froze during "BUILDING THE
4
+ // GRAPH…". The worker builds, filters, writes graph.json and computes the summaries; only the small
5
+ // result object crosses back (never the graph itself — structured-cloning a huge graph would stall
6
+ // the main thread again).
7
+ import { parentPort, workerData } from "node:worker_threads";
8
+ import { writeFileSync, mkdirSync } from "node:fs";
9
+ import { buildInternalGraph } from "./internal-builder.js";
10
+ import { filterGraphForMode, filterGraphByScope, summarizeCommunities, summarizeHotspots } from "./layout.js";
11
+
12
+ (async () => {
13
+ const { repoPath, mode, scope, graphJson, central } = workerData || {};
14
+ try {
15
+ let graph = await buildInternalGraph(repoPath);
16
+ if (mode === "no-tests" || mode === "tests-only") graph = filterGraphForMode(graph, mode);
17
+ if (scope) graph = filterGraphByScope(graph, scope);
18
+ mkdirSync(central, { recursive: true });
19
+ writeFileSync(graphJson, JSON.stringify(graph), "utf8");
20
+ parentPort.postMessage({
21
+ ok: true,
22
+ nodes: graph.nodes.length,
23
+ links: graph.links.length,
24
+ communities: summarizeCommunities(graphJson),
25
+ hotspots: summarizeHotspots(graphJson),
26
+ });
27
+ } catch (e) {
28
+ parentPort.postMessage({ ok: false, error: (e && e.message) || String(e) });
29
+ }
30
+ })();
@@ -0,0 +1,40 @@
1
+ // C# extractor. Symbols: class/interface/struct/enum (+record/delegate via optional queries) +
2
+ // method/constructor + properties/fields. Calls: `Foo()` / `x.Foo()` (name only). Heritage: base_list
3
+ // (C# merges extends+implements into one list). `using` directives name NAMESPACES, not files, and a
4
+ // namespace maps to a file only by convention — so no import edges are attempted; cross-file
5
+ // connectivity comes from the name-based calls pass, same as the other non-JS languages.
6
+ const SYMS_CORE = `
7
+ (class_declaration name: (identifier) @class)
8
+ (interface_declaration name: (identifier) @class)
9
+ (struct_declaration name: (identifier) @class)
10
+ (enum_declaration name: (identifier) @class)
11
+ (method_declaration name: (identifier) @method)
12
+ (constructor_declaration name: (identifier) @method)
13
+ (property_declaration name: (identifier) @field)`;
14
+ // Grammar-version-dependent node types, compiled SEPARATELY: one unknown node type voids the whole
15
+ // query it appears in, so these must never ride along with SYMS_CORE.
16
+ const SYMS_OPTIONAL = [
17
+ `(record_declaration name: (identifier) @class)`,
18
+ `(delegate_declaration name: (identifier) @method)`,
19
+ `(field_declaration (variable_declaration (variable_declarator (identifier) @field)))`,
20
+ ];
21
+
22
+ export default {
23
+ family: "csharp",
24
+ grammars: ["c_sharp"],
25
+ exts: { ".cs": "c_sharp" },
26
+ isWeb: false,
27
+ calls: `
28
+ (invocation_expression function: (identifier) @callee)
29
+ (invocation_expression function: (member_access_expression name: (identifier) @callee))`,
30
+ heritage: [`(base_list (identifier) @super)`, `(base_list (generic_name (identifier) @super))`],
31
+
32
+ pass1(ctx) {
33
+ const { grammar, tree, caps, addSym } = ctx;
34
+ for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
35
+ for (const cap of caps(grammar, src, tree.rootNode)) {
36
+ addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
37
+ }
38
+ }
39
+ },
40
+ };
@@ -0,0 +1,29 @@
1
+ // CSS / SCSS / LESS extractor (web reference graph, not a call graph — orchestrator skips pass 2 for it).
2
+ // Symbols: class/id selectors (indexed in selectorIndex so HTML class/id usage can reference the file).
3
+ // Imports: `@import "x.css"`. NOTE: no dedicated SCSS grammar in tree-sitter-wasms, so nested SCSS selectors
4
+ // are under-captured (files still appear as nodes + resolve JS `import styles from './x.scss'`).
5
+ export default {
6
+ family: "css",
7
+ grammars: ["css"],
8
+ exts: { ".css": "css", ".scss": "css", ".less": "css" },
9
+ isWeb: true,
10
+ calls: "",
11
+ heritage: [],
12
+
13
+ pass1(ctx) {
14
+ const { grammar, tree, fileRel, caps, addNode, links, nodeIds, syms, selectorIndex, addImportEdge, resolveHref } = ctx;
15
+ const addSel = (label, line) => {
16
+ const id = `${fileRel}#${label}@${line}`;
17
+ if (nodeIds.has(id)) return;
18
+ addNode({ id, label, file_type: "code", source_file: fileRel, source_location: `L${line}` });
19
+ links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
20
+ syms.push({ id, name: label, start: line });
21
+ let s = selectorIndex.get(label); if (!s) selectorIndex.set(label, (s = new Set())); s.add(fileRel);
22
+ };
23
+ for (const cap of caps(grammar, `(class_selector (class_name) @c)`, tree.rootNode)) addSel("." + cap.node.text, cap.node.startPosition.row + 1);
24
+ for (const cap of caps(grammar, `(id_selector (id_name) @i)`, tree.rootNode)) addSel("#" + cap.node.text, cap.node.startPosition.row + 1);
25
+ for (const cap of caps(grammar, `(import_statement (string_value) @s)`, tree.rootNode)) {
26
+ const tgt = resolveHref(fileRel, cap.node.text.replace(/^["']|["']$/g, "")); if (tgt) addImportEdge(tgt);
27
+ }
28
+ },
29
+ };
@@ -0,0 +1,53 @@
1
+ // Go extractor. Symbols: func/method(receiver)/type(struct+interface)/top-level var+const.
2
+ // Imports: `import "mod/pkg"` → the package DIRECTORY's files (Go package = dir), resolved via go.mod prefix.
3
+ // Unresolved paths are RECORDED as external imports (stdlib → builtin, modules → ecosystem "Go") so
4
+ // dependency analysis can diff them against go.mod. Calls: bare `foo()` (same-file + same-package resolve
5
+ // in the orchestrator; cross-pkg `pkg.Func()` is a selector — see resolveGoPackage/pass2). No inheritance.
6
+ import { goSpecToPkg } from "./spec-pkg.js";
7
+
8
+ export default {
9
+ family: "go",
10
+ grammars: ["go"],
11
+ exts: { ".go": "go" },
12
+ isWeb: false,
13
+ calls: `(call_expression function: (identifier) @callee)`,
14
+ // cross-package `pkg.Func()` — the orchestrator resolves @operand (imported package alias) → its dir, then
15
+ // finds the method there. Without this, funcs called only from other packages look falsely DEAD.
16
+ selectorCall: `(call_expression function: (selector_expression) @sel)`,
17
+ heritage: [],
18
+
19
+ pass1(ctx) {
20
+ const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, resolveGoImport, dirFiles, goModule, goRequires } = ctx;
21
+
22
+ // ---- symbols ----
23
+ for (const cap of caps(grammar, `(function_declaration name: (identifier) @fn) (method_declaration name: (field_identifier) @method)`, tree.rootNode))
24
+ addSym(cap.node.text, cap.node.startPosition.row + 1, true, { sourceNode: cap.node.parent });
25
+ for (const cap of caps(grammar, `(source_file (type_declaration (type_spec name: (type_identifier) @type)))`, tree.rootNode))
26
+ addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
27
+ for (const cap of caps(grammar, `(source_file (var_declaration (var_spec name: (identifier) @var))) (source_file (const_declaration (const_spec name: (identifier) @const)))`, tree.rootNode))
28
+ addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
29
+
30
+ // ---- imports: package path → package directory → all its .go files ----
31
+ // Also record the package's local name (alias or last path segment) → dir, so cross-package `pkg.Func()`
32
+ // selector calls can resolve in pass 2 (see internal-builder resolveCall).
33
+ for (const cap of caps(grammar, `(import_spec) @spec`, tree.rootNode)) {
34
+ const spec = cap.node;
35
+ const pathNode = spec.namedChildren.find((c) => c.type === "interpreted_string_literal");
36
+ if (!pathNode) continue;
37
+ const importPath = pathNode.text.replace(/^["'`]|["'`]$/g, "");
38
+ const d = resolveGoImport(importPath);
39
+ if (!d) {
40
+ // not a repo package: stdlib (builtin) or an external module — record for dependency analysis
41
+ const line = pathNode.startPosition.row + 1;
42
+ const r = goSpecToPkg(importPath, { requires: goRequires || [], ownModule: goModule || "" });
43
+ if (r) addExternalImport({ spec: importPath, pkg: r.pkg, builtin: r.builtin, ecosystem: "Go", kind: "go-import", line });
44
+ else addExternalImport({ spec: importPath, kind: "go-import", line, unresolved: true }); // own-module path with no matching dir
45
+ continue;
46
+ }
47
+ for (const f of dirFiles.get(d) || []) addImportEdge(f); // Go import = whole package (all files in the dir)
48
+ const aliasNode = spec.namedChildren.find((c) => c.type === "package_identifier");
49
+ const local = aliasNode ? aliasNode.text : importPath.split("/").pop();
50
+ if (local && local !== "_" && local !== ".") imports.set(local, { imported: "*", targetDir: d, goPackage: true });
51
+ }
52
+ },
53
+ };
@@ -0,0 +1,29 @@
1
+ // HTML extractor (web reference graph, not a call graph — orchestrator skips pass 2 for it).
2
+ // Edges: `<link href>` / `<script src>` / `<img src>` → the referenced file (imports). Usage: class="…" / id="…"
3
+ // → recorded in htmlUsages, resolved (after all CSS parsed) to the CSS file(s) defining that selector.
4
+ export default {
5
+ family: "html",
6
+ grammars: ["html"],
7
+ exts: { ".html": "html", ".htm": "html" },
8
+ isWeb: true,
9
+ calls: "",
10
+ heritage: [],
11
+
12
+ pass1(ctx) {
13
+ const { grammar, tree, fileRel, caps, addImportEdge, resolveHref, htmlUsages } = ctx;
14
+ for (const cap of caps(grammar, `(attribute) @attr`, tree.rootNode)) {
15
+ const attr = cap.node;
16
+ const nameNode = attr.namedChildren.find((c) => c.type === "attribute_name"); if (!nameNode) continue;
17
+ const qv = attr.namedChildren.find((c) => c.type === "quoted_attribute_value");
18
+ const valNode = (qv && qv.namedChildren.find((c) => c.type === "attribute_value")) || attr.namedChildren.find((c) => c.type === "attribute_value");
19
+ const name = nameNode.text.toLowerCase(); const value = valNode ? valNode.text : "";
20
+ if (!value) continue;
21
+ const tagNode = attr.parent && attr.parent.namedChildren.find((c) => c.type === "tag_name");
22
+ const tag = tagNode ? tagNode.text.toLowerCase() : "";
23
+ if (name === "href" && tag === "link") { const t = resolveHref(fileRel, value); if (t) addImportEdge(t); }
24
+ else if (name === "src" && (tag === "script" || tag === "img")) { const t = resolveHref(fileRel, value); if (t) addImportEdge(t); }
25
+ else if (name === "class") { for (const cls of value.split(/\s+/).filter(Boolean)) htmlUsages.push({ htmlFile: fileRel, label: "." + cls }); }
26
+ else if (name === "id") { htmlUsages.push({ htmlFile: fileRel, label: "#" + value }); }
27
+ }
28
+ },
29
+ };
@@ -0,0 +1,29 @@
1
+ // Java extractor. Symbols: class/interface/enum + method/constructor + fields.
2
+ // Imports: `import a.b.C;` → the C.java file (package path = dir; suffix-matched). Calls: `x.method()` /
3
+ // `method()` (name only). Heritage: `extends` + `implements`.
4
+ const SYMS = `
5
+ (class_declaration name: (identifier) @class)
6
+ (interface_declaration name: (identifier) @class)
7
+ (enum_declaration name: (identifier) @class)
8
+ (method_declaration name: (identifier) @method)
9
+ (constructor_declaration name: (identifier) @method)
10
+ (field_declaration declarator: (variable_declarator name: (identifier) @field))`;
11
+
12
+ export default {
13
+ family: "java",
14
+ grammars: ["java"],
15
+ exts: { ".java": "java" },
16
+ isWeb: false,
17
+ calls: `(method_invocation name: (identifier) @callee)`,
18
+ heritage: [`(superclass (type_identifier) @super)`, `(super_interfaces (type_list (type_identifier) @super))`],
19
+
20
+ pass1(ctx) {
21
+ const { grammar, tree, caps, addSym, addImportEdge, imports, resolveJavaImport } = ctx;
22
+ for (const cap of caps(grammar, SYMS, tree.rootNode)) addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
23
+ for (const cap of caps(grammar, `(import_declaration (scoped_identifier) @imp)`, tree.rootNode)) {
24
+ const parts = cap.node.text.split("."); const cls = parts[parts.length - 1];
25
+ const tgt = resolveJavaImport(parts); if (!tgt) continue;
26
+ addImportEdge(tgt); imports.set(cls, { imported: "*", targetFile: tgt });
27
+ }
28
+ },
29
+ };