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 +24 -0
- package/docs/releases/v0.3.5.md +50 -0
- package/docs/releases/v0.3.6.md +44 -0
- package/package.json +3 -1
- package/src/analysis/cargo-dependency-evidence.js +3 -0
- package/src/analysis/http-contracts/client-call-parser.js +8 -4
- package/src/analysis/internal-audit.run.js +5 -2
- package/src/graph/builder/lang-rust.js +17 -3
- package/src/precision/lsp-overlay/budget-policy.js +78 -0
- package/src/precision/lsp-overlay/build.js +23 -15
- package/src/precision/lsp-overlay/source-session.js +15 -7
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.
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
252
|
-
|
|
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: "
|
|
151
|
+
confidence: "low",
|
|
152
152
|
title: `Unused export: ${symbol.label.replace(/\(\)$/, "")} — ${symbol.file}`,
|
|
153
|
-
detail: `${symbol.reason}.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
142
|
-
maxReferences
|
|
143
|
-
maxLinks
|
|
144
|
-
timeoutMs
|
|
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
|
-
|
|
66
|
-
const
|
|
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 >
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 >
|
|
126
|
+
if (bytes > 4 * 1024 * 1024 || projectedBytes + bytes > maxOpenBytes) {
|
|
119
127
|
session.truncated = true
|
|
120
128
|
return false
|
|
121
129
|
}
|