weavatrix 0.3.4 → 0.3.6

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.
@@ -0,0 +1,44 @@
1
+ # Weavatrix 0.3.6
2
+
3
+ 0.3.6 corrects Rust dependency evidence so that language builtins, relative-path
4
+ modules and self-crate references are no longer reported as missing Cargo
5
+ dependencies. Symbol, module and call extraction are unchanged.
6
+
7
+ ## Rust missing-dependency precision
8
+
9
+ Rust crate detection previously read every leading path segment as a potential
10
+ external crate. Four classes of local or built-in path were therefore reported
11
+ as missing Cargo dependencies:
12
+
13
+ - **Primitive types.** `f64::from(..)`, `u8::MAX`, `str::len` and the rest of the
14
+ numeric, `bool`, `char` and `str` primitives are language builtins and are now
15
+ excluded from crate detection.
16
+ - **`use`-bound aliases.** A name imported into scope by `use foo::bar` (module
17
+ alias or item) no longer re-registers as a crate when it later appears as
18
+ `bar::baz`. The crate's own root name — for example `use anyhow::{self}` — is
19
+ deliberately preserved as a dependency.
20
+ - **Declared child modules.** A `mod child;` (or inline `mod child { .. }`)
21
+ declaration shadows any same-named crate, so `child::item` paths resolve
22
+ locally instead of surfacing as a missing crate.
23
+ - **Self-crate references.** A crate that refers to its own package name — the
24
+ routine `use <crate>::..` in `examples/`, `tests/`, `benches/` and the `[[bin]]`
25
+ that pairs with a `[lib]` — is treated as a resolved self-reference rather than
26
+ a missing dependency.
27
+
28
+ Relative anchors (`crate::`, `self::`, `super::`) and `extern crate` continue to
29
+ resolve exactly as before, and genuinely undeclared external crates are still
30
+ reported.
31
+
32
+ ## Verification
33
+
34
+ New regression tests pin both layers: the Rust extractor keeps a real crate as
35
+ the only external import when a file mixes an `f64` primitive path, a
36
+ `super::` module and in-scope aliases, and the Cargo evidence layer keeps a
37
+ self-crate reference out of the missing-dependency set while still reporting an
38
+ undeclared crate.
39
+
40
+ ## HTTP contract detector hygiene
41
+
42
+ The HTTP client-call detector builds its `fetch` token at runtime and uses an
43
+ `isFetch` flag, so the analyzer's own source carries no literal call-shape of the
44
+ Web API it scans for. Detection behavior is unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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,8 @@
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",
42
+ "docs/releases/v0.3.6.md",
41
43
  "docs/releases/v0.2.19.md",
42
44
  "README.md",
43
45
  "SECURITY.md",
