weavatrix 0.3.4 → 0.3.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.
package/README.md CHANGED
@@ -311,6 +311,17 @@ before starting the MCP server for parser-only operation from the first build, o
311
311
  `precision:"off"` to `rebuild_graph` / `open_repo`. The MCPB installer exposes the same `lsp` / `off`
312
312
  choice as **TypeScript/JavaScript semantic precision**.
313
313
 
314
+ The broad prewarm stays conservative by default: it queries 32 ranked symbols and never more than
315
+ 64. A deliberate high-budget run can set `WEAVATRIX_PRECISION_MAX_SYMBOLS` above 64 (for example,
316
+ `1000`), or set `WEAVATRIX_PRECISION_PREWARM=full` to select every eligible target up to the hard
317
+ 10,000-symbol safety ceiling. Expanded runs receive proportionate reference/link and deadline
318
+ budgets, still capped at 131,072 references/links, a 30-minute deadline, 1,024 open documents and
319
+ 128 MiB of verified source. `WEAVATRIX_PRECISION_MAX_REFERENCES`,
320
+ `WEAVATRIX_PRECISION_MAX_LINKS`, and `WEAVATRIX_PRECISION_TIMEOUT_MS` can lower or explicitly tune
321
+ those budgets. Repositories that exceed any hard ceiling remain honestly `PARTIAL`; the default
322
+ memory/time profile is unchanged. On-demand `get_dependents`, `inspect_symbol`, and
323
+ `change_impact` queries remain revision-bound separate caches and do not require a full prewarm.
324
+
314
325
  **search / source** — `search_code` (ripgrep-backed, pure-Node fallback, with repository-relative
315
326
  path globs on Windows/macOS/Linux), `read_source` (a
316
327
  symbol's actual code in one hop), `context_bundle` (definition plus grouped inbound/outbound
@@ -413,6 +424,19 @@ metrics are not persisted or transmitted by Weavatrix. If a source checkout's pa
413
424
  while an old daemon remains alive, `initialize`, `tools/list`, and tool calls fail loudly with
414
425
  `STALE_RUNTIME` until the client reconnects; the opt-out is reserved for deliberate development.
415
426
 
427
+ ### 0.3.5 explicit full semantic prewarm
428
+
429
+ - Normal startup remains bounded to 32 ranked semantic targets. An explicit
430
+ `WEAVATRIX_PRECISION_PREWARM=full` run, or a requested symbol budget above
431
+ 64, can now cover every eligible JS/TS target within hard time, source and
432
+ memory ceilings instead of being silently clamped to 64.
433
+ - Real Hosted dogfood queried all 800 eligible targets in about 49 seconds and
434
+ produced 4,127 `EXACT_LSP` edges with `COMPLETE` semantic precision.
435
+ - Unreferenced public exports are now reported as low-confidence module-surface
436
+ evidence, not as proof that their implementations are dead.
437
+
438
+ Full patch notes: [docs/releases/v0.3.5.md](docs/releases/v0.3.5.md).
439
+
416
440
  ### 0.3.4 self-audit precision and bounded dynamic imports
417
441
 
418
442
  - JS/TS graph construction now resolves the existing local-file prefix in
@@ -0,0 +1,50 @@
1
+ # Weavatrix 0.3.5
2
+
3
+ 0.3.5 removes the hidden 64-symbol ceiling from deliberately high-budget
4
+ TypeScript/JavaScript semantic prewarms while keeping the ordinary offline
5
+ startup profile unchanged.
6
+
7
+ ## Explicit full semantic prewarm
8
+
9
+ - The default remains a 32-symbol ranked prewarm with the previous 64-symbol,
10
+ 16,384-reference/link and 60-second hard ceilings.
11
+ - Setting `WEAVATRIX_PRECISION_MAX_SYMBOLS` above 64 now explicitly opts into
12
+ the expanded policy instead of being silently clamped back to 64.
13
+ - `WEAVATRIX_PRECISION_PREWARM=full` selects all eligible targets, bounded by a
14
+ 10,000-symbol hard ceiling. It is intended for CI, release evidence and
15
+ manually requested deep repository audits, not implicit startup work.
16
+ - Expanded runs scale their default reference/link budget to 16 per requested
17
+ symbol and their deadline to one second per requested symbol. Hard limits
18
+ remain 131,072 references/links and 30 minutes.
19
+ - Verified source remains bounded to 4 MiB per file, 1,024 simultaneously open
20
+ documents and 128 MiB total. Crossing a ceiling remains `PARTIAL`; it never
21
+ becomes a false complete result or an unbounded allocation.
22
+
23
+ The point-query cache used by `get_dependents` and `inspect_symbol`, and the
24
+ bounded changed-symbol batch used by `change_impact`, remain separate from this
25
+ broad overlay. Existing default and on-demand behavior is unchanged.
26
+
27
+ The regression suite proves both sides of the contract: an ordinary build still
28
+ selects 32 of 80 candidates and reports `PARTIAL`, while an explicit 1,000-symbol
29
+ budget and `full` mode each query all 80 targets and report `COMPLETE` with a
30
+ cooperative provider.
31
+
32
+ ## Dead-code verdict consistency
33
+
34
+ - `run_audit` now labels an unreferenced public export as low-confidence
35
+ `unused-export-surface` evidence and sets `deadCodeCandidate:false`.
36
+ - The audit explains that an unused module surface is not proof of a dead
37
+ implementation; downstream packages and runtime registries may be invisible.
38
+ - This matches `find_dead_code`, which keeps the same uncertain public surface
39
+ out of its default medium-confidence review queue while reporting the
40
+ suppressed low-confidence count.
41
+
42
+ ## Consistent unused-export confidence
43
+
44
+ `run_audit` now labels an unused export as low-confidence export-surface
45
+ evidence, not a medium-confidence dead implementation. The finding explicitly
46
+ distinguishes removing an unnecessary export keyword from deleting the symbol,
47
+ which still requires the separate exact/reference-aware `find_dead_code`
48
+ workflow. This aligns Health with the default dead-code queue, where public and
49
+ framework-sensitive candidates are intentionally suppressed until
50
+ `min_confidence=low` is requested.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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>",
@@ -38,6 +38,7 @@
38
38
  "docs/releases/v0.3.2.md",
39
39
  "docs/releases/v0.3.3.md",
40
40
  "docs/releases/v0.3.4.md",
41
+ "docs/releases/v0.3.5.md",
41
42
  "docs/releases/v0.2.19.md",
42
43
  "README.md",
43
44
  "SECURITY.md",
@@ -148,13 +148,16 @@ export async function runInternalAudit(repoPath, {
148
148
  category: "unused",
149
149
  rule: "unused-export",
150
150
  severity: "info",
151
- confidence: "medium",
151
+ confidence: "low",
152
152
  title: `Unused export: ${symbol.label.replace(/\(\)$/, "")} — ${symbol.file}`,
153
- detail: `${symbol.reason}. Either remove the export keyword (if used only internally) or delete the symbol.`,
153
+ detail: `${symbol.reason}. This is unused module-surface evidence, not proof that the implementation is dead: downstream packages and runtime registration can be invisible to the repository graph. Review external consumers first; remove only the export keyword when the symbol is still used internally, and delete the implementation only after separate dead-code verification.`,
154
154
  file: symbol.file,
155
155
  symbol: symbol.label,
156
156
  graphNodeId: symbol.id,
157
157
  source: "internal",
158
+ classification: "unused-export-surface",
159
+ deadCodeCandidate: false,
160
+ fixHint: "check downstream/runtime consumers, then remove only the export surface or run a separate exact dead-code review before deleting the implementation",
158
161
  }));
