weavatrix 0.3.0 → 0.3.1

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 CHANGED
@@ -413,6 +413,19 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
413
413
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
414
414
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
415
415
 
416
+ ### 0.3.1 exact impact over oversized diffs
417
+
418
+ - `change_impact` now recovers a bounded per-file unified diff when one large asset exceeds the
419
+ aggregate diff budget. Normal source files retain line/symbol classification; only the files that
420
+ individually exceed the budget stay conservative `unknown`.
421
+ - Real Hosted dogfood recovered all 27 changed paths, mapped 57 changed symbols, reduced the
422
+ conservative seed set from 1,000 to 72, and verified exact direct references for 16/16 selected
423
+ JavaScript/TypeScript symbols. Transitive hops remain explicitly graph-backed.
424
+ - The global LSP overlay remains a bounded prewarm and can honestly be `PARTIAL`; `get_dependents`,
425
+ `inspect_symbol`, and `change_impact` run revision-bound exact point/batch queries beyond that cap.
426
+
427
+ Full patch notes: [docs/releases/v0.3.1.md](docs/releases/v0.3.1.md).
428
+
416
429
  ### 0.3.0 network-free core and exact dependency evidence
417
430
 
418
431
  - The MIT package now contains 34 local tools and no outbound HTTP implementation. Online OSV,
@@ -423,8 +436,8 @@ while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fa
423
436
  - Maven/Gradle imports map to exact class ownership from already installed JARs. Missing JARs,
424
437
  unresolved build expressions and heuristic fallbacks are `PARTIAL`, never a false `COMPLETE`.
425
438
  - Real self-dogfood produced persisted `EXACT_LSP` edges and an exact direct caller for
426
- `startMcpServer`; GraphQL, gRPC and event/Kafka contract joins remain explicit about dynamic
427
- `UNKNOWN` targets.
439
+ `startMcpServer`. GraphQL, gRPC and event/Kafka joins use static evidence plus revision-bound
440
+ runtime/OTLP observations; unobserved dynamic targets remain explicit `UNKNOWN`.
428
441
 
429
442
  Full patch notes: [docs/releases/v0.3.0.md](docs/releases/v0.3.0.md).
430
443
 
@@ -0,0 +1,30 @@
1
+ # Weavatrix 0.3.1
2
+
3
+ 0.3.1 fixes a dogfood-discovered blind spot in symbol-aware change impact when
4
+ one generated or snapshot asset makes the aggregate Git diff exceed its byte
5
+ budget.
6
+
7
+ ## Bounded large-diff recovery
8
+
9
+ - `change_impact` first keeps its normal bounded zero-context diff.
10
+ - If that aggregate diff is oversized, it obtains the complete bounded changed-
11
+ file list and recovers zero-context diffs one file at a time under the same
12
+ global byte and time budgets.
13
+ - Source files that fit retain line and symbol classification. Only individual
14
+ oversized, timed-out or otherwise unreadable files become conservative
15
+ `unknown` seeds.
16
+ - The result remains `PARTIAL/HIGH` while any file lacks textual evidence; the
17
+ fallback improves useful evidence without turning unknown into clean.
18
+
19
+ ## Exact semantic evidence
20
+
21
+ The global TypeScript/JavaScript LSP overlay is a bounded prewarm and may report
22
+ `PARTIAL` on large repositories. Exact workflows are not limited by that global
23
+ cap: `get_dependents` and `inspect_symbol` use revision-bound point queries, and
24
+ `change_impact` uses one bounded exact batch for changed symbols.
25
+
26
+ On the Hosted 0.3 release commit, the repaired fallback recovered 27/27 changed
27
+ paths and 57 mapped symbol changes, reduced the conservative seed set from the
28
+ 1,000 cap to 72, and produced `DIRECT_EXACT_TRANSITIVE_GRAPH` with 16/16 selected
29
+ JS/TS symbols verified by `typescript-language-server`. The remaining large
30
+ snapshot assets stayed explicit `unknown`; transitive hops stayed graph-backed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Local repository intelligence MCP for AI coding agents: reusable architecture graph for fast application understanding, Health, dead code, clones, history, blast radius and architecture safeguards.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -34,6 +34,7 @@
34
34
  "docs/agent-task-benchmark.md",
35
35
  "docs/adr/0001-v0.3-offline-online-split.md",
36
36
  "docs/releases/v0.3.0.md",
37
+ "docs/releases/v0.3.1.md",
37
38
  "docs/releases/v0.2.19.md",
38
39
  "README.md",
39
40
  "SECURITY.md",
