weavatrix 0.1.3 → 0.1.4
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 +32 -12
- package/package.json +1 -1
- package/skill/SKILL.md +20 -13
- package/src/analysis/dead-check.js +8 -3
- package/src/analysis/dep-check-ecosystems.js +157 -0
- package/src/analysis/dep-check.js +32 -144
- package/src/analysis/dep-rules.js +36 -23
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +11 -4
- package/src/analysis/graph-analysis.aggregate.js +27 -26
- package/src/analysis/graph-analysis.edges.js +21 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/internal-audit.collect.js +40 -0
- package/src/analysis/internal-audit.run.js +9 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +17 -0
- package/src/graph/internal-builder.build.js +32 -8
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/relations.js +4 -0
- package/src/mcp/catalog.mjs +4 -4
- package/src/mcp/graph-context.mjs +7 -166
- package/src/mcp/graph-diff.mjs +163 -0
- package/src/mcp/sync-payload.mjs +30 -8
- package/src/mcp/tools-actions.mjs +3 -3
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +11 -54
- package/src/mcp/tools-health.mjs +18 -6
- package/src/mcp/tools-impact.mjs +11 -6
- package/src/mcp-server.mjs +2 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ claude mcp add -s user weavatrix -- node <path-to>/weavatrix/bin/weavatrix-mcp.m
|
|
|
79
79
|
|
|
80
80
|
No graph yet? Ask the agent to call `rebuild_graph`; it builds the missing graph locally.
|
|
81
81
|
`open_repo` can change the active repository and builds a missing graph automatically. A normal
|
|
82
|
-
`open_repo` call also upgrades graphs created before `0.1.
|
|
82
|
+
`open_repo` call also upgrades graphs created before `0.1.4` to edge metadata v2; `build:false`
|
|
83
83
|
probes without building and refuses a legacy graph. Retargeting is offline but intentionally changes
|
|
84
84
|
the filesystem boundary for subsequent tools; omit `retarget` from an explicit capability list when
|
|
85
85
|
a registration must stay pinned to one repository.
|
|
@@ -91,15 +91,16 @@ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — instal
|
|
|
91
91
|
|
|
92
92
|
**graph** — `graph_stats`, `get_node`, `get_neighbors`, `query_graph`, `god_nodes`,
|
|
93
93
|
`shortest_path`, `get_community`, `list_communities`, `module_map`, `get_dependents`,
|
|
94
|
-
`change_impact`, `graph_diff`. Runtime dependencies
|
|
95
|
-
separately where that distinction
|
|
94
|
+
`change_impact`, `graph_diff`. Runtime dependencies, TypeScript type-only coupling and language
|
|
95
|
+
compile-only edges (Rust module/use, Java imports) are reported separately where that distinction
|
|
96
|
+
changes the result.
|
|
96
97
|
|
|
97
98
|
**search / source** — `search_code` (ripgrep-backed, pure-Node fallback), `read_source` (a
|
|
98
99
|
symbol's actual code in one hop), `list_endpoints` (HTTP route inventory:
|
|
99
|
-
Express/Fastify/Nest/Flask/FastAPI/Go mux …)
|
|
100
|
+
Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web …)
|
|
100
101
|
|
|
101
102
|
**health** — `run_audit` (dead code, unused exports, missing/unused npm/Go/Python deps, runtime
|
|
102
|
-
cycles, type-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
|
|
103
|
+
cycles, type-only/compile-only coupling, orphans, boundary rules, offline OSV vulnerabilities + typosquat +
|
|
103
104
|
lockfile drift), `find_duplicates` (MOSS winnowing over method bodies — catches copy-paste even
|
|
104
105
|
after renames), `coverage_map` (existing coverage reports mapped onto the graph; untested hotspots
|
|
105
106
|
ranked by connectivity — tests are never executed)
|
|
@@ -113,12 +114,14 @@ changes the active repository boundary
|
|
|
113
114
|
**online** *(explicit opt-in — see Privacy)* — `refresh_advisories`, `sync_graph`
|
|
114
115
|
|
|
115
116
|
Quality of life: graph tools self-report staleness vs the repo HEAD; ambiguous name lookups are
|
|
116
|
-
disclosed instead of silently guessed; and the server **hot-reloads its
|
|
117
|
-
|
|
117
|
+
disclosed instead of silently guessed; and the server **hot-reloads its watched MCP tool entry
|
|
118
|
+
modules and catalog** when those files change — other MCP helpers and analysis engines require a
|
|
119
|
+
reconnect.
|
|
118
120
|
|
|
119
121
|
## Signal quality and repository configuration
|
|
120
122
|
|
|
121
|
-
Weavatrix `0.1.
|
|
123
|
+
Weavatrix `0.1.4` reduces the most common sources of static-analysis noise while deepening Rust and
|
|
124
|
+
Java graphs:
|
|
122
125
|
|
|
123
126
|
- In Git repositories, graph and clone scans use tracked plus non-ignored untracked files, so
|
|
124
127
|
`.gitignore`-excluded build outputs such as packaged applications do not dominate findings.
|
|
@@ -126,9 +129,22 @@ Weavatrix `0.1.3` reduces the most common sources of static-analysis noise:
|
|
|
126
129
|
not inflate runtime-cycle severity. `module_map`, `change_impact` and structural diffs preserve
|
|
127
130
|
that distinction. `god_nodes` ranks unique neighbors with runtime connectivity first and reports
|
|
128
131
|
repeated occurrences separately.
|
|
132
|
+
- Rust `mod`, `use` and `pub use` paths now resolve between files and modules. They are marked
|
|
133
|
+
compile-only, so they enrich `module_map` and compile-time coupling without inventing runtime
|
|
134
|
+
initialization cycles or promoting compile-time coupling to runtime impact. Axum and actix-web
|
|
135
|
+
routes are included in `list_endpoints`.
|
|
136
|
+
- Java class/interface/enum/record/annotation declarations retain their symbol kind; methods and
|
|
137
|
+
constructors are linked to their declaring type with visibility metadata. Internal
|
|
138
|
+
`extends`/`implements` relationships and resolvable type references link to real declarations.
|
|
139
|
+
Imports are compile-only; call/reference/heritage edges contribute impact. Maven/Gradle Java
|
|
140
|
+
trees use package-aware communities instead of one giant `src` bucket. External or synthetic
|
|
141
|
+
placeholder types are not created merely to inflate graph counts.
|
|
129
142
|
- Dependency checks resolve the nearest workspace manifest and `tsconfig`/`jsconfig` aliases,
|
|
130
143
|
account for framework-owned runtime peers such as Next.js + `react-dom`, and recognize Next.js
|
|
131
144
|
App Router route exports as endpoints.
|
|
145
|
+
- Generated NAPI-RS platform loaders and declared template/example catalogs no longer create
|
|
146
|
+
phantom runtime dependency, orphan or unused-export findings. Conventional template roots are
|
|
147
|
+
inferred conservatively; custom roots can be declared explicitly.
|
|
132
148
|
- `coverage_map` reports coverage as **unavailable** when no supported report exists. That means
|
|
133
149
|
“no data”, not zero coverage.
|
|
134
150
|
- Duplicate output is a review queue, not a verdict: near-identical bodies are clone candidates;
|
|
@@ -149,6 +165,7 @@ repository root:
|
|
|
149
165
|
```json
|
|
150
166
|
{
|
|
151
167
|
"entrypoints": ["scripts/publish-release.mjs"],
|
|
168
|
+
"nonRuntimeRoots": ["library", "catalogs/examples"],
|
|
152
169
|
"python": {
|
|
153
170
|
"managedDependencies": ["numpy", "openvino-genai"],
|
|
154
171
|
"ignoreDependencies": ["vendor-sdk"]
|
|
@@ -157,6 +174,9 @@ repository root:
|
|
|
157
174
|
```
|
|
158
175
|
|
|
159
176
|
`entrypoints` protects framework/script entry files from dead-code classification.
|
|
177
|
+
`nonRuntimeRoots` (alias: `templateRoots`) marks reusable examples/templates that are not deployed
|
|
178
|
+
as one application. It suppresses orphan/dead/unused-export noise and missing/unresolved dependency
|
|
179
|
+
findings when every use is inside those roots. Import edges, cycles and boundary checks remain visible.
|
|
160
180
|
`managedDependencies` declares Python modules supplied by an external runtime;
|
|
161
181
|
`ignoreDependencies` suppresses intentionally unresolved Python packages. Keep the lists narrow:
|
|
162
182
|
they change audit interpretation, not the repository or its dependency installation.
|
|
@@ -175,7 +195,7 @@ tools; both require the explicit `online` group and a tool call:
|
|
|
175
195
|
symbol names and line ranges, import/dependency identifiers, edges and numeric metrics. Unknown
|
|
176
196
|
fields are discarded; source file bodies are never read for sync or included in the payload. The
|
|
177
197
|
endpoint is **yours**, configured via `WEAVATRIX_SYNC_URL` / `WEAVATRIX_SYNC_TOKEN`. Off by default.
|
|
178
|
-
Sync payload v2 preserves type-only edge metadata. Graphs built before `0.1.
|
|
198
|
+
Sync payload v2 preserves type-only and compile-only edge metadata. Graphs built before `0.1.4` must be rebuilt
|
|
179
199
|
once before syncing; a normal `open_repo` call performs that upgrade automatically.
|
|
180
200
|
|
|
181
201
|
If `refresh_advisories` is not listed by the MCP client, that is the expected default: the
|
|
@@ -197,7 +217,7 @@ vulnerability findings. This is where each capability comes from and how it is c
|
|
|
197
217
|
|---|---|---|
|
|
198
218
|
| Network access | `refresh_advisories` sends pinned package names and versions to OSV; `sync_graph` sends a versioned allowlist of graph metadata (relative paths, symbols and line ranges, import/dependency identifiers, edges and numeric metrics). It discards unknown graph fields and does not read source file bodies | `online` is disabled by default; each request requires a tool call, and sync additionally requires `WEAVATRIX_SYNC_URL` |
|
|
199
219
|
| Shell access | Local `git` powers staleness/change impact; `rg` accelerates search; timed-out Windows child processes may be terminated | Used only by the corresponding local operation; it does not imply network access |
|
|
200
|
-
| Debug / dynamic loading | Cache-busted `import()` hot-reloads
|
|
220
|
+
| Debug / dynamic loading | Cache-busted `import()` hot-reloads watched MCP tool entry modules; `createRequire` loads package metadata and parser dependencies | Loads files from the installed package; no `eval` |
|
|
201
221
|
| Environment access | Reads `WEAVATRIX_*` configuration; local child processes inherit the normal host environment | `WEAVATRIX_SYNC_TOKEN` is removed from every child-process and worker environment and read only by `sync_graph` |
|
|
202
222
|
| Filesystem access | Reads the active repository, graph, lockfiles and coverage reports; writes derived graphs and advisory cache | Realpath containment blocks traversal and symlink/junction escapes. `open_repo` is an explicit offline call that changes the active boundary; omit `retarget` in a custom capability list to pin one repository. The optional malware dependency scan may inspect installed dependency caches such as GOPATH |
|
|
203
223
|
| URL strings | Fixed OSV/documentation URLs plus a user-configured sync URL | A URL string causes no request by itself; only the two `online` tools perform requests |
|
|
@@ -226,8 +246,8 @@ npm test # node --test
|
|
|
226
246
|
|
|
227
247
|
Design rule: **no source file exceeds 300 lines.** Larger concerns split into dotted-suffix modules
|
|
228
248
|
behind a slim facade (`foo.js` re-exports `foo.parse.js`, `foo.report.js`, …); the MCP layer lives
|
|
229
|
-
in `src/mcp/` (graph context,
|
|
230
|
-
entry `src/mcp-server.mjs`.
|
|
249
|
+
in `src/mcp/` (graph context, tool entry modules, focused helpers, and the catalog/hot-reload
|
|
250
|
+
loader) behind the thin stdio entry `src/mcp-server.mjs`.
|
|
231
251
|
|
|
232
252
|
## Roadmap
|
|
233
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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@edgehawk.io>",
|
package/skill/SKILL.md
CHANGED
|
@@ -25,13 +25,14 @@ If only `refresh_advisories` or `sync_graph` is missing, do not diagnose a broke
|
|
|
25
25
|
code. A same-name/different-body pair is a divergence candidate, not proof of duplication.
|
|
26
26
|
- **Freshness**: every graph tool appends a staleness warning when the repo has commits newer than
|
|
27
27
|
the graph. Act on it: `rebuild_graph`. A normal `open_repo` builds missing graphs and upgrades
|
|
28
|
-
pre-`0.1.
|
|
28
|
+
pre-`0.1.4` graphs to edge metadata v2; `build:false` deliberately refuses that upgrade.
|
|
29
29
|
- **Ambiguity**: `get_node`/`get_neighbors`/`get_dependents` disclose `matched N nodes; using the
|
|
30
30
|
best-connected` — read that note before trusting the answer; pass an exact node id to pin it.
|
|
31
|
-
- **Runtime versus compile time**: keep runtime cycles
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
- **Runtime versus compile time**: keep runtime cycles separate from TypeScript type-only and
|
|
32
|
+
language compile-only coupling (Rust `mod`/`use`/`pub use`, Java imports). `module_map`,
|
|
33
|
+
`change_impact` and `graph_diff` label the distinction; `god_nodes` ranks unique connectivity and
|
|
34
|
+
reports repeated references separately. Do not schedule a runtime-cycle refactor from
|
|
35
|
+
compile-time-only edges.
|
|
35
36
|
- **Repository universe**: in Git repositories, graph and duplicate scans include tracked plus
|
|
36
37
|
non-ignored untracked files. If an old graph still contains packaged/generated output, rebuild it
|
|
37
38
|
before interpreting the result.
|
|
@@ -69,7 +70,7 @@ If only `refresh_advisories` or `sync_graph` is missing, do not diagnose a broke
|
|
|
69
70
|
`graph_diff` (optionally scoped by `path`). The semantic complement to the textual git diff.
|
|
70
71
|
- **Health sweep**: `run_audit` (filter by `category`/`min_severity`) → `find_duplicates` →
|
|
71
72
|
`coverage_map`. Inspect the source behind shortlisted findings before proposing edits.
|
|
72
|
-
- **API inventory**: `list_endpoints
|
|
73
|
+
- **API inventory**: `list_endpoints` (including Next.js App Router, Rust axum and actix-web).
|
|
73
74
|
- **Find code**: `search_code` (regex + glob) → `get_node` → `read_source`.
|
|
74
75
|
- **Another repo**: `list_known_repos` → `open_repo <path>`
|
|
75
76
|
(builds or upgrades the graph when needed; `build:false` probes without building).
|
|
@@ -77,13 +78,15 @@ If only `refresh_advisories` or `sync_graph` is missing, do not diagnose a broke
|
|
|
77
78
|
## Repository-specific conventions
|
|
78
79
|
|
|
79
80
|
Weavatrix understands nearest workspace manifests, nested `tsconfig`/`jsconfig` aliases,
|
|
80
|
-
framework-owned runtime peers such as Next.js + `react-dom`,
|
|
81
|
-
For project-specific entry points
|
|
82
|
-
`.weavatrix-deps.json` at the repository
|
|
81
|
+
framework-owned runtime peers such as Next.js + `react-dom`, generated NAPI-RS platform loaders,
|
|
82
|
+
and Next.js App Router route exports. For project-specific entry points, reusable template catalogs,
|
|
83
|
+
or Python dependencies supplied by an external runtime, add `.weavatrix-deps.json` at the repository
|
|
84
|
+
root:
|
|
83
85
|
|
|
84
86
|
```json
|
|
85
87
|
{
|
|
86
88
|
"entrypoints": ["scripts/publish-release.mjs"],
|
|
89
|
+
"nonRuntimeRoots": ["library", "catalogs/examples"],
|
|
87
90
|
"python": {
|
|
88
91
|
"managedDependencies": ["numpy", "openvino-genai"],
|
|
89
92
|
"ignoreDependencies": ["vendor-sdk"]
|
|
@@ -91,13 +94,17 @@ For project-specific entry points or Python dependencies supplied by an external
|
|
|
91
94
|
}
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Keep exceptions narrow. `
|
|
95
|
-
|
|
97
|
+
Keep exceptions narrow. `nonRuntimeRoots` (alias `templateRoots`) marks reusable examples that are
|
|
98
|
+
not one deployed application. It suppresses orphan/dead/unused-export noise and missing/unresolved
|
|
99
|
+
dependency findings when every use is inside those roots; graph edges, cycles and boundaries remain
|
|
100
|
+
visible. `managedDependencies` documents modules provided outside the repo's Python manifest;
|
|
101
|
+
`ignoreDependencies` suppresses intentionally unresolved imports.
|
|
96
102
|
|
|
97
103
|
## Sync
|
|
98
104
|
|
|
99
|
-
`sync_graph` uses allowlisted payload v2, including
|
|
100
|
-
built before `0.1.
|
|
105
|
+
`sync_graph` uses allowlisted payload v2, including type-only/compile-only edge metadata but no
|
|
106
|
+
source bodies. A graph built before `0.1.4` must be rebuilt first; normal `open_repo` does this
|
|
107
|
+
automatically. Sync remains
|
|
101
108
|
unavailable until the user opts into `online` and configures `WEAVATRIX_SYNC_URL`.
|
|
102
109
|
|
|
103
110
|
## Troubleshooting
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { posix } from "node:path";
|
|
10
10
|
import { createRepoBoundary } from "../repo-path.js";
|
|
11
|
+
import { isStructuralRelation } from "../graph/relations.js";
|
|
11
12
|
|
|
12
13
|
const IDENT_RE = /[A-Za-z_$][\w$]*/g;
|
|
13
14
|
const bareName = (label) => String(label || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
|
|
@@ -19,7 +20,11 @@ const TEST_FILE = /(^|[\\/])[^\\/]*[._-](test|spec)\.[a-z0-9]+$|(^|[\\/])(test|t
|
|
|
19
20
|
// Framework-owned entry modules are invoked by convention rather than a source import. Keep this narrow:
|
|
20
21
|
// these are Next.js App/Pages Router surfaces and framework metadata files, not every file under `app/`.
|
|
21
22
|
export const NEXT_ENTRY_FILE = /(^|\/)(?:src\/)?app\/(?:.*\/)?(?:page|layout|template|loading|error|global-error|not-found|default|route|robots|sitemap|manifest|opengraph-image|twitter-image|icon|apple-icon)\.[cm]?[jt]sx?$|(^|\/)(?:src\/)?pages\/(?!.*\/(?:components?|lib|utils?)\/).+\.[cm]?[jt]sx?$|(^|\/)(?:middleware|instrumentation)\.[cm]?[jt]s$/i;
|
|
22
|
-
export const
|
|
23
|
+
export const RUST_ENTRY_FILE = /(^|\/)(?:build\.rs|src\/(?:lib|main)\.rs)$/i;
|
|
24
|
+
export const isFrameworkEntryFile = (file) => {
|
|
25
|
+
const normalized = String(file || "").replace(/\\/g, "/");
|
|
26
|
+
return NEXT_ENTRY_FILE.test(normalized) || RUST_ENTRY_FILE.test(normalized);
|
|
27
|
+
};
|
|
23
28
|
|
|
24
29
|
const lineOfNode = (n) => {
|
|
25
30
|
const m = /@(\d+)$/.exec(String(n.id || "")) || /L(\d+)/.exec(String(n.source_location || ""));
|
|
@@ -93,7 +98,7 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
93
98
|
const nodes = graph.nodes || [], links = graph.links || [];
|
|
94
99
|
const ep = (v) => (v && typeof v === "object" ? v.id : v);
|
|
95
100
|
const inbound = new Set();
|
|
96
|
-
for (const l of links) if (l.relation
|
|
101
|
+
for (const l of links) if (!isStructuralRelation(l.relation)) inbound.add(ep(l.target));
|
|
97
102
|
|
|
98
103
|
// whole-repo identifier frequency: a symbol whose name appears MORE than once total (its definition + at least
|
|
99
104
|
// one use, same-file OR cross-file) is referenced. Errs toward "alive" (common-named symbols never flagged).
|
|
@@ -168,7 +173,7 @@ export function computeUnusedExports(graph, sources, { dynamicTargets = new Set(
|
|
|
168
173
|
const nodes = graph.nodes || [], links = graph.links || [];
|
|
169
174
|
const ep = (v) => (v && typeof v === "object" ? v.id : v);
|
|
170
175
|
const inbound = new Set();
|
|
171
|
-
for (const l of links) if (l.relation
|
|
176
|
+
for (const l of links) if (!isStructuralRelation(l.relation)) inbound.add(ep(l.target));
|
|
172
177
|
|
|
173
178
|
const namespaceConsumed = namespaceConsumedFiles(sources, graph);
|
|
174
179
|
const candidates = [];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { makeFinding } from "./findings.js";
|
|
2
|
+
|
|
3
|
+
const TEST_PATH_RE = /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z0-9]+$|_test\.go$/i;
|
|
4
|
+
const escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\w.-])`).test(blob);
|
|
6
|
+
|
|
7
|
+
// ---- Go: set math over ecosystem:"Go" externalImports vs go.mod requires ----
|
|
8
|
+
// goMod = parseGoMod() output. Only DIRECT requires can be "unused" (indirect ones belong to `go mod
|
|
9
|
+
// tidy`); replace targets and the own module never count. Missing = imported module with no require
|
|
10
|
+
// prefix — rare in Go (builds fail), so it usually flags vendored/replaced setups: keep it medium.
|
|
11
|
+
export function computeGoDepFindings({ externalImports = [], goMod = null, nonRuntimeRoots = [] } = {}) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
if (!goMod || !goMod.module) return { findings, declared: new Set() };
|
|
14
|
+
const requires = goMod.requires || [];
|
|
15
|
+
const declared = new Set(requires.map((r) => r.path));
|
|
16
|
+
const replaced = new Set((goMod.replaces || []).map((r) => r.from));
|
|
17
|
+
const moduleOf = (spec) => { let best = ""; for (const p of declared) if ((spec === p || spec.startsWith(p + "/")) && p.length > best.length) best = p; return best; };
|
|
18
|
+
|
|
19
|
+
const usedModules = new Set();
|
|
20
|
+
const missing = new Map(); // pkg → { files:Set, lines:Map }
|
|
21
|
+
const inNonRuntimeRoot = (file) => (nonRuntimeRoots || []).some((root) => {
|
|
22
|
+
const f = String(file || "").replace(/\\/g, "/"), r = String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
23
|
+
return !!r && (f === r || f.startsWith(`${r}/`));
|
|
24
|
+
});
|
|
25
|
+
for (const e of externalImports) {
|
|
26
|
+
if (e.ecosystem !== "Go" || e.builtin || e.unresolved || !e.pkg) continue;
|
|
27
|
+
const mod = moduleOf(e.spec || e.pkg) || (declared.has(e.pkg) ? e.pkg : "");
|
|
28
|
+
if (mod) { usedModules.add(mod); continue; }
|
|
29
|
+
let m = missing.get(e.pkg);
|
|
30
|
+
if (!m) missing.set(e.pkg, (m = { files: new Set(), lines: new Map() }));
|
|
31
|
+
m.files.add(e.file);
|
|
32
|
+
if (!m.lines.has(e.file)) m.lines.set(e.file, e.line || 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const r of requires) {
|
|
36
|
+
if (r.indirect || usedModules.has(r.path)) continue;
|
|
37
|
+
findings.push(makeFinding({
|
|
38
|
+
category: "unused",
|
|
39
|
+
rule: "unused-dep",
|
|
40
|
+
severity: "low",
|
|
41
|
+
confidence: "medium",
|
|
42
|
+
title: `Unused Go module: ${r.path}`,
|
|
43
|
+
detail: `"${r.path}" is required (direct) in go.mod but no .go file imports it or any of its packages. Build-tag-guarded files can hide usage — confirm with \`go mod tidy\` before removing.`,
|
|
44
|
+
package: r.path,
|
|
45
|
+
version: r.version,
|
|
46
|
+
source: "internal",
|
|
47
|
+
fixHint: "go mod tidy (drops requires nothing imports)",
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
for (const [pkg, use] of missing) {
|
|
51
|
+
if (replaced.has(pkg)) continue;
|
|
52
|
+
const files = [...use.files];
|
|
53
|
+
if (files.every(inNonRuntimeRoot)) continue;
|
|
54
|
+
findings.push(makeFinding({
|
|
55
|
+
category: "unused",
|
|
56
|
+
rule: "missing-dep",
|
|
57
|
+
severity: "medium",
|
|
58
|
+
confidence: "medium",
|
|
59
|
+
title: `Missing Go module: ${pkg}`,
|
|
60
|
+
detail: `"${pkg}" is imported by ${files.length} file(s) but go.mod has no matching require — a replace/workspace/vendor setup, or the module was never added.`,
|
|
61
|
+
package: pkg,
|
|
62
|
+
file: files[0],
|
|
63
|
+
line: use.lines.get(files[0]) || 0,
|
|
64
|
+
evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
|
|
65
|
+
source: "internal",
|
|
66
|
+
fixHint: `go get ${pkg}`,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
return { findings, declared };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- Python: ecosystem:"PyPI" externalImports vs requirements/pyproject/Pipfile ----
|
|
73
|
+
// Import→dist naming is heuristic (yaml→PyYAML, python-X/X-python variants), so matching is GENEROUS for
|
|
74
|
+
// suppression and every unused finding stays low-confidence. CLI-only tools (pytest, black, gunicorn …)
|
|
75
|
+
// and stub/plugin conventions (types-*, *-stubs, pytest-*, flake8-*) are never flagged unused.
|
|
76
|
+
const PY_TOOL_DISTS = new Set(("pytest tox nox black ruff flake8 pylint mypy pyright isort bandit coverage pre-commit pip setuptools wheel build twine poetry poetry-core " +
|
|
77
|
+
"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(" "));
|
|
78
|
+
const pyNorm = (n) => String(n || "").toLowerCase().replace(/[-_.]+/g, "-");
|
|
79
|
+
|
|
80
|
+
export function computePyDepFindings({
|
|
81
|
+
externalImports = [], pyManifest = null, configTexts = new Map(),
|
|
82
|
+
managedDependencies = [], ignoredDependencies = [], nonRuntimeRoots = [],
|
|
83
|
+
} = {}) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
const deps = (pyManifest && pyManifest.deps) || [];
|
|
86
|
+
const present = !!(pyManifest && pyManifest.present);
|
|
87
|
+
const declared = new Set(deps.map((d) => pyNorm(d.name)));
|
|
88
|
+
const managed = new Set((managedDependencies || []).map(pyNorm));
|
|
89
|
+
const ignored = new Set((ignoredDependencies || []).map(pyNorm));
|
|
90
|
+
const inNonRuntimeRoot = (file) => (nonRuntimeRoots || []).some((root) => {
|
|
91
|
+
const f = String(file || "").replace(/\\/g, "/"), r = String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
92
|
+
return !!r && (f === r || f.startsWith(`${r}/`));
|
|
93
|
+
});
|
|
94
|
+
for (const name of managed) declared.add(name);
|
|
95
|
+
|
|
96
|
+
const used = new Map(); // top import → { dist, files:Set, lines:Map }
|
|
97
|
+
for (const e of externalImports) {
|
|
98
|
+
if (e.ecosystem !== "PyPI" || e.builtin || e.unresolved || !e.pkg) continue;
|
|
99
|
+
const top = String(e.spec || e.pkg).split(".")[0];
|
|
100
|
+
let u = used.get(top);
|
|
101
|
+
if (!u) used.set(top, (u = { dist: e.pkg, files: new Set(), lines: new Map() }));
|
|
102
|
+
u.files.add(e.file);
|
|
103
|
+
if (!u.lines.has(e.file)) u.lines.set(e.file, e.line || 0);
|
|
104
|
+
}
|
|
105
|
+
// generous match: does declared dist D cover import top t (dist guess g)?
|
|
106
|
+
const covers = (D, t, g) => {
|
|
107
|
+
const d = pyNorm(D), nt = pyNorm(t), ng = pyNorm(g);
|
|
108
|
+
return d === nt || d === ng || d === `python-${nt}` || d === `${nt}-python` || d === `${nt}-binary` || d.replace(/\d+$/, "") === nt.replace(/\d+$/, "");
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const configBlob = [...configTexts.values()].join("\n");
|
|
112
|
+
if (present) {
|
|
113
|
+
for (const d of deps) {
|
|
114
|
+
const n = pyNorm(d.name);
|
|
115
|
+
if (managed.has(n) || ignored.has(n)) continue;
|
|
116
|
+
if (d.buildSystem || PY_TOOL_DISTS.has(n) || /^types-|-stubs$|^pytest-|^flake8-|^sphinx/.test(n)) continue;
|
|
117
|
+
let hit = false;
|
|
118
|
+
for (const [top, u] of used) if (covers(d.name, top, u.dist)) { hit = true; break; }
|
|
119
|
+
if (hit || mentioned(configBlob, d.name)) continue;
|
|
120
|
+
findings.push(makeFinding({
|
|
121
|
+
category: "unused",
|
|
122
|
+
rule: "unused-dep",
|
|
123
|
+
severity: d.dev ? "info" : "low",
|
|
124
|
+
confidence: "low",
|
|
125
|
+
title: `Unused Python dependency: ${d.name}`,
|
|
126
|
+
detail: `"${d.name}" is declared but no .py file imports a module that maps to it. Import-name↔package-name mapping is heuristic and plugins/CLI tools load dynamically — review before removing.`,
|
|
127
|
+
package: d.name,
|
|
128
|
+
source: "internal",
|
|
129
|
+
fixHint: `remove "${d.name}" from the manifest after confirming nothing imports or shells out to it`,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const [top, u] of used) {
|
|
134
|
+
if (ignored.has(pyNorm(top)) || ignored.has(pyNorm(u.dist)) || managed.has(pyNorm(top)) || managed.has(pyNorm(u.dist))) continue;
|
|
135
|
+
let hit = false;
|
|
136
|
+
for (const D of declared) if (covers(D, top, u.dist)) { hit = true; break; }
|
|
137
|
+
if (hit) continue;
|
|
138
|
+
const files = [...u.files];
|
|
139
|
+
if (files.every(inNonRuntimeRoot)) continue;
|
|
140
|
+
const testOnly = files.every((f) => TEST_PATH_RE.test(f));
|
|
141
|
+
findings.push(makeFinding({
|
|
142
|
+
category: "unused",
|
|
143
|
+
rule: "missing-dep",
|
|
144
|
+
severity: present ? (testOnly ? "low" : "medium") : "low",
|
|
145
|
+
confidence: present ? "medium" : "low",
|
|
146
|
+
title: `Missing Python dependency: ${u.dist}`,
|
|
147
|
+
detail: `"${top}" is imported by ${files.length} file(s) but ${present ? "no declared dependency provides it" : "the repo has no Python dependency manifest (requirements.txt / pyproject / Pipfile); a bundled or managed runtime may provide it"}${u.dist !== top ? ` (PyPI package is likely "${u.dist}")` : ""}.${present ? "" : " If this is intentional, declare it under python.managedDependencies in .weavatrix-deps.json."}`,
|
|
148
|
+
package: u.dist,
|
|
149
|
+
file: files[0],
|
|
150
|
+
line: u.lines.get(files[0]) || 0,
|
|
151
|
+
evidence: files.slice(0, 5).map((f) => ({ file: f, line: u.lines.get(f) || 0, snippet: "" })),
|
|
152
|
+
source: "internal",
|
|
153
|
+
fixHint: present ? `pip install ${u.dist} (and add it to requirements.txt / pyproject)` : `add ${u.dist} to a Python manifest, or declare it as a managed runtime dependency`,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
return { findings, declared, managed };
|
|
157
|
+
}
|
|
@@ -47,7 +47,7 @@ const mentioned = (blob, name) => new RegExp(`(^|[^\\w@.-])${escRe(name)}($|[^\\
|
|
|
47
47
|
// configTexts — Map<fileName, text> of root config files + CI workflows (mention scanning)
|
|
48
48
|
export function computeDepFindings({
|
|
49
49
|
externalImports = [], pkg = {}, workspacePkgNames = new Set(), configTexts = new Map(),
|
|
50
|
-
aliases = [], scope = "", manifest = "package.json",
|
|
50
|
+
aliases = [], scope = "", manifest = "package.json", nonRuntimeRoots = [],
|
|
51
51
|
} = {}) {
|
|
52
52
|
const findings = [];
|
|
53
53
|
const meta = { scope: scope || ".", manifest };
|
|
@@ -68,6 +68,32 @@ export function computeDepFindings({
|
|
|
68
68
|
};
|
|
69
69
|
const allDeclared = new Set(Object.values(sections).flatMap((s) => Object.keys(s)));
|
|
70
70
|
const selfName = String(pkg.name || "");
|
|
71
|
+
const normRoots = (nonRuntimeRoots || []).map((root) => String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "")).filter(Boolean);
|
|
72
|
+
const isNonRuntimeFile = (file) => {
|
|
73
|
+
const normalized = String(file || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
74
|
+
return normRoots.some((root) => normalized === root || normalized.startsWith(`${root}/`));
|
|
75
|
+
};
|
|
76
|
+
const npmNameParts = (name) => {
|
|
77
|
+
const value = String(name || "");
|
|
78
|
+
const slash = value.startsWith("@") ? value.indexOf("/") : -1;
|
|
79
|
+
return slash > 0 ? { scope: value.slice(0, slash), base: value.slice(slash + 1) } : { scope: "", base: value };
|
|
80
|
+
};
|
|
81
|
+
const selfParts = npmNameParts(selfName);
|
|
82
|
+
const configuredNapi = npmNameParts(pkg.napi?.name || selfName);
|
|
83
|
+
const napiBinding = {
|
|
84
|
+
scope: configuredNapi.scope || (configuredNapi.base === selfParts.base ? selfParts.scope : ""),
|
|
85
|
+
base: configuredNapi.base,
|
|
86
|
+
};
|
|
87
|
+
const NAPI_PLATFORM_SUFFIX = /^(?:android-(?:arm64|arm-eabi)|win32-(?:x64|ia32|arm64)-msvc|darwin-(?:universal|x64|arm64)|freebsd-(?:x64|arm64)|linux-(?:x64|arm64|arm|riscv64|s390x|ppc64|loong64)-(?:gnu|musl|gnueabihf|musleabihf)|aix-ppc64|sunos-x64|wasm32-wasi(?:-preview1-threads)?)$/;
|
|
88
|
+
const isGeneratedNapiPackage = (name, files) => {
|
|
89
|
+
if (!pkg.napi || !napiBinding.base || !files.every((file) => /(^|\/)index\.[cm]?js$/i.test(String(file || "").replace(/\\/g, "/")))) return false;
|
|
90
|
+
const candidate = npmNameParts(name);
|
|
91
|
+
if (candidate.scope !== napiBinding.scope || !candidate.base.startsWith(`${napiBinding.base}-`)) return false;
|
|
92
|
+
return NAPI_PLATFORM_SUFFIX.test(candidate.base.slice(napiBinding.base.length + 1));
|
|
93
|
+
};
|
|
94
|
+
const isGeneratedNapiBinary = (entry) => !!pkg.napi
|
|
95
|
+
&& /(^|\/)index\.[cm]?js$/i.test(String(entry.file || "").replace(/\\/g, "/"))
|
|
96
|
+
&& /^\.\/[^/]+\.node$/i.test(String(entry.spec || ""));
|
|
71
97
|
// Some frameworks consume required peers internally, so application source legitimately never imports
|
|
72
98
|
// them. Keep this deliberately narrow: react-dom is a required Next.js runtime peer in the same package
|
|
73
99
|
// scope, not a general React exemption and not a repo-wide whitelist.
|
|
@@ -134,6 +160,7 @@ export function computeDepFindings({
|
|
|
134
160
|
for (const [name, use] of usedPackages) {
|
|
135
161
|
if (allDeclared.has(name) || name === selfName || workspacePkgNames.has(name)) continue;
|
|
136
162
|
const files = [...use.files];
|
|
163
|
+
if (files.every(isNonRuntimeFile) || isGeneratedNapiPackage(name, files)) continue;
|
|
137
164
|
const testOnly = files.every((f) => /(^|[/\\])(test|tests|__tests__|spec|e2e|__mocks__)([/\\]|$)|[._-](test|spec)\.[a-z]+$/i.test(f));
|
|
138
165
|
findings.push(makeFinding({
|
|
139
166
|
category: "unused",
|
|
@@ -176,6 +203,7 @@ export function computeDepFindings({
|
|
|
176
203
|
let unresolvedCount = 0;
|
|
177
204
|
for (const e of externalImports) {
|
|
178
205
|
if (!e.unresolved) continue;
|
|
206
|
+
if (isNonRuntimeFile(e.file) || isGeneratedNapiBinary(e)) continue;
|
|
179
207
|
unresolvedCount++;
|
|
180
208
|
const key = e.file + "|" + e.spec;
|
|
181
209
|
if (unresolvedSeen.has(key) || unresolvedSeen.size >= 100) continue; // cap the findings, keep the count honest
|
|
@@ -216,6 +244,7 @@ const ownsFile = (scope, file) => !scope || file === scope || String(file || "")
|
|
|
216
244
|
// Node's package scope and prevents nested Next/Vite apps from inheriting the root manifest by accident.
|
|
217
245
|
export function computeScopedDepFindings({
|
|
218
246
|
externalImports = [], packageScopes = [], workspacePkgNames = new Set(), configTexts = new Map(),
|
|
247
|
+
nonRuntimeRoots = [],
|
|
219
248
|
} = {}) {
|
|
220
249
|
const scopes = packageScopes.length
|
|
221
250
|
? packageScopes.map((s) => ({ ...s, root: normScope(s.root) })).sort((a, b) => b.root.length - a.root.length)
|
|
@@ -233,6 +262,7 @@ export function computeScopedDepFindings({
|
|
|
233
262
|
const r = computeDepFindings({
|
|
234
263
|
externalImports: importsByScope.get(s), pkg: s.pkg || {}, workspacePkgNames, configTexts: scopeConfig,
|
|
235
264
|
aliases: s.aliases || [], scope: s.root, manifest: s.manifest || (s.root ? `${s.root}/package.json` : "package.json"),
|
|
265
|
+
nonRuntimeRoots,
|
|
236
266
|
});
|
|
237
267
|
findings.push(...r.findings);
|
|
238
268
|
for (const [name, use] of r.usedPackages) usedPackages.set(`${s.root || "."}:${name}`, use);
|
|
@@ -241,146 +271,4 @@ export function computeScopedDepFindings({
|
|
|
241
271
|
return { findings, usedPackages, declared };
|
|
242
272
|
}
|
|
243
273
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// ---- Go: set math over ecosystem:"Go" externalImports vs go.mod requires ----
|
|
247
|
-
// goMod = parseGoMod() output. Only DIRECT requires can be "unused" (indirect ones belong to `go mod
|
|
248
|
-
// tidy`); replace targets and the own module never count. Missing = imported module with no require
|
|
249
|
-
// prefix — rare in Go (builds fail), so it usually flags vendored/replaced setups: keep it medium.
|
|
250
|
-
export function computeGoDepFindings({ externalImports = [], goMod = null } = {}) {
|
|
251
|
-
const findings = [];
|
|
252
|
-
if (!goMod || !goMod.module) return { findings, declared: new Set() };
|
|
253
|
-
const requires = goMod.requires || [];
|
|
254
|
-
const declared = new Set(requires.map((r) => r.path));
|
|
255
|
-
const replaced = new Set((goMod.replaces || []).map((r) => r.from));
|
|
256
|
-
const moduleOf = (spec) => { let best = ""; for (const p of declared) if ((spec === p || spec.startsWith(p + "/")) && p.length > best.length) best = p; return best; };
|
|
257
|
-
|
|
258
|
-
const usedModules = new Set();
|
|
259
|
-
const missing = new Map(); // pkg → { files:Set, lines:Map }
|
|
260
|
-
for (const e of externalImports) {
|
|
261
|
-
if (e.ecosystem !== "Go" || e.builtin || e.unresolved || !e.pkg) continue;
|
|
262
|
-
const mod = moduleOf(e.spec || e.pkg) || (declared.has(e.pkg) ? e.pkg : "");
|
|
263
|
-
if (mod) { usedModules.add(mod); continue; }
|
|
264
|
-
let m = missing.get(e.pkg);
|
|
265
|
-
if (!m) missing.set(e.pkg, (m = { files: new Set(), lines: new Map() }));
|
|
266
|
-
m.files.add(e.file);
|
|
267
|
-
if (!m.lines.has(e.file)) m.lines.set(e.file, e.line || 0);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const r of requires) {
|
|
271
|
-
if (r.indirect || usedModules.has(r.path)) continue;
|
|
272
|
-
findings.push(makeFinding({
|
|
273
|
-
category: "unused",
|
|
274
|
-
rule: "unused-dep",
|
|
275
|
-
severity: "low",
|
|
276
|
-
confidence: "medium",
|
|
277
|
-
title: `Unused Go module: ${r.path}`,
|
|
278
|
-
detail: `"${r.path}" is required (direct) in go.mod but no .go file imports it or any of its packages. Build-tag-guarded files can hide usage — confirm with \`go mod tidy\` before removing.`,
|
|
279
|
-
package: r.path,
|
|
280
|
-
version: r.version,
|
|
281
|
-
source: "internal",
|
|
282
|
-
fixHint: "go mod tidy (drops requires nothing imports)",
|
|
283
|
-
}));
|
|
284
|
-
}
|
|
285
|
-
for (const [pkg, use] of missing) {
|
|
286
|
-
if (replaced.has(pkg)) continue;
|
|
287
|
-
const files = [...use.files];
|
|
288
|
-
findings.push(makeFinding({
|
|
289
|
-
category: "unused",
|
|
290
|
-
rule: "missing-dep",
|
|
291
|
-
severity: "medium",
|
|
292
|
-
confidence: "medium",
|
|
293
|
-
title: `Missing Go module: ${pkg}`,
|
|
294
|
-
detail: `"${pkg}" is imported by ${files.length} file(s) but go.mod has no matching require — a replace/workspace/vendor setup, or the module was never added.`,
|
|
295
|
-
package: pkg,
|
|
296
|
-
file: files[0],
|
|
297
|
-
line: use.lines.get(files[0]) || 0,
|
|
298
|
-
evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
|
|
299
|
-
source: "internal",
|
|
300
|
-
fixHint: `go get ${pkg}`,
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
return { findings, declared };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ---- Python: ecosystem:"PyPI" externalImports vs requirements/pyproject/Pipfile ----
|
|
307
|
-
// Import→dist naming is heuristic (yaml→PyYAML, python-X/X-python variants), so matching is GENEROUS for
|
|
308
|
-
// suppression and every unused finding stays low-confidence. CLI-only tools (pytest, black, gunicorn …)
|
|
309
|
-
// and stub/plugin conventions (types-*, *-stubs, pytest-*, flake8-*) are never flagged unused.
|
|
310
|
-
const PY_TOOL_DISTS = new Set(("pytest tox nox black ruff flake8 pylint mypy pyright isort bandit coverage pre-commit pip setuptools wheel build twine poetry poetry-core " +
|
|
311
|
-
"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(" "));
|
|
312
|
-
const pyNorm = (n) => String(n || "").toLowerCase().replace(/[-_.]+/g, "-");
|
|
313
|
-
|
|
314
|
-
export function computePyDepFindings({
|
|
315
|
-
externalImports = [], pyManifest = null, configTexts = new Map(),
|
|
316
|
-
managedDependencies = [], ignoredDependencies = [],
|
|
317
|
-
} = {}) {
|
|
318
|
-
const findings = [];
|
|
319
|
-
const deps = (pyManifest && pyManifest.deps) || [];
|
|
320
|
-
const present = !!(pyManifest && pyManifest.present);
|
|
321
|
-
const declared = new Set(deps.map((d) => pyNorm(d.name)));
|
|
322
|
-
const managed = new Set((managedDependencies || []).map(pyNorm));
|
|
323
|
-
const ignored = new Set((ignoredDependencies || []).map(pyNorm));
|
|
324
|
-
for (const name of managed) declared.add(name);
|
|
325
|
-
|
|
326
|
-
const used = new Map(); // top import → { dist, files:Set, lines:Map }
|
|
327
|
-
for (const e of externalImports) {
|
|
328
|
-
if (e.ecosystem !== "PyPI" || e.builtin || e.unresolved || !e.pkg) continue;
|
|
329
|
-
const top = String(e.spec || e.pkg).split(".")[0];
|
|
330
|
-
let u = used.get(top);
|
|
331
|
-
if (!u) used.set(top, (u = { dist: e.pkg, files: new Set(), lines: new Map() }));
|
|
332
|
-
u.files.add(e.file);
|
|
333
|
-
if (!u.lines.has(e.file)) u.lines.set(e.file, e.line || 0);
|
|
334
|
-
}
|
|
335
|
-
// generous match: does declared dist D cover import top t (dist guess g)?
|
|
336
|
-
const covers = (D, t, g) => {
|
|
337
|
-
const d = pyNorm(D), nt = pyNorm(t), ng = pyNorm(g);
|
|
338
|
-
return d === nt || d === ng || d === `python-${nt}` || d === `${nt}-python` || d === `${nt}-binary` || d.replace(/\d+$/, "") === nt.replace(/\d+$/, "");
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const configBlob = [...configTexts.values()].join("\n");
|
|
342
|
-
if (present) {
|
|
343
|
-
for (const d of deps) {
|
|
344
|
-
const n = pyNorm(d.name);
|
|
345
|
-
if (managed.has(n) || ignored.has(n)) continue;
|
|
346
|
-
if (d.buildSystem || PY_TOOL_DISTS.has(n) || /^types-|-stubs$|^pytest-|^flake8-|^sphinx/.test(n)) continue;
|
|
347
|
-
let hit = false;
|
|
348
|
-
for (const [top, u] of used) if (covers(d.name, top, u.dist)) { hit = true; break; }
|
|
349
|
-
if (hit || mentioned(configBlob, d.name)) continue;
|
|
350
|
-
findings.push(makeFinding({
|
|
351
|
-
category: "unused",
|
|
352
|
-
rule: "unused-dep",
|
|
353
|
-
severity: d.dev ? "info" : "low",
|
|
354
|
-
confidence: "low",
|
|
355
|
-
title: `Unused Python dependency: ${d.name}`,
|
|
356
|
-
detail: `"${d.name}" is declared but no .py file imports a module that maps to it. Import-name↔package-name mapping is heuristic and plugins/CLI tools load dynamically — review before removing.`,
|
|
357
|
-
package: d.name,
|
|
358
|
-
source: "internal",
|
|
359
|
-
fixHint: `remove "${d.name}" from the manifest after confirming nothing imports or shells out to it`,
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
for (const [top, u] of used) {
|
|
364
|
-
if (ignored.has(pyNorm(top)) || ignored.has(pyNorm(u.dist)) || managed.has(pyNorm(top)) || managed.has(pyNorm(u.dist))) continue;
|
|
365
|
-
let hit = false;
|
|
366
|
-
for (const D of declared) if (covers(D, top, u.dist)) { hit = true; break; }
|
|
367
|
-
if (hit) continue;
|
|
368
|
-
const files = [...u.files];
|
|
369
|
-
const testOnly = files.every((f) => TEST_PATH_RE.test(f));
|
|
370
|
-
findings.push(makeFinding({
|
|
371
|
-
category: "unused",
|
|
372
|
-
rule: "missing-dep",
|
|
373
|
-
severity: present ? (testOnly ? "low" : "medium") : "low",
|
|
374
|
-
confidence: present ? "medium" : "low",
|
|
375
|
-
title: `Missing Python dependency: ${u.dist}`,
|
|
376
|
-
detail: `"${top}" is imported by ${files.length} file(s) but ${present ? "no declared dependency provides it" : "the repo has no Python dependency manifest (requirements.txt / pyproject / Pipfile); a bundled or managed runtime may provide it"}${u.dist !== top ? ` (PyPI package is likely "${u.dist}")` : ""}.${present ? "" : " If this is intentional, declare it under python.managedDependencies in .weavatrix-deps.json."}`,
|
|
377
|
-
package: u.dist,
|
|
378
|
-
file: files[0],
|
|
379
|
-
line: u.lines.get(files[0]) || 0,
|
|
380
|
-
evidence: files.slice(0, 5).map((f) => ({ file: f, line: u.lines.get(f) || 0, snippet: "" })),
|
|
381
|
-
source: "internal",
|
|
382
|
-
fixHint: present ? `pip install ${u.dist} (and add it to requirements.txt / pyproject)` : `add ${u.dist} to a Python manifest, or declare it as a managed runtime dependency`,
|
|
383
|
-
}));
|
|
384
|
-
}
|
|
385
|
-
return { findings, declared, managed };
|
|
386
|
-
}
|
|
274
|
+
export { computeGoDepFindings, computePyDepFindings } from "./dep-check-ecosystems.js";
|