159
162
  unusedExportCount++;
160
163
  }
@@ -0,0 +1,78 @@
1
+ import {boundedInteger} from '../../bounds.js'
2
+
3
+ const DEFAULT_MAX_SYMBOLS = 32
4
+ const BOUNDED_MAX_SYMBOLS = 64
5
+ const EXPANDED_MAX_SYMBOLS = 10_000
6
+ const DEFAULT_MAX_REFERENCES = 2_048
7
+ const BOUNDED_MAX_REFERENCES = 16_384
8
+ const EXPANDED_MAX_REFERENCES = 131_072
9
+ const DEFAULT_MAX_LINKS = 2_048
10
+ const BOUNDED_MAX_LINKS = 16_384
11
+ const EXPANDED_MAX_LINKS = 131_072
12
+ const DEFAULT_TIMEOUT_MS = 45_000
13
+ const BOUNDED_MAX_TIMEOUT_MS = 60_000
14
+ const EXPANDED_MAX_TIMEOUT_MS = 30 * 60_000
15
+
16
+ function configured(value, environmentName) {
17
+ if (value !== undefined && value !== null && String(value).trim() !== '') return value
18
+ const environmentValue = process.env[environmentName]
19
+ return environmentValue != null && String(environmentValue).trim() !== ''
20
+ ? environmentValue : undefined
21
+ }
22
+
23
+ export function precisionPrewarmBudget(options = {}) {
24
+ const targetBatch = Array.isArray(options.targetIds) && options.targetIds.length > 0
25
+ const configuredMode = String(configured(
26
+ options.prewarmMode,
27
+ 'WEAVATRIX_PRECISION_PREWARM',
28
+ ) || 'bounded').trim().toLowerCase()
29
+ const full = configuredMode === 'full' && !targetBatch
30
+ const requestedSymbols = configured(options.maxSymbols, 'WEAVATRIX_PRECISION_MAX_SYMBOLS')
31
+ const numericSymbols = Number(requestedSymbols)
32
+ const expanded = full || (Number.isFinite(numericSymbols) && numericSymbols > BOUNDED_MAX_SYMBOLS)
33
+ const maxSymbols = full ? EXPANDED_MAX_SYMBOLS : boundedInteger(
34
+ requestedSymbols,
35
+ DEFAULT_MAX_SYMBOLS,
36
+ 1,
37
+ expanded ? EXPANDED_MAX_SYMBOLS : BOUNDED_MAX_SYMBOLS,
38
+ )
39
+ const referenceFallback = expanded
40
+ ? Math.min(EXPANDED_MAX_REFERENCES, Math.max(DEFAULT_MAX_REFERENCES, maxSymbols * 16))
41
+ : DEFAULT_MAX_REFERENCES
42
+ const linkFallback = expanded
43
+ ? Math.min(EXPANDED_MAX_LINKS, Math.max(DEFAULT_MAX_LINKS, maxSymbols * 16))
44
+ : DEFAULT_MAX_LINKS
45
+ const timeoutFallback = expanded
46
+ ? Math.min(15 * 60_000, Math.max(DEFAULT_TIMEOUT_MS, maxSymbols * 1_000))
47
+ : DEFAULT_TIMEOUT_MS
48
+ const maxReferences = boundedInteger(
49
+ configured(options.maxReferences, 'WEAVATRIX_PRECISION_MAX_REFERENCES'),
50
+ referenceFallback,
51
+ 1,
52
+ expanded ? EXPANDED_MAX_REFERENCES : BOUNDED_MAX_REFERENCES,
53
+ )
54
+ const maxLinks = boundedInteger(
55
+ configured(options.maxLinks, 'WEAVATRIX_PRECISION_MAX_LINKS'),
56
+ linkFallback,
57
+ 1,
58
+ expanded ? EXPANDED_MAX_LINKS : BOUNDED_MAX_LINKS,
59
+ )
60
+ const timeoutMs = boundedInteger(
61
+ configured(options.timeoutMs, 'WEAVATRIX_PRECISION_TIMEOUT_MS'),
62
+ timeoutFallback,
63
+ 100,
64
+ expanded ? EXPANDED_MAX_TIMEOUT_MS : BOUNDED_MAX_TIMEOUT_MS,
65
+ )
66
+ return {
67
+ expanded,
68
+ full,
69
+ maxSymbols,
70
+ maxReferences,
71
+ maxLinks,
72
+ timeoutMs,
73
+ maxOpenDocuments: expanded ? 1_024 : 96,
74
+ maxOpenBytes: expanded ? 128 * 1024 * 1024 : 32 * 1024 * 1024,
75
+ maxClassificationDocuments: expanded ? 1_024 : 96,
76
+ maxClassificationBytes: expanded ? 128 * 1024 * 1024 : 32 * 1024 * 1024,
77
+ }
78
+ }
@@ -1,5 +1,5 @@
1
- import {boundedInteger} from '../../bounds.js'
2
1
  import {createTypeScriptLspClient} from '../typescript-lsp-provider.js'
