weavatrix 0.2.3 → 0.2.5

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 (36) hide show
  1. package/README.md +91 -20
  2. package/SECURITY.md +18 -0
  3. package/package.json +3 -1
  4. package/skill/SKILL.md +49 -13
  5. package/src/analysis/dead-check.js +48 -2
  6. package/src/analysis/dead-code-review.js +22 -8
  7. package/src/analysis/duplicates.compute.js +11 -6
  8. package/src/analysis/git-ref-graph.js +10 -2
  9. package/src/analysis/hot-path-review.js +228 -0
  10. package/src/analysis/internal-audit.run.js +36 -0
  11. package/src/analysis/source-complexity.report.js +5 -0
  12. package/src/analysis/source-complexity.walk.js +64 -10
  13. package/src/build-graph.js +49 -10
  14. package/src/graph/build-worker.js +21 -1
  15. package/src/graph/builder/lang-java.js +1 -0
  16. package/src/graph/builder/lang-js.js +24 -0
  17. package/src/graph/builder/lang-python.js +161 -4
  18. package/src/graph/freshness-probe.js +3 -3
  19. package/src/graph/incremental-refresh.js +1 -1
  20. package/src/graph/internal-builder.barrels.js +33 -4
  21. package/src/graph/internal-builder.build.js +52 -8
  22. package/src/mcp/catalog.mjs +14 -11
  23. package/src/mcp/graph-context.mjs +143 -14
  24. package/src/mcp/sync-payload.mjs +2 -1
  25. package/src/mcp/tools-actions.mjs +59 -20
  26. package/src/mcp/tools-company.mjs +11 -3
  27. package/src/mcp/tools-graph.mjs +12 -6
  28. package/src/mcp/tools-health.mjs +112 -11
  29. package/src/mcp/tools-impact.mjs +24 -6
  30. package/src/mcp/tools-source.mjs +237 -0
  31. package/src/mcp-server.mjs +99 -11
  32. package/src/path-classification.js +2 -2
  33. package/src/precision/lsp-client.js +682 -0
  34. package/src/precision/lsp-overlay.js +872 -0
  35. package/src/precision/symbol-query.js +137 -0
  36. package/src/precision/typescript-lsp-provider.js +682 -0
@@ -7,9 +7,10 @@
7
7
  // Spawned by Claude Code / Codex as a plain Node child (node mcp-server.mjs <graph.json> <repoRoot>).
8
8
  // Speaks newline-delimited JSON-RPC 2.0 over stdio (the MCP stdio transport).
9
9
  //
10
- // .mjs on purpose: guarantees ESM parsing regardless of the nearest package.json. The server itself
11
- // resolves nothing from node_modules at runtime (ripgrep is probed, with a pure-Node fallback); only
12
- // the graph BUILDER pulls in web-tree-sitter + its WASM grammars when a build is requested.
10
+ // .mjs on purpose: guarantees ESM parsing regardless of the nearest package.json. Runtime analyzers
11
+ // resolve only dependencies bundled with Weavatrix: web-tree-sitter grammars for graph builds and the
12
+ // pinned TypeScript language server for bounded JS/TS semantic evidence. Repository packages/scripts
13
+ // are never executed to provide either analyzer.
13
14
  //
14
15
  // STDOUT is the protocol channel — nothing but JSON-RPC frames may be written there. All diagnostics
15
16
  // go to stderr. Two argv forms:
@@ -24,8 +25,10 @@ import {graphOutDirForRepo} from './graph/layout.js'
24
25
  import {createRequire} from 'node:module'
25
26
  import {createStalenessNoticeGate} from './mcp/staleness-notice.mjs'
26
27
  import {normalizeToolResult} from './mcp/tool-result.mjs'
27
- import {buildGraphForRepo} from './build-graph.js'
28
+ import {buildGraphForRepo, defaultPrecisionMode} from './build-graph.js'
28
29
  import {persistedFreshnessMatches, repositoryFreshnessProbe} from './graph/freshness-probe.js'
