weavatrix 0.2.14 → 0.2.16

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 (65) hide show
  1. package/README.md +69 -13
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.16.md +53 -0
  4. package/package.json +6 -2
  5. package/src/analysis/cargo-dependency-evidence.js +111 -0
  6. package/src/analysis/cargo-manifests.js +74 -0
  7. package/src/analysis/dep-check-ecosystems.js +3 -0
  8. package/src/analysis/dep-check.js +8 -18
  9. package/src/analysis/dependency/conventions.js +16 -0
  10. package/src/analysis/dependency/scoped-dependencies.js +7 -2
  11. package/src/analysis/dependency/source-references.js +21 -0
  12. package/src/analysis/duplicates.tokenize.js +12 -2
  13. package/src/analysis/endpoints-java.js +2 -5
  14. package/src/analysis/findings.js +12 -0
  15. package/src/analysis/go-dependency-evidence.js +68 -0
  16. package/src/analysis/health-capabilities.js +1 -1
  17. package/src/analysis/http-contracts/analysis.js +2 -0
  18. package/src/analysis/http-contracts/client-call-detection.js +1 -0
  19. package/src/analysis/http-contracts/client-call-parser.js +18 -4
  20. package/src/analysis/internal-audit/dependency-health.js +15 -13
  21. package/src/analysis/internal-audit/python-manifests.js +18 -5
  22. package/src/analysis/internal-audit/supply-chain.js +2 -0
  23. package/src/analysis/internal-audit.reach.js +20 -0
  24. package/src/analysis/internal-audit.run.js +13 -7
  25. package/src/analysis/jvm-dependency-evidence.js +102 -56
  26. package/src/analysis/jvm-manifests.js +114 -0
  27. package/src/analysis/source-correctness.js +2 -5
  28. package/src/analysis/task-retrieval.js +3 -14
  29. package/src/analysis/transport-contracts.js +157 -0
  30. package/src/graph/builder/lang-go.js +4 -2
  31. package/src/graph/builder/lang-java.js +12 -3
  32. package/src/graph/builder/lang-rust.js +22 -3
  33. package/src/graph/freshness-probe.js +1 -1
  34. package/src/graph/internal-builder.build.js +2 -2
  35. package/src/graph/internal-builder.resolvers.js +18 -8
  36. package/src/mcp/actions/advisories.mjs +2 -3
  37. package/src/mcp/catalog.mjs +7 -4
  38. package/src/mcp/company-contract-verdict.mjs +42 -0
  39. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  40. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  41. package/src/mcp/git-output.mjs +10 -0
  42. package/src/mcp/graph/context-seeds.mjs +3 -14
  43. package/src/mcp/graph/reverse-reach.mjs +42 -0
  44. package/src/mcp/sync/evidence-duplicates.mjs +1 -4
  45. package/src/mcp/tools-company.mjs +28 -53
  46. package/src/mcp/tools-impact-change.mjs +2 -38
  47. package/src/mcp/tools-impact-precision.mjs +89 -0
  48. package/src/mcp/tools-impact.mjs +53 -136
  49. package/src/path-classification.js +14 -0
  50. package/src/precision/lsp-overlay/build.js +10 -0
  51. package/src/precision/lsp-overlay/contract.js +1 -1
  52. package/src/precision/symbol-query.js +39 -0
  53. package/src/precision/typescript-provider/client.js +16 -3
  54. package/src/precision/typescript-provider/discovery.js +1 -1
  55. package/src/precision/typescript-provider/isolated-runtime.js +39 -0
  56. package/src/precision/typescript-provider/project-host.js +11 -6
  57. package/src/precision/typescript-provider/project-safety.js +5 -0
  58. package/src/security/advisory-store.js +2 -2
  59. package/src/security/installed-jvm-rust.js +46 -0
  60. package/src/security/installed.js +21 -1
  61. package/src/security/malware-heuristics.sweep.js +1 -1
  62. package/src/security/registry-sig.classify.js +2 -1
  63. package/src/security/registry-sig.rules.js +3 -3
  64. package/src/util.js +8 -0
  65. package/docs/releases/v0.2.14.md +0 -93
@@ -2,53 +2,16 @@
2
2
  // last rebuild (graph_diff), and the blast radius of the current change set (change_impact).
3
3
  // Hot-reloadable (re-imported by catalog.mjs on change).
4
4
  import {readFileSync} from 'node:fs'
5
- import {spawnSync} from 'node:child_process'
6
- import {childProcessEnv} from '../child-env.js'
7
5
  import {
8
6
  isSymbol, degreeOf, labelOf, resolveNodeInfo, ambiguityNote,
9
7
  rawGraph, prevGraphPathFor, edgeEndpoint, diffGraphs, formatGraphDiff,
10
8
  } from './graph-context.mjs'
11
- import {readCoverageForRepo} from '../analysis/coverage-reports.js'
12
- import {isStructuralRelation} from '../graph/relations.js'
9
+ import {querySymbolPrecision} from '../precision/symbol-query.js'
10
+ import {graphWithExactDirectReferences, refineChangeImpact} from './tools-impact-precision.mjs'
11
+ import {reverseReach} from './graph/reverse-reach.mjs'
13
12
  import {tChangeImpactV2} from './tools-impact-change.mjs'
