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 +15 -2
- package/docs/releases/v0.3.1.md +30 -0
- package/package.json +2 -1
- package/src/analysis/change-classification.js +72 -10
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
|
|
427
|
-
`UNKNOWN
|
|
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.
|
|
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:
|
|
35
|
-
text:
|
|
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 {
|
|
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
|
|
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 && !
|
|
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
|
-
|
|
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: {
|
|
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
|
}
|