30
+ import {activeLspClientCount, beginLspClientShutdown, shutdownActiveLspClients} from './precision/lsp-client.js'
31
+ import {PRECISION_OVERLAY_V, precisionSemanticInputsMatch, readPrecisionOverlay} from './precision/lsp-overlay.js'
29
32
 
30
33
  // version comes from package.json so serverInfo can never drift from the published package again
31
34
  const PKG_VERSION = (() => { try { return createRequire(import.meta.url)('../package.json').version } catch { return '0.0.0' } })()
@@ -33,6 +36,16 @@ const SERVER_INFO = {name: 'weavatrix', version: PKG_VERSION}
33
36
  const DEFAULT_PROTOCOL = '2024-11-05'
34
37
  const log = (...a) => process.stderr.write(`[weavatrix] ${a.join(' ')}\n`)
35
38
 
39
+ async function settleWithin(promise, timeoutMs) {
40
+ let timer
41
+ const settled = await Promise.race([
42
+ Promise.resolve(promise).then(() => true, () => true),
43
+ new Promise((resolveSettled) => { timer = setTimeout(() => resolveSettled(false), timeoutMs) }),
44
+ ])
45
+ if (timer) clearTimeout(timer)
46
+ return settled
47
+ }
48
+
36
49
  // argv[2] is a repo DIRECTORY in the npx form — derive the graph location from the standard layout;
37
50
  // otherwise it is the graph.json path and the repo root follows it.
38
51
  let GRAPH_PATH = process.argv[2]