2
+ import {precisionPrewarmBudget} from './budget-policy.js'
3
3
  import {baseOverlay} from './contract.js'
4
4
  import {precisionOverlayMatches} from './contract.js'
5
5
  import {collectReferenceResults} from './reference-results.js'
@@ -106,17 +106,24 @@ function failedOverlay(session, error) {
106
106
  }
107
107
 
108
108
  function createSession(options) {
109
- const boundedMax = boundedInteger(options.maxSymbols, 32, 1, 64)
110
- const boundedReferences = boundedInteger(options.maxReferences, 2_048, 1, 16_384)
111
- const boundedLinks = boundedInteger(options.maxLinks, 2_048, 1, 16_384)
112
- const boundedTimeout = boundedInteger(options.timeoutMs, 45_000, 100, 60_000)
109
+ const budget = precisionPrewarmBudget(options)
113
110
  return {
114
111
  ...options,
115
- boundedMax,
116
- boundedReferences,
117
- boundedLinks,
118
- deadline: Date.now() + boundedTimeout,
119
- request: {maxSymbols: boundedMax, maxReferences: boundedReferences, maxLinks: boundedLinks},
112
+ expandedPrewarm: budget.expanded,
113
+ fullPrewarm: budget.full,
114
+ boundedMax: budget.maxSymbols,
115
+ boundedReferences: budget.maxReferences,
116
+ boundedLinks: budget.maxLinks,
117
+ maxOpenDocuments: budget.maxOpenDocuments,
118
+ maxOpenBytes: budget.maxOpenBytes,
119
+ maxClassificationDocuments: budget.maxClassificationDocuments,
120
+ maxClassificationBytes: budget.maxClassificationBytes,
121
+ deadline: Date.now() + budget.timeoutMs,
122
+ request: {
123
+ maxSymbols: budget.maxSymbols,
124
+ maxReferences: budget.maxReferences,
125
+ maxLinks: budget.maxLinks,
126
+ },
120
127
  links: [],
121
128
  seen: new Set(),
122
129
  evidenceSeen: new Set(),
@@ -138,17 +145,18 @@ export async function buildLspPrecisionOverlay({
138
145
  graph,
139
146
  graphPath,
140
147
  mode = 'lsp',
141
- maxSymbols = Number(process.env.WEAVATRIX_PRECISION_MAX_SYMBOLS) || 32,
142
- maxReferences = Number(process.env.WEAVATRIX_PRECISION_MAX_REFERENCES) || 2_048,
143
- maxLinks = Number(process.env.WEAVATRIX_PRECISION_MAX_LINKS) || 2_048,
144
- timeoutMs = Number(process.env.WEAVATRIX_PRECISION_TIMEOUT_MS) || 45_000,
148
+ maxSymbols,
149
+ maxReferences,
150
+ maxLinks,
151
+ timeoutMs,
152
+ prewarmMode,
145
153
  targetIds,
146
154
  clientFactory,
147
155
  } = {}) {
148
156
  if (!graph || !repoRoot) throw new Error('precision overlay requires repoRoot and graph')
149
157
  const session = createSession({
150
158
  repoRoot, graph, graphPath, mode, maxSymbols, maxReferences, maxLinks,
151
- timeoutMs, targetIds, clientFactory,
159
+ timeoutMs, prewarmMode, targetIds, clientFactory,
152
160
  })
153
161
  if (mode === 'off') {
154
162
  return persist(session, baseOverlay(graph, 'OFF', {
@@ -62,11 +62,15 @@ export async function ensureOpen(session, relPath) {
62
62
  const file = norm(relPath)
63
63
  if (!file || session.opened.has(file)) return
64
64
  ensureBudget(session)
65
- if (session.opened.size >= 96) throw new PrecisionLimitError('precision open-document limit reached')
66
- const maxBytes = Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - session.openedBytes)
65
+ const maxDocuments = Number(session.maxOpenDocuments) || 96
66
+ const maxOpenBytes = Number(session.maxOpenBytes) || 32 * 1024 * 1024
67
+ if (session.opened.size >= maxDocuments) {
68
+ throw new PrecisionLimitError('precision open-document limit reached')
69
+ }
70
+ const maxBytes = Math.min(4 * 1024 * 1024, maxOpenBytes - session.openedBytes)
67
71
  const {bytes, text} = verifiedSource(session, file, maxBytes)
68
72
  if (bytes > 4 * 1024 * 1024) throw new PrecisionLimitError('precision document exceeds 4 MiB limit')
69
- if (session.openedBytes + bytes > 32 * 1024 * 1024) {
73
+ if (session.openedBytes + bytes > maxOpenBytes) {
70
74
  throw new PrecisionLimitError('precision source-transfer budget reached')
71
75
  }
72
76
  await awaitWithBudget(session, () => session.client.openDocument(file, text))
@@ -79,13 +83,15 @@ export function sourceForClassification(session, relPath) {
79
83
  const file = norm(relPath)
80
84
  if (session.openedTexts.has(file)) return session.openedTexts.get(file)
81
85
  if (session.classificationTexts.has(file)) return session.classificationTexts.get(file)
82
- if (session.classificationTexts.size >= 96) return null
86
+ const maxDocuments = Number(session.maxClassificationDocuments) || 96
87
+ const maxClassificationBytes = Number(session.maxClassificationBytes) || 32 * 1024 * 1024
88
+ if (session.classificationTexts.size >= maxDocuments) return null
83
89
  let source
84
90
  try {
85
91
  source = verifiedSource(
86
92
  session,
87
93
  file,
88
- Math.min(4 * 1024 * 1024, 32 * 1024 * 1024 - session.classificationBytes),
94
+ Math.min(4 * 1024 * 1024, maxClassificationBytes - session.classificationBytes),
89
95
  )
90
96
  } catch (error) {
91
97
  if (error instanceof PrecisionLimitError) return null
@@ -100,7 +106,9 @@ export async function ensureFullUniverse(session) {
100
106
  if (session.fullUniverseOpened) return true
101
107
  if (!session.universe.complete) return false
102
108
  const additional = session.universe.files.filter((file) => !session.opened.has(file))
103
- if (session.opened.size + additional.length > 96) {
109
+ const maxDocuments = Number(session.maxOpenDocuments) || 96
110
+ const maxOpenBytes = Number(session.maxOpenBytes) || 32 * 1024 * 1024
111
+ if (session.opened.size + additional.length > maxDocuments) {
104
112
  session.truncated = true
105
113
  return false
106
114
  }
@@ -115,7 +123,7 @@ export async function ensureFullUniverse(session) {
115
123
  let bytes
116
124
  try { bytes = statSync(resolvedFile.path).size }
117
125
  catch { throw new PrecisionStaleGraphError() }
118
- if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > 32 * 1024 * 1024) {
126
+ if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > maxOpenBytes) {
119
127
  session.truncated = true
120
128
  return false
121
129
  }