14
13
  import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
15
14
 
16
- function reverseReach(g, seeds, maxDepth) {
17
- const states = new Map([...seeds].map((id) => [String(id), {
18
- runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
19
- }]))
20
- const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
21
- for (let cursor = 0; cursor < frontier.length; cursor++) {
22
- const current = frontier[cursor]
23
- if (current.depth >= maxDepth) continue
24
- for (const e of g.inn.get(current.id) || []) {
25
- if (isStructuralRelation(e.relation) || e.barrelProxy === true) continue
26
- const id = String(e.id)
27
- const compileOnly = current.compileOnly || e.typeOnly === true || e.compileOnly === true
28
- const depth = current.depth + 1
29
- const entry = states.get(id) || {
30
- runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null,
31
- }
32
- const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
33
- const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
34
- const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
35
- if (entry[depthKey] != null && entry[depthKey] <= depth) continue
36
- entry[depthKey] = depth
37
- entry[relationKey] = e.relation || 'rel'
38
- entry[provenanceKey] = e.provenance || 'UNKNOWN'
39
- states.set(id, entry)
40
- frontier.push({id, depth, compileOnly})
41
- }
42
- }
43
- return new Map([...states].map(([id, entry]) => [id, {
44
- ...entry,
45
- depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
46
- compileOnly: entry.runtimeDepth == null,
47
- relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
48
- provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
49
- }]))
50
- }
51
-
52
15
  const impactKind = (entry) => {
53
16
  if (entry?.runtimeDepth != null) {
54
17
  return entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime'
@@ -59,7 +22,7 @@ const impactKind = (entry) => {
59
22
  // Transitive blast-radius: who is affected if this node changes. Walks REVERSE dependency edges
60
23
  // (calls/imports/inherits — not structural `contains`) out to `depth`. For a symbol, also seeds its
61
24
  // containing file, because importers depend on the file rather than the individual symbol.
62
- export function tGetDependents(g, {label, depth = 3, max_nodes = 40, include_container_importers = false} = {}) {
25
+ function getDependentsFromGraph(g, {label, depth = 3, max_nodes = 40, include_container_importers = false} = {}, precisionNote = null) {
63
26
  const info = resolveNodeInfo(g, label)
64
27
  const n = info.node
65
28
  if (!n) return `No node found matching "${label}".`
@@ -81,18 +44,64 @@ export function tGetDependents(g, {label, depth = 3, max_nodes = 40, include_con
81
44
  .filter(([nid]) => !seeds.has(nid))
82
45
  .map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid)}))
83
46
  .sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
84
- if (!ranked.length) return [note, `No dependents found for ${n.label ?? id} within depth ${maxDepth} — nothing in the graph calls, imports, or inherits it.`].filter(Boolean).join('\n')
47
+ if (!ranked.length) return [note, precisionNote, `No dependents found for ${n.label ?? id} within depth ${maxDepth} — nothing in the graph calls, imports, or inherits it.`].filter(Boolean).join('\n')
85
48
  const shown = ranked.slice(0, cap)
86
49
  const runtimeCount = ranked.filter((entry) => !entry.entry.compileOnly).length
87
50
  const compileCount = ranked.length - runtimeCount
88
51
  return [
89
52
  note,
53
+ precisionNote,
90
54
  `Dependents of ${n.label ?? id} (reverse calls/imports/inherits, depth ≤${maxDepth}): ${ranked.length} found (${runtimeCount} runtime, ${compileCount} compile-time-only), showing ${shown.length} by proximity + connectivity.`,
91
55
  containingFile ? `Includes importers of its containing file ${labelOf(g, containingFile)} by explicit request.` : null,
92
56
  ...shown.map((r) => ` [d${r.d} ${impactKind(r.entry)}] ${r.entry.relation || 'rel'} [${r.entry.provenance || 'UNKNOWN'}] ${labelOf(g, r.id)} (deg ${r.deg}) [${r.id}]`),
93
57
  ].filter(Boolean).join('\n')
94
58
  }
95
59
 
60
+ export function tGetDependents(g, args = {}, ctx = {}) {
61
+ const info = resolveNodeInfo(g, args.label)
62
+ const id = String(info.node?.id || '')
63
+ const requested = ['auto', 'graph', 'lsp'].includes(args.precision) ? args.precision : 'auto'
64
+ const canQuery = isSymbol(id) && ctx.repoRoot && ctx.graphPath
65
+ && (requested === 'lsp' || (requested === 'auto' && g.graphPrecisionMode !== 'off'))
66
+ if (!canQuery) {
67
+ const note = isSymbol(id) && requested !== 'graph'
68
+ ? 'Semantic precision: graph-only; an exact point query was unavailable or disabled.' : null
69
+ return getDependentsFromGraph(g, args, note)
70
+ }
71
+ return (async () => {
72
+ try {
73
+ const result = await querySymbolPrecision({
74
+ repoRoot: ctx.repoRoot,
75
+ graphPath: ctx.graphPath,
76
+ targetId: id,
77
+ maxReferences: Math.max(100, Math.min(5_000, Number(args.max_references) || 1_000)),
78
+ timeoutMs: Math.max(1_000, Math.min(60_000, Number(args.timeout_ms) || 30_000)),
79
+ clientFactory: ctx.precisionClientFactory,
80
+ })
81
+ const exactGraph = graphWithExactDirectReferences(g, id, result.overlay)
82
+ if (exactGraph) {
83
+ const count = (result.overlay.links || []).filter((link) => String(link?.target || '') === id).length
84
+ return getDependentsFromGraph(
85
+ exactGraph,
86
+ args,
87
+ `Semantic precision: EXACT_LSP point query${result.cached ? ' (cache hit)' : ''}; ${count} classified direct reference edge(s).`,
88
+ )
89
+ }
90
+ return getDependentsFromGraph(
91
+ g,
92
+ args,
93
+ `Semantic precision: ${result.overlay?.state || 'UNAVAILABLE'}; exact absence was not proven, so graph edges are retained (${result.overlay?.reason || 'incomplete project coverage'}).`,
94
+ )
95
+ } catch (error) {
96
+ return getDependentsFromGraph(
97
+ g,
98
+ args,
99
+ `Semantic precision: UNAVAILABLE; graph evidence retained (${error?.message || 'point query failed'}).`,
100
+ )
101
+ }
102
+ })()
103
+ }
104
+
96
105
  // Re-query the last rebuild's before/after pair (graph.prev.json vs graph.json), optionally scoped.
97
106
  export async function tGraphDiff(g, args, ctx) {
98
107
  const current = rawGraph(ctx)
@@ -141,104 +150,12 @@ export async function tGraphDiff(g, args, ctx) {
141
150
  }
142
151
 
143
152
  // ---- change impact -------------------------------------------------------------------------------
144
- function gitLines(repoRoot, args) {
145
- const res = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
146
- if (res.status !== 0) return null
147
- return String(res.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
148
- }
149
-
150
- function resolveImpactBase(repoRoot, requested) {
151
- const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
152
- for (const ref of candidates) {
153
- const ok = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
154
- if (ok.status === 0) return ref
155
- }
156
- return null
157
- }
158
-
159
153
  // Blast radius of a change, without any GitHub API: diff the CURRENT change (branch
160
154
  // commits since the merge-base + staged/unstaged + untracked) against a base ref, map the changed
161
155
  // files and their symbols onto the graph, and walk REVERSE dependency edges — everything the change
162
156
  // can break, ranked by proximity + connectivity, with file-level test coverage attached so the
163
157
  // untested part of the blast radius stands out. The pre-PR review, in one call.
164
158
  export function tChangeImpact(g, args, ctx) {
165
- return tChangeImpactV2(g, args, ctx)
166
- }
167
-
168
- // Kept private during the v2 rollout so the surrounding get_dependents/graph_diff implementation is
169
- // untouched; focused tests exercise the exported symbol-aware path above.
170
- function tChangeImpactLegacy(g, args, ctx) {
171
- if (!ctx.repoRoot) return 'change_impact needs the repo root (not provided to this server).'
172
- // Explicit file list (e.g. a PR's changed files) skips the local git diff entirely — this is how
173
- // a NOT-checked-out PR gets its impact assessed.
174
- const explicit = Array.isArray(args.files)
175
- ? [...new Set(args.files.map((f) => String(f).replace(/\\/g, '/').trim()).filter(Boolean))]
176
- : null
177
- let changed
178
- let sourceLabel
179
- if (explicit) {
180
- if (!explicit.length) return 'files was provided but empty — pass repo-relative paths, or omit it to diff the local change.'
181
- changed = explicit
182
- sourceLabel = `for ${changed.length} provided file(s)`
183
- } else {
184
- const base = resolveImpactBase(ctx.repoRoot, args.base ? String(args.base).trim() : '')
185
- if (!base) return `Could not resolve a base ref${args.base ? ` ("${args.base}")` : ''} — pass base explicitly (e.g. origin/main or HEAD~1).`
186
- const committed = gitLines(ctx.repoRoot, ['diff', '--name-only', `${base}...HEAD`])
187
- if (committed === null) return `git diff against ${base} failed — is ${ctx.repoRoot} a git repository?`
188
- const uncommitted = gitLines(ctx.repoRoot, ['diff', '--name-only', 'HEAD']) || []
189
- const untracked = gitLines(ctx.repoRoot, ['ls-files', '--others', '--exclude-standard']) || []
190
- changed = [...new Set([...committed, ...uncommitted, ...untracked])]
191
- sourceLabel = `vs ${base}`
192
- if (!changed.length) return `No changes vs ${base} — working tree clean and no branch commits.`
193
- }
194
-
195
- const changedSet = new Set(changed)
196
- const seeds = new Set()
197
- const unmapped = []
198
- for (const file of changed) {
199
- if (!g.byId.has(file)) { unmapped.push(file); continue }
200
- seeds.add(file)
201
- for (const e of g.out.get(file) || []) if (e.relation === 'contains') seeds.add(String(e.id))
202
- }
203
- if (!seeds.size) {
204
- return [
205
- `${changed.length} changed file(s) ${sourceLabel}, but none are in the graph — new files or non-code.`,
206
- `Run rebuild_graph and retry for the full picture.`,
207
- `Changed: ${changed.slice(0, 20).join(', ')}${changed.length > 20 ? ', …' : ''}`,
208
- ].join('\n')
209
- }
210
-
211
- const maxDepth = Math.max(1, Math.min(4, Number(args.depth) || 2))
212
- const cap = Math.max(5, Math.min(120, Number(args.max_nodes) || 40))
213
- const reached = reverseReach(g, seeds, maxDepth)
214
- const fileOfNode = (id) => { const s = String(id); const h = s.indexOf('#'); return h < 0 ? s : s.slice(0, h) }
215
- const impacted = [...reached.entries()]
216
- .filter(([nid]) => !seeds.has(nid) && !changedSet.has(fileOfNode(nid)))
217
- .map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid), file: fileOfNode(nid)}))
218
- .sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
219
-
220
- // coverage overlay from EXISTING reports (fractions 0..1) — see coverage_map for details
221
- const knownFiles = (rawGraph(ctx).nodes || []).filter((n) => !String(n.id).includes('#') && n.source_file).map((n) => n.source_file)
222
- const coverage = readCoverageForRepo(ctx.repoRoot, knownFiles)
223
- const covOf = (file) => coverage.get(String(file).replace(/\\/g, '/'))?.pct ?? null
224
- const pctStr = (v) => (v == null ? 'cov n/a' : `cov ${Math.round(v * 100)}%`)
225
- const hasCoverage = coverage.size > 0
226
-
227
- const shown = impacted.slice(0, cap)
228
- const runtimeImpacted = impacted.filter((entry) => !entry.entry.compileOnly).length
229
- const compileImpacted = impacted.length - runtimeImpacted
230
- const untestedHotspots = shown.filter((n) => { const c = covOf(n.file); return c != null && c < 0.5 && n.deg >= 5 })
231
- return [
232
- `Change impact ${sourceLabel}: ${changed.length} changed file(s) → ${seeds.size} graph seed(s) incl. their symbols${unmapped.length ? `; ${unmapped.length} file(s) not in the graph (new/non-code — rebuild_graph refreshes)` : ''}.`,
233
- impacted.length
234
- ? `${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
235
- : `Nothing else in the graph depends on the changed code within ${maxDepth} hop(s).`,
236
- hasCoverage ? `Coverage: existing report mapped where available.` : `Coverage: unavailable (no supported report found); per-node coverage labels omitted.`,
237
- ...shown.map((n) => ` [d${n.d} ${impactKind(n.entry)}] ${n.entry.relation || 'rel'} ${labelOf(g, n.id)} (deg ${n.deg}${hasCoverage ? `, ${pctStr(covOf(n.file))}` : ''}) [${n.id}]`),
238
- untestedHotspots.length ? `` : null,
239
- untestedHotspots.length ? `Untested hotspots in the blast radius (<50% coverage, deg ≥5) — cover these before shipping:` : null,
240
- ...untestedHotspots.slice(0, 10).map((n) => ` ${labelOf(g, n.id)} (${pctStr(covOf(n.file))}, deg ${n.deg}) ${n.file}`),
241
- ``,
242
- `Drill into any node with get_dependents / read_source; per-symbol coverage via coverage_map.`,
243
- ].filter((x) => x != null).join('\n')
159
+ const baseline = tChangeImpactV2(g, args, ctx)
160
+ return refineChangeImpact(g, args, ctx, baseline, tChangeImpactV2)
244
161
  }
@@ -5,6 +5,20 @@ import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
5
5
  import { createRepoBoundary } from "./repo-path.js";
6
6
 
7
7
  export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
8
+ export const PATH_CLASS_QUERY_TERMS = Object.freeze({
9
+ test: Object.freeze(["test", "tests", "testing", "unit"]),
10
+ e2e: Object.freeze(["e2e", "playwright", "cypress"]),
11
+ generated: Object.freeze(["generated", "autogenerated", "dist"]),
12
+ mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
13
+ story: Object.freeze(["story", "stories", "storybook"]),
14
+ docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
15
+ benchmark: Object.freeze(["benchmark", "benchmarks", "bench"]),
16
+ temp: Object.freeze(["temp", "temporary", "tmp"]),
17
+ });
18
+ export const PATH_CLASS_TASK_QUERY_TERMS = Object.freeze({
19
+ ...PATH_CLASS_QUERY_TERMS,
20
+ test: Object.freeze([...PATH_CLASS_QUERY_TERMS.test, "spec", "coverage", "verify"]),
21
+ });
8
22
  const PATH_CLASS_SET = new Set(PATH_CLASS_NAMES);
9
23
  const MAX_CONFIG_BYTES = 64 * 1024;
10
24
  const MAX_RULES_PER_CLASS = 64;
@@ -51,8 +51,14 @@ function completedOverlay(session) {
51
51
  language: 'typescript/javascript',
52
52
  capability: 'textDocument/references',
53
53
  status: state,
54
+ configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
55
+ repoLocalPluginLoads: false,
54
56
  }],
55
57
  semanticInputFingerprint: session.semanticInputs.fingerprint,
58
+ pluginPolicy: {
59
+ configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
60
+ repoLocalPluginLoads: false,
61
+ },
56
62
  coverage: coverage(session),
57
63
  links: session.links,
58
64
  referenceEvidence: session.referenceEvidence,
@@ -181,6 +187,10 @@ export async function buildLspPrecisionOverlay({
181
187
  return persist(session, baseOverlay(graph, 'COMPLETE', {
182
188
  request: session.request,
183
189
  semanticInputFingerprint: session.semanticInputs.fingerprint,
190
+ pluginPolicy: {
191
+ configuredPluginsSuppressed: session.semanticInputs.pluginsSuppressed || 0,
192
+ repoLocalPluginLoads: false,
193
+ },
184
194
  reason: 'no eligible JavaScript/TypeScript semantic targets',
185
195
  }))
186
196
  }
@@ -1,6 +1,6 @@
1
1
  import {typeScriptLspContract} from '../typescript-lsp-provider.js'
2
2
 
3
- export const PRECISION_OVERLAY_V = 3
3
+ export const PRECISION_OVERLAY_V = 4
4
4
  export const PRECISION_FILE = 'precision.json'
5
5
  export const JS_TS_FILE = /\.(?:[cm]?[jt]sx?)$/i
6
6
  export const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value)
@@ -182,3 +182,42 @@ export async function querySymbolPrecision({
182
182
  inFlight.delete(flightKey)
183
183
  }
184
184
  }
185
+
186
+ export async function querySymbolsPrecision({
187
+ repoRoot,
188
+ graphPath,
189
+ targetIds,
190
+ maxReferences = 5_000,
191
+ timeoutMs = 45_000,
192
+ clientFactory,
193
+ } = {}) {
194
+ if (!repoRoot || !graphPath || !Array.isArray(targetIds) || !targetIds.length) {
195
+ throw new Error('batch symbol precision requires repoRoot, graphPath, and targetIds')
196
+ }
197
+ const ids = [...new Set(targetIds.map(String).filter(Boolean))].slice(0, 16)
198
+ const boundedReferences = boundedInteger(maxReferences, 5_000, 1, 16_384)
199
+ const boundedTimeout = boundedInteger(timeoutMs, 45_000, 1_000, 60_000)
200
+ const rawGraph = loadRawGraph(graphPath)
201
+ const graph = rawGraph.graphPrecisionMode === 'off' ? {...rawGraph, graphPrecisionMode: 'lsp'} : rawGraph
202
+ const nodes = new Map((graph.nodes || []).map((node) => [String(node?.id || ''), node]))
203
+ for (const id of ids) {
204
+ const target = nodes.get(id)
205
+ if (!target) throw new Error(`precision target is absent from the active graph: ${id}`)
206
+ if (!target.selection_start || !/\.(?:[cm]?[jt]sx?)$/i.test(String(target.source_file || ''))) {
207
+ throw new Error(`exact batch precision does not support target: ${id}`)
208
+ }
209
+ }
210
+ const startedAt = Date.now()
211
+ const overlay = await buildLspPrecisionOverlay({
212
+ repoRoot,
213
+ graph,
214
+ mode: 'lsp',
215
+ maxSymbols: ids.length,
216
+ maxReferences: boundedReferences,
217
+ maxLinks: boundedReferences,
218
+ timeoutMs: boundedTimeout,
219
+ targetIds: ids,
220
+ clientFactory,
221
+ })
222
+ return {overlay, targetIds: ids, elapsedMs: Date.now() - startedAt}
223
+ }
@@ -6,11 +6,13 @@ import {
6
6
  typeScriptLanguageId,
7
7
  typeScriptLspContract,
8
8
  } from './discovery.js'
9
+ import {isolateTypeScriptRuntime} from './isolated-runtime.js'
9
10
 
10
11
  /** Starts Weavatrix's bundled TypeScript server without executing repository configuration. */
11
12
  export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} = {}) {
12
13
  const discovered = discoverTypeScriptProvider()
13
14
  const absoluteRepoRoot = resolve(repoRoot)
15
+ const isolated = isolateTypeScriptRuntime(discovered.tsserverPath)
14
16
  let client
15
17
  let reportedTypeScript = null
16
18
  try {
@@ -41,11 +43,16 @@ export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} =
41
43
  initializationOptions: {
42
44
  hostInfo: 'weavatrix',
43
45
  disableAutomaticTypingAcquisition: true,
44
- tsserver: {path: discovered.tsserverPath},
46
+ tsserver: {path: isolated.tsserverPath},
47
+ // An explicit empty list keeps the language-server plugin manager
48
+ // from adding --globalPlugins or --pluginProbeLocations. tsserver's
49
+ // local-plugin loading remains disabled by default as well.
50
+ plugins: [],
45
51
  },
46
52
  })
47
53
  } catch (error) {
48
54
  client?.kill(error)
55
+ isolated.cleanup()
49
56
  throw error
50
57
  }
51
58
  return Object.freeze({
@@ -63,7 +70,13 @@ export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} =
63
70
  },
64
71
  definition(relPath, position) { return client.definition({filePath: relPath, position}) },
65
72
  closeDocument(relPath) { return client.closeDocument(relPath) },
66
- close(shutdownTimeoutMs = timeoutMs) { return client.shutdown({timeoutMs: shutdownTimeoutMs}) },
67
- kill() { client.kill() },
73
+ async close(shutdownTimeoutMs = timeoutMs) {
74
+ try { return await client.shutdown({timeoutMs: shutdownTimeoutMs}) }
75
+ finally { isolated.cleanup() }
76
+ },
77
+ kill() {
78
+ try { client.kill() }
79
+ finally { isolated.cleanup() }
80
+ },
68
81
  })
69
82
  }
@@ -4,7 +4,7 @@ import {createRequire} from 'node:module'
4
4
 
5
5
  const requireFromWeavatrix = createRequire(import.meta.url)
6
6
  export const PROVIDER = 'typescript-language-server'
7
- export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-v3'
7
+ export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-v4-plugin-suppression'
8
8
  let discoveredProvider = null
9
9
 
10
10
  function resolveOwn(specifier) {
@@ -0,0 +1,39 @@
1
+ import {copyFileSync, linkSync, mkdirSync, mkdtempSync, readdirSync, rmSync} from 'node:fs'
2
+ import {tmpdir} from 'node:os'
3
+ import {basename, dirname, join} from 'node:path'
4
+
5
+ const RUNTIME_FILES = new Set([
6
+ 'tsserver.js', '_tsserver.js', 'typescript.js',
7
+ 'typingsInstaller.js', '_typingsInstaller.js', 'typesMap.json', 'watchGuard.js',
8
+ ])
9
+
10
+ function materialize(source, target) {
11
+ try { linkSync(source, target) }
12
+ catch { copyFileSync(source, target) }
13
+ }
14
+
15
+ export function isolateTypeScriptRuntime(tsserverPath) {
16
+ const sourceLib = dirname(tsserverPath)
17
+ const sourceRoot = dirname(sourceLib)
18
+ const root = mkdtempSync(join(tmpdir(), 'weavatrix-tsserver-'))
19
+ const lib = join(root, 'lib')
20
+ mkdirSync(lib, {recursive: true, mode: 0o700})
21
+ try {
22
+ materialize(join(sourceRoot, 'package.json'), join(root, 'package.json'))
23
+ const files = readdirSync(sourceLib).filter((name) =>
24
+ RUNTIME_FILES.has(name) || /^lib(?:\..+)?\.d\.ts$/i.test(name))
25
+ for (const name of files) materialize(join(sourceLib, name), join(lib, basename(name)))
26
+ let cleaned = false
27
+ return {
28
+ tsserverPath: join(lib, 'tsserver.js'),
29
+ cleanup() {
30
+ if (cleaned) return
31
+ cleaned = true
32
+ try { rmSync(root, {recursive: true, force: true}) } catch { /* process cleanup fallback */ }
33
+ },
34
+ }
35
+ } catch (error) {
36
+ try { rmSync(root, {recursive: true, force: true}) } catch { /* best effort */ }
37
+ throw error
38
+ }
39
+ }
@@ -154,7 +154,7 @@ export function parseRepoConfig(context, configPath, budget) {
154
154
  const state = {
155
155
  outsideAccess: false,
156
156
  limitExceeded: false,
157
- configuredPlugins: false,
157
+ configuredPlugins: new Set(),
158
158
  bytes: 0,
159
159
  records: new Map(),
160
160
  }
@@ -197,8 +197,9 @@ export function parseRepoConfig(context, configPath, budget) {
197
197
  }
198
198
  try {
199
199
  const raw = ts.parseConfigFileTextToJson(path, body.toString('utf8')).config
200
- if (Array.isArray(raw?.compilerOptions?.plugins) && raw.compilerOptions.plugins.length) {
201
- state.configuredPlugins = true
200
+ for (const plugin of raw?.compilerOptions?.plugins || []) {
201
+ const name = typeof plugin?.name === 'string' ? plugin.name.trim() : ''
202
+ if (name) state.configuredPlugins.add(name.slice(0, 256))
202
203
  }
203
204
  } catch { /* parser diagnostics below decide safety */ }
204
205
  return body.toString('utf8')
@@ -213,8 +214,9 @@ export function parseRepoConfig(context, configPath, budget) {
213
214
  try { parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host) }
214
215
  catch { return {complete: false, reason: 'CONFIG_PARSE_FAILED'} }
215
216
  const errors = [...diagnostics, ...(parsed?.errors || [])]
216
- if (state.configuredPlugins || (Array.isArray(parsed?.options?.plugins) && parsed.options.plugins.length)) {
217
- return {complete: false, reason: 'CONFIGURED_TSSERVER_PLUGINS'}
217
+ for (const plugin of parsed?.options?.plugins || []) {
218
+ const name = typeof plugin?.name === 'string' ? plugin.name.trim() : ''
219
+ if (name) state.configuredPlugins.add(name.slice(0, 256))
218
220
  }
219
221
  if (budget.reason) return {complete: false, reason: budget.reason}
220
222
  if (!parsed || state.outsideAccess || state.limitExceeded
@@ -244,6 +246,9 @@ export function parseRepoConfig(context, configPath, budget) {
244
246
  projectFiles,
245
247
  projectKeys,
246
248
  configRecords: state.records,
247
- plugins: [],
249
+ // The bundled provider runs an isolated tsserver copy whose plugin search
250
+ // root contains only TypeScript runtime files. Keep configured plugin names
251
+ // as evidence, but never execute them from the inspected repository/bundle.
252
+ plugins: [...state.configuredPlugins].sort(),
248
253
  }
249
254
  }
@@ -57,6 +57,7 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
57
57
 
58
58
  const configRecords = new Map()
59
59
  const projectFiles = new Set()
60
+ const configuredPlugins = new Set()
60
61
  const projects = {}
61
62
  for (let cursor = 0; cursor < queue.length; cursor++) {
62
63
  if (Date.now() >= budget.deadline) return {safe: false, reason: 'SAFETY_DEADLINE', fingerprint: null}
@@ -68,7 +69,9 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
68
69
  projects[configRel] = {
69
70
  projectFiles: parsed.projectFiles,
70
71
  configFiles: [...parsed.configRecords.keys()].sort(),
72
+ configuredPlugins: parsed.plugins,
71
73
  }
74
+ for (const plugin of parsed.plugins || []) configuredPlugins.add(plugin)
72
75
  for (const [file, digest] of parsed.configRecords) {
73
76
  configRecords.set(file, digest)
74
77
  if (configRecords.size > MAX_CONFIG_FILES) {
@@ -133,6 +136,8 @@ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
133
136
  projectFiles: [...projectFiles].sort(),
134
137
  fileConfigs,
135
138
  projects,
139
+ configuredPlugins: [...configuredPlugins].sort(),
140
+ pluginsSuppressed: configuredPlugins.size,
136
141
  }
137
142
  }
138
143
 
@@ -17,7 +17,7 @@ export const DEFAULT_STORE = join(homedir(), ".weavatrix", "advisories.json");
17
17
  const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
18
18
  const OSV_VULN_URL = "https://api.osv.dev/v1/vulns/";
19
19
  const DEFAULT_FETCH_TIMEOUT_MS = Number(process.env.WEAVATRIX_OSV_TIMEOUT_MS || 20000);
20
- export const OSV_SUPPORTED_ECOSYSTEMS = new Set(["npm", "PyPI", "Go"]);
20
+ export const OSV_SUPPORTED_ECOSYSTEMS = new Set(["npm", "PyPI", "Go", "Maven", "crates.io"]);
21
21
 
22
22
  const keyOf = (ecosystem, name) => `${ecosystem}|${ecosystem === "PyPI" ? String(name).toLowerCase().replace(/[-_.]+/g, "-") : name}`;
23
23
 
@@ -121,7 +121,7 @@ export async function refreshAdvisories({ installed = [], storePath = DEFAULT_ST
121
121
  ok: false,
122
122
  queried: 0,
123
123
  unsupported,
124
- error: "No OSV-supported pinned package versions found to check. weavatrix currently queries OSV for npm, PyPI, and Go packages with concrete versions.",
124
+ error: "No OSV-supported pinned package versions found to check. Weavatrix queries OSV for npm, PyPI, Go, Maven/Gradle, and crates.io packages with concrete versions.",
125
125
  };
126
126
  }
127
127
  const store = loadStore(storePath);
@@ -0,0 +1,46 @@
1
+ import { createRepoBoundary } from "../repo-path.js";
2
+ import { listRepoFiles, readRepoText } from "../analysis/internal-audit/repo-files.js";
3
+ import { parseCargoLockPackages } from "../analysis/cargo-manifests.js";
4
+ import { parseGradleDependencies, parseGradleLockPackages, parseGradleVersionCatalog, parseMavenPom } from "../analysis/jvm-manifests.js";
5
+
6
+ const dedupe = (items) => {
7
+ const seen = new Set();
8
+ return items.filter((item) => {
9
+ const key = `${item.ecosystem}|${item.name}|${item.version}`;
10
+ if (!item.name || !item.version || seen.has(key)) return false;
11
+ seen.add(key); return true;
12
+ });
13
+ };
14
+ const concreteVersion = (value) => !!value && !/[\[\](),${}*+]/.test(String(value));
15
+
16
+ export function collectJvmRustInstalled(repoPath) {
17
+ const boundary = createRepoBoundary(repoPath);
18
+ if (!boundary.root) return [];
19
+ const files = listRepoFiles(repoPath);
20
+ const installed = [];
21
+ for (const file of files.filter((name) => /(^|\/)Cargo\.lock$/i.test(name))) {
22
+ installed.push(...parseCargoLockPackages(readRepoText(boundary, file)));
23
+ }
24
+ for (const file of files.filter((name) => /(^|\/)pom\.xml$/i.test(name))) {
25
+ const parsed = parseMavenPom(readRepoText(boundary, file));
26
+ installed.push(...parsed.dependencies.filter((item) => concreteVersion(item.version)).map((item) => ({
27
+ ecosystem: "Maven", name: item.name, version: item.version, dev: item.scope === "test",
28
+ integrity: "", source: "pom",
29
+ })));
30
+ }
31
+ const catalog = new Map();
32
+ for (const file of files.filter((name) => /(^|\/)libs\.versions\.toml$/i.test(name))) {
33
+ for (const [alias, entry] of parseGradleVersionCatalog(readRepoText(boundary, file))) catalog.set(alias, entry);
34
+ }
35
+ for (const file of files.filter((name) => /(^|\/)(?:build|settings|[^/]+)\.gradle(?:\.kts)?$/i.test(name))) {
36
+ const dependencies = parseGradleDependencies(readRepoText(boundary, file), catalog);
37
+ installed.push(...dependencies.filter((item) => concreteVersion(item.version)).map((item) => ({
38
+ ecosystem: "Maven", name: item.name, version: item.version,
39
+ dev: /test/i.test(item.scope || ""), integrity: "", source: "gradle-manifest",
40
+ })));
41
+ }
42
+ for (const file of files.filter((name) => /(^|\/)(?:gradle\.lockfile|dependency-locks\/[^/]+\.lockfile)$/i.test(name))) {
43
+ installed.push(...parseGradleLockPackages(readRepoText(boundary, file)));
44
+ }
45
+ return dedupe(installed);
46
+ }
@@ -7,6 +7,8 @@ import { join, relative } from "node:path";
7
7
  import { parseGoMod } from "../analysis/manifests.js";
8
8
  import { uniqueBy } from "../util.js";
9
9
  import { createRepoBoundary } from "../repo-path.js";
10
+ import { collectJvmRustInstalled } from "./installed-jvm-rust.js";
11
+ import { listRepoFiles, readRepoText } from "../analysis/internal-audit/repo-files.js";
10
12
 
11
13
  const pep503 = (name) => String(name).toLowerCase().replace(/[-_.]+/g, "-"); // PyPI canonical name
12
14
 
@@ -256,5 +258,23 @@ export function collectInstalled(repoPath) {
256
258
  if (!vers) merged.push(d);
257
259
  else if (!vers.has(d.version)) drift.push({ name: d.name, locked: locked.get(d.name).version, installed: d.version });
258
260
  }
259
- return { installed: dedupe([...merged, ...py, ...go]), drift };
261
+ const jvmRust = collectJvmRustInstalled(repoPath);
262
+ // The earlier fast path covers roots and immediate services. Complete the tracked repository
263
+ // universe so deeply nested Python/Go services are not silently omitted from advisory coverage.
264
+ const repositoryFiles = listRepoFiles(repoPath);
265
+ const allPython = repositoryFiles.flatMap((file) => {
266
+ const text = readRepoText(boundary, file);
267
+ if (/(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(file) || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(file)) return parseRequirements(text || "");
268
+ if (/(^|\/)poetry\.lock$/i.test(file)) return parseTomlLockPackages(text || "", "poetry-lock");
269
+ if (/(^|\/)uv\.lock$/i.test(file)) return parseTomlLockPackages(text || "", "uv-lock");
270
+ if (/(^|\/)Pipfile\.lock$/i.test(file)) { try { return parsePipfileLock(JSON.parse(text)); } catch { return []; } }
271
+ return [];
272
+ });
273
+ const allGo = repositoryFiles.flatMap((file) => {
274
+ const text = readRepoText(boundary, file) || "";
275
+ if (/(^|\/)go\.sum$/i.test(file)) return parseGoSum(text);
276
+ if (/(^|\/)go\.mod$/i.test(file)) return parseGoModPackages(text);
277
+ return [];
278
+ });
279
+ return { installed: dedupe([...merged, ...py, ...go, ...allPython, ...allGo, ...jvmRust]), drift };
260
280
  }
@@ -74,7 +74,7 @@ export async function rgSweep(rg, contentRoots, timeoutMs) {
74
74
  const lineText = String(obj.data?.lines?.text || "");
75
75
  const subText = String(obj.data?.submatches?.[0]?.match?.text || "");
76
76
  for (const at of matchRuleOffsets(rule, lineText, subText)) {
77
- const text = (lineText ? lineText.slice(Math.max(0, at - 60), at + 180) : subText).trim().slice(0, 240);
77
+ const text = (lineText ? lineText.slice(Math.max(0, at - 160), at + 180) : subText).trim().slice(0, 340);
78
78
  hits.push({ pkg, file, line: obj.data?.line_number || 0, text, rule });
79
79
  }
80
80
  }
@@ -88,6 +88,7 @@ const BENIGN_URL_HOSTS = [
88
88
  "apache.org", "opensource.org", "gnu.org", "creativecommons.org", "choosealicense.com",
89
89
  "mozilla.org", "nodejs.org", "npmjs.com", "npmjs.org", "yarnpkg.com", "python.org", "pypi.org",
90
90
  "golang.org", "go.dev", "godoc.org", "readthedocs.io", "readthedocs.org", "editorconfig.org",
91
+ "aka.ms", "example.com", "example.org", "example.net",
91
92
  ];
92
93
  const DOC_URL_PATH_RE = /\/(docs?|documentation|wiki|issues?|pull|blob|tree|releases|licenses?|rfc\d*|help|manual|guide|spec|schemas?)([/#?.]|$)/i;
93
94
  const URL_TOKEN_RE = /https?:\/\/[^\s'"`)\\<>]+/gi;
@@ -109,7 +110,7 @@ export function isBenignUrlContext(text) {
109
110
  const s = String(text || "");
110
111
  const urls = [...s.matchAll(URL_TOKEN_RE)];
111
112
  if (!urls.length) return false;
112
- if (COMMENT_MARKER_RE.test(s.slice(Math.max(0, urls[0].index - 80), urls[0].index))) return true;
113
+ if (COMMENT_MARKER_RE.test(s.slice(0, urls[0].index))) return true;
113
114
  return urls.every((m) => {
114
115
  const pre = s.slice(Math.max(0, m.index - 80), m.index);
115
116
  const post = s.slice(m.index + m[0].length, m.index + m[0].length + 48);