weavatrix 0.2.7 → 0.2.8

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
@@ -216,6 +216,25 @@ disclosed instead of silently guessed; and the server **hot-reloads its watched
216
216
  modules and catalog** when those files change — other MCP helpers and analysis engines require a
217
217
  reconnect.
218
218
 
219
+ ### 0.2.8 trust and precision patch
220
+
221
+ - Dead-code review no longer labels a declaration `test-only` when it has a same-file production
222
+ use. Revision-bound positive evidence from an exact `inspect_symbol` point query also removes that
223
+ declaration from later dead-code candidates instead of remaining isolated in its cache.
224
+ - Python dependency checks discover nested `requirements*.txt`, `requirements/*.txt`,
225
+ `pyproject.toml`, and `Pipfile` manifests and assign imports to their nearest manifest scope.
226
+ A root `test.py` is classified consistently as test code.
227
+ - Endpoint extraction masks commented-out routes without shifting source offsets and rejects
228
+ primitive path-to-name maps such as `{'/.codex': 'claude'}` while retaining real route tables.
229
+ - Rust symbols now expose concrete kinds, owner types, visibility/export state, selection ranges and
230
+ structural owner/member edges. This is parser evidence; Rust semantic/LSP precision remains
231
+ explicitly unavailable.
232
+ - Natural-language retrieval honors explicit Rust, Python, TypeScript, JavaScript, Go, Java, and C#
233
+ intent in mixed repositories. Compact `verified_change` text now includes decisive exact reference
234
+ counts and bounded inbound caller names for every selected edit symbol.
235
+
236
+ Full patch notes: [docs/releases/v0.2.8.md](docs/releases/v0.2.8.md).
237
+
219
238
  ### 0.2.7 verified-change workflow
220
239
 
221
240
  - New `verified_change` composes task retrieval, exact symbol context, change impact, immutable Git
@@ -0,0 +1,82 @@
1
+ # Weavatrix 0.2.8
2
+
3
+ ## Trust before surface area
4
+
5
+ 0.2.8 is a correctness release. It does not add another MCP tool. It fixes false positives and
6
+ evidence hand-off failures found while running 0.2.7 against current JavaScript, TypeScript, Python,
7
+ and Rust repositories.
8
+
9
+ ## Dead-code liveness
10
+
11
+ - A production declaration is no longer classified as `test-only` merely because the same identifier
12
+ appears in tests. Same-file occurrences beyond the indexed declaration count as conservative
13
+ production-use evidence.
14
+ - Exact positive point-query results are now revision/config fingerprint checked and consumed by
15
+ `find_dead_code`. If `inspect_symbol` found a reference, the symbol cannot remain in the dead queue.
16
+ - Complete exact zero-reference point results can still raise internal candidates to strong static
17
+ evidence. Partial, stale, malformed, or configuration-drifted cache entries fail closed.
18
+
19
+ These remain review candidates. Static absence never authorizes automatic deletion.
20
+
21
+ ## Python manifest ownership
22
+
23
+ Python dependency collection now discovers nested `requirements*.txt`, `requirements/*.txt`,
24
+ `pyproject.toml`, and `Pipfile` manifests. Each import is judged against its nearest ancestor Python
25
+ manifest. A nested service therefore sees its own declared dependencies, while unrelated sibling code
26
+ cannot inherit them accidentally.
27
+
28
+ The conventional root filename `test.py` is now classified as test code by both path classification
29
+ and dependency severity logic.
30
+
31
+ ## Endpoint false-positive guards
32
+
33
+ Endpoint extraction masks line and block comments while preserving source offsets. Commented-out
34
+ Express/Hono/Go/Rust/Java-style routes no longer enter the inventory.
35
+
36
+ Direct object routes also require an executable handler value. Primitive path maps such as
37
+ `{'/.codex': 'claude'}` are configuration data, not `ANY /.codex` endpoints.
38
+
39
+ ## Rust graph metadata
40
+
41
+ Rust functions, methods, structs, enums, traits, types, modules, constants, statics, macros, and unions
42
+ now carry concrete `symbol_kind` values. Impl/trait functions retain `member_of`, visibility, selection
43
+ ranges and structural owner/member edges; public module declarations retain exported state.
44
+
45
+ This improves graph navigation and dead-code classification but remains tree-sitter parser evidence.
46
+ 0.2.8 does not bundle rust-analyzer and does not claim exact Rust references.
47
+
48
+ ## Mixed-language retrieval and compact proof
49
+
50
+ `query_graph` and workflows that reuse its seed ranking constrain explicit Rust, Python, TypeScript,
51
+ JavaScript, Go, Java, or C# questions to matching source extensions. Multiple explicitly named
52
+ languages form a union; exact `seed_files` continue to be the strongest pinning mechanism.
53
+
54
+ Compact `verified_change` output now lists each selected symbol's exact/bounded reference occurrence
55
+ count, referenced-file count, inbound-container count, and up to three inbound names. The short form
56
+ therefore cannot hide that a supposedly unused symbol has a known caller.
57
+
58
+ ## Scope and remaining competitor gaps
59
+
60
+ The local engine still has no vector embedding search, runtime trace ingestion, clone-to-ADR layer,
61
+ Python/Rust/Java LSP integration, whole-program CFG, value propagation, or taint engine. Those are
62
+ separate precision projects, not claims attached to this patch release. Independent end-to-end
63
+ competitor scores also remain missing until collected under the blind benchmark protocol.
64
+
65
+ ## Verification
66
+
67
+ - Full Node test suite: 487 tests, 484 passed, 3 platform skips, 0 failed.
68
+ - Deterministic language/cross-repository/lifecycle benchmark: `PASS`.
69
+ - Real pinned corpus: 6 of 6 repositories `PASS` across TypeScript, JavaScript, Python, Go, Java,
70
+ and Rust.
71
+ - Agent routing microbenchmark: 3 of 3 expected symbols, 0 false positives. Independent competitor
72
+ comparison remains `INCOMPLETE` because no blind same-task result was supplied.
73
+ - npm package dry-run succeeds for `weavatrix@0.2.8`; the portable MCPB validates, initializes its
74
+ staged TypeScript precision provider, passes the repository-plugin execution guard, and packs.
75
+ - Direct dogfood checks confirm the original regressions: local `USER_HEADER` is not dead/test-only;
76
+ nested `autobot/requirements.txt` covers `slack_bolt`; commented `DELETE /:id` and primitive
77
+ `ANY /.codex` are absent; explicit Rust retrieval returns only `.rs` seeds.
78
+
79
+ ## Tool catalog
80
+
81
+ 0.2.8 exposes the same 36 MCP tools as 0.2.7. The default offline profile exposes 33; pinned exposes
82
+ 30. Network tools remain absent from offline and pinned profiles.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "Weavatrix — code graph & blast-radius MCP server for AI coding agents: change impact, transitive dependents, health audit, duplicate detection, and coverage mapping over any local repository.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -26,7 +26,7 @@
26
26
  "skill",
