weavatrix 0.3.14 → 1.1.0
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/LICENSE +1 -0
- package/README.md +132 -200
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +10 -67
- package/server.json +38 -0
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.14.md +0 -37
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports-tarpaulin.js +0 -112
- package/src/analysis/coverage-reports.js +0 -246
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -79
- package/src/analysis/endpoints/extract.js +0 -120
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -296
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -202
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -201
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -199
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -121
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -220
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -226
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,194 +0,0 @@
|
|
|
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,23 +0,0 @@
|
|
|
1
|
-
import {hasPathClass} from '../../path-classification.js'
|
|
2
|
-
|
|
3
|
-
export const DEAD_CODE_CONFIDENCE_RANK = Object.freeze({high: 0, medium: 1, low: 2})
|
|
4
|
-
const DYNAMIC_CODE_RE = /(?:\brequire\s*\(\s*(?!["'])|\bcreateRequire\s*\(|\b__import__\s*\(|\bimportlib\.|(?:^|[^\w.$])(?:eval|exec)\s*\()/m
|
|
5
|
-
const JS_DYNAMIC_IMPORT_RE = /\bimport\s*\(/
|
|
6
|
-
const JS_LIKE_PATH_RE = /\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i
|
|
7
|
-
export const REFLECTION_CODE_RE = /(?:\b(?:Class\.forName|get(?:Declared)?Method|getattr|setattr|hasattr|Method\.Invoke|GetMethod|GetProcAddress|dlsym)\s*\(|\b(?:globals|locals)\s*\(\s*\)\s*\[|\breflect\.[A-Za-z_$][\w$]*\s*\()/i
|
|
8
|
-
export const normalizedReviewPath = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
9
|
-
|
|
10
|
-
export function hasDynamicCode(source, file = '') {
|
|
11
|
-
const text = String(source || '')
|
|
12
|
-
return DYNAMIC_CODE_RE.test(text) || (JS_LIKE_PATH_RE.test(String(file)) && JS_DYNAMIC_IMPORT_RE.test(text))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
16
|
-
|
|
17
|
-
export function deadCodePathAllowed(info, {includeTests, includeClassified}) {
|
|
18
|
-
if (!includeTests && hasPathClass(info, 'test', 'e2e')) return {ok: false, bucket: 'tests'}
|
|
19
|
-
if (!includeClassified && (info?.excluded || hasPathClass(info, ...NON_PRODUCT_CLASSES))) {
|
|
20
|
-
return {ok: false, bucket: 'classified'}
|
|
21
|
-
}
|
|
22
|
-
return {ok: true}
|
|
23
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Conservative, source-free review queue for statically unreferenced code. This deliberately builds
|
|
2
|
-
// on computeDead instead of inventing a second liveness model. Candidates are evidence for review,
|
|
3
|
-
// never deletion instructions: framework entry, dynamic loading, reflection and public API surfaces
|
|
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";
|
|
7
|
-
import { createPathClassifier } from "../path-classification.js";
|
|
8
|
-
import {
|
|
9
|
-
DEAD_CODE_CONFIDENCE_RANK as CONFIDENCE_RANK, hasDynamicCode,
|
|
10
|
-
REFLECTION_CODE_RE as REFLECTION_RE, deadCodePathAllowed as pathAllowed,
|
|
11
|
-
normalizedReviewPath as normalizedPath,
|
|
12
|
-
} from './dead-code-review/policy.js'
|
|
13
|
-
import { symbolCandidate, fileCandidate } from './dead-code-review/candidates.js'
|
|
14
|
-
|
|
15
|
-
// Pure review model. Filesystem collection/entry inference stays in the MCP adapter so tests can pass
|
|
16
|
-
// exact source maps and convention evidence without touching the working tree.
|
|
17
|
-
export function computeDeadCodeReview(graph, sources, options = {}) {
|
|
18
|
-
const entrySet = options.entrySet instanceof Set ? options.entrySet : new Set(options.entrySet || []);
|
|
19
|
-
const dynamicTargets = options.dynamicTargets instanceof Set ? options.dynamicTargets : new Set(options.dynamicTargets || []);
|
|
20
|
-
const frameworkByFile = new Map((options.frameworkEvidence || []).map((entry) => [normalizedPath(entry.file), entry]));
|
|
21
|
-
const classifier = options.pathClassifier || createPathClassifier(null);
|
|
22
|
-
const classificationCache = new Map();
|
|
23
|
-
const classify = (file, source) => {
|
|
24
|
-
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, { content: source }));
|
|
25
|
-
return classificationCache.get(file);
|
|
26
|
-
};
|
|
27
|
-
const repoSignals = {
|
|
28
|
-
dynamicLoading: (graph.externalImports || []).some((entry) => entry?.dynamic) || [...sources].some(([file, text]) => hasDynamicCode(text, file)),
|
|
29
|
-
reflection: [...sources.values()].some((text) => REFLECTION_RE.test(String(text || ""))),
|
|
30
|
-
};
|
|
31
|
-
const includeTests = options.includeTests === true;
|
|
32
|
-
const includeClassified = options.includeClassified === true;
|
|
33
|
-
const minConfidence = Object.hasOwn(CONFIDENCE_RANK, options.minConfidence) ? options.minConfidence : "medium";
|
|
34
|
-
const pathPrefix = normalizedPath(options.path || "").replace(/\/+$/, "");
|
|
35
|
-
const requestedKinds = new Set(Array.isArray(options.kinds) && options.kinds.length ? options.kinds : ["file", "function", "method", "symbol"]);
|
|
36
|
-
const exactNoReferenceIds = new Set(graph.precisionNoReferenceSymbols || options.exactNoReferenceIds || []);
|
|
37
|
-
const context = { sources, entrySet, dynamicTargets, frameworkByFile, classify, repoSignals, exactNoReferenceIds };
|
|
38
|
-
const dead = computeDead(graph, sources, { entrySet });
|
|
39
|
-
const nodesById = new Map((graph.nodes || []).map((node) => [String(node.id), node]));
|
|
40
|
-
const rawSymbols = [...dead.deadSymbols, ...(dead.testOnlySymbols || []).map((item) => ({...item, testOnly: true}))]
|
|
41
|
-
.map((item) => ({ item, node: nodesById.get(String(item.id)) }))
|
|
42
|
-
.filter((entry) => entry.node)
|
|
43
|
-
.map(({ item, node }) => symbolCandidate(item, node, context));
|
|
44
|
-
const symbolsByFile = new Map();
|
|
45
|
-
for (const candidate of rawSymbols) {
|
|
46
|
-
if (!symbolsByFile.has(candidate.file)) symbolsByFile.set(candidate.file, []);
|
|
47
|
-
symbolsByFile.get(candidate.file).push(candidate);
|
|
48
|
-
}
|
|
49
|
-
const rawFiles = dead.deadFiles.map((item) => fileCandidate(item, symbolsByFile.get(normalizedPath(item.file)) || [], context));
|
|
50
|
-
const raw = [...rawSymbols, ...rawFiles];
|
|
51
|
-
const suppressed = { tests: 0, classified: 0, confidence: 0, path: 0, kind: 0 };
|
|
52
|
-
const candidates = [];
|
|
53
|
-
for (const candidate of raw) {
|
|
54
|
-
// Node-level test surfaces (Rust #[cfg(test)] symbols in production files) follow the same
|
|
55
|
-
// include_tests policy as path-classified test files.
|
|
56
|
-
if (!includeTests && nodesById.get(String(candidate.id))?.test_surface === true) { suppressed.tests += 1; continue; }
|
|
57
|
-
const info = classify(candidate.file, sources.get(candidate.file));
|
|
58
|
-
const allowed = pathAllowed(info, { includeTests, includeClassified });
|
|
59
|
-
if (!allowed.ok) { suppressed[allowed.bucket] += 1; continue; }
|
|
60
|
-
if (pathPrefix && candidate.file !== pathPrefix && !candidate.file.startsWith(`${pathPrefix}/`)) { suppressed.path += 1; continue; }
|
|
61
|
-
if (!requestedKinds.has(candidate.kind)) { suppressed.kind += 1; continue; }
|
|
62
|
-
if (CONFIDENCE_RANK[candidate.confidence] > CONFIDENCE_RANK[minConfidence]) { suppressed.confidence += 1; continue; }
|
|
63
|
-
candidates.push(candidate);
|
|
64
|
-
}
|
|
65
|
-
candidates.sort((left, right) =>
|
|
66
|
-
CONFIDENCE_RANK[left.confidence] - CONFIDENCE_RANK[right.confidence]
|
|
67
|
-
|| left.file.localeCompare(right.file)
|
|
68
|
-
|| left.line - right.line
|
|
69
|
-
|| left.id.localeCompare(right.id));
|
|
70
|
-
|
|
71
|
-
const warnings = [{
|
|
72
|
-
code: "STATIC_LIVENESS_IS_NOT_RUNTIME_PROOF",
|
|
73
|
-
message: "No static reference is not proof of dead runtime code. Review every candidate; never bulk-delete or auto-delete.",
|
|
74
|
-
}];
|
|
75
|
-
if (repoSignals.dynamicLoading) warnings.push({
|
|
76
|
-
code: "DYNAMIC_LOADING_PRESENT",
|
|
77
|
-
message: "Dynamic loading exists in the repository; static callers may be incomplete.",
|
|
78
|
-
});
|
|
79
|
-
if (repoSignals.reflection) warnings.push({
|
|
80
|
-
code: "REFLECTION_PRESENT",
|
|
81
|
-
message: "Reflection-like APIs exist in the repository; public or name-addressed symbols may be invoked without graph edges.",
|
|
82
|
-
});
|
|
83
|
-
if (suppressed.confidence) warnings.push({
|
|
84
|
-
code: "LOW_CONFIDENCE_SUPPRESSED",
|
|
85
|
-
message: `${suppressed.confidence} low-confidence candidate(s), including public/framework-sensitive code, were suppressed; set min_confidence=low to review them explicitly.`,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
candidates,
|
|
90
|
-
warnings,
|
|
91
|
-
suppressed,
|
|
92
|
-
repoSignals,
|
|
93
|
-
totals: {
|
|
94
|
-
indexedSymbols: dead.stats.symbols,
|
|
95
|
-
indexedFiles: dead.stats.files,
|
|
96
|
-
rawDeadSymbols: rawSymbols.length,
|
|
97
|
-
rawTestOnlySymbols: (dead.testOnlySymbols || []).length,
|
|
98
|
-
rawDeadFiles: rawFiles.length,
|
|
99
|
-
reviewCandidates: candidates.length,
|
|
100
|
-
byConfidence: {
|
|
101
|
-
high: candidates.filter((candidate) => candidate.confidence === "high").length,
|
|
102
|
-
medium: candidates.filter((candidate) => candidate.confidence === "medium").length,
|
|
103
|
-
low: candidates.filter((candidate) => candidate.confidence === "low").length,
|
|
104
|
-
},
|
|
105
|
-
byEvidenceTier: {
|
|
106
|
-
strongStatic: candidates.filter((candidate) => candidate.evidenceTier === "STRONG_STATIC_EVIDENCE").length,
|
|
107
|
-
boundedStatic: candidates.filter((candidate) => candidate.evidenceTier === "BOUNDED_STATIC_EVIDENCE").length,
|
|
108
|
-
highUncertainty: candidates.filter((candidate) => candidate.evidenceTier === "HIGH_UNCERTAINTY").length,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
policy: {
|
|
112
|
-
verdict: "REVIEW_REQUIRED",
|
|
113
|
-
autoDelete: false,
|
|
114
|
-
minConfidence,
|
|
115
|
-
includeTests,
|
|
116
|
-
includeClassified,
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
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$|(^|[/\\])test(?:_[^/\\]*)?\.py$/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
|
-
reason: "A direct go.mod requirement has no matching recorded Go import; build-tagged usage remains possible.",
|
|
44
|
-
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.`,
|
|
45
|
-
package: r.path,
|
|
46
|
-
version: r.version,
|
|
47
|
-
source: "internal",
|
|
48
|
-
fixHint: "go mod tidy (drops requires nothing imports)",
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
for (const [pkg, use] of missing) {
|
|
52
|
-
if (replaced.has(pkg)) continue;
|
|
53
|
-
const files = [...use.files];
|
|
54
|
-
if (files.every(inNonRuntimeRoot)) continue;
|
|
55
|
-
findings.push(makeFinding({
|
|
56
|
-
category: "unused",
|
|
57
|
-
rule: "missing-dep",
|
|
58
|
-
severity: "medium",
|
|
59
|
-
confidence: "medium",
|
|
60
|
-
title: `Missing Go module: ${pkg}`,
|
|
61
|
-
reason: "A recorded Go import has no matching direct go.mod requirement or replace entry.",
|
|
62
|
-
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.`,
|
|
63
|
-
package: pkg,
|
|
64
|
-
file: files[0],
|
|
65
|
-
line: use.lines.get(files[0]) || 0,
|
|
66
|
-
evidence: files.slice(0, 5).map((f) => ({ file: f, line: use.lines.get(f) || 0, snippet: "" })),
|
|
67
|
-
source: "internal",
|
|
68
|
-
fixHint: `go get ${pkg}`,
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
return { findings, declared };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---- Python: ecosystem:"PyPI" externalImports vs requirements/pyproject/Pipfile ----
|
|
75
|
-
// Import→dist naming is heuristic (yaml→PyYAML, python-X/X-python variants), so matching is GENEROUS for
|
|
76
|
-
// suppression and every unused finding stays low-confidence. CLI-only tools (pytest, black, gunicorn …)
|
|
77
|
-
// and stub/plugin conventions (types-*, *-stubs, pytest-*, flake8-*) are never flagged unused.
|
|
78
|
-
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 " +
|
|
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
|
-
const pyNorm = (n) => String(n || "").toLowerCase().replace(/[-_.]+/g, "-");
|
|
81
|
-
|
|
82
|
-
function computePyDepFindingsFlat({
|
|
83
|
-
externalImports = [], pyManifest = null, configTexts = new Map(),
|
|
84
|
-
managedDependencies = [], ignoredDependencies = [], nonRuntimeRoots = [],
|
|
85
|
-
} = {}) {
|
|
86
|
-
const findings = [];
|
|
87
|
-
const deps = (pyManifest && pyManifest.deps) || [];
|
|
88
|
-
const present = !!(pyManifest && pyManifest.present);
|
|
89
|
-
const declared = new Set(deps.map((d) => pyNorm(d.name)));
|
|
90
|
-
const managed = new Set((managedDependencies || []).map(pyNorm));
|
|
91
|
-
const ignored = new Set((ignoredDependencies || []).map(pyNorm));
|
|
92
|
-
const inNonRuntimeRoot = (file) => (nonRuntimeRoots || []).some((root) => {
|
|
93
|
-
const f = String(file || "").replace(/\\/g, "/"), r = String(root || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
94
|
-
return !!r && (f === r || f.startsWith(`${r}/`));
|
|
95
|
-
});
|
|
96
|
-
for (const name of managed) declared.add(name);
|
|
97
|
-
|
|
98
|
-
const used = new Map(); // top import → { dist, files:Set, lines:Map }
|
|
99
|
-
for (const e of externalImports) {
|
|
100
|
-
if (e.ecosystem !== "PyPI" || e.builtin || e.unresolved || !e.pkg) continue;
|
|
101
|
-
const top = String(e.spec || e.pkg).split(".")[0];
|
|
102
|
-
let u = used.get(top);
|
|
103
|
-
if (!u) used.set(top, (u = { dist: e.pkg, files: new Set(), lines: new Map() }));
|
|
104
|
-
u.files.add(e.file);
|
|
105
|
-
if (!u.lines.has(e.file)) u.lines.set(e.file, e.line || 0);
|
|
106
|
-
}
|
|
107
|
-
// generous match: does declared dist D cover import top t (dist guess g)?
|
|
108
|
-
const covers = (D, t, g) => {
|
|
109
|
-
const d = pyNorm(D), nt = pyNorm(t), ng = pyNorm(g);
|
|
110
|
-
return d === nt || d === ng || d === `python-${nt}` || d === `${nt}-python` || d === `${nt}-binary` || d.replace(/\d+$/, "") === nt.replace(/\d+$/, "");
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const configBlob = [...configTexts.values()].join("\n");
|
|
114
|
-
if (present) {
|
|
115
|
-
for (const d of deps) {
|
|
116
|
-
const n = pyNorm(d.name);
|
|
117
|
-
if (managed.has(n) || ignored.has(n)) continue;
|
|
118
|
-
if (d.buildSystem || PY_TOOL_DISTS.has(n) || /^types-|-stubs$|^pytest-|^flake8-|^sphinx/.test(n)) continue;
|
|
119
|
-
let hit = false;
|
|
120
|
-
for (const [top, u] of used) if (covers(d.name, top, u.dist)) { hit = true; break; }
|
|
121
|
-
if (hit || mentioned(configBlob, d.name)) continue;
|
|
122
|
-
findings.push(makeFinding({
|
|
123
|
-
category: "unused",
|
|
124
|
-
rule: "unused-dep",
|
|
125
|
-
severity: d.dev ? "info" : "low",
|
|
126
|
-
confidence: "low",
|
|
127
|
-
title: `Unused Python dependency: ${d.name}`,
|
|
128
|
-
reason: "No recorded Python import maps to this declared distribution; import-to-distribution mapping and plugin loading are heuristic.",
|
|
129
|
-
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.`,
|
|
130
|
-
package: d.name,
|
|
131
|
-
source: "internal",
|
|
132
|
-
fixHint: `remove "${d.name}" from the manifest after confirming nothing imports or shells out to it`,
|
|
133
|
-
}));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
for (const [top, u] of used) {
|
|
137
|
-
if (ignored.has(pyNorm(top)) || ignored.has(pyNorm(u.dist)) || managed.has(pyNorm(top)) || managed.has(pyNorm(u.dist))) continue;
|
|
138
|
-
let hit = false;
|
|
139
|
-
for (const D of declared) if (covers(D, top, u.dist)) { hit = true; break; }
|
|
140
|
-
if (hit) continue;
|
|
141
|
-
const files = [...u.files];
|
|
142
|
-
if (files.every(inNonRuntimeRoot)) continue;
|
|
143
|
-
const testOnly = files.every((f) => TEST_PATH_RE.test(f));
|
|
144
|
-
findings.push(makeFinding({
|
|
145
|
-
category: "unused",
|
|
146
|
-
rule: "missing-dep",
|
|
147
|
-
severity: present ? (testOnly ? "low" : "medium") : "low",
|
|
148
|
-
confidence: present ? "medium" : "low",
|
|
149
|
-
title: `Missing Python dependency: ${u.dist}`,
|
|
150
|
-
reason: present
|
|
151
|
-
? `A recorded Python import maps to "${u.dist}", but no declared distribution covers it.`
|
|
152
|
-
: `A recorded Python import maps to "${u.dist}", but no Python dependency manifest is present; a managed runtime may provide it.`,
|
|
153
|
-
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."}`,
|
|
154
|
-
package: u.dist,
|
|
155
|
-
file: files[0],
|
|
156
|
-
line: u.lines.get(files[0]) || 0,
|
|
157
|
-
evidence: files.slice(0, 5).map((f) => ({ file: f, line: u.lines.get(f) || 0, snippet: "" })),
|
|
158
|
-
source: "internal",
|
|
159
|
-
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`,
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
return { findings, declared, managed };
|
|
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
|
-
verification: finding.rule === "missing-dep"
|
|
198
|
-
? { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: scope.present ? "NOT_FOUND" : "NOT_PRESENT", files: scope.manifests || [] }, indexedSourceImports: { status: "FOUND", count: finding.evidence?.length || 1, files: (finding.evidence || []).map((item) => item.file) }, mapping: "PEP 503 plus bounded import-to-distribution aliases" }
|
|
199
|
-
: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "REVIEW_REQUIRED", manifestDeclaration: { status: "FOUND", files: scope.manifests || [] }, indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] }, mapping: "PEP 503 plus bounded import-to-distribution aliases" },
|
|
200
|
-
})));
|
|
201
|
-
for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
|
|
202
|
-
for (const name of result.managed) managed.add(name);
|
|
203
|
-
}
|
|
204
|
-
return { findings, declared, managed };
|
|
205
|
-
}
|