weavatrix 0.3.7 → 0.3.9
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 +129 -942
- package/docs/releases/v0.3.8.md +21 -0
- package/docs/releases/v0.3.9.md +36 -0
- package/package.json +3 -1
- package/scripts/run-agent-task-benchmark.mjs +0 -1
- package/src/analysis/change-classification.js +9 -19
- package/src/analysis/dead-code-review/candidates.js +194 -0
- package/src/analysis/dead-code-review.js +4 -188
- package/src/analysis/git-history/collector.js +1 -43
- package/src/analysis/git-history.js +2 -1
- package/src/analysis/git-ref-graph.js +4 -10
- package/src/analysis/internal-audit/repo-files.js +2 -6
- package/src/git-exec.js +67 -0
- package/src/graph/freshness-probe.js +2 -5
- package/src/graph/internal-builder.langs.js +11 -24
- package/src/mcp/git-output.mjs +2 -5
- package/src/mcp/graph/context-state.mjs +4 -5
- package/src/mcp/health/audit-format.mjs +2 -5
- package/src/mcp/tools-impact-change.mjs +2 -3
- package/src/mcp-rg.mjs +2 -4
- package/src/mcp-source-tools.mjs +4 -5
- package/src/process.js +18 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Weavatrix 0.3.8
|
|
2
|
+
|
|
3
|
+
0.3.8 is a documentation and internal-structure release. Tool behavior and
|
|
4
|
+
output are unchanged.
|
|
5
|
+
|
|
6
|
+
## Slimmer README
|
|
7
|
+
|
|
8
|
+
The README is refocused on the current release — install, configuration, tools,
|
|
9
|
+
benchmarks and the security model — and the accumulated per-version changelog is
|
|
10
|
+
removed in favor of the existing per-version notes under
|
|
11
|
+
[docs/releases/](../releases/). This also slims the README rendered on the npm
|
|
12
|
+
package page.
|
|
13
|
+
|
|
14
|
+
## Internal module split
|
|
15
|
+
|
|
16
|
+
`src/analysis/dead-code-review.js` is split along the repository's existing
|
|
17
|
+
facade convention: the per-candidate record builders (`symbolCandidate`,
|
|
18
|
+
`fileCandidate`) move to `src/analysis/dead-code-review/candidates.js`, while the
|
|
19
|
+
orchestrator and the public `computeDeadCodeReview` export stay in place.
|
|
20
|
+
Importers and returned records are unchanged, and both files are now within the
|
|
21
|
+
300-line owner-module budget.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Weavatrix 0.3.9
|
|
2
|
+
|
|
3
|
+
0.3.9 consolidates every subprocess launch behind a small number of audited
|
|
4
|
+
chokepoints. Tool behavior and output are unchanged — verified byte-identical
|
|
5
|
+
against 0.3.8 on real repositories.
|
|
6
|
+
|
|
7
|
+
## Single-chokepoint subprocess execution
|
|
8
|
+
|
|
9
|
+
Git and other subprocess launches were scattered across roughly fifteen modules,
|
|
10
|
+
each with its own near-identical spawn wrapper. They now route through two
|
|
11
|
+
focused modules:
|
|
12
|
+
|
|
13
|
+
- **git** — every git command runs through `src/git-exec.js`: `runGit` for
|
|
14
|
+
synchronous run-and-collect calls and `boundedGitCommand` for the streaming
|
|
15
|
+
history collector.
|
|
16
|
+
- **everything else** — ripgrep resolution and search, `tar` archive extraction,
|
|
17
|
+
and `where`/`which` binary probing run through `src/process.js`: the existing
|
|
18
|
+
async `runCommand` plus a new synchronous `runCommandSync`.
|
|
19
|
+
|
|
20
|
+
Every launch now applies the same credential-stripped child environment,
|
|
21
|
+
`windowsHide`, bounded timeout, and `-C <repoRoot>`/`cwd` containment in one
|
|
22
|
+
place. The number of modules importing `node:child_process` drops from fifteen
|
|
23
|
+
to four — the two runners plus the long-lived language-server client — removing
|
|
24
|
+
duplicated wrappers and making the whole process surface auditable from a single
|
|
25
|
+
point.
|
|
26
|
+
|
|
27
|
+
This is a pure refactor: graph construction, change impact, Health, history and
|
|
28
|
+
search produce identical results. A byte-level comparison of the entire graph
|
|
29
|
+
(every node, edge and external import, hashed with SHA-256) against the published
|
|
30
|
+
0.3.8 matched exactly on the analytics, controller-rest-api and Weavatrix
|
|
31
|
+
repositories.
|
|
32
|
+
|
|
33
|
+
## Minor
|
|
34
|
+
|
|
35
|
+
Removed two redundant module imports (`node:perf_hooks`, `node:process`) that
|
|
36
|
+
re-imported values already available as Node globals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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>",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"docs/releases/v0.3.5.md",
|
|
42
42
|
"docs/releases/v0.3.6.md",
|
|
43
43
|
"docs/releases/v0.3.7.md",
|
|
44
|
+
"docs/releases/v0.3.8.md",
|
|
45
|
+
"docs/releases/v0.3.9.md",
|
|
44
46
|
"docs/releases/v0.2.19.md",
|
|
45
47
|
"README.md",
|
|
46
48
|
"SECURITY.md",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Symbol-aware git-diff classification for change_impact.
|
|
2
|
-
import {
|
|
3
|
-
import {childProcessEnv} from '../child-env.js'
|
|
2
|
+
import {runGit} from '../git-exec.js'
|
|
4
3
|
import {createPathClassifier} from '../path-classification.js'
|
|
5
4
|
import {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
6
5
|
import {
|
|
@@ -33,13 +32,10 @@ function recoverBoundedDiff(repoRoot, base, files, limits) {
|
|
|
33
32
|
fallbackFiles.push(...files.slice(index))
|
|
34
33
|
break
|
|
35
34
|
}
|
|
36
|
-
const result =
|
|
37
|
-
'
|
|
35
|
+
const result = runGit(repoRoot, [
|
|
36
|
+
'diff', '--no-ext-diff', '--find-renames', '--no-color',
|
|
38
37
|
'--unified=0', String(base), '--', files[index],
|
|
39
|
-
], {
|
|
40
|
-
encoding: 'utf8', windowsHide: true, timeout: Math.min(2_000, remainingMs),
|
|
41
|
-
maxBuffer: remainingBytes + 1, env: childProcessEnv(),
|
|
42
|
-
})
|
|
38
|
+
], {timeout: Math.min(2_000, remainingMs), maxBuffer: remainingBytes + 1})
|
|
43
39
|
const chunk = String(result.stdout || '')
|
|
44
40
|
if (result.status !== 0 || !chunk || Buffer.byteLength(chunk) > remainingBytes) {
|
|
45
41
|
fallbackFiles.push(files[index])
|
|
@@ -51,11 +47,8 @@ function recoverBoundedDiff(repoRoot, base, files, limits) {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
function runGitDiff(repoRoot, base, limits) {
|
|
54
|
-
const args = ['
|
|
55
|
-
const result =
|
|
56
|
-
encoding: 'utf8', windowsHide: true, timeout: 12_000,
|
|
57
|
-
maxBuffer: limits.maxDiffBytes + 1, env: childProcessEnv(),
|
|
58
|
-
})
|
|
50
|
+
const args = ['diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
|
|
51
|
+
const result = runGit(repoRoot, args, {timeout: 12_000, maxBuffer: limits.maxDiffBytes + 1})
|
|
59
52
|
const output = String(result.stdout || '')
|
|
60
53
|
const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(output) > limits.maxDiffBytes
|
|
61
54
|
if (result.status === 0 && !oversized) return {available: true, text: output, error: null}
|
|
@@ -63,12 +56,9 @@ function runGitDiff(repoRoot, base, limits) {
|
|
|
63
56
|
let fallbackTruncated = false
|
|
64
57
|
let recoveredText = ''
|
|
65
58
|
if (oversized) {
|
|
66
|
-
const names =
|
|
67
|
-
'
|
|
68
|
-
], {
|
|
69
|
-
encoding: 'utf8', windowsHide: true, timeout: 12_000,
|
|
70
|
-
maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096), env: childProcessEnv(),
|
|
71
|
-
})
|
|
59
|
+
const names = runGit(repoRoot, [
|
|
60
|
+
'diff', '--name-only', '-z', '--no-ext-diff', '--find-renames', String(base), '--',
|
|
61
|
+
], {timeout: 12_000, maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096)})
|
|
72
62
|
if (names.status === 0) {
|
|
73
63
|
const all = String(names.stdout || '').split('\0').map(normalizeChangePath).filter(Boolean)
|
|
74
64
|
const boundedFiles = [...new Set(all)].sort((a, b) => a.localeCompare(b)).slice(0, limits.maxFiles)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Per-candidate review records for the dead-code review queue, split out of dead-code-review.js so the
|
|
2
|
+
// orchestrator stays within the file budget. The confidence, caveat, evidence and verification model is
|
|
3
|
+
// unchanged; symbolCandidate and fileCandidate are pure over the context built by computeDeadCodeReview.
|
|
4
|
+
import { isFrameworkEntryFile } from "../dead-check.js";
|
|
5
|
+
import {
|
|
6
|
+
hasDynamicCode, REFLECTION_CODE_RE as REFLECTION_RE, normalizedReviewPath as normalizedPath,
|
|
7
|
+
} from "./policy.js";
|
|
8
|
+
|
|
9
|
+
const lineOf = (node) => {
|
|
10
|
+
const match = /@(\d+)$/.exec(String(node?.id || "")) || /L(\d+)/.exec(String(node?.source_location || ""));
|
|
11
|
+
return match ? Number(match[1]) : 0;
|
|
12
|
+
};
|
|
13
|
+
const bareLabel = (value) => String(value || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
|
|
14
|
+
|
|
15
|
+
function kindOf(node) {
|
|
16
|
+
const symbolKind = String(node?.symbol_kind || "").toLowerCase();
|
|
17
|
+
if (symbolKind === "method" || symbolKind === "constructor") return "method";
|
|
18
|
+
if (["function", "function_definition", "func", "fn"].includes(symbolKind)) return "function";
|
|
19
|
+
// Old graphs may not retain symbol_kind. Only use call syntax as a compatibility fallback;
|
|
20
|
+
// member_of alone also describes fields and must never promote them to methods.
|
|
21
|
+
if (!symbolKind && /\([^)]*\)\s*$/.test(String(node?.label || ""))) return node?.member_of ? "method" : "function";
|
|
22
|
+
return "symbol";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPublicSurface(node) {
|
|
26
|
+
const visibility = String(node?.visibility || "").toLowerCase();
|
|
27
|
+
return node?.exported === true || visibility === "public" || visibility === "protected";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function symbolCandidate(item, node, context) {
|
|
31
|
+
const file = normalizedPath(item.file);
|
|
32
|
+
const source = String(context.sources.get(file) || "");
|
|
33
|
+
const pathInfo = context.classify(file, source);
|
|
34
|
+
const publicSurface = isPublicSurface(node);
|
|
35
|
+
const testOnly = item.testOnly === true;
|
|
36
|
+
const externalEntry = context.entrySet.has(file) || isFrameworkEntryFile(file);
|
|
37
|
+
const framework = context.frameworkByFile.get(file) || null;
|
|
38
|
+
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
39
|
+
const reflectionFile = REFLECTION_RE.test(source);
|
|
40
|
+
const kind = kindOf(node);
|
|
41
|
+
const exactNoReference = context.exactNoReferenceIds.has(String(item.id));
|
|
42
|
+
const internallyScoped = String(node?.visibility || "").toLowerCase() === "private"
|
|
43
|
+
|| (!publicSurface && ["method", "function"].includes(kind));
|
|
44
|
+
// Static absence alone is never high-confidence dead code. High is reserved for a successfully
|
|
45
|
+
// queried semantic declaration whose language server also returned no in-workspace references.
|
|
46
|
+
let confidence = internallyScoped && exactNoReference ? "high" : "medium";
|
|
47
|
+
const caveats = [];
|
|
48
|
+
|
|
49
|
+
if (internallyScoped && !exactNoReference) {
|
|
50
|
+
caveats.push("No complete exact semantic no-reference result is available for this declaration; static absence remains medium confidence.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (publicSurface) {
|
|
54
|
+
confidence = "low";
|
|
55
|
+
caveats.push("Public/exported APIs can be consumed by downstream packages, interfaces, reflection, dependency injection, templates, or configuration outside this repository.");
|
|
56
|
+
}
|
|
57
|
+
if (externalEntry || framework) {
|
|
58
|
+
confidence = "low";
|
|
59
|
+
caveats.push(framework?.reason || "This file is an externally entered or framework-owned surface; static inbound edges are not complete usage evidence.");
|
|
60
|
+
}
|
|
61
|
+
if (node?.decorated) {
|
|
62
|
+
confidence = "low";
|
|
63
|
+
caveats.push("Decorators/annotations can register this symbol without a direct caller.");
|
|
64
|
+
}
|
|
65
|
+
if (dynamicFile) {
|
|
66
|
+
confidence = "low";
|
|
67
|
+
caveats.push("The declaring file uses or is reached through dynamic loading, so the static graph can miss callers.");
|
|
68
|
+
}
|
|
69
|
+
if (reflectionFile || (publicSurface && context.repoSignals.reflection)) {
|
|
70
|
+
confidence = "low";
|
|
71
|
+
caveats.push("Reflection is present and may invoke names without a resolvable static edge.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const evidenceTier = confidence === "high" && exactNoReference
|
|
75
|
+
? "STRONG_STATIC_EVIDENCE"
|
|
76
|
+
: confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE";
|
|
77
|
+
const remainingChecks = [
|
|
78
|
+
...(!exactNoReference ? ["Run an exact language-server reference query for this declaration."] : []),
|
|
79
|
+
...(publicSurface ? ["Check downstream/external consumers of the public API."] : []),
|
|
80
|
+
...(externalEntry || framework ? ["Inspect framework registration and externally entered call paths."] : []),
|
|
81
|
+
...(dynamicFile ? ["Resolve dynamic import/require targets and name-based dispatch."] : []),
|
|
82
|
+
...(reflectionFile || (publicSurface && context.repoSignals.reflection) ? ["Inspect reflection/annotation/configuration consumers."] : []),
|
|
83
|
+
"Run targeted tests after any removal.",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
id: String(item.id),
|
|
88
|
+
kind,
|
|
89
|
+
classification: testOnly ? `test-only-${kind}` : publicSurface ? `public-${kind}` : kind === "method" ? "internal-method" : kind === "function" ? "internal-function" : "unreferenced-symbol",
|
|
90
|
+
file,
|
|
91
|
+
line: lineOf(node),
|
|
92
|
+
symbol: bareLabel(node?.label || item.label),
|
|
93
|
+
owner: node?.member_of || null,
|
|
94
|
+
symbolKind: node?.symbol_kind || null,
|
|
95
|
+
visibility: node?.visibility || (node?.exported ? "exported" : "internal"),
|
|
96
|
+
confidence,
|
|
97
|
+
evidenceTier,
|
|
98
|
+
actionability: evidenceTier === "STRONG_STATIC_EVIDENCE" ? "PRIORITY_MANUAL_REVIEW" : "MANUAL_REVIEW",
|
|
99
|
+
reason: item.reason,
|
|
100
|
+
evidence: [
|
|
101
|
+
...(testOnly ? [{kind: item.evidence || "graph", fact: `Only test/e2e consumers were found${item.testConsumerFiles?.length ? `: ${item.testConsumerFiles.join(", ")}` : "."}`}]
|
|
102
|
+
: [{ kind: "graph", fact: "No inbound non-structural graph edge targets this symbol." }, { kind: "source-index", fact: "Its identifier has no second indexed occurrence that establishes a caller." }]),
|
|
103
|
+
...(exactNoReference ? [{kind: "exact-lsp", fact: "The active language server returned no in-workspace references for this exact declaration."}] : []),
|
|
104
|
+
],
|
|
105
|
+
caveats,
|
|
106
|
+
publicApi: publicSurface,
|
|
107
|
+
externallyEnteredFile: externalEntry,
|
|
108
|
+
pathClasses: pathInfo.classes || [],
|
|
109
|
+
matchedPathRule: pathInfo.matchedRule || null,
|
|
110
|
+
verification: {
|
|
111
|
+
graphInboundRuntimeEdge: "NOT_FOUND",
|
|
112
|
+
indexedSecondOccurrence: testOnly ? "TEST_ONLY" : "NOT_FOUND",
|
|
113
|
+
exactLanguageServerReferences: exactNoReference ? "ZERO_CONFIRMED" : "NOT_CHECKED_OR_INCOMPLETE",
|
|
114
|
+
recognizedEntryPoint: externalEntry ? "FOUND" : "NOT_FOUND",
|
|
115
|
+
dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED_IN_DECLARING_FILE",
|
|
116
|
+
reflectionRisk: reflectionFile || (publicSurface && context.repoSignals.reflection) ? "PRESENT" : "NOT_OBSERVED",
|
|
117
|
+
publicApi: publicSurface ? "YES" : "NO",
|
|
118
|
+
decision: "MANUAL_REVIEW_REQUIRED",
|
|
119
|
+
},
|
|
120
|
+
remainingChecks,
|
|
121
|
+
autoDelete: false,
|
|
122
|
+
reviewAction: testOnly
|
|
123
|
+
? "Confirm that no production/config/framework consumer exists; decide whether the declaration is intentional test support or removable with its tests. Never auto-delete."
|
|
124
|
+
: "Confirm with read_source, get_dependents, exact search, framework/config inspection and tests; never auto-delete.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function fileCandidate(item, symbols, context) {
|
|
129
|
+
const file = normalizedPath(item.file);
|
|
130
|
+
const source = String(context.sources.get(file) || "");
|
|
131
|
+
const pathInfo = context.classify(file, source);
|
|
132
|
+
const publicSymbols = symbols.filter((symbol) => symbol.publicApi);
|
|
133
|
+
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
134
|
+
const reflectionFile = REFLECTION_RE.test(source);
|
|
135
|
+
// Whole-file liveness always remains at most medium: external launchers/manifests can exist outside
|
|
136
|
+
// the indexed import graph even when every internal symbol signal is otherwise strong.
|
|
137
|
+
let confidence = "medium";
|
|
138
|
+
const caveats = ["Files can be launched by scripts, plugins, manifests, framework conventions, generated consumers, or external tooling without an import edge."];
|
|
139
|
+
if (publicSymbols.length) {
|
|
140
|
+
confidence = "low";
|
|
141
|
+
caveats.push(`${publicSymbols.length} indexed public/exported symbol(s) may be consumed outside this repository.`);
|
|
142
|
+
}
|
|
143
|
+
if (dynamicFile) {
|
|
144
|
+
confidence = "low";
|
|
145
|
+
caveats.push("Dynamic loading is present in or targets this file.");
|
|
146
|
+
}
|
|
147
|
+
if (reflectionFile) {
|
|
148
|
+
confidence = "low";
|
|
149
|
+
caveats.push("Reflection is present in this file.");
|
|
150
|
+
}
|
|
151
|
+
const remainingChecks = [
|
|
152
|
+
"Inspect package scripts, manifests, framework/plugin discovery and deployment configuration.",
|
|
153
|
+
"Check external launchers and consumers outside the indexed repository.",
|
|
154
|
+
...(dynamicFile ? ["Resolve dynamic import/require targets."] : []),
|
|
155
|
+
...(reflectionFile ? ["Inspect reflection/name-based consumers."] : []),
|
|
156
|
+
"Run targeted tests after any removal.",
|
|
157
|
+
];
|
|
158
|
+
return {
|
|
159
|
+
id: `file:${file}`,
|
|
160
|
+
kind: "file",
|
|
161
|
+
classification: "unreferenced-file",
|
|
162
|
+
file,
|
|
163
|
+
line: 1,
|
|
164
|
+
symbol: null,
|
|
165
|
+
owner: null,
|
|
166
|
+
symbolKind: null,
|
|
167
|
+
visibility: null,
|
|
168
|
+
confidence,
|
|
169
|
+
evidenceTier: confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE",
|
|
170
|
+
actionability: "MANUAL_REVIEW",
|
|
171
|
+
reason: item.reason,
|
|
172
|
+
evidence: [
|
|
173
|
+
{ kind: "graph", fact: "No indexed module imports this file." },
|
|
174
|
+
{ kind: "symbol-liveness", fact: "Every indexed symbol in the file is statically unreferenced." },
|
|
175
|
+
],
|
|
176
|
+
caveats,
|
|
177
|
+
publicApi: publicSymbols.length > 0,
|
|
178
|
+
externallyEnteredFile: false,
|
|
179
|
+
pathClasses: pathInfo.classes || [],
|
|
180
|
+
matchedPathRule: pathInfo.matchedRule || null,
|
|
181
|
+
verification: {
|
|
182
|
+
graphInboundModuleEdge: "NOT_FOUND",
|
|
183
|
+
indexedSymbolsReferenced: "NONE",
|
|
184
|
+
recognizedEntryPoint: "NOT_FOUND",
|
|
185
|
+
dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED",
|
|
186
|
+
reflectionRisk: reflectionFile ? "PRESENT" : "NOT_OBSERVED",
|
|
187
|
+
externalConsumerCheck: "NOT_POSSIBLE_FROM_REPOSITORY_GRAPH",
|
|
188
|
+
decision: "MANUAL_REVIEW_REQUIRED",
|
|
189
|
+
},
|
|
190
|
+
remainingChecks,
|
|
191
|
+
autoDelete: false,
|
|
192
|
+
reviewAction: "Verify package scripts, manifests, framework discovery, dynamic loading and external consumers; never auto-delete.",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -1,200 +1,16 @@
|
|
|
1
1
|
// Conservative, source-free review queue for statically unreferenced code. This deliberately builds
|
|
2
2
|
// on computeDead instead of inventing a second liveness model. Candidates are evidence for review,
|
|
3
3
|
// never deletion instructions: framework entry, dynamic loading, reflection and public API surfaces
|
|
4
|
-
// lower confidence and remain explicit in the returned record.
|
|
5
|
-
|
|
4
|
+
// lower confidence and remain explicit in the returned record. Per-candidate record shapes live in
|
|
5
|
+
// dead-code-review/candidates.js; this module owns collection, filtering and the summary envelope.
|
|
6
|
+
import { computeDead } from "./dead-check.js";
|
|
6
7
|
import { createPathClassifier } from "../path-classification.js";
|
|
7
8
|
import {
|
|
8
9
|
DEAD_CODE_CONFIDENCE_RANK as CONFIDENCE_RANK, hasDynamicCode,
|
|
9
10
|
REFLECTION_CODE_RE as REFLECTION_RE, deadCodePathAllowed as pathAllowed,
|
|
10
11
|
normalizedReviewPath as normalizedPath,
|
|
11
12
|
} from './dead-code-review/policy.js'
|
|
12
|
-
|
|
13
|
-
const match = /@(\d+)$/.exec(String(node?.id || "")) || /L(\d+)/.exec(String(node?.source_location || ""));
|
|
14
|
-
return match ? Number(match[1]) : 0;
|
|
15
|
-
};
|
|
16
|
-
const bareLabel = (value) => String(value || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
|
|
17
|
-
|
|
18
|
-
function kindOf(node) {
|
|
19
|
-
const symbolKind = String(node?.symbol_kind || "").toLowerCase();
|
|
20
|
-
if (symbolKind === "method" || symbolKind === "constructor") return "method";
|
|
21
|
-
if (["function", "function_definition", "func", "fn"].includes(symbolKind)) return "function";
|
|
22
|
-
// Old graphs may not retain symbol_kind. Only use call syntax as a compatibility fallback;
|
|
23
|
-
// member_of alone also describes fields and must never promote them to methods.
|
|
24
|
-
if (!symbolKind && /\([^)]*\)\s*$/.test(String(node?.label || ""))) return node?.member_of ? "method" : "function";
|
|
25
|
-
return "symbol";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isPublicSurface(node) {
|
|
29
|
-
const visibility = String(node?.visibility || "").toLowerCase();
|
|
30
|
-
return node?.exported === true || visibility === "public" || visibility === "protected";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function symbolCandidate(item, node, context) {
|
|
34
|
-
const file = normalizedPath(item.file);
|
|
35
|
-
const source = String(context.sources.get(file) || "");
|
|
36
|
-
const pathInfo = context.classify(file, source);
|
|
37
|
-
const publicSurface = isPublicSurface(node);
|
|
38
|
-
const testOnly = item.testOnly === true;
|
|
39
|
-
const externalEntry = context.entrySet.has(file) || isFrameworkEntryFile(file);
|
|
40
|
-
const framework = context.frameworkByFile.get(file) || null;
|
|
41
|
-
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
42
|
-
const reflectionFile = REFLECTION_RE.test(source);
|
|
43
|
-
const kind = kindOf(node);
|
|
44
|
-
const exactNoReference = context.exactNoReferenceIds.has(String(item.id));
|
|
45
|
-
const internallyScoped = String(node?.visibility || "").toLowerCase() === "private"
|
|
46
|
-
|| (!publicSurface && ["method", "function"].includes(kind));
|
|
47
|
-
// Static absence alone is never high-confidence dead code. High is reserved for a successfully
|
|
48
|
-
// queried semantic declaration whose language server also returned no in-workspace references.
|
|
49
|
-
let confidence = internallyScoped && exactNoReference ? "high" : "medium";
|
|
50
|
-
const caveats = [];
|
|
51
|
-
|
|
52
|
-
if (internallyScoped && !exactNoReference) {
|
|
53
|
-
caveats.push("No complete exact semantic no-reference result is available for this declaration; static absence remains medium confidence.");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (publicSurface) {
|
|
57
|
-
confidence = "low";
|
|
58
|
-
caveats.push("Public/exported APIs can be consumed by downstream packages, interfaces, reflection, dependency injection, templates, or configuration outside this repository.");
|
|
59
|
-
}
|
|
60
|
-
if (externalEntry || framework) {
|
|
61
|
-
confidence = "low";
|
|
62
|
-
caveats.push(framework?.reason || "This file is an externally entered or framework-owned surface; static inbound edges are not complete usage evidence.");
|
|
63
|
-
}
|
|
64
|
-
if (node?.decorated) {
|
|
65
|
-
confidence = "low";
|
|
66
|
-
caveats.push("Decorators/annotations can register this symbol without a direct caller.");
|
|
67
|
-
}
|
|
68
|
-
if (dynamicFile) {
|
|
69
|
-
confidence = "low";
|
|
70
|
-
caveats.push("The declaring file uses or is reached through dynamic loading, so the static graph can miss callers.");
|
|
71
|
-
}
|
|
72
|
-
if (reflectionFile || (publicSurface && context.repoSignals.reflection)) {
|
|
73
|
-
confidence = "low";
|
|
74
|
-
caveats.push("Reflection is present and may invoke names without a resolvable static edge.");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const evidenceTier = confidence === "high" && exactNoReference
|
|
78
|
-
? "STRONG_STATIC_EVIDENCE"
|
|
79
|
-
: confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE";
|
|
80
|
-
const remainingChecks = [
|
|
81
|
-
...(!exactNoReference ? ["Run an exact language-server reference query for this declaration."] : []),
|
|
82
|
-
...(publicSurface ? ["Check downstream/external consumers of the public API."] : []),
|
|
83
|
-
...(externalEntry || framework ? ["Inspect framework registration and externally entered call paths."] : []),
|
|
84
|
-
...(dynamicFile ? ["Resolve dynamic import/require targets and name-based dispatch."] : []),
|
|
85
|
-
...(reflectionFile || (publicSurface && context.repoSignals.reflection) ? ["Inspect reflection/annotation/configuration consumers."] : []),
|
|
86
|
-
"Run targeted tests after any removal.",
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
id: String(item.id),
|
|
91
|
-
kind,
|
|
92
|
-
classification: testOnly ? `test-only-${kind}` : publicSurface ? `public-${kind}` : kind === "method" ? "internal-method" : kind === "function" ? "internal-function" : "unreferenced-symbol",
|
|
93
|
-
file,
|
|
94
|
-
line: lineOf(node),
|
|
95
|
-
symbol: bareLabel(node?.label || item.label),
|
|
96
|
-
owner: node?.member_of || null,
|
|
97
|
-
symbolKind: node?.symbol_kind || null,
|
|
98
|
-
visibility: node?.visibility || (node?.exported ? "exported" : "internal"),
|
|
99
|
-
confidence,
|
|
100
|
-
evidenceTier,
|
|
101
|
-
actionability: evidenceTier === "STRONG_STATIC_EVIDENCE" ? "PRIORITY_MANUAL_REVIEW" : "MANUAL_REVIEW",
|
|
102
|
-
reason: item.reason,
|
|
103
|
-
evidence: [
|
|
104
|
-
...(testOnly ? [{kind: item.evidence || "graph", fact: `Only test/e2e consumers were found${item.testConsumerFiles?.length ? `: ${item.testConsumerFiles.join(", ")}` : "."}`}]
|
|
105
|
-
: [{ kind: "graph", fact: "No inbound non-structural graph edge targets this symbol." }, { kind: "source-index", fact: "Its identifier has no second indexed occurrence that establishes a caller." }]),
|
|
106
|
-
...(exactNoReference ? [{kind: "exact-lsp", fact: "The active language server returned no in-workspace references for this exact declaration."}] : []),
|
|
107
|
-
],
|
|
108
|
-
caveats,
|
|
109
|
-
publicApi: publicSurface,
|
|
110
|
-
externallyEnteredFile: externalEntry,
|
|
111
|
-
pathClasses: pathInfo.classes || [],
|
|
112
|
-
matchedPathRule: pathInfo.matchedRule || null,
|
|
113
|
-
verification: {
|
|
114
|
-
graphInboundRuntimeEdge: "NOT_FOUND",
|
|
115
|
-
indexedSecondOccurrence: testOnly ? "TEST_ONLY" : "NOT_FOUND",
|
|
116
|
-
exactLanguageServerReferences: exactNoReference ? "ZERO_CONFIRMED" : "NOT_CHECKED_OR_INCOMPLETE",
|
|
117
|
-
recognizedEntryPoint: externalEntry ? "FOUND" : "NOT_FOUND",
|
|
118
|
-
dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED_IN_DECLARING_FILE",
|
|
119
|
-
reflectionRisk: reflectionFile || (publicSurface && context.repoSignals.reflection) ? "PRESENT" : "NOT_OBSERVED",
|
|
120
|
-
publicApi: publicSurface ? "YES" : "NO",
|
|
121
|
-
decision: "MANUAL_REVIEW_REQUIRED",
|
|
122
|
-
},
|
|
123
|
-
remainingChecks,
|
|
124
|
-
autoDelete: false,
|
|
125
|
-
reviewAction: testOnly
|
|
126
|
-
? "Confirm that no production/config/framework consumer exists; decide whether the declaration is intentional test support or removable with its tests. Never auto-delete."
|
|
127
|
-
: "Confirm with read_source, get_dependents, exact search, framework/config inspection and tests; never auto-delete.",
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function fileCandidate(item, symbols, context) {
|
|
132
|
-
const file = normalizedPath(item.file);
|
|
133
|
-
const source = String(context.sources.get(file) || "");
|
|
134
|
-
const pathInfo = context.classify(file, source);
|
|
135
|
-
const publicSymbols = symbols.filter((symbol) => symbol.publicApi);
|
|
136
|
-
const dynamicFile = context.dynamicTargets.has(file) || hasDynamicCode(source, file);
|
|
137
|
-
const reflectionFile = REFLECTION_RE.test(source);
|
|
138
|
-
// Whole-file liveness always remains at most medium: external launchers/manifests can exist outside
|
|
139
|
-
// the indexed import graph even when every internal symbol signal is otherwise strong.
|
|
140
|
-
let confidence = "medium";
|
|
141
|
-
const caveats = ["Files can be launched by scripts, plugins, manifests, framework conventions, generated consumers, or external tooling without an import edge."];
|
|
142
|
-
if (publicSymbols.length) {
|
|
143
|
-
confidence = "low";
|
|
144
|
-
caveats.push(`${publicSymbols.length} indexed public/exported symbol(s) may be consumed outside this repository.`);
|
|
145
|
-
}
|
|
146
|
-
if (dynamicFile) {
|
|
147
|
-
confidence = "low";
|
|
148
|
-
caveats.push("Dynamic loading is present in or targets this file.");
|
|
149
|
-
}
|
|
150
|
-
if (reflectionFile) {
|
|
151
|
-
confidence = "low";
|
|
152
|
-
caveats.push("Reflection is present in this file.");
|
|
153
|
-
}
|
|
154
|
-
const remainingChecks = [
|
|
155
|
-
"Inspect package scripts, manifests, framework/plugin discovery and deployment configuration.",
|
|
156
|
-
"Check external launchers and consumers outside the indexed repository.",
|
|
157
|
-
...(dynamicFile ? ["Resolve dynamic import/require targets."] : []),
|
|
158
|
-
...(reflectionFile ? ["Inspect reflection/name-based consumers."] : []),
|
|
159
|
-
"Run targeted tests after any removal.",
|
|
160
|
-
];
|
|
161
|
-
return {
|
|
162
|
-
id: `file:${file}`,
|
|
163
|
-
kind: "file",
|
|
164
|
-
classification: "unreferenced-file",
|
|
165
|
-
file,
|
|
166
|
-
line: 1,
|
|
167
|
-
symbol: null,
|
|
168
|
-
owner: null,
|
|
169
|
-
symbolKind: null,
|
|
170
|
-
visibility: null,
|
|
171
|
-
confidence,
|
|
172
|
-
evidenceTier: confidence === "low" ? "HIGH_UNCERTAINTY" : "BOUNDED_STATIC_EVIDENCE",
|
|
173
|
-
actionability: "MANUAL_REVIEW",
|
|
174
|
-
reason: item.reason,
|
|
175
|
-
evidence: [
|
|
176
|
-
{ kind: "graph", fact: "No indexed module imports this file." },
|
|
177
|
-
{ kind: "symbol-liveness", fact: "Every indexed symbol in the file is statically unreferenced." },
|
|
178
|
-
],
|
|
179
|
-
caveats,
|
|
180
|
-
publicApi: publicSymbols.length > 0,
|
|
181
|
-
externallyEnteredFile: false,
|
|
182
|
-
pathClasses: pathInfo.classes || [],
|
|
183
|
-
matchedPathRule: pathInfo.matchedRule || null,
|
|
184
|
-
verification: {
|
|
185
|
-
graphInboundModuleEdge: "NOT_FOUND",
|
|
186
|
-
indexedSymbolsReferenced: "NONE",
|
|
187
|
-
recognizedEntryPoint: "NOT_FOUND",
|
|
188
|
-
dynamicLoadingRisk: dynamicFile ? "PRESENT" : "NOT_OBSERVED",
|
|
189
|
-
reflectionRisk: reflectionFile ? "PRESENT" : "NOT_OBSERVED",
|
|
190
|
-
externalConsumerCheck: "NOT_POSSIBLE_FROM_REPOSITORY_GRAPH",
|
|
191
|
-
decision: "MANUAL_REVIEW_REQUIRED",
|
|
192
|
-
},
|
|
193
|
-
remainingChecks,
|
|
194
|
-
autoDelete: false,
|
|
195
|
-
reviewAction: "Verify package scripts, manifests, framework discovery, dynamic loading and external consumers; never auto-delete.",
|
|
196
|
-
};
|
|
197
|
-
}
|
|
13
|
+
import { symbolCandidate, fileCandidate } from './dead-code-review/candidates.js'
|
|
198
14
|
|
|
199
15
|
// Pure review model. Filesystem collection/entry inference stays in the MCP adapter so tests can pass
|
|
200
16
|
// exact source maps and convention evidence without touching the working tree.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {spawn} from 'node:child_process'
|
|
2
1
|
import {isWeavatrixIgnored} from '../../path-ignore.js'
|
|
3
2
|
import {
|
|
4
3
|
boundedHistoryInteger,
|
|
@@ -10,48 +9,7 @@ import {
|
|
|
10
9
|
const HEADER_SEPARATOR = '\x1e'
|
|
11
10
|
const FIELD_SEPARATOR = '\x1f'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn(command, args, {
|
|
16
|
-
cwd: options.cwd, env: options.env, shell: false, windowsHide: true,
|
|
17
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
|
-
})
|
|
19
|
-
const stdout = [], stderr = []
|
|
20
|
-
let stdoutBytes = 0, stderrBytes = 0, truncated = false, timedOut = false, settled = false
|
|
21
|
-
const finish = (callback) => {
|
|
22
|
-
if (settled) return
|
|
23
|
-
settled = true
|
|
24
|
-
clearTimeout(timer)
|
|
25
|
-
callback()
|
|
26
|
-
}
|
|
27
|
-
const stop = () => { try { child.kill('SIGKILL') } catch { /* process already exited */ } }
|
|
28
|
-
const timer = setTimeout(() => { timedOut = true; stop() }, options.timeoutMs)
|
|
29
|
-
child.stdout?.on('data', (chunk) => {
|
|
30
|
-
if (truncated) return
|
|
31
|
-
const remaining = options.maxOutputBytes - stdoutBytes
|
|
32
|
-
if (remaining <= 0) { truncated = true; stop(); return }
|
|
33
|
-
const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining)
|
|
34
|
-
stdout.push(kept); stdoutBytes += kept.length
|
|
35
|
-
if (kept.length !== chunk.length) { truncated = true; stop() }
|
|
36
|
-
})
|
|
37
|
-
child.stderr?.on('data', (chunk) => {
|
|
38
|
-
const remaining = 64 * 1024 - stderrBytes
|
|
39
|
-
if (remaining <= 0) return
|
|
40
|
-
const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining)
|
|
41
|
-
stderr.push(kept); stderrBytes += kept.length
|
|
42
|
-
})
|
|
43
|
-
child.on('error', (error) => finish(() => reject(error)))
|
|
44
|
-
child.on('close', (exitCode) => finish(() => {
|
|
45
|
-
if (timedOut) return reject(new Error('git history collection timed out'))
|
|
46
|
-
resolve({
|
|
47
|
-
stdout: Buffer.concat(stdout),
|
|
48
|
-
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
49
|
-
exitCode: Number(exitCode ?? 1),
|
|
50
|
-
truncated,
|
|
51
|
-
})
|
|
52
|
-
}))
|
|
53
|
-
})
|
|
54
|
-
}
|
|
12
|
+
// git subprocess execution (sync runGit + streaming boundedGitCommand) lives in ../../git-exec.js.
|
|
55
13
|
|
|
56
14
|
const statNumber = (value) => value === '-'
|
|
57
15
|
? {value: 0, binary: true}
|
|
@@ -3,7 +3,8 @@ import {childProcessEnv} from '../child-env.js'
|
|
|
3
3
|
import {createRepoBoundary} from '../repo-path.js'
|
|
4
4
|
import {loadWeavatrixIgnore} from '../path-ignore.js'
|
|
5
5
|
import {buildGitHistoryAnalytics} from './git-history/analytics.js'
|
|
6
|
-
import {
|
|
6
|
+
import {parseGitNumstatLog} from './git-history/collector.js'
|
|
7
|
+
import {boundedGitCommand} from '../git-exec.js'
|
|
7
8
|
import {
|
|
8
9
|
boundedHistoryInteger,
|
|
9
10
|
GIT_FORMAT,
|
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
import {mkdtempSync, mkdirSync, rmSync} from 'node:fs'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
6
|
import {tmpdir} from 'node:os'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import {runGit} from '../git-exec.js'
|
|
8
|
+
import {runCommandSync} from '../process.js'
|
|
9
9
|
import {buildInternalGraph} from '../graph/internal-builder.js'
|
|
10
10
|
import {filterGraphForMode} from '../graph/graph-filter.js'
|
|
11
11
|
|
|
12
12
|
const SAFE_REF = /^(?!-)[A-Za-z0-9][A-Za-z0-9._\/@{}+~^-]{0,199}$/
|
|
13
13
|
|
|
14
14
|
function git(repoRoot, args, timeout = 15_000) {
|
|
15
|
-
return
|
|
16
|
-
encoding: 'utf8', timeout, env: childProcessEnv(), windowsHide: true,
|
|
17
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
18
|
-
})
|
|
15
|
+
return runGit(repoRoot, args, {timeout, maxBuffer: 4 * 1024 * 1024})
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export function resolveGitCommit(repoRoot, requestedRef) {
|
|
@@ -41,10 +38,7 @@ export async function withGitRefCheckout(repoRoot, requestedRef, operation) {
|
|
|
41
38
|
if (archived.status !== 0) {
|
|
42
39
|
return {ok: false, error: `git archive failed for ${resolved.ref}: ${String(archived.stderr || '').trim() || 'unknown error'}`}
|
|
43
40
|
}
|
|
44
|
-
const extracted =
|
|
45
|
-
encoding: 'utf8', timeout: 60_000, env: childProcessEnv(), windowsHide: true,
|
|
46
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
47
|
-
})
|
|
41
|
+
const extracted = runCommandSync('tar', ['-xf', archive, '-C', checkout], {timeout: 60_000, maxBuffer: 4 * 1024 * 1024})
|
|
48
42
|
if (extracted.status !== 0) {
|
|
49
43
|
return {ok: false, error: `temporary Git archive extraction failed: ${String(extracted.stderr || '').trim() || 'tar unavailable'}`}
|
|
50
44
|
}
|