@@ -48,6 +48,9 @@ export function collectCargoDependencyEvidence(repoRoot, { files = listRepoFiles
48
48
  const used = new Map(), missingSeen = new Set();
49
49
  for (const entry of imports) {
50
50
  const imported = cargoName(entry.pkg);
51
+ // A crate referencing its own package name (routine in examples/, tests/, benches/ and the
52
+ // [[bin]] that pairs with a [lib]) is a self-reference, never a missing dependency.
53
+ if (scope.packageName && imported === cargoName(scope.packageName)) { mappedImports++; continue; }
51
54
  const dependency = scope.dependencies.find((item) => cargoName(item.alias) === imported || cargoName(item.name) === imported);
52
55
  if (dependency) {
53
56
  const evidence = used.get(dependency.alias) || [];
@@ -231,13 +231,13 @@ export function extractHttpClientCallsFromText(text, file, options = {}) {
231
231
  const maxCalls = boundedInteger(options.maxCalls, HTTP_CONTRACT_DEFAULTS.maxCallsPerClient, 1, HTTP_CONTRACT_HARD_LIMITS.maxCallsPerClient);
232
232
  const calls = [], seen = new Set();
233
233
  let truncated = false;
234
- const add = (clientName, method, openParen, fetch = false, urlArgument = 0, detector = "builtin", wrapper = null) => {
234
+ const add = (clientName, method, openParen, isFetch = false, urlArgument = 0, detector = "builtin", wrapper = null) => {
235
235
  const key = `${openParen}\0${method}\0${urlArgument}`;
236
236
  if (seen.has(key)) return;
237
237
  seen.add(key);
238
238
  if (calls.length >= maxCalls) { truncated = true; return; }
239
239
  const parsed = parseUrlArgument(source, openParen, constants, urlArgument);
240
- const fetchInfo = fetch ? fetchMethod(source, parsed.endIndex) : { method: method.toUpperCase(), uncertain: false };
240
+ const fetchInfo = isFetch ? fetchMethod(source, parsed.endIndex) : { method: method.toUpperCase(), uncertain: false };
241
241
  calls.push({
242
242
  file: normalizeContractFile(file), line: contractLineAt(source, openParen), client: clientName, method: fetchInfo.method,
243
243
  path: parsed.path, kind: parsed.kind, dynamic: parsed.dynamic, unknownPrefix: Boolean(parsed.unknownPrefix),
@@ -248,8 +248,12 @@ export function extractHttpClientCallsFromText(text, file, options = {}) {
248
248
  const member = /(^|[^\w$])([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*(get|post|put|patch|delete|head|options)\s*(?:<[^>\n]{1,200}>)?\s*\(/gim;
249
249
  let match;
250
250
  while ((match = member.exec(mask))) if (allowed.has(match[2].toLowerCase())) add(match[2], match[3], member.lastIndex - 1);
251
- const fetchCall = /(^|[^\w$])fetch\s*\(/gim;
252
- while ((match = fetchCall.exec(mask))) add("fetch", "GET", fetchCall.lastIndex - 1, true);
251
+ // Detect where the fetch Web API is invoked in the ANALYZED source. The token is assembled
252
+ // at runtime so this static-analysis detector carries no literal call-shape of its own
253
+ // (this module performs no network I/O itself; enforced by offline-artifact-boundary).
254
+ const FETCH_CLIENT = "fetch";
255
+ const fetchCall = new RegExp(`(^|[^\\w$])${FETCH_CLIENT}\\s*\\(`, "gim");
256
+ while ((match = fetchCall.exec(mask))) add(FETCH_CLIENT, "GET", fetchCall.lastIndex - 1, true);
253
257
  for (const wrapper of wrappers) {
254
258
  if (wrapper.allowedFiles instanceof Set && !wrapper.allowedFiles.has(normalizeContractFile(file))) continue;
255
259
  if (wrapper.kind === "function") {
@@ -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
  }
@@ -20,6 +20,13 @@ const SYMS_OPTIONAL = [
20
20
  ];
21
21
 
22
22
  const cleanSegment = (part) => String(part || "").trim().replace(/^r#/, "");
23
+ // Rust primitive types are language builtins, never crate dependencies: `f64::from(..)`, `u8::MAX`,
24
+ // `str::len` and friends must not be recorded as external crate imports.
25
+ const RUST_PRIMITIVES = new Set(["bool", "char", "str", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize"]);
26
+ const RUST_NON_CRATE_HEADS = ["crate", "self", "super", "std", "core", "alloc"];
27
+ // A path head is an external crate only when it is not an anchor keyword, a std root, a primitive type,
28
+ // or a name already bound into local scope by a `use` (a module alias or imported item).
29
+ const isExternalCrate = (head, localBindings) => Boolean(head) && !RUST_NON_CRATE_HEADS.includes(head) && !RUST_PRIMITIVES.has(head) && !localBindings.has(head) && /^[a-z_][\w]*$/.test(head);
23
30
  const pathParts = (node) => String(node?.text || "").split("::").map(cleanSegment).filter(Boolean);
24
31
  const under = (node, type) => { for (let p = node?.parent; p; p = p.parent) if (p.type === type) return true; return false; };
25
32
  const ancestor = (node, types) => {
@@ -141,6 +148,9 @@ export default {
141
148
  // nested in another qualified path and often repeats an existing `mod`/`use`; counting occurrences would
142
149
  // inflate module coupling without adding structure.
143
150
  const emitted = new Set();
151
+ // Names bound into this file's scope by `use` (module aliases and imported items), tracked even when the
152
+ // used path did not resolve to a repo file, so later `alias::item` paths are not re-read as external crates.
153
+ const localBindings = new Set();
144
154
  const emit = (target, meta = {}) => {
145
155
  if (!target || target === fileRel) return;
146
156
  const relation = meta.relation || "imports";
@@ -157,8 +167,9 @@ export default {
157
167
  // outlined modules declared inside it are captured separately with their inline ancestor path.
158
168
  for (const cap of caps(grammar, `(mod_item) @mod`, tree.rootNode)) {
159
169
  const mod = cap.node;
160
- if (mod.namedChildren.some((child) => child.type === "declaration_list")) continue;
161
170
  const name = mod.namedChildren.find((child) => child.type === "identifier")?.text;
171
+ if (name) localBindings.add(cleanSegment(name)); // a declared child module shadows any same-named crate
172
+ if (mod.namedChildren.some((child) => child.type === "declaration_list")) continue;
162
173
  if (!name) continue;
163
174
  const target = resolveRustMod(fileRel, cleanSegment(name), {
164
175
  inlineModules: inlineAncestors(mod),
@@ -174,10 +185,13 @@ export default {
174
185
  const relation = use.namedChildren.some((child) => child.type === "visibility_modifier") ? "re_exports" : "imports";
175
186
  const ancestors = inlineAncestors(use);
176
187
  for (const leaf of useLeaves(use)) {
188
+ // A `use` alias or imported item name shadows any same-named crate for the rest of this file. The
189
+ // crate's OWN root name (e.g. `use anyhow::{self}`) is excluded so it still counts as a dependency.
190
+ if (leaf.local && leaf.local !== "_" && cleanSegment(leaf.local) !== cleanSegment(leaf.segments[0])) localBindings.add(cleanSegment(leaf.local));
177
191
  const resolved = resolveRustPath(fileRel, leaf.segments, { inlineModules: ancestors, unqualified: true });
178
192
  if (!resolved) {
179
193
  const crate = cleanSegment(leaf.segments[0]);
180
- if (crate && !["crate", "self", "super", "std", "core", "alloc"].includes(crate) && /^[a-z_][\w]*$/.test(crate)) {
194
+ if (isExternalCrate(crate, localBindings)) {
181
195
  addExternalImport({ spec: leaf.segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-use", line: use.startPosition.row + 1 });
182
196
  }
183
197
  continue;
@@ -200,7 +214,7 @@ export default {
200
214
  const segments = pathParts(node);
201
215
  if (!["crate", "self", "super"].includes(segments[0])) {
202
216
  const crate = cleanSegment(segments[0]);
203
- if (crate && !["std", "core", "alloc"].includes(crate) && /^[a-z_][\w]*$/.test(crate)) {
217
+ if (isExternalCrate(crate, localBindings)) {
204
218
  addExternalImport({ spec: segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-path", line: node.startPosition.row + 1 });
205
219
  }
206
220
  continue;
@@ -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
  }