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.
- package/README.md +90 -21
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +108 -43
- package/src/analysis/allowed-test-runner.js +20 -9
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +22 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -330
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-verified-change.mjs +12 -4
- package/src/mcp-server.mjs +49 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- 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
|
-
|
|
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
|
+
}
|