27
27
  "scripts/run-agent-task-benchmark.mjs",
28
28
  "docs/agent-task-benchmark.md",
29
- "docs/releases/v0.2.7.md",
29
+ "docs/releases/v0.2.8.md",
30
30
  "README.md",
31
31
  "SECURITY.md",
32
32
  "LICENSE"
@@ -126,14 +126,29 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
126
126
 
127
127
  const symbolNames = new Set([...symById.values()].map((node) => bareName(node.label)).filter(Boolean));
128
128
  const occurrenceFiles = new Map();
129
+ const occurrenceCounts = new Map();
129
130
  for (const [file, text] of sources) for (const match of String(text || "").matchAll(IDENT_RE)) {
130
131
  const name = match[0];
131
132
  if (!symbolNames.has(name)) continue;
132
133
  const files = occurrenceFiles.get(name) || new Set();
133
134
  files.add(file);
134
135
  occurrenceFiles.set(name, files);
136
+ const counts = occurrenceCounts.get(name) || new Map();
137
+ counts.set(file, (counts.get(file) || 0) + 1);
138
+ occurrenceCounts.set(name, counts);
135
139
  }
136
140
 
141
+ const declarationCounts = new Map();
142
+ for (const node of symById.values()) {
143
+ const name = bareName(node.label);
144
+ if (!name) continue;
145
+ const key = `${node.source_file}\0${name}`;
146
+ declarationCounts.set(key, (declarationCounts.get(key) || 0) + 1);
147
+ }
148
+ const exactReferenceIds = new Set(graph.precisionReferenceSymbols || []);
149
+ const exactProductionReferenceIds = new Set(graph.precisionProductionReferenceSymbols || []);
150
+ const exactTestReferenceIds = new Set(graph.precisionTestReferenceSymbols || []);
151
+
137
152
  // decorated defs (@app.route/@app.event/@pytest.fixture…) are entered by the framework: trust the
138
153
  // builder's flag when present, else walk the source line(s) above the definition (graph-builder graphs).
139
154
  const lineCache = new Map();
@@ -155,6 +170,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
155
170
 
156
171
  const isReferenced = (n) => {
157
172
  if (inbound.has(n.id)) return true; // a real graph edge targets it
173
+ if (exactReferenceIds.has(String(n.id))) return true; // revision-bound point-query evidence found a caller
158
174
  const name = bareName(n.label);
159
175
  if (!name || !/^[A-Za-z_$]/.test(name)) return true; // selectors/odd labels → don't flag
160
176
  if (/^__\w+__$/.test(name)) return true; // dunders are invoked implicitly (with/str/==/iter…), never spelled
@@ -182,8 +198,14 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
182
198
  const name = bareName(n.label);
183
199
  const occurrenceSet = occurrenceFiles.get(name) || new Set();
184
200
  const externalOccurrences = [...occurrenceSet].filter((file) => file !== n.source_file);
201
+ const localOccurrences = occurrenceCounts.get(name)?.get(n.source_file) || 0;
202
+ const localDeclarations = declarationCounts.get(`${n.source_file}\0${name}`) || 0;
203
+ const hasLocalProductionUse = localOccurrences > localDeclarations;
185
204
  const lexicalTestOnly = externalOccurrences.length > 0 && externalOccurrences.every((file) => isTestFile(file));
186
- if (hasProductionInbound || (!hasTestInbound && !lexicalTestOnly)) continue;
205
+ const hasExactProductionInbound = exactProductionReferenceIds.has(String(n.id));
206
+ const hasExactTestInbound = exactTestReferenceIds.has(String(n.id));
207
+ if (hasProductionInbound || hasExactProductionInbound || hasLocalProductionUse
208
+ || (!hasTestInbound && !hasExactTestInbound && !lexicalTestOnly)) continue;
187
209
  testOnlySymbols.push({
188
210
  id: n.id,
189
211
  file: n.source_file,
@@ -191,7 +213,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
191
213
  test: false,
192
214
  reason: "referenced only from test/e2e code; no production consumer was found",
193
215
  testConsumerFiles: [...new Set(sourceFiles.filter((file) => file && isTestFile(file)))].sort(),
194
- evidence: hasTestInbound ? "graph" : "lexical",
216
+ evidence: hasTestInbound ? "graph" : hasExactTestInbound ? "exact-semantic" : "lexical",
195
217
  publicApi: n.exported === true || ["public", "protected"].includes(String(n.visibility || "").toLowerCase()),
196
218
  });
197
219
  }
@@ -1,6 +1,6 @@
1
1
  import { makeFinding } from "./findings.js";
2
2
 
3
- const TEST_PATH_RE = /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z0-9]+$|_test\.go$/i;
3
+ const TEST_PATH_RE = /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z0-9]+$|_test\.go$|(^|[/\\])test(?:_[^/\\]*)?\.py$/i;
4
4
  const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
5
  const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
6
6
 
@@ -79,7 +79,7 @@ const PY_TOOL_DISTS = new Set(("pytest tox nox black ruff flake8 pylint mypy pyr
79
79
  "pip-tools uv virtualenv pipenv gunicorn uwsgi supervisor ipython jupyter jupyterlab notebook ipykernel codecov autopep8 yapf commitizen detect-secrets safety pip-audit hatchling flit flit-core pdm").split(" "));
80
80
  const pyNorm = (n) => String(n || "").toLowerCase().replace(/[-_.]+/g, "-");
81
81
 
