weavatrix 0.3.8 → 0.3.10

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 (48) hide show
  1. package/README.md +17 -0
  2. package/docs/releases/v0.3.10.md +76 -0
  3. package/docs/releases/v0.3.9.md +36 -0
  4. package/package.json +4 -2
  5. package/scripts/run-agent-task-benchmark.mjs +0 -1
  6. package/skill/SKILL.md +13 -6
  7. package/src/analysis/change-classification.js +9 -19
  8. package/src/analysis/dead-check.js +8 -6
  9. package/src/analysis/dead-code-review.js +3 -0
  10. package/src/analysis/dep-rules.js +1 -1
  11. package/src/analysis/git-history/collector.js +1 -43
  12. package/src/analysis/git-history.js +2 -1
  13. package/src/analysis/git-ref-graph.js +4 -10
  14. package/src/analysis/hot-path-review.js +1 -1
  15. package/src/analysis/http-contracts/analysis.js +9 -1
  16. package/src/analysis/internal-audit/repo-files.js +2 -6
  17. package/src/analysis/structure/findings.js +21 -1
  18. package/src/analysis/task-retrieval.js +2 -0
  19. package/src/build-graph.js +9 -1
  20. package/src/git-exec.js +67 -0
  21. package/src/graph/builder/lang-rust.js +36 -0
  22. package/src/graph/freshness-probe.js +3 -6
  23. package/src/graph/graph-filter.js +5 -3
  24. package/src/graph/incremental-refresh.js +1 -1
  25. package/src/graph/internal-builder.build.js +2 -1
  26. package/src/graph/internal-builder.langs.js +11 -24
  27. package/src/mcp/actions/graph-lifecycle.mjs +3 -3
  28. package/src/mcp/catalog.mjs +2 -2
  29. package/src/mcp/evidence-snapshot.structure.mjs +5 -2
  30. package/src/mcp/git-output.mjs +2 -5
  31. package/src/mcp/graph/context-seeds.mjs +1 -0
  32. package/src/mcp/graph/context-state.mjs +4 -5
  33. package/src/mcp/graph/tools-query.mjs +28 -2
  34. package/src/mcp/health/audit-format.mjs +2 -5
  35. package/src/mcp/health/endpoints.mjs +34 -5
  36. package/src/mcp/sync/payload-v2.mjs +1 -0
  37. package/src/mcp/tools-company.mjs +9 -0
  38. package/src/mcp/tools-endpoints.mjs +54 -12
  39. package/src/mcp/tools-graph-hubs.mjs +1 -0
  40. package/src/mcp/tools-impact-change.mjs +2 -3
  41. package/src/mcp/tools-impact.mjs +9 -1
  42. package/src/mcp-rg.mjs +2 -4
  43. package/src/mcp-source-tools.mjs +4 -5
  44. package/src/precision/lsp-overlay/build.js +9 -4
  45. package/src/precision/lsp-overlay/contract.js +1 -1
  46. package/src/precision/lsp-overlay/target-index.js +8 -3
  47. package/src/precision/symbol-query.js +2 -1
  48. package/src/process.js +18 -1
@@ -12,6 +12,7 @@ export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx
12
12
  const classificationByFile = new Map()