@@ -22,6 +22,34 @@ export {CHANGE_CLASSIFICATION_LIMITS} from './change-classification/options.js'
22
22
 
23
23
  const VERDICT_RANK = Object.freeze({LOW: 0, MEDIUM: 1, HIGH: 2})
24
24
 
25
+ function recoverBoundedDiff(repoRoot, base, files, limits) {
26
+ const deadline = Date.now() + 12_000
27
+ let text = ''
28
+ const fallbackFiles = []
29
+ for (let index = 0; index < files.length; index++) {
30
+ const remainingMs = deadline - Date.now()
31
+ const remainingBytes = limits.maxDiffBytes - Buffer.byteLength(text)
32
+ if (remainingMs <= 0 || remainingBytes < 1_024) {
33
+ fallbackFiles.push(...files.slice(index))
34
+ break
35
+ }
36
+ const result = spawnSync('git', [
37
+ '-C', repoRoot, 'diff', '--no-ext-diff', '--find-renames', '--no-color',
38
+ '--unified=0', String(base), '--', files[index],
39
+ ], {
40
+ encoding: 'utf8', windowsHide: true, timeout: Math.min(2_000, remainingMs),
41
+ maxBuffer: remainingBytes + 1, env: childProcessEnv(),
42
+ })
43
+ const chunk = String(result.stdout || '')
44
+ if (result.status !== 0 || !chunk || Buffer.byteLength(chunk) > remainingBytes) {
45
+ fallbackFiles.push(files[index])
46
+ continue
47
+ }
48
+ text += chunk
49
+ }
50
+ return {text, fallbackFiles}
51
+ }
52
+
25
53
  function runGitDiff(repoRoot, base, limits) {
26
54
  const args = ['-C', repoRoot, 'diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
27
55
  const result = spawnSync('git', args, {
@@ -30,10 +58,32 @@ function runGitDiff(repoRoot, base, limits) {
30
58
  })
31
59
  if (result.status === 0) return {available: true, text: String(result.stdout || ''), error: null}
32
60
  const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(String(result.stdout || '')) > limits.maxDiffBytes
61
+ let fallbackFiles = []
62
+ let fallbackTruncated = false
63
+ let recoveredText = ''
64
+ if (oversized) {
65
+ const names = spawnSync('git', [
66
+ '-C', repoRoot, 'diff', '--name-only', '-z', '--no-ext-diff', '--find-renames', String(base), '--',
67
+ ], {
68
+ encoding: 'utf8', windowsHide: true, timeout: 12_000,
69
+ maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096), env: childProcessEnv(),
70
+ })
71
+ if (names.status === 0) {
72
+ const all = String(names.stdout || '').split('\0').map(normalizeChangePath).filter(Boolean)
73
+ const boundedFiles = [...new Set(all)].sort((a, b) => a.localeCompare(b)).slice(0, limits.maxFiles)
74
+ fallbackTruncated = all.length > limits.maxFiles
75
+ const recovered = recoverBoundedDiff(repoRoot, base, boundedFiles, limits)
76
+ recoveredText = recovered.text
77
+ fallbackFiles = recovered.fallbackFiles
78
+ }
79
+ }
33
80
  return {
34
- available: false,
35
- text: String(result.stdout || ''),
81
+ available: recoveredText.length > 0,
82
+ text: recoveredText,
36
83
  oversized,
84
+ partialFallback: recoveredText.length > 0,
85
+ fallbackFiles,
86
+ fallbackTruncated,
37
87
  error: oversized
38
88
  ? 'git diff exceeded the byte limit'
39
89
  : String(result.stderr || result.error?.message || 'git diff unavailable').trim(),
@@ -49,11 +99,17 @@ function collectDiffInput({diffText, repoRoot, base, explicitFiles, limits}) {
49
99
  if (typeof diffText === 'string') return {source: 'provided-diff', available: true, text: diffText, reason: '', oversized: false}
50
100
  if (repoRoot && base) {
51
101
  const result = runGitDiff(repoRoot, base, limits)
52
- return {source: 'git-diff', available: result.available, text: result.text, reason: result.error || '', oversized: result.oversized === true}
102
+ return {
103
+ source: 'git-diff', available: result.available, text: result.text,
104
+ reason: result.error || '', oversized: result.oversized === true,
105
+ fallbackFiles: result.fallbackFiles || [], fallbackTruncated: result.fallbackTruncated === true,
106
+ partialFallback: result.partialFallback === true,
107
+ }
53
108
  }
54
109
  return {
55
110
  source: 'files-only', available: false, text: '', oversized: false,
56
111
  reason: 'no unified diff was provided and no repoRoot/base pair was available',
112
+ fallbackFiles: [], fallbackTruncated: false, partialFallback: false,
57
113
  }
58
114
  }
59
115
 
@@ -78,6 +134,7 @@ export function classifyChangeImpact({
78
134
  const limits = changeLimits(requestedLimits)
79
135
  const explicitFiles = validExplicitFiles(files)
80
136
  const input = collectDiffInput({diffText, repoRoot, base, explicitFiles, limits})
137
+ const fallbackFiles = validExplicitFiles([...explicitFiles, ...(input.fallbackFiles || [])]).slice(0, limits.maxFiles)
81
138
  const indexed = indexChangeGraph(graph, limits)
82
139
  const pathClassifier = createPathClassifier(repoRoot)
83
140
  const parsed = input.available
@@ -85,15 +142,16 @@ export function classifyChangeImpact({
85
142
  : {files: [], changedLines: 0, byteLength: 0, truncated: input.oversized, oversized: input.oversized, limits}
86
143
  const analyzed = parsed.files.map((file) => classifyTestSurface(analyzeParsedFile(file, indexed, {includeAddedSeeds}), pathClassifier))
87
144
  const represented = new Set(analyzed.flatMap((file) => [file.oldPath, file.newPath].filter(Boolean)))
88
- for (const file of explicitFiles) if (!represented.has(file)) analyzed.push(classifyTestSurface(
145
+ for (const file of fallbackFiles) if (!represented.has(file)) analyzed.push(classifyTestSurface(
89
146
  unknownChangedFile(file, indexed, input.available ? 'explicitly changed file had no textual hunk' : input.reason),
90
147
  pathClassifier,
91
148
  ))
92
- if (!input.available && !explicitFiles.length) analyzed.push(unknownChangedFile('(diff unavailable)', indexed, input.reason))
149
+ if (!input.available && !fallbackFiles.length) analyzed.push(unknownChangedFile('(diff unavailable)', indexed, input.reason))
93
150
  analyzed.sort((a, b) => a.path.localeCompare(b.path))
94
151
 
95
- const incomplete = parsed.truncated || parsed.oversized || input.oversized
96
- if (incomplete) for (const file of analyzed) {
152
+ const incomplete = parsed.truncated || parsed.oversized || input.oversized || input.fallbackTruncated
153
+ const parsedEvidenceIncomplete = parsed.truncated || parsed.oversized || (input.oversized && !input.partialFallback)
154
+ if (parsedEvidenceIncomplete) for (const file of analyzed) {
97
155
  if (file.classification === 'test-only') continue
98
156
  file.classification = 'unknown'
99
157
  file.reason = 'diff was truncated/oversized; symbol-level classification is incomplete'
@@ -108,11 +166,11 @@ export function classifyChangeImpact({
108
166
  ? 'HIGH' : file.classification === 'body-changed' ? 'MEDIUM' : 'LOW'
109
167
  if (VERDICT_RANK[next] > VERDICT_RANK[verdict]) verdict = next
110
168
  }
111
- if (!input.available || parsed.truncated || seeds.truncated) verdict = 'HIGH'
169
+ if (!input.available || input.oversized || parsed.truncated || seeds.truncated) verdict = 'HIGH'
112
170
  const counts = Object.fromEntries(Object.keys(CHANGE_CLASS_RANK)
113
171
  .map((name) => [name, analyzed.filter((file) => file.classification === name).length]))
114
172
  return {
115
- ok: input.available && !parsed.truncated && !seeds.truncated,
173
+ ok: input.available && !input.oversized && !parsed.truncated && !seeds.truncated,
116
174
  source: input.source,
117
175
  verdict,
118
176
  reasons: classificationReasons(counts, {
@@ -130,6 +188,10 @@ export function classifyChangeImpact({
130
188
  seeds: seeds.items.length,
131
189
  totalSeedsBeforeCap: seeds.total,
132
190
  },
133
- bounds: {...limits, diffBytes: parsed.byteLength, changedLines: parsed.changedLines, truncated: !!parsed.truncated || seeds.truncated},
191
+ bounds: {
192
+ ...limits, diffBytes: parsed.byteLength, changedLines: parsed.changedLines,
193
+ fallbackFiles: input.fallbackFiles?.length || 0,
194
+ truncated: input.oversized === true || !!parsed.truncated || seeds.truncated || input.fallbackTruncated === true,
195
+ },
134
196
  }
135
197
  }