@@ -77,7 +90,7 @@ async function main() {
77
90
  // ctx owns the CURRENT target: rebuild_graph reloads it, open_repo retargets graphPath/repoRoot
78
91
  // at runtime. loadInto always reads ctx.graphPath so both paths share one loader.
79
92
  const ctx = {graphPath: GRAPH_PATH, repoRoot: REPO_ROOT, reload: null}
80
- const loadInto = () => { graph = loadGraph(ctx.graphPath); graphError = null; return graph }
93
+ const loadInto = () => { graph = loadGraph(ctx.graphPath, {repoRoot: ctx.repoRoot}); graphError = null; return graph }
81
94
  ctx.reload = () => { api.resetStalenessCache(); try { return loadInto() } catch (e) { graphError = e.message; return null } }
82
95
  try {
83
96
  if (!ctx.graphPath) throw new Error('no graph.json path given (argv[2])')
@@ -96,6 +109,8 @@ async function main() {
96
109
  // ordinary tools use a per-call graph/context snapshot, so an explicitly retargetable registration
97
110
  // cannot mix one repo's in-memory graph with another repo's source root under concurrent MCP calls.
98
111
  let targetMutation = Promise.resolve()
112
+ let shuttingDown = false
113
+ let shutdownPromise = null
99
114
  const refreshProbeCache = new Map()
100
115
  const configuredDebounce = process.env.WEAVATRIX_AUTO_REFRESH_DEBOUNCE_MS == null
101
116
  ? 2_000
@@ -103,13 +118,31 @@ async function main() {
103
118
  const refreshDebounceMs = Math.max(0, Math.min(5_000, Number.isFinite(configuredDebounce) ? configuredDebounce : 2_000))
104
119
  const autoRefresh = async (callCtx, currentGraph) => {
105
120
  if (!callCtx?.repoRoot || !callCtx?.graphPath) return {graph: null, refresh: null}
106
- const probeKey = `${callCtx.graphPath}\0${currentGraph?.graphBuildMode || 'full'}`
121
+ const activePrecision = currentGraph?.graphPrecisionMode || defaultPrecisionMode()
122
+ const probeKey = `${callCtx.graphPath}\0${currentGraph?.graphBuildMode || 'full'}\0${activePrecision}`
123
+ // Semantic inputs include ignored configs and configured project files that Git status does
124
+ // not see. Check their bounded fingerprint before the ordinary source freshness debounce;
125
+ // exact evidence must have no stale window, even on back-to-back tool calls.
126
+ let semanticInputsChanged = false
127
+ if (activePrecision === 'lsp' && currentGraph) {
128
+ try {
129
+ const overlay = readPrecisionOverlay(callCtx.graphPath, currentGraph)
130
+ semanticInputsChanged = typeof overlay?.semanticInputFingerprint === 'string'
131
+ && !precisionSemanticInputsMatch(overlay, callCtx.repoRoot, currentGraph)
132
+ } catch {
133
+ semanticInputsChanged = true
134
+ }
135
+ }
107
136
  const cachedProbe = refreshProbeCache.get(probeKey)
108
- if (currentGraph && cachedProbe && Date.now() - cachedProbe.checkedAt < refreshDebounceMs) {
137
+ if (!semanticInputsChanged && currentGraph && cachedProbe && Date.now() - cachedProbe.checkedAt < refreshDebounceMs) {
109
138
  return {graph: currentGraph, refresh: {kind: 'none', revision: currentGraph.graphRevision || null, changedFiles: 0}}
110
139
  }
111
140
  const beforeProbe = repositoryFreshnessProbe(callCtx.repoRoot)
112
- if (beforeProbe && currentGraph && (
141
+ const precisionMissing = activePrecision === 'lsp' && (
142
+ Number(currentGraph?.precisionOverlayV) !== PRECISION_OVERLAY_V
143
+ || semanticInputsChanged
144
+ )
145
+ if (!precisionMissing && beforeProbe && currentGraph && (
113
146
  cachedProbe?.probe === beforeProbe
114
147
  || persistedFreshnessMatches(currentGraph, beforeProbe, currentGraph.graphBuildMode || 'full')
115
148
  )) {
@@ -118,12 +151,13 @@ async function main() {
118
151
  }
119
152
  const result = await buildGraphForRepo(callCtx.repoRoot, {
120
153
  mode: currentGraph?.graphBuildMode || 'full',
154
+ precision: activePrecision,
121
155
  scope: '',
122
156
  outDir: dirname(callCtx.graphPath),
123
157
  })
124
158
  if (!result.ok) throw new Error(result.error || 'automatic graph refresh failed')
125
159
  api.resetStalenessCache()
126
- const fresh = loadGraph(callCtx.graphPath)
160
+ const fresh = loadGraph(callCtx.graphPath, {repoRoot: callCtx.repoRoot})
127
161
  const afterProbe = repositoryFreshnessProbe(callCtx.repoRoot)
128
162
  if (afterProbe && afterProbe === beforeProbe) refreshProbeCache.set(probeKey, {probe: afterProbe, checkedAt: Date.now()})
129
163
  else refreshProbeCache.delete(probeKey)
@@ -138,10 +172,53 @@ async function main() {
138
172
  },
139
173
  }
140
174
  }
141
- const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n')
175
+ const send = (msg) => {
176
+ // A request that was already running when the client disconnected may complete during the
177
+ // bounded drain. Do not write a late reply into a closed protocol pipe.
178
+ if (shuttingDown || process.stdout.destroyed || !process.stdout.writable) return false
179
+ try {
180
+ return process.stdout.write(JSON.stringify(msg) + '\n')
181
+ } catch (error) {
182
+ if (error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED') return false
183
+ throw error
184
+ }
185
+ }
142
186
  const reply = (id, result) => send({jsonrpc: '2.0', id, result})
143
187
  const fail = (id, code, message) => send({jsonrpc: '2.0', id, error: {code, message}})
144
188
 
189
+ const requestShutdown = (reason, exitCode = 0) => {
190
+ if (shutdownPromise) return shutdownPromise
191
+ shuttingDown = true
192
+ // This is synchronous and deliberately precedes every await: a graph build that reaches its
193
+ // precision phase during the drain must not create a fresh TLS/tsserver process tree.
194
+ beginLspClientShutdown()
195
+ process.stdin.pause()
196
+ const activeAtStart = activeLspClientCount()
197
+ log(`shutdown requested (${reason}); draining graph work and ${activeAtStart} semantic provider(s)`)
198
+ shutdownPromise = (async () => {
199
+ // Give an ordinary auto-refresh a chance to commit and close its provider itself. A wedged
200
+ // parse/query is bounded; active semantic children are then closed or tree-killed, after
201
+ // which the mutation gets a final window to observe that cancellation and release locks.
202
+ const initiallyDrained = await settleWithin(targetMutation, 2_500)
203
+ const semantic = await shutdownActiveLspClients({timeoutMs: 3_000})
204
+ const fullyDrained = initiallyDrained || await settleWithin(targetMutation, 1_500)
205
+ log(`shutdown cleanup: graph=${fullyDrained ? 'drained' : 'bounded-timeout'}, semantic=${semantic.requested} requested/${semantic.remaining} remaining${semantic.timedOut ? ' (forced)' : ''}`)
206
+ })().catch((error) => {
207
+ log(`shutdown cleanup failed: ${error.stack || error.message}`)
208
+ }).finally(() => {
209
+ process.exit(exitCode)
210
+ })
211
+ return shutdownPromise
212
+ }
213
+ process.stdout.on('error', (error) => {
214
+ if (error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED') {
215
+ void requestShutdown('stdout disconnected')
216
+ return
217
+ }
218
+ log(`stdout error: ${error.stack || error.message}`)
219
+ void requestShutdown('stdout error', 1)
220
+ })
221
+
145
222
  let loadedVersion = hotVersion(catalog.HOT_FILES)
146
223
  let lastFailedVersion = 0
147
224
  const maybeHotReload = async () => {
@@ -164,6 +241,10 @@ async function main() {
164
241
  const handle = async (msg) => {
165
242
  const {id, method, params} = msg
166
243
  const isNotification = id === undefined || id === null
244
+ if (shuttingDown) {
245
+ if (!isNotification) fail(id, -32000, 'MCP server is shutting down')
246
+ return
247
+ }
167
248
  if (method === 'initialize') {
168
249
  if (params?.protocolVersion) protocolVersion = String(params.protocolVersion)
169
250
  return reply(id, {protocolVersion, capabilities: {tools: {listChanged: true}}, serverInfo: SERVER_INFO})
@@ -172,10 +253,14 @@ async function main() {
172
253
  if (method === 'ping') return reply(id, {})
173
254
  if (method === 'tools/list') {
174
255
  await maybeHotReload()
256
+ if (shuttingDown) return
175
257
  return reply(id, {tools: api.tools.map(({name, description, inputSchema, outputSchema}) => ({name, description, inputSchema, outputSchema}))})
176
258
  }
177
259
  if (method === 'tools/call') {
178
260
  await maybeHotReload()
261
+ // EOF can arrive while the hot-reload import is pending. Do not enqueue a graph mutation
262
+ // after requestShutdown captured the mutation chain it is responsible for draining.
263
+ if (shuttingDown) return
179
264
  const tool = api.byName.get(params?.name)
180
265
  if (!tool) return reply(id, {content: [{type: 'text', text: `Unknown tool: ${params?.name}`}], isError: true})
181
266
  const refreshesGraph = tool.cap === 'graph' || tool.cap === 'health' || tool.refreshGraph === true
@@ -236,6 +321,7 @@ async function main() {
236
321
  let buf = ''
237
322
  process.stdin.setEncoding('utf8')
238
323
  process.stdin.on('data', (chunk) => {
324
+ if (shuttingDown) return
239
325
  buf += chunk
240
326
  let nl
241
327
  while ((nl = buf.indexOf('\n')) >= 0) {
@@ -256,7 +342,9 @@ async function main() {
256
342
  })
257
343
  }
258
344
  })
259
- process.stdin.on('end', () => process.exit(0))
345
+ process.stdin.on('end', () => { void requestShutdown('stdin EOF') })
346
+ process.on('SIGTERM', () => { void requestShutdown('SIGTERM') })
347
+ process.on('SIGINT', () => { void requestShutdown('SIGINT') })
260
348
  log('ready')
261
349
  }
262
350
 
@@ -80,8 +80,8 @@ const DEFAULT_RULES = [
80
80
  },
81
81
  {
82
82
  category: "temp",
83
- pattern: "__temp working/generated root",
84
- regex: /(^|\/)__temp(\/|$)/i,
83
+ pattern: "__temp or .tmp-* working/generated root",
84
+ regex: /(^|\/)(?:__temp|\.tmp(?:[-_][^/]*)?)(\/|$)/i,
85
85
  },
86
86
  ];
87
87