weavatrix 0.2.12 → 0.2.14

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 (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -0,0 +1,161 @@
1
+ import {readFileSync} from 'node:fs'
2
+ import {extname} from 'node:path'
3
+ import {Parser, EXT_LANG, FAMILY, LANGS} from './internal-builder.langs.js'
4
+ import {addJavaReferences} from './internal-builder.java.js'
5
+ import {resolveJsBarrels} from './internal-builder.barrels.js'
6
+ import {createPass2Resolution} from './builder/pass2-resolution.js'
7
+ import {createGoReceiverResolution} from './builder/go-receiver-resolution.js'
8
+
9
+ export function runInternalGraphPass2({
10
+ files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
11
+ symIdsByFileName, importedLocals, jsExports,
12
+ }) {
13
+ const resolution = createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName})
14
+ const {dirSymbols, resolveNamedSymbol, resolveCall, resolveJavaType} = resolution
15
+ const {resolveNamespaceMember, reExportOccurrences} = resolveJsBarrels({
16
+ jsExports, importedLocals, links,
17
+ resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? 'type' : 'value'),
18
+ })
19
+ const enclosing = (file, line) => {
20
+ let best = null
21
+ for (const symbol of perFileSymbols.get(file) || []) {
22
+ if (line < symbol.start || line > symbol.end) continue
23
+ if (!best || symbol.start > best.start || (symbol.start === best.start && symbol.end < best.end)) best = symbol
24
+ }
25
+ return best
26
+ }
27
+ const goReceivers = createGoReceiverResolution({resolution, nodeById, importedLocals})
28
+ for (const absolute of files) {
29
+ const file = rel(absolute), grammar = EXT_LANG[extname(absolute)]
30
+ if (!grammar) continue
31
+ const lang = LANGS[FAMILY[grammar]]
32
+ if (!lang || lang.isWeb || !langs[grammar]) continue
33
+ let code
34
+ try { code = readFileSync(absolute, 'utf8') } catch { continue }
35
+ const parser = new Parser()
36
+ parser.setLanguage(langs[grammar])
37
+ let tree
38
+ try { tree = parser.parse(code) } catch { continue }
39
+ if (!lang.customCalls) for (const capture of caps(grammar, lang.calls, tree.rootNode)) {
40
+ const caller = enclosing(file, capture.node.startPosition.row + 1)
41
+ if (!caller) continue
42
+ const target = resolveCall(capture.node.text, file)
43
+ if (target && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
44
+ }
45
+ if (typeof lang.pass2 === 'function') try {
46
+ lang.pass2({
47
+ grammar, tree, fileRel: file, code, caps, field, enclosing, links, nodeById,
48
+ perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
49
+ })
50
+ } catch { /* one language-specific resolver never sinks the graph */ }
51
+ if (lang.selectorCall) for (const capture of caps(grammar, lang.selectorCall, tree.rootNode)) {
52
+ const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
53
+ if (!operand || !member) continue
54
+ const caller = enclosing(file, selector.startPosition.row + 1)
55
+ if (!caller) continue
56
+ const bindings = goReceivers.receiverBindings(selector, file)
57
+ const imported = operand.type === 'identifier' && !bindings.has(operand.text)
58
+ ? importedLocals.get(file)?.get(operand.text) : null
59
+ let target = imported?.targetDir ? dirSymbols.get(imported.targetDir)?.get(member.text) : null
60
+ let resolved = !!target
61
+ if (!target) {
62
+ let receiver = null
63
+ if (operand.type === 'identifier') receiver = bindings.get(operand.text) || null
64
+ else if (operand.type === 'selector_expression') {
65
+ const base = field(operand, 'operand')
66
+ const baseType = base?.type === 'identifier' ? bindings.get(base.text) : null
67
+ receiver = goReceivers.fieldType(baseType, field(operand, 'field')?.text)
68
+ }
69
+ target = goReceivers.exactMethod(receiver, member.text)
70
+ resolved = !!target
71
+ if (!target && !receiver) target = goReceivers.uniqueMethod(goReceivers.dirOf(file), member.text)
72
+ }
73
+ if (target && target !== caller.id) links.push({
74
+ source: caller.id, target, relation: 'calls', confidence: 'INFERRED',
75
+ ...(resolved ? {provenance: 'RESOLVED'} : {}), line: selector.startPosition.row + 1,
76
+ })
77
+ }
78
+ for (const heritageSpec of lang.heritage || []) {
79
+ const query = typeof heritageSpec === 'string' ? heritageSpec : heritageSpec.query
80
+ const relation = typeof heritageSpec === 'string' ? 'inherits' : (heritageSpec.relation || 'inherits')
81
+ for (const capture of caps(grammar, query, tree.rootNode)) {
82
+ const owner = enclosing(file, capture.node.startPosition.row + 1)
83
+ if (!owner) continue
84
+ const target = FAMILY[grammar] === 'java' ? resolveJavaType(capture.node.text, file) : resolveCall(capture.node.text, file)
85
+ if (target && target !== owner.id) links.push({source: owner.id, target, relation, confidence: 'INFERRED'})
86
+ }
87
+ }
88
+ if (FAMILY[grammar] === 'js') addJavaScriptReferences({
89
+ grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember,
90
+ resolveNamedSymbol, enclosing, links,
91
+ })
92
+ if (FAMILY[grammar] === 'go') addGoReferences({
93
+ grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links,
94
+ })
95
+ if (FAMILY[grammar] === 'java') addJavaReferences({grammar, tree, fileRel: file, caps, resolveJavaType, enclosing, links})
96
+ tree.delete()
97
+ }
98
+ return {reExportOccurrences}
99
+ }
100
+
101
+ function addJavaScriptReferences({grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember, resolveNamedSymbol, enclosing, links}) {
102
+ for (const capture of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
103
+ const object = field(capture.node, 'object'), property = field(capture.node, 'property')
104
+ if (!object || object.type !== 'identifier' || !property) continue
105
+ const imported = importedLocals.get(file)?.get(object.text)
106
+ if (!imported || !['*', 'default'].includes(imported.imported) || imported.typeOnly) continue
107
+ const origin = resolveNamespaceMember(file, imported, property.text, 'call')
108
+ if (origin.status !== 'resolved') continue
109
+ const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, 'value')
110
+ const caller = enclosing(file, capture.node.startPosition.row + 1)
111
+ if (target && caller && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
112
+ }
113
+ for (const capture of caps(grammar, `[(jsx_opening_element name: (_) @jsx) (jsx_self_closing_element name: (_) @jsx)]`, tree.rootNode)) {
114
+ const parts = capture.node.text.split('.'), localName = parts[0]
115
+ if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue
116
+ const imported = importedLocals.get(file)?.get(localName)
117
+ if (!imported?.targetFile || imported.typeOnly) continue
118
+ let targetFile = imported.originFile || imported.targetFile
119
+ let importedName = imported.originName || imported.imported
120
+ if (imported.imported === '*' && parts.length > 1) {
121
+ const origin = resolveNamespaceMember(file, imported, parts.at(-1), 'jsx')
122
+ if (origin.status === 'resolved') { targetFile = origin.origin.file; importedName = origin.origin.name }
123
+ }
124
+ const target = resolveNamedSymbol(targetFile, importedName, 'value') || resolveNamedSymbol(targetFile, localName, 'value')
125
+ if (!target) continue
126
+ const owner = enclosing(file, capture.node.startPosition.row + 1)
127
+ links.push({source: owner?.id || file, target, relation: 'references', confidence: 'INFERRED', usage: 'jsx', line: capture.node.startPosition.row + 1})
128
+ }
129
+ if (grammar === 'javascript') return
130
+ for (const capture of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
131
+ const name = capture.node.text, imported = importedLocals.get(file)?.get(name)
132
+ const targetFile = imported?.originFile || imported?.targetFile || file
133
+ const targetName = imported?.originName || imported?.imported || name
134
+ const target = resolveNamedSymbol(targetFile, targetName, 'type')
135
+ if (!target || String(target).startsWith(`${file}#${name}@${capture.node.startPosition.row + 1}`)) continue
136
+ const source = enclosing(file, capture.node.startPosition.row + 1)?.id || file
137
+ if (source !== target) links.push({source, target, relation: 'references', confidence: 'EXTRACTED', provenance: 'RESOLVED', typeOnly: true, line: capture.node.startPosition.row + 1, usage: 'type'})
138
+ }
139
+ }
140
+
141
+ function addGoReferences({grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links}) {
142
+ const seen = new Set()
143
+ const emit = (source, target) => {
144
+ const key = `${source}>${target}`
145
+ if (!target || target === source || seen.has(key)) return
146
+ seen.add(key); links.push({source, target, relation: 'references', confidence: 'INFERRED'})
147
+ }
148
+ const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '', symbols = dirSymbols.get(dir)
149
+ for (const capture of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) {
150
+ const target = symbols?.get(capture.node.text)
151
+ if (!target || target.slice(0, target.indexOf('#')) === file) continue
152
+ emit(enclosing(file, capture.node.startPosition.row + 1)?.id || file, target)
153
+ }
154
+ for (const capture of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
155
+ const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
156
+ if (!operand || operand.type !== 'identifier' || !member) continue
157
+ const imported = importedLocals.get(file)?.get(operand.text)
158
+ const target = imported?.targetDir && dirSymbols.get(imported.targetDir)?.get(member.text)
159
+ if (target) emit(enclosing(file, selector.startPosition.row + 1)?.id || file, target)
160
+ }
161
+ }
@@ -6,6 +6,7 @@ import { readFileSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { parseGoMod } from "../analysis/manifests.js";
8
8
  import { createRepoBoundary } from "../repo-path.js";
9
+ import { createRustResolvers } from "./resolvers/rust.js";
9
10
 
10
11
  export function buildResolvers(repoDir, fileSet) {
11
12
  const boundary = createRepoBoundary(repoDir);
@@ -48,111 +49,7 @@ export function buildResolvers(repoDir, fileSet) {
48
49
  return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
49
50
  };
50
51
 
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
- };
52
+ const { resolveRustMod, resolveRustPath } = createRustResolvers(fileSet);
156
53
 
157
54
  // Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
158
55
  // Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
@@ -0,0 +1,117 @@
1
+ import {join} from 'node:path'
2
+
3
+ const cleanRustRel = (path) => String(path || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/').replace(/\/$/, '').replace(/^\.$/, '')
4
+ const rustDir = (path) => {
5
+ const clean = cleanRustRel(path)
6
+ const index = clean.lastIndexOf('/')
7
+ return index < 0 ? '' : clean.slice(0, index)
8
+ }
9
+ const rustBase = (path) => {
10
+ const clean = cleanRustRel(path)
11
+ const index = clean.lastIndexOf('/')
12
+ return index < 0 ? clean : clean.slice(index + 1)
13
+ }
14
+ const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((part) => part != null && part !== '')))
15
+
16
+ export function createRustResolvers(fileSet) {
17
+ const rustFiles = new Set([...fileSet].filter((file) => file.endsWith('.rs')))
18
+ const rustRoots = new Map()
19
+ for (const file of rustFiles) {
20
+ const base = rustBase(file)
21
+ if (base !== 'lib.rs' && base !== 'main.rs') continue
22
+ const dir = rustDir(file)
23
+ let root = rustRoots.get(dir)
24
+ if (!root) rustRoots.set(dir, (root = {base: dir, lib: null, main: null}))
25
+ root[base === 'lib.rs' ? 'lib' : 'main'] = file
26
+ }
27
+ const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length)
28
+
29
+ const rustContext = (fromRel) => {
30
+ const clean = cleanRustRel(fromRel)
31
+ const base = rustBase(clean)
32
+ const dir = rustDir(clean)
33
+ if (base === 'lib.rs' || base === 'main.rs') return {base: dir, rootFile: clean}
34
+ if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return {base: dir, rootFile: clean}
35
+ for (const root of rustRootList) {
36
+ if (!root.base || clean.startsWith(root.base + '/')) return {base: root.base, rootFile: root.lib || root.main}
37
+ }
38
+ return {base: dir, rootFile: clean}
39
+ }
40
+ const rustModuleBase = (fromRel) => {
41
+ const context = rustContext(fromRel)
42
+ if (context.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel)
43
+ const name = rustBase(fromRel)
44
+ if (name === 'lib.rs' || name === 'main.rs' || name === 'mod.rs') return rustDir(fromRel)
45
+ return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ''))
46
+ }
47
+ const rustInlineBase = (fromRel, inlineModules = []) => {
48
+ let base = rustModuleBase(fromRel)
49
+ for (let index = 0; index < inlineModules.length; index++) {
50
+ const module = inlineModules[index] || {}
51
+ if (module.path) {
52
+ const parent = index === 0 ? rustDir(fromRel) : base
53
+ base = rustJoin(parent, module.path)
54
+ } else base = rustJoin(base, module.name)
55
+ }
56
+ return base
57
+ }
58
+ const rustModuleFile = (moduleBase, context) => {
59
+ const clean = cleanRustRel(moduleBase)
60
+ if (clean === cleanRustRel(context.base) && context.rootFile && rustFiles.has(context.rootFile)) return context.rootFile
61
+ const flat = clean + '.rs'
62
+ if (rustFiles.has(flat)) return flat
63
+ const legacy = rustJoin(clean, 'mod.rs')
64
+ return rustFiles.has(legacy) ? legacy : null
65
+ }
66
+
67
+ const resolveRustMod = (fromRel, name, {inlineModules = [], explicitPath = ''} = {}) => {
68
+ const clean = cleanRustRel(fromRel)
69
+ if (explicitPath) {
70
+ const parent = inlineModules.length ? rustInlineBase(clean, inlineModules) : rustDir(clean)
71
+ const target = rustJoin(parent, explicitPath)
72
+ return rustFiles.has(target) ? target : null
73
+ }
74
+ const targetBase = rustJoin(rustInlineBase(clean, inlineModules), String(name || '').replace(/^r#/, ''))
75
+ return rustModuleFile(targetBase, rustContext(clean))
76
+ }
77
+
78
+ const resolveRustPath = (fromRel, rawSegments, {inlineModules = [], unqualified = true} = {}) => {
79
+ const clean = cleanRustRel(fromRel)
80
+ const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || '').split('::'))
81
+ .map((segment) => String(segment).trim().replace(/^r#/, '')).filter(Boolean)
82
+ if (!segments.length) return null
83
+ const context = rustContext(clean)
84
+ const current = rustInlineBase(clean, inlineModules)
85
+ const rest = [...segments]
86
+ const starts = []
87
+ let anchored = false
88
+ if (rest[0] === 'crate') { anchored = true; rest.shift(); starts.push(context.base) }
89
+ else if (rest[0] === 'self') { anchored = true; rest.shift(); starts.push(current) }
90
+ else if (rest[0] === 'super') {
91
+ anchored = true
92
+ let base = current
93
+ while (rest[0] === 'super') { rest.shift(); base = rustDir(base) }
94
+ if (context.base && base !== context.base && !base.startsWith(context.base + '/')) return null
95
+ starts.push(base)
96
+ } else if (unqualified) {
97
+ starts.push(context.base)
98
+ if (current !== context.base) starts.push(current)
99
+ } else return null
100
+
101
+ for (const start of starts) {
102
+ const minimum = anchored ? 0 : 1
103
+ for (let used = rest.length; used >= minimum; used--) {
104
+ const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), context)
105
+ if (target) return {
106
+ targetFile: target,
107
+ consumed: segments.length - rest.length + used,
108
+ remaining: rest.slice(used),
109
+ anchored,
110
+ }
111
+ }
112
+ }
113
+ return null
114
+ }
115
+
116
+ return {resolveRustMod, resolveRustPath}
117
+ }
@@ -0,0 +1,18 @@
1
+ import {refreshAdvisories, storeMeta, DEFAULT_STORE} from '../../security/advisory-store.js'
2
+ import {collectInstalled} from '../../security/installed.js'
3
+
4
+ export async function tRefreshAdvisories(g, args, ctx) {
5
+ if (!ctx.repoRoot) return 'No repo root — cannot collect installed packages.'
6
+ const {installed} = collectInstalled(ctx.repoRoot)
7
+ if (!installed.length) return 'No pinned packages found in lockfiles (npm/yarn/pip/poetry/uv/go) — nothing to query.'
8
+ const res = await refreshAdvisories({installed, repoKey: ctx.repoRoot, timeoutMs: Number(args.timeout_ms) || undefined})
9
+ if (res.ok === false) return `Advisory refresh failed: ${res.error}`
10
+ const meta = storeMeta()
11
+ return [
12
+ `Advisory store ${res.status === 'PARTIAL' ? 'partially refreshed' : 'refreshed'} from OSV.dev: ${res.queriedOk ?? res.queried}/${res.queried} package versions queried successfully, ${res.vulnerable} with known advisories (${res.fetched} advisory records fetched).`,
13
+ res.unsupported ? `${res.unsupported} packages skipped (ecosystem not OSV-queryable — npm/PyPI/Go only).` : null,
14
+ res.errors?.length ? `Partial: ${res.errors.length} request error(s), first: ${res.errors[0]}` : null,
15
+ `Store: ${DEFAULT_STORE} (${meta.advisoryCount} advisories, fetched ${meta.fetchedAt}). run_audit now reflects it — offline.`,
16
+ ].filter(Boolean).join('\n')
17
+ }
18
+
@@ -0,0 +1,148 @@
1
+ import {existsSync, readFileSync, realpathSync, statSync, writeFileSync} from 'node:fs'
2
+ import {dirname, isAbsolute, join} from 'node:path'
3
+ import {diffGraphs, formatGraphDiff, prevGraphPathFor} from '../graph-context.mjs'
4
+ import {buildGraphForRepo, defaultPrecisionMode} from '../../build-graph.js'
5
+ import {graphHomeDir, graphOutDirForModule, graphOutDirForRepo} from '../../graph/layout.js'
6
+ import {liveRepositoryRecords, registerRepository} from '../../graph/repo-registry.js'
7
+ import {precisionSemanticInputsMatch, readPrecisionOverlay} from '../../precision/lsp-overlay.js'
8
+
9
+ export async function tRebuildGraph(g, args, ctx) {
10
+ if (!ctx.repoRoot) return 'Rebuild needs the repo root (not provided to this server).'
11
+ const mode = ['no-tests', 'tests-only', 'full'].includes(args.mode)
12
+ ? args.mode
13
+ : ['no-tests', 'tests-only', 'full'].includes(g?.graphBuildMode) ? g.graphBuildMode : 'full'
14
+ const precision = args.precision === 'off' ? 'off' : args.precision === 'lsp' ? 'lsp' : (g?.graphPrecisionMode || defaultPrecisionMode())
15
+ const scope = String(args.scope || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
16
+ if (scope) {
17
+ const scopedDir = graphOutDirForModule(ctx.repoRoot, scope)
18
+ const scoped = await buildGraphForRepo(ctx.repoRoot, {mode, scope, precision, outDir: scopedDir})
19
+ if (!scoped || !scoped.ok) return `Scoped graph build failed: ${(scoped && scoped.error) || 'unknown error'}`
20
+ return [
21
+ `Built an isolated scoped graph for ${scope} (${mode}). ${scoped.log || ''}.`,
22
+ `The active full-repository graph was not replaced, so this operation cannot report a false full→scope structural delta.`,
23
+ `Scoped graph: ${join(scopedDir, 'graph.json')}`,
24
+ ].join('\n')
25
+ }
26
+ // snapshot the outgoing state: bytes → graph.prev.json (for graph_diff later), struct → inline delta
27
+ let prevBytes = null
28
+ try { prevBytes = readFileSync(ctx.graphPath) } catch { /* first build — nothing to diff against */ }
29
+ let before = null
30
+ try { before = prevBytes ? JSON.parse(prevBytes.toString('utf8')) : null } catch { before = null }
31
+ const res = await buildGraphForRepo(ctx.repoRoot, {mode, scope: '', precision, outDir: ctx.graphPath ? dirname(ctx.graphPath) : undefined})
32
+ if (!res || !res.ok) return `Graph rebuild failed: ${(res && res.error) || 'unknown error'}`
33
+ if (prevBytes) { try { writeFileSync(prevGraphPathFor(ctx.graphPath), prevBytes) } catch { /* snapshot is best-effort */ } }
34
+ const fresh = ctx.reload() // refresh THIS server's in-memory graph so subsequent tool calls see the new graph
35
+ let afterStatic = null
36
+ try { afterStatic = JSON.parse(readFileSync(ctx.graphPath, 'utf8')) } catch { /* reload already reports the failure */ }
37
+ const beforeMode = ['full', 'no-tests', 'tests-only'].includes(before?.graphBuildMode) ? before.graphBuildMode : 'full'
38
+ const afterMode = ['full', 'no-tests', 'tests-only'].includes(afterStatic?.graphBuildMode) ? afterStatic.graphBuildMode : 'full'
39
+ const delta = before && afterStatic
40
+ ? beforeMode === afterMode
41
+ ? formatGraphDiff(diffGraphs(before, afterStatic))
42
+ : `Structural delta not computed: build mode changed from ${beforeMode} to ${afterMode}, so the node/edge universes are not comparable.`
43
+ : null
44
+ return [
45
+ `Rebuilt the graph (${mode}). ${res.log || ''}. In-memory graph reloaded — graph tools now reflect it.`,
46
+ delta
47
+ ].filter(Boolean).join('\n\n')
48
+ }
49
+
50
+ // Retarget this server at ANOTHER local repository at runtime — one weavatrix registration serves any
51
+ // repo. Loads <parent>/weavatrix-graphs/<name>/graph.json (the central layout graphs
52
+ // always live in), building it first when missing. On a failed load the previous repo stays active.
53
+ export async function tOpenRepo(g, args, ctx) {
54
+
55
+ const requestedPath = String(args.path || '').trim()
56
+ if (!requestedPath) return 'Provide "path" — an absolute path to a local repository folder.'
57
+ if (!isAbsolute(requestedPath)) return 'open_repo requires an absolute repository path.'
58
+ let repoPath
59
+ try { repoPath = realpathSync.native(requestedPath) } catch { return `Path not found: ${requestedPath}` }
60
+ try { if (!statSync(repoPath).isDirectory()) return `Not a directory: ${requestedPath}` } catch { return `Path not found: ${requestedPath}` }
61
+ if (!existsSync(join(repoPath, '.git'))) return `Not a Git repository: ${requestedPath}`
62
+ const graphPath = join(graphOutDirForRepo(repoPath), 'graph.json')
63
+ const graphExists = existsSync(graphPath)
64
+ const requestedMode = ['no-tests', 'tests-only', 'full'].includes(args.mode) ? args.mode : null
65
+ const requestedPrecision = ['lsp', 'off'].includes(args.precision) ? args.precision : null
66
+ let built = false
67
+ let upgrade = false
68
+ let schemaUpgrade = false
69
+ let precisionUpgrade = false
70
+ let savedMode = 'full'
71
+ let savedPrecision = defaultPrecisionMode()
72
+ if (graphExists) {
73
+ try {
74
+ const saved = JSON.parse(readFileSync(graphPath, 'utf8'))
75
+ savedMode = ['no-tests', 'tests-only', 'full'].includes(saved.graphBuildMode) ? saved.graphBuildMode : 'full'
76
+ savedPrecision = ['lsp', 'off'].includes(saved.graphPrecisionMode) ? saved.graphPrecisionMode : defaultPrecisionMode()
77
+ schemaUpgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
78
+ || !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
79
+ || !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 5
80
+ || !Number.isInteger(saved.reExportOccurrencesV) || saved.reExportOccurrencesV < 1
81
+ || !Number.isInteger(saved.symbolSpacesV) || saved.symbolSpacesV < 1
82
+ || !Number.isInteger(saved.physicalFileLocV) || saved.physicalFileLocV < 1
83
+ if (savedPrecision === 'lsp') {
84
+ const overlay = readPrecisionOverlay(graphPath, saved)
85
+ precisionUpgrade = !overlay || (typeof overlay.semanticInputFingerprint === 'string'
86
+ && !precisionSemanticInputsMatch(overlay, repoPath, saved))
87
+ }
88
+ upgrade = schemaUpgrade || precisionUpgrade
89
+ } catch {
90
+ upgrade = true
91
+ }
92
+ }
93
+ const modeMismatch = graphExists && requestedMode != null && requestedMode !== savedMode
94
+ const precisionMismatch = graphExists && requestedPrecision != null && requestedPrecision !== savedPrecision
95
+ if (!graphExists || upgrade || modeMismatch || precisionMismatch) {
96
+ if (args.build === false) {
97
+ if (modeMismatch && !upgrade) {
98
+ return `The existing graph for ${repoPath} was built in ${savedMode}, but ${requestedMode} was requested. Re-call without build:false to rebuild it before switching.`
99
+ }
100
+ if (precisionMismatch && !upgrade) {
101
+ return `The existing graph for ${repoPath} uses semantic precision ${savedPrecision}, but ${requestedPrecision} was requested. Re-call without build:false to rebuild it before switching.`
102
+ }
103
+ return upgrade
104
+ ? precisionUpgrade && !schemaUpgrade
105
+ ? `The existing graph for ${repoPath} lacks current revision-matched semantic precision evidence. Re-call without build:false to refresh it before switching.`
106
+ : `The existing graph for ${repoPath} predates current graph metadata (typed edges, provenance, or physical file LOC). Re-call without build:false to upgrade it before switching.`
107
+ : `No graph yet for ${repoPath} (expected at ${graphPath}). Re-call without build:false to build one — large repos can take minutes.`
108
+ }
109
+ const mode = requestedMode || (graphExists ? savedMode : 'full')
110
+ const precision = requestedPrecision || (graphExists ? savedPrecision : defaultPrecisionMode())
111
+ const res = await buildGraphForRepo(repoPath, {mode, scope: '', precision})
112
+ if (!res || !res.ok) return `Graph build failed for ${repoPath}: ${(res && res.error) || 'unknown error'}`
113
+ built = true
114
+ }
115
+ const prev = {graphPath: ctx.graphPath, repoRoot: ctx.repoRoot}
116
+ ctx.graphPath = graphPath
117
+ ctx.repoRoot = repoPath
118
+ const loaded = ctx.reload()
119
+ if (!loaded) {
120
+ ctx.graphPath = prev.graphPath
121
+ ctx.repoRoot = prev.repoRoot
122
+ ctx.reload()
123
+ return `Failed to load ${graphPath} — still targeting the previous repo (${prev.repoRoot || 'none'}).`
124
+ }
125
+ registerRepository({repoPath, graphDir: graphOutDirForRepo(repoPath), graphHome: graphHomeDir()})
126
+ const buildNote = built ? (upgrade ? ' (graph upgraded to current edge/precision metadata)' : modeMismatch ? ' (graph rebuilt in the requested mode)' : precisionMismatch ? ' (semantic precision mode updated)' : ' (graph built fresh)') : ''
127
+ return [
128
+ `Opened ${repoPath}${buildNote}: ${loaded.nodes.length} nodes / ${loaded.links.length} edges. All tools now target this repo.`,
129
+ `Graph: ${graphPath}`,
130
+ `Build mode: ${loaded.graphBuildMode || 'full'}`,
131
+ `Semantic precision: ${loaded.precision?.state || 'UNAVAILABLE'} (${loaded.precision?.verifiedEdges || 0} EXACT_LSP edge(s))`,
132
+ ].join('\n')
133
+ }
134
+
135
+ // Sibling repos that already have a built graph in the central weavatrix-graphs folder — open_repo candidates.
136
+ export function tListKnownRepos(g, args, ctx) {
137
+ const root = graphHomeDir()
138
+ const norm = (p) => String(p).replace(/[\\/]+/g, '/').toLowerCase()
139
+ const rows = liveRepositoryRecords(root).map((record) => ({
140
+ ...record,
141
+ builtAt: statSync(join(record.graphDir, 'graph.json')).mtime.toISOString(),
142
+ }))
143
+ if (!rows.length) return `No registered graphs under ${root}. Build a repository once with open_repo or rebuild_graph.`
144
+ return [
145
+ `Known repositories (${rows.length}) in ${root}:`,
146
+ ...rows.map((r) => ` ${norm(r.repoPath) === norm(ctx.repoRoot) ? '»' : ' '} ${r.label} [${r.repositoryId}] — graph built ${r.builtAt} (${r.repoPath})`),
147
+ ].join('\n')
148
+ }