13
13
  const isNonProduct = (node) => {
14
14
  if (include_classified === true) return false
15
+ if (node?.test_surface === true) return true
15
16
  const file = sourceFileOf(node)
16
17
  if (!file) return false
17
18
  if (!classificationByFile.has(file)) classificationByFile.set(file, classifier.explain(file))
@@ -1,7 +1,6 @@
1
1
  // Symbol-aware change_impact implementation, isolated from the other hot impact tools so its
2
2
  // classifier/evidence contract can be tested without perturbing get_dependents or graph_diff.
3
- import {spawnSync} from 'node:child_process'
4
- import {childProcessEnv} from '../child-env.js'
3
+ import {runGit} from '../git-exec.js'
5
4
  import {fileOfId} from '../graph/node-id.js'
6
5
  import {classifyChangeImpact} from '../analysis/change-classification.js'
7
6
  import {readCoverageForRepo} from '../analysis/coverage-reports.js'
@@ -12,7 +11,7 @@ import {gitLines} from './git-output.mjs'
12
11
  import {toolResult} from './tool-result.mjs'
13
12
 
14
13
  function gitValue(repoRoot, args) {
15
- const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
14
+ const result = runGit(repoRoot, args)
16
15
  if (result.status !== 0) return null
17
16
  return String(result.stdout || '').trim() || null
18
17
  }
@@ -87,10 +87,18 @@ export function tGetDependents(g, args = {}, ctx = {}) {
87
87
  `Semantic precision: EXACT_LSP point query${result.cached ? ' (cache hit)' : ''}; ${count} classified direct reference edge(s).`,
88
88
  )
89
89
  }
90
+ // Never echo COMPLETE here ('COMPLETE; exact absence was not proven' is a contradiction),
91
+ // but keep NONE/UNAVAILABLE truthful — PARTIAL implies probing actually began.
92
+ const fallbackState = result.overlay?.state === 'NONE' ? 'NONE'
93
+ : result.overlay?.state === 'UNAVAILABLE' ? 'UNAVAILABLE' : 'PARTIAL'
90
94
  return getDependentsFromGraph(
91
95
  g,
92
96
  args,
93
- `Semantic precision: ${result.overlay?.state || 'UNAVAILABLE'}; exact absence was not proven, so graph edges are retained (${result.overlay?.reason || 'incomplete project coverage'}).`,
97
+ fallbackState === 'NONE'
98
+ ? `Semantic precision: NONE; exact references are not available for this language/target, so graph edges are retained (${result.overlay?.reason || 'no eligible JavaScript/TypeScript semantic targets'}).`
99
+ : fallbackState === 'UNAVAILABLE'
100
+ ? `Semantic precision: UNAVAILABLE; graph evidence retained (${result.overlay?.reason || 'semantic point query unavailable'}).`
101
+ : `Semantic precision: PARTIAL; exact absence was not proven, so graph edges are retained (${result.overlay?.reason || 'incomplete project coverage'}).`,
94
102
  )