82
- export function computePyDepFindings({
82
+ function computePyDepFindingsFlat({
83
83
  externalImports = [], pyManifest = null, configTexts = new Map(),
84
84
  managedDependencies = [], ignoredDependencies = [], nonRuntimeRoots = [],
85
85
  } = {}) {
@@ -161,3 +161,42 @@ export function computePyDepFindings({
161
161
  }
162
162
  return { findings, declared, managed };
163
163
  }
164
+
165
+ const normPyScope = (root) => String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
166
+ const pyScopeOwns = (root, file) => !root || file === root || String(file || "").replace(/\\/g, "/").startsWith(`${root}/`);
167
+
168
+ export function computePyDepFindings(options = {}) {
169
+ const scopes = Array.isArray(options.pyManifest?.scopes) ? options.pyManifest.scopes : [];
170
+ if (!scopes.length) return computePyDepFindingsFlat(options);
171
+ const normalized = scopes.map((scope) => ({ ...scope, root: normPyScope(scope.root) }))
172
+ .sort((left, right) => right.root.length - left.root.length);
173
+ if (!normalized.some((scope) => !scope.root)) normalized.push({ root: "", present: false, deps: [], manifests: [] });
174
+ const importsByScope = new Map(normalized.map((scope) => [scope, []]));
175
+ const configByScope = new Map(normalized.map((scope) => [scope, new Map()]));
176
+ for (const entry of options.externalImports || []) {
177
+ const owner = normalized.find((scope) => pyScopeOwns(scope.root, entry.file)) || normalized.at(-1);
178
+ importsByScope.get(owner).push(entry);
179
+ }
180
+ for (const [file, text] of options.configTexts || new Map()) {
181
+ const owner = normalized.find((scope) => pyScopeOwns(scope.root, file)) || normalized.at(-1);
182
+ configByScope.get(owner).set(file, text);
183
+ }
184
+ const findings = [];
185
+ const declared = new Set();
186
+ const managed = new Set();
187
+ for (const scope of normalized) {
188
+ const result = computePyDepFindingsFlat({
189
+ ...options,
190
+ externalImports: importsByScope.get(scope),
191
+ configTexts: configByScope.get(scope),
192
+ pyManifest: {present: scope.present, deps: scope.deps || []},
193
+ });
194
+ findings.push(...result.findings.map((finding) => ({
195
+ ...finding,
196
+ ...(scope.manifests?.length ? {manifest: scope.manifests[0]} : {}),
197
+ })));
198
+ for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
199
+ for (const name of result.managed) managed.add(name);
200
+ }
201
+ return { findings, declared, managed };
202
+ }
@@ -27,6 +27,38 @@ function lineAt(text, index) {
27
27
  return line;
28
28
  }
29
29
 
30
+ // Regex extractors must never see commented-out routes. Preserve string literals and every source
31
+ // offset, but replace comment bodies with spaces so endpoint line numbers still refer to the original
32
+ // file. Python `#` comments are enabled only for .py files; Rust attributes such as #[get] stay intact.
33
+ function maskComments(text, { hashComments = false } = {}) {
34
+ const chars = String(text || "").split("");
35
+ let quote = "", escaped = false, lineComment = false, blockComment = false;
36
+ for (let i = 0; i < chars.length; i++) {
37
+ const ch = chars[i], next = chars[i + 1];
38
+ if (lineComment) {
39
+ if (ch === "\n" || ch === "\r") lineComment = false;
40
+ else chars[i] = " ";
41
+ continue;
42
+ }
43
+ if (blockComment) {
44
+ if (ch === "*" && next === "/") { chars[i] = chars[i + 1] = " "; i++; blockComment = false; }
45
+ else if (ch !== "\n" && ch !== "\r") chars[i] = " ";
46
+ continue;
47
+ }
48
+ if (quote) {
49
+ if (escaped) escaped = false;
50
+ else if (ch === "\\") escaped = true;
51
+ else if (ch === quote) quote = "";
52
+ continue;
53
+ }
54
+ if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; }
55
+ if (ch === "/" && next === "/") { chars[i] = chars[i + 1] = " "; i++; lineComment = true; continue; }
56
+ if (ch === "/" && next === "*") { chars[i] = chars[i + 1] = " "; i++; blockComment = true; continue; }
57
+ if (hashComments && ch === "#") { chars[i] = " "; lineComment = true; }
58
+ }
59
+ return chars.join("");
60
+ }
61
+
30
62
  // best-effort bare handler name from a value expression: the LAST identifier, unwrapping wrappers like
31
63
  // executionRoute(queryHandlers.executeQuery) → executeQuery, asyncHandler(fn) → fn, a.b.c → c.
32
64
  // An INLINE handler (arrow / function literal) has no named method to join to, so it returns "" (the
@@ -75,6 +107,7 @@ export function extractEndpointsFromText(text, file) {
75
107
  const py = /\.py$/i.test(file);
76
108
  const rust = /\.rs$/i.test(file);
77
109
  const java = /\.java$/i.test(file);
110
+ const scanText = maskComments(text, { hashComments: py });
78
111
  const add = (method, path, expr, idx) => {
79
112
  const p = cleanPath(path);
80
113
  if (!looksLikePath(p)) return;
@@ -90,13 +123,13 @@ export function extractEndpointsFromText(text, file) {
90
123
  const seen = new Set();
91
124
  const direct = /\bexport\s+(?:(?:async|declare)\s+)*(?:function\s+|(?:const|let|var)\s+)(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
92
125
  let nm;
93
- while ((nm = direct.exec(text))) {
126
+ while ((nm = direct.exec(scanText))) {
94
127
  const method = nm[1].toUpperCase();
95
128
  if (!seen.has(method)) { seen.add(method); add(method, nextPath, method, nm.index); }
96
129
  }
97
130
  const lists = /\bexport\s*\{([^}]+)\}/g;
98
131
  let lm;
99
- while ((lm = lists.exec(text))) {
132
+ while ((lm = lists.exec(scanText))) {
100
133
  for (const item of lm[1].split(",")) {
101
134
  const mm = /^\s*([A-Za-z_$][\w$]*)(?:\s+as\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS))?\s*$/.exec(item);
102
135
  if (!mm) continue;
@@ -108,9 +141,9 @@ export function extractEndpointsFromText(text, file) {
108
141
  }
109
142
  }
110
143
 
111
- if (rust) extractRustEndpoints(text, add);
144
+ if (rust) extractRustEndpoints(scanText, add);
112
145
  if (java) {
113
- out.push(...extractSpringEndpoints(text, file));
146
+ out.push(...extractSpringEndpoints(scanText, file));
114
147
  return out; // generic JS-style method calls would turn Java HTTP clients into fake server routes
115
148
  }
116
149
 
@@ -118,14 +151,14 @@ export function extractEndpointsFromText(text, file) {
118
151
  // find each "…": { or "…": expr, where the key looks like a path
119
152
  const objKeyRe = /(["'`])(\/[^"'`]*)\1\s*:\s*(\{)?/g;
120
153
  let m;
121
- while ((m = objKeyRe.exec(text))) {
154
+ while ((m = objKeyRe.exec(scanText))) {
122
155
  const path = m[2], keyIdx = m.index;
123
156
  if (m[3]) {
124
157
  // object of METHOD: handler — scan to the matching close brace (routes objects are shallow)
125
158
  let i = objKeyRe.lastIndex, depth = 1;
126
159
  const start = i;
127
- while (i < text.length && depth > 0) { const c = text[i]; if (c === "{") depth++; else if (c === "}") depth--; i++; }
128
- const body = text.slice(start, i - 1);
160
+ while (i < scanText.length && depth > 0) { const c = scanText[i]; if (c === "{") depth++; else if (c === "}") depth--; i++; }
161
+ const body = scanText.slice(start, i - 1);
129
162
  objKeyRe.lastIndex = i;
130
163
  if (OPENAPI_BLOCK.test(body)) continue; // documentation, not a route table
131
164
  const methodRe = /\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b\s*:\s*([^,\n}]+)/gi;
@@ -133,9 +166,10 @@ export function extractEndpointsFromText(text, file) {
133
166
  while ((mm = methodRe.exec(body))) add(mm[1], path, mm[2], keyIdx);
134
167
  } else {
135
168
  // "/path": handlerExpr — a direct handler (any method); grab up to the next , or }
136
- const tail = text.slice(objKeyRe.lastIndex, objKeyRe.lastIndex + 200);
169
+ const tail = scanText.slice(objKeyRe.lastIndex, objKeyRe.lastIndex + 200);
137
170
  const em = /^([^,\n}]+)/.exec(tail);
138
- if (em && !/^\s*\{/.test(em[1])) add("ANY", path, em[1], keyIdx);
171
+ // A string/number/array value is an ordinary path/name lookup, not an executable handler.
172
+ if (em && !/^\s*(?:\{|["'`\[]|[-+]?\d|true\b|false\b|null\b|undefined\b)/i.test(em[1])) add("ANY", path, em[1], keyIdx);
139
173
  }
140
174
  }
141
175
 
@@ -146,7 +180,7 @@ export function extractEndpointsFromText(text, file) {
146
180
  // FRONTEND code, not server routes. A server route also REQUIRES a handler arg (an identifier/function),
147
181
  // so a bare `client.get("/x")` or one whose 2nd arg is a config object literal `{…}` is skipped.
148
182
  const callRe = /(?<!@)\b([\w$]+)\s*\.\s*(get|post|put|patch|delete|head|options|all)\s*\(\s*(["'`])(\/[^"'`]*)\3\s*(?:,\s*([\s\S]{0,160}?))?\)/gi;
149
- while ((m = callRe.exec(text))) {
183
+ while ((m = callRe.exec(scanText))) {
150
184
  const caller = m[1], arg2 = String(m[5] || "").trim();
151
185
  if (HTTP_CLIENT_CALLER.test(caller)) continue; // axios/http/fetch/apiClient… → a client request
152
186
  if (!arg2 || arg2[0] === "{") continue; // no handler, or a config object → not a route def
@@ -155,14 +189,14 @@ export function extractEndpointsFromText(text, file) {
155
189
 
156
190
  // ---- Go net/http: mux.HandleFunc("/path", handler) / http.Handle("/path", h) ------------------
157
191
  const goRe = /\.\s*(?:HandleFunc|Handle)\s*\(\s*(["'`])(\/[^"'`]*)\1\s*,\s*([\s\S]{0,120}?)\)/g;
158
- while ((m = goRe.exec(text))) add("ANY", m[2], m[3], m.index);
192
+ while ((m = goRe.exec(scanText))) add("ANY", m[2], m[3], m.index);
159
193
 
160
194
  // ---- decorators: @app.get("/path") / @router.post("/path") / @Get("/path") -------------------
161
195
  if (py || /\.(ts|js|tsx|jsx|cjs|mjs)$/i.test(file)) {
162
196
  const decoRe = /@[\w$]*\.?\s*(get|post|put|patch|delete|head|options)\s*\(\s*(["'`])(\/[^"'`]*)\2/gi;
163
- while ((m = decoRe.exec(text))) {
197
+ while ((m = decoRe.exec(scanText))) {
164
198
  // the handler is the def/function on a following line — best-effort: next def name
165
- const after = text.slice(decoRe.lastIndex, decoRe.lastIndex + 200);
199
+ const after = scanText.slice(decoRe.lastIndex, decoRe.lastIndex + 200);
166
200
  const fn = /\b(?:def|async\s+def|function|const|export\s+function)\s+([A-Za-z_$][\w$]*)/.exec(after);
167
201
  add(m[1], m[3], fn ? fn[1] : "", m.index);
168
202
  }
@@ -3,7 +3,7 @@
3
3
  import { readFileSync, readdirSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
- import { parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps } from "./manifests.js";
6
+ import { parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503 } from "./manifests.js";
7
7
  import { createRepoBoundary } from "../repo-path.js";
8
8
  import { childProcessEnv } from "../child-env.js";
9
9
  import { filterWeavatrixIgnored } from "../path-ignore.js";
@@ -254,28 +254,61 @@ export function workspacePkgNames(repoRoot, pkg) {
254
254
 
255
255
  export const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i;
256
256
 
257
- // Python declared deps: root requirements*.txt/.in + requirements/ dir + pyproject.toml + Pipfile.
257
+ // Python declared deps are owned by the nearest manifest root, matching nested service/monorepo
258
+ // layouts. `requirements/*.txt` belongs to the directory above `requirements`; a colocated
259
+ // requirements.txt, pyproject.toml or Pipfile owns its own directory.
258
260
  // present=false (no manifest at all) softens missing-dep findings instead of suppressing them.
259
261
  export function collectPyManifest(repoRoot) {
260
- const deps = [];
261
- let present = false;
262
- let names = [];
263
262
  const boundary = createRepoBoundary(repoRoot);
264
- try { names = readdirSync(boundary.root).filter((n) => /^requirements[\w.-]*\.(txt|in)$/i.test(n)); } catch { /* unreadable root */ }
265
- try {
266
- const requirements = boundary.resolve("requirements");
267
- if (requirements.ok) names.push(...readdirSync(requirements.path).filter((n) => /\.(txt|in)$/i.test(n)).map((n) => `requirements/${n}`));
268
- } catch { /* no requirements dir */ }
269
- for (const n of names) {
270
- const t = readRepoText(boundary, n);
263
+ const scopes = new Map();
264
+ const scopeFor = (root) => {
265
+ const normalized = normRoot(root);
266
+ if (!scopes.has(normalized)) scopes.set(normalized, { root: normalized, present: false, deps: [], manifests: [] });
267
+ return scopes.get(normalized);
268
+ };
269
+ const addManifest = (root, manifest, parsedDeps, present = true) => {
270
+ if (!present) return;
271
+ const scope = scopeFor(root);
272
+ scope.present = true;
273
+ scope.manifests.push(manifest);
274
+ scope.deps.push(...parsedDeps.map((dep) => ({ ...dep, manifest })));
275
+ };
276
+ const files = listRepoFiles(repoRoot);
277
+ for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
278
+ || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
279
+ const t = readRepoText(boundary, file);
271
280
  if (t == null) continue;
272
- present = true;
273
- const dev = /dev|test|lint|doc|ci/i.test(n.replace(/^requirements[/\\]?/i, ""));
274
- for (const d of parseRequirementsNames(t)) deps.push({ ...d, dev });
281
+ const parent = normRoot(dirname(file));
282
+ const root = /(^|\/)requirements$/i.test(parent) ? normRoot(dirname(parent)) : parent;
283
+ const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf("/") + 1));
284
+ addManifest(root, file, parseRequirementsNames(t).map((dep) => ({ ...dep, dev })));
275
285
  }
276
- const pp = readRepoText(boundary, "pyproject.toml");
277
- if (pp != null) { const r = parsePyprojectDeps(pp); if (r.present) { present = true; deps.push(...r.deps); } }
278
- const pf = readRepoText(boundary, "Pipfile");
279
- if (pf != null) { const r = parsePipfileDeps(pf); if (r.present) { present = true; deps.push(...r.deps); } }
280
- return { present, deps };
286
+ for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
287
+ const parsed = parsePyprojectDeps(readRepoText(boundary, file));
288
+ addManifest(dirname(file), file, parsed.deps, parsed.present);
289
+ }
290
+ for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
291
+ const parsed = parsePipfileDeps(readRepoText(boundary, file));
292
+ addManifest(dirname(file), file, parsed.deps, parsed.present);
293
+ }
294
+ const normalizedScopes = [...scopes.values()]
295
+ .map((scope) => {
296
+ const seen = new Set();
297
+ return {
298
+ ...scope,
299
+ manifests: [...new Set(scope.manifests)].sort(),
300
+ deps: scope.deps.filter((dep) => {
301
+ const key = pep503(dep.name);
302
+ if (!key || seen.has(key)) return false;
303
+ seen.add(key);
304
+ return true;
305
+ }),
306
+ };
307
+ })
308
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
309
+ return {
310
+ present: normalizedScopes.some((scope) => scope.present),
311
+ deps: normalizedScopes.flatMap((scope) => scope.deps),
312
+ scopes: normalizedScopes,
313
+ };
281
314
  }
@@ -4,23 +4,43 @@
4
4
  // `crate/self/super` paths resolve to repo-local .rs or */mod.rs files. External crates deliberately stay out
5
5
  // of this adapter; Cargo dependency analysis owns them.
6
6
  const SYMS_CORE = `
7
- (function_item name: (identifier) @method)
8
- (struct_item name: (type_identifier) @class)
9
- (enum_item name: (type_identifier) @class)
10
- (trait_item name: (type_identifier) @class)
11
- (type_item name: (type_identifier) @class)
12
- (mod_item name: (identifier) @class)
13
- (const_item name: (identifier) @field)
14
- (static_item name: (identifier) @field)`;
7
+ (function_item name: (identifier) @function)
8
+ (struct_item name: (type_identifier) @struct)
9
+ (enum_item name: (type_identifier) @enum)
10
+ (trait_item name: (type_identifier) @trait)
11
+ (type_item name: (type_identifier) @type)
12
+ (mod_item name: (identifier) @module)
13
+ (const_item name: (identifier) @constant)
14
+ (static_item name: (identifier) @static)`;
15
15
  // Grammar-version-dependent node types, compiled SEPARATELY (one unknown type voids its whole query).
16
16
  const SYMS_OPTIONAL = [
17
- `(macro_definition name: (identifier) @method)`,
18
- `(union_item name: (type_identifier) @class)`,
17
+ `(function_signature_item name: (identifier) @function)`,
18
+ `(macro_definition name: (identifier) @macro)`,
19
+ `(union_item name: (type_identifier) @union)`,
19
20
  ];
20
21
 
21
22
  const cleanSegment = (part) => String(part || "").trim().replace(/^r#/, "");
22
23
  const pathParts = (node) => String(node?.text || "").split("::").map(cleanSegment).filter(Boolean);
23
24
  const under = (node, type) => { for (let p = node?.parent; p; p = p.parent) if (p.type === type) return true; return false; };
25
+ const ancestor = (node, types) => {
26
+ for (let parent = node?.parent; parent; parent = parent.parent) if (types.has(parent.type)) return parent;
27
+ return null;
28
+ };
29
+ const memberOwners = new Set(["impl_item", "trait_item"]);
30
+ const publicVisibility = (declaration, owner) => {
31
+ if (owner?.type === "trait_item") return "public";
32
+ const source = String(declaration?.text || "").trimStart();
33
+ if (/^pub\s/.test(source)) return "public";
34
+ if (/^pub\s*\(/.test(source)) return "protected";
35
+ return "private";
36
+ };
37
+ const ownerName = (owner, field) => {
38
+ if (!owner) return "";
39
+ if (owner.type === "trait_item") return field(owner, "name")?.text || "";
40
+ const type = field(owner, "type")?.text || "";
41
+ const withoutGenerics = type.replace(/<[^<>]*>/g, "");
42
+ return (withoutGenerics.match(/[A-Za-z_]\w*/g) || []).at(-1) || "";
43
+ };
24
44
 
25
45
  function pathAttribute(modNode) {
26
46
  for (let prev = modNode?.previousNamedSibling; prev?.type === "attribute_item"; prev = prev.previousNamedSibling) {
@@ -93,12 +113,29 @@ export default {
93
113
  ],
94
114
 
95
115
  pass1(ctx) {
96
- const { grammar, tree, fileRel, caps, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath } = ctx;
116
+ const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath, links, nameToId } = ctx;
117
+ const owned = [];
97
118
  for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
98
119
  for (const cap of caps(grammar, src, tree.rootNode)) {
99
- addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
120
+ const declaration = cap.node.parent;
121
+ const owner = cap.name === "function" ? ancestor(cap.node, memberOwners) : null;
122
+ const memberOf = ownerName(owner, field);
123
+ const symbolKind = cap.name === "function" ? (memberOf ? "method" : "function") : cap.name;
124
+ const visibility = publicVisibility(declaration, owner);
125
+ const id = addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "function", {
126
+ sourceNode: declaration,
127
+ selectionNode: cap.node,
128
+ symbolKind,
129
+ ...(memberOf ? { memberOf, visibility } : { moduleDeclaration: true }),
130
+ ...(!memberOf && visibility === "public" ? { exported: true } : {}),
131
+ });
132
+ if (id && memberOf) owned.push({owner: memberOf, id});
100
133
  }
101
134
  }
135
+ for (const member of owned) {
136
+ const ownerId = nameToId.get(member.owner);
137
+ if (ownerId && ownerId !== member.id) links.push({source: ownerId, target: member.id, relation: "method", confidence: "EXTRACTED"});
138
+ }
102
139
 
103
140
  // File dependency edges are intentionally unique per source/target/relation. A qualified path can be
104
141
  // nested in another qualified path and often repeats an existing `mod`/`use`; counting occurrences would
@@ -163,6 +163,30 @@ const CLASS_QUERY_TERMS = Object.freeze({
163
163
  })
164
164
  const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
165
165
  const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
166
+ const LANGUAGE_EXTENSIONS = Object.freeze({
167
+ rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
168
+ javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
169
+ })
170
+
171
+ function requestedLanguages(query) {
172
+ const raw = String(query || '')
173
+ const words = new Set(wordsOf(query))
174
+ const requested = new Set()
175
+ if (words.has('rust')) requested.add('rust')
176
+ if (words.has('python') || words.has('py')) requested.add('python')
177
+ if (words.has('typescript') || words.has('ts') || /typescript/i.test(raw)) requested.add('typescript')
178
+ if (words.has('javascript') || words.has('js') || words.has('nodejs') || /javascript|node\.?js/i.test(raw)) requested.add('javascript')
179
+ if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
180
+ if (words.has('java')) requested.add('java')
181
+ if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
182
+ return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
183
+ }
184
+
185
+ const matchesLanguage = (node, extensions) => {
186
+ if (!extensions.size) return true
187
+ const match = /\.([^.\/]+)$/.exec(sourceFileOf(node))
188
+ return !!match && extensions.has(match[1].toLowerCase())
189
+ }
166
190
 
167
191
  const sourceFileOf = (node) => {
168
192
  const source = node?.source_file || String(node?.id ?? '').split('#', 1)[0]
@@ -266,6 +290,14 @@ function conceptScore(g, node, concept, queryContext) {
266
290
  else if (words.has(term)) match = Math.max(match, primary ? 36 : 25)
267
291
  else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
268
292
  })
293
+ const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
294
+ const languageConcept = {
295
+ rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'],
296
+ typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'],
297
+ javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'],
298
+ golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs'],
299
+ }[concept.raw]
300
+ if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
269
301
  const fileNode = !isSymbol(node.id)
270
302
  const depth = source ? source.split('/').length : 9
271
303
  if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))
@@ -288,13 +320,16 @@ export function findSeeds(g, query, limit = 8, {repoRoot = null} = {}) {
288
320
  const concepts = queryConcepts(query)
289
321
  if (!concepts.length || limit <= 0) return []
290
322
  const requestedClasses = requestedPathClasses(query)
323
+ const languageExtensions = requestedLanguages(query)
291
324
  const queryContext = {
292
325
  runtimeIntent: concepts.some((concept) => concept.id === 'bootstrap' || concept.id === 'tool-execution'),
293
326
  maintenanceIntent: wordsOf(query).some((word) => ['script', 'scripts', 'build', 'release', 'publish', 'packaging'].includes(word)),
327
+ languageExtensions,
294
328
  }
295
329
  const classifier = createPathClassifier(repoRoot)
296
330
  const classificationCache = new Map()
297
- const rows = g.nodes.filter((node) => isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
331
+ const rows = g.nodes.filter((node) => matchesLanguage(node, languageExtensions)
332
+ && isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
298
333
  const scores = concepts.map((concept) => conceptScore(g, node, concept, queryContext))
299
334
  return {node, scores, total: Math.max(...scores) + scores.reduce((sum, score) => sum + score, 0) / 10}
300
335
  })
@@ -22,6 +22,7 @@ import {toolResult} from './tool-result.mjs'
22
22
  import {createPathClassifier, hasPathClass} from '../path-classification.js'
23
23
  import {createRepoBoundary} from '../repo-path.js'
24
24
  import {filterGraphForMode} from '../graph/graph-filter.js'
25
+ import {readCachedSymbolPrecisionEvidence} from '../precision/symbol-query.js'
25
26
 
26
27
  const NON_PRODUCT_DUPLICATE_CLASSES = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
27
28
  const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
@@ -101,7 +102,27 @@ export function tFindDuplicates(g, args, ctx) {
101
102
  // candidates. It never returns an automatic-delete verdict.
102
103
  export function tFindDeadCode(g, args, ctx) {
103
104
  if (!ctx.repoRoot) return 'Dead-code review needs the repo root (not provided to this server).'
104
- const graph = effectiveRawGraph(ctx)
105
+ const effectiveGraph = effectiveRawGraph(ctx)
106
+ const pointEvidence = readCachedSymbolPrecisionEvidence({repoRoot: ctx.repoRoot, graphPath: ctx.graphPath, graph: effectiveGraph})
107
+ const graph = {
108
+ ...effectiveGraph,
109
+ precisionReferenceSymbols: [...new Set([
110
+ ...(effectiveGraph.precisionReferenceSymbols || []),
111
+ ...pointEvidence.referenceSymbols,
112
+ ])],
113
+ precisionProductionReferenceSymbols: [...new Set([
114
+ ...(effectiveGraph.precisionProductionReferenceSymbols || []),
115
+ ...pointEvidence.productionReferenceSymbols,
116
+ ])],
117
+ precisionTestReferenceSymbols: [...new Set([
118
+ ...(effectiveGraph.precisionTestReferenceSymbols || []),
119
+ ...pointEvidence.testReferenceSymbols,
120
+ ])],
121
+ precisionNoReferenceSymbols: [...new Set([
122
+ ...(effectiveGraph.precisionNoReferenceSymbols || []),
123
+ ...pointEvidence.noReferenceSymbols,
124
+ ])],
125
+ }
105
126
  const boundary = createRepoBoundary(ctx.repoRoot)
106
127
  const pkg = readRepoJson(boundary, 'package.json') || {}
107
128
  const rules = readRepoJson(boundary, '.weavatrix-deps.json') || {}
@@ -62,6 +62,21 @@ function architectureState(result) {
62
62
  return result.verification.new?.length || String(result.verification.status).toUpperCase() === 'FAIL' ? 'BLOCKED' : 'PASS'
63
63
  }
64
64
 
65
+ function exactUsageLines(contexts, g) {
66
+ if (!contexts.length) return []
67
+ return contexts.map((context) => {
68
+ const node = g.byId.get(String(context.symbol || context.definition?.id || ''))
69
+ const label = String(context.definition?.label || node?.label || context.symbol || 'symbol')
70
+ if (context.status !== 'OK') return `Exact usage: ${label} — unavailable (${context.status || 'UNKNOWN'}).`
71
+ const exact = context.evidence?.state === 'EXACT'
72
+ const occurrences = Number(context.references?.occurrences) || 0
73
+ const files = Number(context.references?.files) || 0
74
+ const inbound = Array.isArray(context.inbound?.shown) ? context.inbound.shown.slice(0, 3) : []
75
+ const callers = inbound.map((item) => `${item.label || item.id}${item.file ? ` [${item.file}]` : ''}`).join(', ')
76
+ return `${exact ? 'Exact usage' : 'Bounded usage'}: ${label} — ${occurrences} reference occurrence(s)${files ? ` in ${files} file(s)` : ''}; ${Number(context.inbound?.total) || 0} inbound container(s)${callers ? `: ${callers}` : ''}.`
77
+ })
78
+ }
79
+
65
80
  function decide({phase, impact, graph, architecture, duplicates, api, tests, suggestedTests, testCoverageState}) {
66
81
  if (phase === 'plan') return tests.state === 'BLOCKED'
67
82
  ? {verdict: 'BLOCKED', blockers: [`targeted test plan was rejected: ${tests.reason || 'invalid request'}`], unknowns: []}
@@ -162,6 +177,7 @@ export async function tVerifiedChange(g, args = {}, ctx = {}, tools = {}, permis
162
177
  `${decision.verdict} — verified_change ${phase}`, `Task: ${String(args.task).slice(0, 500)}`,
163
178
  `Change: ${changedFiles.length} file(s), ${impact.seeds?.ids?.length || 0} exact seed(s), blast radius ${impact.blastRadius?.impacted || 0}.`,
164
179
  `Edit context: ${retrieval.selected.length} symbol(s); ${contexts.length} exact bundle(s); data-flow ${dataFlow.status} (${dataFlow.edges.length} call edge(s)).`,
180
+ ...exactUsageLines(contexts, g),
165
181
  `Ratchets: graph ${graph.state}; architecture ${architecture.state}; duplicates ${duplicates.state}; API ${api.state}; tests ${testProof.state}.`,
166
182
  ...decision.blockers.map((item) => `BLOCKER: ${item}`), ...decision.unknowns.map((item) => `UNKNOWN: ${item}`),
167
183
  ].join('\n')
@@ -46,7 +46,7 @@ const DEFAULT_RULES = [
46
46
  {
47
47
  category: "test",
48
48
  pattern: "test/spec roots and conventional test filenames",
49
- regex: /(^|\/)(?:__tests?__|tests?|spec)(\/|$)|\.(?:test|itest|spec)\.[^.\/]+$|_test\.go$|(^|\/)test_[^/]*\.py$/i,
49
+ regex: /(^|\/)(?:__tests?__|tests?|spec)(\/|$)|\.(?:test|itest|spec)\.[^.\/]+$|_test\.go$|(^|\/)test(?:_[^/]*)?\.py$/i,
50
50
  },
51
51
  {
52
52
  category: "generated",
@@ -7,6 +7,7 @@ import {
7
7
  precisionOverlayMatches,
8
8
  precisionSemanticInputsMatch,
9
9
  } from './lsp-overlay.js'
10
+ import {createPathClassifier, hasPathClass} from '../path-classification.js'
10
11
 
11
12
  export const SYMBOL_PRECISION_CACHE_V = 1
12
13
  export const SYMBOL_PRECISION_CACHE_FILE = 'precision-symbols.json'
@@ -79,6 +80,56 @@ function cacheable(overlay) {
79
80
  && overlay?.reason === 'semantic precision stopped at a configured safety limit'
80
81
  }
81
82
 
83
+ // Point-query results intentionally live outside the broad precision overlay. Health tools still
84
+ // need to consume revision-bound positive evidence: once an exact query found a reference, the
85
+ // same symbol must not remain in a dead-code queue. This reader is synchronous and fail-closed;
86
+ // stale, malformed, incomplete no-reference, or semantically changed entries contribute nothing.
87
+ export function readCachedSymbolPrecisionEvidence({repoRoot, graphPath, graph} = {}) {
88
+ const empty = {referenceSymbols: [], productionReferenceSymbols: [], testReferenceSymbols: [], noReferenceSymbols: []}
89
+ if (!repoRoot || !graphPath || !graph) return empty
90
+ const precisionGraph = graph.graphPrecisionMode === 'off' ? {...graph, graphPrecisionMode: 'lsp'} : graph
91
+ const nodesById = new Map((precisionGraph.nodes || []).map((node) => [String(node.id), node]))
92
+ const classifier = createPathClassifier(repoRoot)
93
+ const referenced = new Set()
94
+ const production = new Set()
95
+ const tests = new Set()
96
+ const noReference = new Set()
97
+ const semanticMatches = new Map()
98
+ for (const entry of readCache(symbolPrecisionCachePath(graphPath)).entries) {
99
+ const overlay = entry?.overlay
100
+ if (!overlay || !precisionOverlayMatches(overlay, precisionGraph, {request: overlay.request})) continue
101
+ const fingerprint = String(overlay.semanticInputFingerprint || '')
102
+ if (!semanticMatches.has(fingerprint)) semanticMatches.set(
103
+ fingerprint,
104
+ precisionSemanticInputsMatch(overlay, repoRoot, precisionGraph),
105
+ )
106
+ if (!semanticMatches.get(fingerprint)) continue
107
+ const targetId = String(entry.targetId || '')
108
+ if (!nodesById.has(targetId)) continue
109
+ const locations = Array.isArray(overlay.locations)
110
+ ? overlay.locations.filter((location) => String(location?.target || '') === targetId)
111
+ : []
112
+ if (locations.length) {
113
+ referenced.add(targetId)
114
+ for (const location of locations) {
115
+ const file = String(location?.file || nodesById.get(String(location?.source || ''))?.source_file || '')
116
+ if (!file) continue
117
+ const info = classifier.explain(file, {content: ''})
118
+ if (hasPathClass(info, 'test', 'e2e')) tests.add(targetId)
119
+ else production.add(targetId)
120
+ }
121
+ }
122
+ if (overlay.state === 'COMPLETE' && Array.isArray(overlay.noReferenceSymbols)
123
+ && overlay.noReferenceSymbols.some((id) => String(id) === targetId)) noReference.add(targetId)
124
+ }
125
+ return {
126
+ referenceSymbols: [...referenced],
127
+ productionReferenceSymbols: [...production],
128
+ testReferenceSymbols: [...tests],
129
+ noReferenceSymbols: [...noReference],
130
+ }
131
+ }
132
+
82
133
  export async function querySymbolPrecision({
83
134
  repoRoot,
84
135
  graphPath,
@@ -1,93 +0,0 @@
1
- # Weavatrix 0.2.7
2
-
3
- ## Proof-carrying changes
4
-
5
- `verified_change` is a new high-level MCP workflow for planning and verifying a code change. It
6
- accepts a natural-language task, an optional unified diff or changed-file hints, and an immutable Git
7
- baseline. Its stable result is one of:
8
-
9
- - `PASS`: every applicable enabled proof completed and no ratchet failed;
10
- - `BLOCKED`: a test failed, a new duplicate intersects changed code, a runtime cycle appeared, an
11
- architecture ratchet failed, or bounded API evidence found an HTTP method mismatch;
12
- - `UNKNOWN`: required evidence is missing, partial, disabled, or not configured.
13
-
14
- The structured result contains intent-expanded task retrieval combined with exact changed-symbol
15
- seeds, up to five compact LSP/graph edit contexts, symbol-aware blast radius, affected-test paths,
16
- bounded JS/TS call-argument-to-parameter evidence, Git graph drift, architecture verification, a
17
- new-duplicate baseline comparison, and optional cross-repository HTTP contract evidence.
18
-
19
- The workflow respects the existing capability boundary. Exact source/LSP context requires `source`,
20
- duplicate comparison and architecture verification require `health`, and API tracing requires
21
- `crossrepo`. A narrow custom profile is never silently bypassed.
22
-
23
- ## Safe targeted tests
24
-
25
- The workflow does not accept shell commands. It can execute at most five existing `package.json`
26
- scripts whose names match the test/check/verify allowlist. Execution requires both
27
- `run_tests:true` on the tool call and `WEAVATRIX_ALLOW_TEST_RUNS=1` in the server environment. The
28
- default remains read-only and returns the test plan or `UNKNOWN` when runtime evidence was requested
29
- but not authorized.
30
-
31
- ## Malware scanner correction
32
-
33
- The npm content sweep now starts at actual installed package roots instead of the entire
34
- `node_modules` container. Hidden package-manager caches and hosted release snapshots are therefore
35
- not attributed as dependencies and cannot make Weavatrix flag its own malware-signature source.
36
-
37
- Heuristic remediation no longer says to treat a package as compromised or rotate all reachable
38
- secrets. It asks the operator to quarantine and inspect the package, verify origin and lockfile
39
- integrity, and rotate secrets only after execution or credential exposure is confirmed.
40
-
41
- Static heuristic findings now have a hard `high` severity ceiling. They expose
42
- `confirmedExecution:false` plus explicit runtime/origin/lockfile/exposure verification states;
43
- `critical` is reserved for independently confirmed malicious advisories or runtime evidence.
44
-
45
- Package-lock install-script drift now compares only `preinstall`, `install`, and `postinstall`.
46
- Development/publication hooks such as `prepare` no longer create false high-severity drift findings.
47
-
48
- ## Signal-quality corrections
49
-
50
- - `query_graph` applies production-first path classification to the traversal, not only seed
51
- selection. Tests, generated code, fixtures, docs, benchmarks and excluded paths require an
52
- explicit question class or `include_classified:true`. Exact non-product seed files remain usable.
53
- - `verified_change` task retrieval uses the same production-first policy while always retaining
54
- exact changed symbols. The bundled routing microbenchmark moved from one test-symbol false
55
- positive (`0.25`) to zero (`0`) without losing any of its three expected production targets.
56
- - Unreferenced constant/field leaves that do not match the query are hidden by default; use
57
- `include_low_signal:true` for diagnostic expansion.
58
- - `hot_path_review` now defaults to a focused score gate of 85 plus a narrow strong-local fallback.
59
- On the 0.2.7 source tree this reduced the default queue from 717 diagnostic candidates to 116 of
60
- 1,043 analyzed symbols; `min_score:0` restores the full diagnostic set.
61
- - `find_dead_code` adds `STRONG_STATIC_EVIDENCE`, `BOUNDED_STATIC_EVIDENCE`, and
62
- `HIGH_UNCERTAINTY` tiers, structured completed/remaining checks, and `autoDelete:false` on every
63
- candidate. Strong static evidence means exact zero LSP references, not permission to delete.
64
- - npm unused/missing/duplicate dependency findings now carry manifest section, indexed-source,
65
- script/config, package-scope and unresolved dynamic/plugin verification state. Unused dependencies
66
- remain `REVIEW_REQUIRED`; missing dependencies retain the exact importing-file evidence.
67
-
68
- ## Benchmark contract
69
-
70
- `npm run benchmark:agent` runs a deterministic task-to-symbol routing microbenchmark and reports task
71
- success, false-positive rate, estimated output tokens, and duration. This is intentionally labelled
72
- as a routing microbenchmark, not an autonomous end-to-end change benchmark.
73
-
74
- Independent Codebase Memory and Serena results may be supplied with
75
- `--independent-results <json>` using the blind evaluator contract in
76
- [`docs/agent-task-benchmark.md`](../agent-task-benchmark.md). Without same-task results for all three
77
- systems, comparison status is `INCOMPLETE`; `npm run benchmark:agent:release` fails closed. The
78
- repository does not manufacture competitor scores.
79
-
80
- ## Scope and limitations
81
-
82
- - The interprocedural evidence maps call arguments to declared callee parameters for bounded JS/TS
83
- graph call edges. It is not a CFG, value-propagation engine, or taint analysis.
84
- - An absent architecture contract produces `UNKNOWN`; provisional budgets are guidance, not policy.
85
- - A static path to a test is not proof that the test executed. A `PASS` that depends on runtime
86
- behavior requires explicitly authorized test execution.
87
- - API proof is optional and registry-scoped. No client match is `UNKNOWN`, never proof of no external
88
- consumers.
89
-
90
- ## Tool catalog
91
-
92
- 0.2.7 exposes 36 MCP tools. The default offline profile exposes 33; pinned exposes 30. Network tools
93
- remain absent from the offline and pinned profiles.