95
103
  } catch (error) {
96
104
  return getDependentsFromGraph(
package/src/mcp-rg.mjs CHANGED
@@ -1,9 +1,7 @@
1
1
  import { readdirSync, existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { spawnSync } from 'node:child_process'
4
- import { childProcessEnv } from './child-env.js'
5
3
  import { createRequire } from 'node:module'
6
- import process from 'node:process'
4
+ import { runCommandSync } from './process.js'
7
5
 
8
6
  const rgInInstall = (base) => [
9
7
  join(base, 'resources', 'app', 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg.exe'),
@@ -43,7 +41,7 @@ export function createRgResolver(selfDir) {
43
41
  } catch { /* optional packaged ripgrep path */ }
44
42
  for (const c of editorRgCandidates()) if (existsSync(c)) return (rgPath = c)
45
43
  try {
46
- const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['rg'], {encoding: 'utf8', env: childProcessEnv(), timeout: 3000, windowsHide: true})
44
+ const probe = runCommandSync(process.platform === 'win32' ? 'where' : 'which', ['rg'], {timeout: 3000})
47
45
  const p = probe.status === 0 ? probe.stdout.split(/\r?\n/)[0].trim() : ''
48
46
  if (p && existsSync(p)) return (rgPath = p)
49
47
  } catch { /* optional PATH probe */ }
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
- import { spawnSync } from 'node:child_process'
3
2
  import { extname, isAbsolute, join, relative, resolve } from 'node:path'
4
3
  import { resolveRepoPath } from './repo-path.js'
5
- import { childProcessEnv } from './child-env.js'
4
+ import { runCommandSync } from './process.js'
6
5
  import { toolResult } from './mcp/tool-result.mjs'
7
6
 
8
7
  const SEARCH_SKIP = new Set(['.git', 'node_modules', 'dist', 'build', 'out', '.next', 'coverage', 'vendor', '.venv', 'venv', 'env', 'target', '__pycache__', '.idea', '.vscode', '.cache', 'bin', 'obj', 'weavatrix-graphs'])
@@ -11,7 +10,7 @@ const MAX_SEARCH_FILE_BYTES = 1024 * 1024
11
10
  const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024
12
11
  const MAX_SOURCE_CONTEXT_LINES = 1000
13
12
 
14
- function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spawnRg = spawnSync) {
13
+ function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spawnRg = runCommandSync) {
15
14
  const rg = resolveRg()
16
15
  if (!rg) return null
17
16
  // Exclude node_modules explicitly: --hidden + the default gitignore stack is the only thing that
@@ -24,7 +23,7 @@ function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spa
24
23
  // against repository-relative paths on Windows too. Passing an absolute search root makes rg
25
24
  // compare the glob with a drive-prefixed path and silently return no matches.
26
25
  args.push('--', query, '.')
27
- const res = spawnRg(rg, args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 15000, env: childProcessEnv() })
26
+ const res = spawnRg(rg, args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 15000 })
28
27
  if (res.status !== 0 && res.status !== 1) return null
29
28
  const out = []
30
29
  for (const line of (res.stdout || '').split(/\r?\n/)) {
@@ -93,7 +92,7 @@ function nodeGrep(repoRoot, query, { isRegex, glob, maxResults }) {
93
92
  return out
94
93
  }
95
94
 
96
- export function searchCode({ repoRoot, resolveRg, spawnRg = spawnSync }, { query, is_regex = false, max_results = 40, glob } = {}) {
95
+ export function searchCode({ repoRoot, resolveRg, spawnRg = runCommandSync }, { query, is_regex = false, max_results = 40, glob } = {}) {
97
96
  if (!query) return 'Provide a "query" string.'
98
97
  if (!repoRoot || !existsSync(repoRoot)) return 'Source search unavailable: repo root not provided to this MCP server.'
99
98
  const max = Math.max(1, Math.min(200, Number(max_results) || 40))
@@ -39,7 +39,9 @@ function coverage(session, verifiedEdges = session.links.length) {
39
39
  }
40
40
 
41
41
  function completedOverlay(session) {
42
- const state = session.errors || session.truncated || session.unclassifiedReferences
42
+ // Coherence guard: COMPLETE must mean every selected target was actually probed.
43
+ const underQueried = session.queried < session.targets.length
44
+ const state = session.errors || session.truncated || session.unclassifiedReferences || underQueried
43
45
  ? 'PARTIAL' : 'COMPLETE'
44
46
  return baseOverlay(session.graph, state, {
45
47
  request: session.request,
@@ -67,7 +69,9 @@ function completedOverlay(session) {
67
69
  ...(session.errors ? {reason: `${session.errors} semantic request(s) failed or were refused`}
68
70
  : session.truncated ? {reason: 'semantic precision stopped at a configured safety limit'}
69
71
  : session.unclassifiedReferences
70
- ? {reason: 'some exact references could not be classified as runtime or type-only'} : {}),
72
+ ? {reason: 'some exact references could not be classified as runtime or type-only'}
73
+ : underQueried
74
+ ? {reason: 'semantic precision stopped before probing every selected target'} : {}),
71
75
  })
72
76
  }
73
77
 
@@ -185,14 +189,15 @@ export async function buildLspPrecisionOverlay({
185
189
  }
186
190
  if (graphPath) {
187
191
  const cached = readPrecisionOverlay(graphPath, graph)
188
- if (cached?.state === 'COMPLETE'
192
+ // NONE (no eligible targets) is as stable as COMPLETE for an unchanged fingerprint.
193
+ if ((cached?.state === 'COMPLETE' || cached?.state === 'NONE')
189
194
  && precisionOverlayMatches(cached, graph, {request: session.request})
190
195
  && cached.semanticInputFingerprint === session.semanticInputs.fingerprint) return cached
191
196
  }
192
197
  session.eligible = eligibleTargets(graph, session.boundedMax, targetIds)
193
198
  session.targets = session.eligible.targets
194
199
  if (!session.targets.length) {
195
- return persist(session, baseOverlay(graph, 'COMPLETE', {
200
+ return persist(session, baseOverlay(graph, 'NONE', {
196
201
  request: session.request,
197
202
  semanticInputFingerprint: session.semanticInputs.fingerprint,
198
203
  pluginPolicy: {
@@ -1,6 +1,6 @@
1
1
  import {typeScriptLspContract} from '../typescript-lsp-provider.js'
2
2
 
3
- export const PRECISION_OVERLAY_V = 4
3
+ export const PRECISION_OVERLAY_V = 5
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)
@@ -99,10 +99,15 @@ export function eligibleTargets(graph, limit, requestedIds = null) {
99
99
  const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]))
100
100
  if (Array.isArray(requestedIds) && requestedIds.length) {
101
101
  const ids = [...new Set(requestedIds.map(String))]
102
- const targets = ids.map((id) => byId.get(id))
102
+ const eligible = ids.map((id) => byId.get(id))
103
103
  .filter((node) => node?.selection_start && JS_TS_FILE.test(String(node.source_file || '')))
104
- .slice(0, limit)
105
- return {targets, total: ids.length, orphanIds: new Set()}
104
+ // total counts only eligible targets so coverage fractions stay honest when ids were dropped.
105
+ return {
106
+ targets: eligible.slice(0, limit),
107
+ total: eligible.length,
108
+ droppedRequested: ids.length - eligible.length,
109
+ orphanIds: new Set(),
110
+ }
106
111
  }
107
112
  const ranked = new Map()
108
113
  const inbound = new Set()
@@ -75,7 +75,8 @@ async function storeEntry(path, entry) {
75
75
  }
76
76
 
77
77
  function cacheable(overlay) {
78
- if (overlay?.state === 'COMPLETE') return true
78
+ // NONE is stable for the fingerprint, but only COMPLETE ever proves absence (see reader below).
79
+ if (overlay?.state === 'COMPLETE' || overlay?.state === 'NONE') return true
79
80
  return overlay?.state === 'PARTIAL'
80
81
  && overlay?.reason === 'semantic precision stopped at a configured safety limit'
81
82
  }
package/src/process.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Subprocess runner shared by the search engines and security sweeps.
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import { childProcessEnv } from "./child-env.js";
4
4
 
5
5
  // Windows: .cmd/.ps1 shims (npx, etc.) can't be spawned directly by Node — they need a
@@ -76,3 +76,20 @@ export function runCommand(command, args = [], options = {}) {
76
76
  child.on("close", (code) => finish(() => (timedOut ? reject(new Error("Command timed out")) : resolve({ stdout, stderr, exitCode: code }))));
77
77
  });
78
78
  }
79
+
80
+ // Synchronous run-and-collect variant for callers that resolve a real binary and read its output in
81
+ // one shot (ripgrep resolution/search, tar extraction). These launch a resolved .exe or bare binary
82
+ // directly, so no shell quoting is needed; the child env is stripped of connector secrets and a
83
+ // bounded timeout/buffer applies. Returns the raw spawnSync result ({status, stdout, stderr, error}).
84
+ export function runCommandSync(command, args = [], options = {}) {
85
+ const spawnOptions = {
86
+ cwd: options.cwd || undefined,
87
+ encoding: options.encoding ?? "utf8",
88
+ env: childProcessEnv(options.env),
89
+ windowsHide: true,
90
+ };
91
+ if (options.timeout != null) spawnOptions.timeout = options.timeout;
92
+ if (options.maxBuffer != null) spawnOptions.maxBuffer = options.maxBuffer;
93
+ if (options.stdio != null) spawnOptions.stdio = options.stdio;
94
+ return spawnSync(command, args, spawnOptions);
95
+ }