weavatrix 0.3.14 → 1.0.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 +231 -199
- 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 +8 -66
- 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,246 +0,0 @@
|
|
|
1
|
-
// graph-builder-coverage.js — coverage-report readers for the graph analysis. Parses Istanbul (summary/final),
|
|
2
|
-
// lcov, Python coverage.json, and Go coverage.out into per-file line-hit maps, resolving report paths onto the
|
|
3
|
-
// graph's known files. Split out of graph-builder-analysis.js (pure except for reading the repo's coverage files).
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, relative, resolve } from "node:path";
|
|
6
|
-
import { createRepoBoundary } from "../repo-path.js";
|
|
7
|
-
import { readTarpaulinCoverage } from "./coverage-reports-tarpaulin.js";
|
|
8
|
-
|
|
9
|
-
export function normRepoPath(value) {
|
|
10
|
-
return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function dirOfRepoPath(value) {
|
|
14
|
-
const p = normRepoPath(value);
|
|
15
|
-
const i = p.lastIndexOf("/");
|
|
16
|
-
return i >= 0 ? p.slice(0, i) : "";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function normalizeRepoParts(value) {
|
|
20
|
-
const out = [];
|
|
21
|
-
for (const part of normRepoPath(value).split("/")) {
|
|
22
|
-
if (!part || part === ".") continue;
|
|
23
|
-
if (part === "..") out.pop();
|
|
24
|
-
else out.push(part);
|
|
25
|
-
}
|
|
26
|
-
return out.join("/");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function pct01(value) {
|
|
30
|
-
const n = Number(value);
|
|
31
|
-
if (!Number.isFinite(n)) return null;
|
|
32
|
-
return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function pctFromCounts(covered, total) {
|
|
36
|
-
const c = Number(covered);
|
|
37
|
-
const t = Number(total);
|
|
38
|
-
if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
|
|
39
|
-
return Math.max(0, Math.min(1, c / t));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function resolveCoverageFile(rawPath, knownFiles, repoRoot) {
|
|
43
|
-
const known = (knownFiles || []).map(normalizeRepoParts).filter(Boolean);
|
|
44
|
-
const knownSet = new Set(known);
|
|
45
|
-
const candidates = [];
|
|
46
|
-
const add = (value) => {
|
|
47
|
-
const p = normalizeRepoParts(value);
|
|
48
|
-
if (p && !candidates.includes(p)) candidates.push(p);
|
|
49
|
-
};
|
|
50
|
-
const raw = String(rawPath || "").replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
|
|
51
|
-
const normalizedRaw = normRepoPath(raw);
|
|
52
|
-
add(normalizedRaw);
|
|
53
|
-
add(normalizedRaw.replace(/^\.\//, ""));
|
|
54
|
-
if (repoRoot) {
|
|
55
|
-
try {
|
|
56
|
-
const rel = normRepoPath(relative(repoRoot, raw));
|
|
57
|
-
if (rel && !rel.startsWith("..")) add(rel);
|
|
58
|
-
} catch {
|
|
59
|
-
/* best effort */
|
|
60
|
-
}
|
|
61
|
-
const rootPrefix = `${normRepoPath(resolve(repoRoot)).toLowerCase()}/`;
|
|
62
|
-
const lowerRaw = normalizedRaw.toLowerCase();
|
|
63
|
-
if (lowerRaw.startsWith(rootPrefix)) add(normalizedRaw.slice(rootPrefix.length));
|
|
64
|
-
}
|
|
65
|
-
for (const candidate of candidates) if (knownSet.has(candidate)) return candidate;
|
|
66
|
-
const lower = normalizedRaw.toLowerCase();
|
|
67
|
-
const suffixHit = known
|
|
68
|
-
.slice()
|
|
69
|
-
.sort((a, b) => b.length - a.length)
|
|
70
|
-
.find((file) => lower.endsWith(`/${file.toLowerCase()}`) || lower === file.toLowerCase());
|
|
71
|
-
return suffixHit || candidates[0] || "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function mergeCoverageEntry(map, rawPath, entry, knownFiles, repoRoot) {
|
|
75
|
-
const file = resolveCoverageFile(rawPath, knownFiles, repoRoot);
|
|
76
|
-
if (!file) return;
|
|
77
|
-
const current = map.get(file) || {};
|
|
78
|
-
map.set(file, {
|
|
79
|
-
pct: entry.pct != null ? entry.pct : current.pct ?? null,
|
|
80
|
-
total: entry.total != null ? entry.total : current.total ?? null,
|
|
81
|
-
covered: entry.covered != null ? entry.covered : current.covered ?? null,
|
|
82
|
-
lines: entry.lines || current.lines || null,
|
|
83
|
-
source: current.source && entry.source ? `${current.source}, ${entry.source}` : entry.source || current.source || ""
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function addLineHit(lines, line, hit) {
|
|
88
|
-
const value = Number(line);
|
|
89
|
-
if (!Number.isFinite(value) || value <= 0) return;
|
|
90
|
-
const n = Math.round(value);
|
|
91
|
-
if (n < 1) return;
|
|
92
|
-
const current = lines.get(n);
|
|
93
|
-
lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function parseIstanbulSummary(map, filePath, knownFiles, repoRoot) {
|
|
97
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
98
|
-
for (const [raw, value] of Object.entries(json || {})) {
|
|
99
|
-
if (raw === "total" || !value || typeof value !== "object") continue;
|
|
100
|
-
const lines = value.lines || value.statements || {};
|
|
101
|
-
mergeCoverageEntry(map, raw, {
|
|
102
|
-
pct: pct01(lines.pct),
|
|
103
|
-
total: Number.isFinite(Number(lines.total)) ? Number(lines.total) : null,
|
|
104
|
-
covered: Number.isFinite(Number(lines.covered)) ? Number(lines.covered) : null,
|
|
105
|
-
source: "coverage-summary.json"
|
|
106
|
-
}, knownFiles, repoRoot);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseIstanbulFinal(map, filePath, knownFiles, repoRoot) {
|
|
111
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
112
|
-
for (const [raw, record] of Object.entries(json || {})) {
|
|
113
|
-
if (!record || typeof record !== "object") continue;
|
|
114
|
-
const statementMap = record.statementMap || {};
|
|
115
|
-
const hits = record.s || {};
|
|
116
|
-
const lines = new Map();
|
|
117
|
-
for (const [id, loc] of Object.entries(statementMap)) {
|
|
118
|
-
const hit = Number(hits[id]) || 0;
|
|
119
|
-
const start = Math.max(1, Math.round(Number(loc?.start?.line) || 0));
|
|
120
|
-
const end = Math.max(start, Math.round(Number(loc?.end?.line) || start));
|
|
121
|
-
for (let line = start; line <= end; line++) addLineHit(lines, line, hit);
|
|
122
|
-
}
|
|
123
|
-
const total = lines.size;
|
|
124
|
-
const covered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
125
|
-
mergeCoverageEntry(map, record.path || raw, {
|
|
126
|
-
pct: pctFromCounts(covered, total),
|
|
127
|
-
total,
|
|
128
|
-
covered,
|
|
129
|
-
lines,
|
|
130
|
-
source: "coverage-final.json"
|
|
131
|
-
}, knownFiles, repoRoot);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function parseLcov(map, filePath, knownFiles, repoRoot) {
|
|
136
|
-
const flush = (record) => {
|
|
137
|
-
if (!record || !record.file) return;
|
|
138
|
-
const total = record.total ?? record.lines.size;
|
|
139
|
-
const covered = record.covered ?? [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
140
|
-
mergeCoverageEntry(map, record.file, {
|
|
141
|
-
pct: pctFromCounts(covered, total),
|
|
142
|
-
total,
|
|
143
|
-
covered,
|
|
144
|
-
lines: record.lines,
|
|
145
|
-
source: "lcov.info"
|
|
146
|
-
}, knownFiles, repoRoot);
|
|
147
|
-
};
|
|
148
|
-
let current = null;
|
|
149
|
-
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
150
|
-
const line = rawLine.trim();
|
|
151
|
-
if (line.startsWith("SF:")) {
|
|
152
|
-
flush(current);
|
|
153
|
-
current = { file: line.slice(3), lines: new Map(), total: null, covered: null };
|
|
154
|
-
} else if (current && line.startsWith("DA:")) {
|
|
155
|
-
const [lineNo, count] = line.slice(3).split(",");
|
|
156
|
-
addLineHit(current.lines, lineNo, count);
|
|
157
|
-
} else if (current && line.startsWith("LF:")) {
|
|
158
|
-
current.total = Number(line.slice(3)) || current.total;
|
|
159
|
-
} else if (current && line.startsWith("LH:")) {
|
|
160
|
-
current.covered = Number(line.slice(3)) || current.covered;
|
|
161
|
-
} else if (line === "end_of_record") {
|
|
162
|
-
flush(current);
|
|
163
|
-
current = null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
flush(current);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function parseCoveragePyJson(map, filePath, knownFiles, repoRoot) {
|
|
170
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
171
|
-
for (const [raw, file] of Object.entries(json?.files || {})) {
|
|
172
|
-
const lines = new Map();
|
|
173
|
-
for (const line of file.executed_lines || []) addLineHit(lines, line, 1);
|
|
174
|
-
for (const line of file.missing_lines || []) if (!lines.has(Number(line))) addLineHit(lines, line, 0);
|
|
175
|
-
const summary = file.summary || {};
|
|
176
|
-
const total = Number(summary.num_statements) || lines.size;
|
|
177
|
-
const covered = Number(summary.covered_lines) || [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
178
|
-
mergeCoverageEntry(map, raw, {
|
|
179
|
-
pct: pct01(summary.percent_covered ?? summary.percent_covered_display) ?? pctFromCounts(covered, total),
|
|
180
|
-
total,
|
|
181
|
-
covered,
|
|
182
|
-
lines,
|
|
183
|
-
source: "coverage.json"
|
|
184
|
-
}, knownFiles, repoRoot);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseGoCoverage(map, filePath, knownFiles, repoRoot) {
|
|
189
|
-
const byFile = new Map();
|
|
190
|
-
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
191
|
-
const line = rawLine.trim();
|
|
192
|
-
if (!line || line.startsWith("mode:")) continue;
|
|
193
|
-
const m = line.match(/^(.+):(\d+)\.\d+,(\d+)\.\d+\s+\d+\s+(\d+)$/);
|
|
194
|
-
if (!m) continue;
|
|
195
|
-
const record = byFile.get(m[1]) || { lines: new Map() };
|
|
196
|
-
const start = Math.max(1, Number(m[2]) || 1);
|
|
197
|
-
const end = Math.max(start, Number(m[3]) || start);
|
|
198
|
-
for (let n = start; n <= end; n++) addLineHit(record.lines, n, Number(m[4]) || 0);
|
|
199
|
-
byFile.set(m[1], record);
|
|
200
|
-
}
|
|
201
|
-
for (const [raw, record] of byFile) {
|
|
202
|
-
const total = record.lines.size;
|
|
203
|
-
const covered = [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
204
|
-
mergeCoverageEntry(map, raw, {
|
|
205
|
-
pct: pctFromCounts(covered, total),
|
|
206
|
-
total,
|
|
207
|
-
covered,
|
|
208
|
-
lines: record.lines,
|
|
209
|
-
source: "coverage.out"
|
|
210
|
-
}, knownFiles, repoRoot);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function parseTarpaulinJson(map, filePath, knownFiles, repoRoot) {
|
|
215
|
-
for (const record of readTarpaulinCoverage(filePath)) {
|
|
216
|
-
mergeCoverageEntry(map, record.path, record.entry, knownFiles, repoRoot);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function readCoverageForRepo(repoRoot, knownFiles) {
|
|
221
|
-
const out = new Map();
|
|
222
|
-
if (!repoRoot) return out;
|
|
223
|
-
const boundary = createRepoBoundary(repoRoot);
|
|
224
|
-
const candidates = [
|
|
225
|
-
["coverage/coverage-summary.json", parseIstanbulSummary],
|
|
226
|
-
["coverage/coverage-final.json", parseIstanbulFinal],
|
|
227
|
-
["coverage/lcov.info", parseLcov],
|
|
228
|
-
["lcov.info", parseLcov],
|
|
229
|
-
["coverage/coverage.json", parseCoveragePyJson],
|
|
230
|
-
["coverage.json", parseCoveragePyJson],
|
|
231
|
-
["tarpaulin-report.json", parseTarpaulinJson],
|
|
232
|
-
["coverage/tarpaulin-report.json", parseTarpaulinJson],
|
|
233
|
-
["coverage.out", parseGoCoverage],
|
|
234
|
-
["cover.out", parseGoCoverage]
|
|
235
|
-
];
|
|
236
|
-
for (const [candidate, parser] of candidates) {
|
|
237
|
-
const resolved = boundary.resolve(candidate);
|
|
238
|
-
if (!resolved.ok || !existsSync(resolved.path)) continue;
|
|
239
|
-
try {
|
|
240
|
-
parser(out, resolved.path, knownFiles, repoRoot);
|
|
241
|
-
} catch {
|
|
242
|
-
/* malformed/foreign coverage report: ignore */
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const MAX_REPRESENTATIVE_CYCLE_FILES = 64;
|
|
2
|
-
|
|
3
|
-
// Keep the complete closed route for normal cycles. Very large SCCs still get a closed, deterministic
|
|
4
|
-
// head/tail route without letting one finding consume the whole audit response.
|
|
5
|
-
export function formatRepresentativeCycle(cycle, maxFiles = MAX_REPRESENTATIVE_CYCLE_FILES) {
|
|
6
|
-
const raw = Array.isArray(cycle) ? cycle.map(String).filter(Boolean) : [];
|
|
7
|
-
if (!raw.length) return "";
|
|
8
|
-
const closed = raw.length > 1 && raw[0] === raw[raw.length - 1] ? raw : [...raw, raw[0]];
|
|
9
|
-
const cap = Math.max(4, Number(maxFiles) || MAX_REPRESENTATIVE_CYCLE_FILES);
|
|
10
|
-
if (closed.length <= cap + 1) return closed.join(" → ");
|
|
11
|
-
const side = Math.max(2, Math.floor(cap / 2));
|
|
12
|
-
const omitted = closed.length - side * 2;
|
|
13
|
-
return `${closed.slice(0, side).join(" → ")} → … ${omitted} file(s) omitted … → ${closed.slice(-side).join(" → ")}`;
|
|
14
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import {readFileSync} from 'node:fs'
|
|
2
|
-
import {extname} from 'node:path'
|
|
3
|
-
import ts from 'typescript'
|
|
4
|
-
import {createRepoBoundary} from '../repo-path.js'
|
|
5
|
-
|
|
6
|
-
const lineOfNode = (node) => Number(String(node?.source_location || '').match(/^L(\d+)/i)?.[1] || String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
|
|
7
|
-
const fileOfNode = (node) => String(node?.source_file || String(node?.id || '').split('#')[0]).replace(/\\/g, '/')
|
|
8
|
-
const cleanName = (value) => String(value || '').replace(/\(\)$/, '').split('.').pop()
|
|
9
|
-
const callName = (node) => node.expression?.name?.text || node.expression?.text || node.expression?.escapedText || ''
|
|
10
|
-
|
|
11
|
-
function scriptKind(file) {
|
|
12
|
-
const ext = extname(file).toLowerCase()
|
|
13
|
-
if (ext === '.tsx') return ts.ScriptKind.TSX
|
|
14
|
-
if (ext === '.jsx') return ts.ScriptKind.JSX
|
|
15
|
-
if (ext === '.ts' || ext === '.mts' || ext === '.cts') return ts.ScriptKind.TS
|
|
16
|
-
return ts.ScriptKind.JS
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parse(cache, boundary, file) {
|
|
20
|
-
if (cache.has(file)) return cache.get(file)
|
|
21
|
-
if (!/\.[cm]?[jt]sx?$/i.test(file)) return null
|
|
22
|
-
const resolved = boundary.resolve(file)
|
|
23
|
-
if (!resolved.ok) return null
|
|
24
|
-
try {
|
|
25
|
-
const source = ts.createSourceFile(file, readFileSync(resolved.path, 'utf8'), ts.ScriptTarget.Latest, true, scriptKind(file))
|
|
26
|
-
cache.set(file, source)
|
|
27
|
-
return source
|
|
28
|
-
} catch { return null }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function parametersFor(source, target) {
|
|
32
|
-
const expected = cleanName(target?.label)
|
|
33
|
-
const expectedLine = lineOfNode(target)
|
|
34
|
-
const choices = []
|
|
35
|
-
const visit = (node) => {
|
|
36
|
-
if (Array.isArray(node.parameters) && node.name) {
|
|
37
|
-
const name = cleanName(node.name.text || node.name.getText?.(source))
|
|
38
|
-
if (!expected || name === expected) {
|
|
39
|
-
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
|
|
40
|
-
choices.push({line, parameters: node.parameters.map((item) => item.name.getText(source).slice(0, 80))})
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
ts.forEachChild(node, visit)
|
|
44
|
-
}
|
|
45
|
-
visit(source)
|
|
46
|
-
choices.sort((a, b) => Math.abs(a.line - expectedLine) - Math.abs(b.line - expectedLine))
|
|
47
|
-
return choices[0]?.parameters || []
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function callsAt(source, line, expectedName) {
|
|
51
|
-
const found = []
|
|
52
|
-
const visit = (node) => {
|
|
53
|
-
if (ts.isCallExpression(node)) {
|
|
54
|
-
const at = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
|
|
55
|
-
if ((!line || at === line) && (!expectedName || cleanName(callName(node)) === expectedName)) found.push({node, line: at})
|
|
56
|
-
}
|
|
57
|
-
ts.forEachChild(node, visit)
|
|
58
|
-
}
|
|
59
|
-
visit(source)
|
|
60
|
-
return found
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Bounded interprocedural evidence: maps arguments at graph callsites to the callee's declared
|
|
64
|
-
// parameters. This is not a control-flow graph, value propagation, or taint-analysis claim.
|
|
65
|
-
export function extractCallArgumentEvidence({graph, repoRoot, seedIds = [], depth = 2, maxEdges = 40} = {}) {
|
|
66
|
-
const boundary = createRepoBoundary(repoRoot)
|
|
67
|
-
const cache = new Map()
|
|
68
|
-
const queue = (seedIds || []).map((id) => ({id: String(id), hop: 0}))
|
|
69
|
-
const visited = new Set()
|
|
70
|
-
const seenCalls = new Set()
|
|
71
|
-
const edges = []
|
|
72
|
-
let unsupported = 0
|
|
73
|
-
while (queue.length && edges.length < maxEdges) {
|
|
74
|
-
const current = queue.shift()
|
|
75
|
-
if (visited.has(current.id) || current.hop >= depth) continue
|
|
76
|
-
visited.add(current.id)
|
|
77
|
-
const calls = [
|
|
78
|
-
...(graph.out.get(current.id) || []).filter((edge) => edge.relation === 'calls')
|
|
79
|
-
.map((edge) => ({fromId: current.id, toId: String(edge.id), line: Number(edge.line) || 0, nextId: String(edge.id)})),
|
|
80
|
-
...(graph.inn.get(current.id) || []).filter((edge) => edge.relation === 'calls')
|
|
81
|
-
.map((edge) => ({fromId: String(edge.id), toId: current.id, line: Number(edge.line) || 0, nextId: String(edge.id)})),
|
|
82
|
-
]
|
|
83
|
-
for (const callEdge of calls) {
|
|
84
|
-
const callKey = `${callEdge.fromId}\0${callEdge.toId}\0${callEdge.line}`
|
|
85
|
-
if (seenCalls.has(callKey)) continue
|
|
86
|
-
seenCalls.add(callKey)
|
|
87
|
-
const from = graph.byId.get(callEdge.fromId)
|
|
88
|
-
const to = graph.byId.get(callEdge.toId)
|
|
89
|
-
const callerSource = parse(cache, boundary, fileOfNode(from))
|
|
90
|
-
const targetSource = parse(cache, boundary, fileOfNode(to))
|
|
91
|
-
if (!from || !to || !callerSource || !targetSource) { unsupported++; continue }
|
|
92
|
-
const params = parametersFor(targetSource, to)
|
|
93
|
-
const callsites = callsAt(callerSource, callEdge.line, cleanName(to.label))
|
|
94
|
-
if (!callsites.length) { unsupported++; continue }
|
|
95
|
-
for (const call of callsites.slice(0, 3)) {
|
|
96
|
-
edges.push({
|
|
97
|
-
from: callEdge.fromId, to: callEdge.toId, hop: current.hop + 1,
|
|
98
|
-
file: fileOfNode(from), line: call.line,
|
|
99
|
-
arguments: call.node.arguments.slice(0, 12).map((argument, index) => ({
|
|
100
|
-
index, expression: argument.getText(callerSource).slice(0, 160), parameter: params[index] || null,
|
|
101
|
-
})),
|
|
102
|
-
state: 'EXTRACTED',
|
|
103
|
-
})
|
|
104
|
-
if (edges.length >= maxEdges) break
|
|
105
|
-
}
|
|
106
|
-
if (current.hop + 1 < depth) queue.push({id: callEdge.nextId, hop: current.hop + 1})
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
model: 'bounded call-argument-to-parameter evidence (not CFG or taint analysis)',
|
|
111
|
-
status: edges.length ? (unsupported ? 'PARTIAL' : 'COMPLETE') : 'UNAVAILABLE',
|
|
112
|
-
edges, unsupportedEdges: unsupported, capped: edges.length >= maxEdges,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
// Portable, language-agnostic DEAD-code checker. A symbol is DEAD when it has NO inbound graph edge (nothing
|
|
2
|
-
// calls/imports/references/inherits it) AND its bare name appears NOWHERE in the repo outside its own file.
|
|
3
|
-
// A file is DEAD when nothing imports it, it isn't an entry point, and all its symbols are dead.
|
|
4
|
-
//
|
|
5
|
-
// Pure core `computeDead(graph, sources)` (sources = Map<fileRel, text>) works on any graph-builder-schema graph
|
|
6
|
-
// and is fully testable with no filesystem. It only needs {nodes, links} + source text. See [[graph-builder-internalization]].
|
|
7
|
-
import { posix } from "node:path";
|
|
8
|
-
import { isStructuralRelation } from "../graph/relations.js";
|
|
9
|
-
import { createSqlDeadVerdict } from "../graph/builder/lang-sql.js";
|
|
10
|
-
import { createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
11
|
-
|
|
12
|
-
const IDENT_RE = /[A-Za-z_$][\w$]*/g;
|
|
13
|
-
const bareName = (label) => String(label || "").replace(/\s*\(.*$/, "").replace(/[()]/g, "").trim();
|
|
14
|
-
// Ignore comment-only lexical mentions, which are documentation rather than callers. Keep strings on
|
|
15
|
-
// purpose: registries/reflection often address a live symbol by name, and static liveness must stay
|
|
16
|
-
// conservative there. This only strips comments that start a logical line, avoiding a language parser
|
|
17
|
-
// guess around inline comment markers, regex literals, or URLs.
|
|
18
|
-
const lexicalEvidenceText = (value) => {
|
|
19
|
-
let inBlockComment = false;
|
|
20
|
-
return String(value || "").split(/\r?\n/).map((line) => {
|
|
21
|
-
let rest = line;
|
|
22
|
-
while (true) {
|
|
23
|
-
if (inBlockComment) {
|
|
24
|
-
const end = rest.indexOf("*/");
|
|
25
|
-
if (end < 0) return "";
|
|
26
|
-
inBlockComment = false;
|
|
27
|
-
rest = rest.slice(end + 2);
|
|
28
|
-
}
|
|
29
|
-
const trimmed = rest.trimStart();
|
|
30
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("#")) return "";
|
|
31
|
-
if (!trimmed.startsWith("/*")) return rest;
|
|
32
|
-
inBlockComment = true;
|
|
33
|
-
rest = trimmed.slice(2);
|
|
34
|
-
}
|
|
35
|
-
}).join("\n");
|
|
36
|
-
};
|
|
37
|
-
// entry surfaces are never dead even with no inbound edge (framework/CLI/HTTP enter them externally).
|
|
38
|
-
// Exported for internal-audit.js (reachability entry set) — keep the two in lockstep.
|
|
39
|
-
export const ENTRY_FILE = /(^|[\\/])(index|main|app|server|cli|cmd|bootstrap|entry|run|__main__|manage|wsgi|asgi|setup|conftest)\.[a-z0-9]+$|(^|[\\/])(bin|cmd)[\\/]|(^|[\\/])main\.go$/i;
|
|
40
|
-
const defaultPathClassifier = createPathClassifier(null);
|
|
41
|
-
const isTestFile = (file) => hasPathClass(defaultPathClassifier.explain(file), "test", "e2e");
|
|
42
|
-
|
|
43
|
-
// Framework-owned entry modules are invoked by convention rather than a source import. Keep this narrow:
|
|
44
|
-
// these are Next.js App/Pages Router surfaces and framework metadata files, not every file under `app/`.
|
|
45
|
-
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;
|
|
46
|
-
const RUST_ENTRY_FILE = /(^|\/)(?:build\.rs|src\/(?:lib|main)\.rs)$/i;
|
|
47
|
-
export const isFrameworkEntryFile = (file) => {
|
|
48
|
-
const normalized = String(file || "").replace(/\\/g, "/");
|
|
49
|
-
return NEXT_ENTRY_FILE.test(normalized) || RUST_ENTRY_FILE.test(normalized);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const lineOfNode = (n) => {
|
|
53
|
-
const m = /@(\d+)$/.exec(String(n.id || "")) || /L(\d+)/.exec(String(n.source_location || ""));
|
|
54
|
-
return m ? Number(m[1]) : 0;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// Older graphs marked every method of an exported class as exported. Verify that an `exported` node is
|
|
58
|
-
// actually a module-surface declaration before reporting it. New graphs expose symbol_kind/member_of and
|
|
59
|
-
// already keep members unexported, but the source check preserves correctness for cached v0.1.2 graphs.
|
|
60
|
-
function hasModuleExport(source, node, name) {
|
|
61
|
-
if (node.member_of || node.symbol_kind === "method") return false;
|
|
62
|
-
const lines = String(source || "").split(/\r?\n/);
|
|
63
|
-
const line = lines[Math.max(0, lineOfNode(node) - 1)] || "";
|
|
64
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
-
const direct = new RegExp(`\\bexport\\s+(?:default\\s+)?(?:(?:declare|abstract|async)\\s+)*(?:function|class|const|let|var|enum|interface|type|namespace)\\s+${escaped}\\b`);
|
|
66
|
-
if (direct.test(line) || direct.test(String(source || ""))) return true;
|
|
67
|
-
const exportDefault = new RegExp(`\\bexport\\s+default\\s+${escaped}\\b`);
|
|
68
|
-
if (exportDefault.test(String(source || ""))) return true;
|
|
69
|
-
// `export { local }`, `export { local as publicName }` and CommonJS explicit exports.
|
|
70
|
-
const exportList = new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`, "s");
|
|
71
|
-
const commonJs = new RegExp(`(?:module\\.exports\\s*=\\s*\\{[^}]*\\b${escaped}\\b|(?:module\\.exports|exports)\\.${escaped}\\s*=)`, "s");
|
|
72
|
-
return exportList.test(String(source || "")) || commonJs.test(String(source || ""));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function namespaceConsumedFiles(sources, graph) {
|
|
76
|
-
const files = new Set(sources.keys());
|
|
77
|
-
const consumed = new Set();
|
|
78
|
-
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
79
|
-
const resolve = (from, spec) => {
|
|
80
|
-
if (!spec.startsWith(".")) return "";
|
|
81
|
-
const base = posix.normalize(posix.join(posix.dirname(from.replace(/\\/g, "/")), spec));
|
|
82
|
-
for (const ext of extensions) {
|
|
83
|
-
const p = `${base}${ext}`;
|
|
84
|
-
if (files.has(p)) return p;
|
|
85
|
-
}
|
|
86
|
-
for (const ext of extensions.slice(1)) {
|
|
87
|
-
const p = `${base}/index${ext}`;
|
|
88
|
-
if (files.has(p)) return p;
|
|
89
|
-
}
|
|
90
|
-
return "";
|
|
91
|
-
};
|
|
92
|
-
for (const [file, textValue] of sources) {
|
|
93
|
-
const text = String(textValue || "");
|
|
94
|
-
const patterns = [
|
|
95
|
-
/\bimport\s+\*\s+as\s+[A-Za-z_$][\w$]*\s+from\s*(["'])([^"']+)\1/g,
|
|
96
|
-
/\bexport\s+\*\s+from\s*(["'])([^"']+)\1/g,
|
|
97
|
-
/\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*require\(\s*(["'])([^"']+)\1\s*\)/g,
|
|
98
|
-
];
|
|
99
|
-
for (const re of patterns) {
|
|
100
|
-
let m;
|
|
101
|
-
while ((m = re.exec(text))) {
|
|
102
|
-
const target = resolve(file, m[2]);
|
|
103
|
-
if (target) consumed.add(target);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// New graphs retain the exact specifier on file import edges, which also resolves namespace imports
|
|
108
|
-
// expressed through tsconfig aliases (the relative-path fallback above intentionally cannot guess those).
|
|
109
|
-
for (const link of graph.links || []) {
|
|
110
|
-
if (link.relation !== "imports" && link.relation !== "re_exports") continue;
|
|
111
|
-
const from = String(link.source?.id || link.source || ""), target = String(link.target?.id || link.target || "");
|
|
112
|
-
if (!link.specifier || from.includes("#") || target.includes("#") || !sources.has(from)) continue;
|
|
113
|
-
const spec = String(link.specifier).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
|
-
const text = String(sources.get(from) || "");
|
|
115
|
-
if (new RegExp(`(?:import\\s+\\*\\s+as\\s+[A-Za-z_$][\\w$]*\\s+from|export\\s+\\*\\s+from)\\s*["']${spec}["']`).test(text)) consumed.add(target);
|
|
116
|
-
}
|
|
117
|
-
return consumed;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
121
|
-
const nodes = graph.nodes || [], links = graph.links || [];
|
|
122
|
-
const ep = (v) => (v && typeof v === "object" ? v.id : v);
|
|
123
|
-
const inbound = new Set();
|
|
124
|
-
const inboundSources = new Map();
|
|
125
|
-
const nodesById = new Map(nodes.map((node) => [String(node.id), node]));
|
|
126
|
-
for (const l of links) if (!isStructuralRelation(l.relation)) {
|
|
127
|
-
const target = String(ep(l.target));
|
|
128
|
-
const source = String(ep(l.source));
|
|
129
|
-
inbound.add(target);
|
|
130
|
-
const values = inboundSources.get(target) || [];
|
|
131
|
-
values.push(source);
|
|
132
|
-
inboundSources.set(target, values);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// whole-repo identifier frequency: a symbol whose name appears MORE than once total (its definition + at least
|
|
136
|
-
// one use, same-file OR cross-file) is referenced. Errs toward "alive" (common-named symbols never flagged).
|
|
137
|
-
const globalFreq = new Map();
|
|
138
|
-
for (const [, text] of sources) for (const m of lexicalEvidenceText(text).matchAll(IDENT_RE)) { const n = m[0]; globalFreq.set(n, (globalFreq.get(n) || 0) + 1); }
|
|
139
|
-
|
|
140
|
-
const symById = new Map();
|
|
141
|
-
const symsByFile = new Map();
|
|
142
|
-
for (const n of nodes) {
|
|
143
|
-
if (!String(n.id).includes("#")) continue;
|
|
144
|
-
symById.set(n.id, n);
|
|
145
|
-
(symsByFile.get(n.source_file) || symsByFile.set(n.source_file, []).get(n.source_file)).push(n);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const symbolNames = new Set([...symById.values()].map((node) => bareName(node.label)).filter(Boolean));
|
|
149
|
-
const occurrenceFiles = new Map();
|
|
150
|
-
const occurrenceCounts = new Map();
|
|
151
|
-
for (const [file, text] of sources) for (const match of lexicalEvidenceText(text).matchAll(IDENT_RE)) {
|
|
152
|
-
const name = match[0];
|
|
153
|
-
if (!symbolNames.has(name)) continue;
|
|
154
|
-
const files = occurrenceFiles.get(name) || new Set();
|
|
155
|
-
files.add(file);
|
|
156
|
-
occurrenceFiles.set(name, files);
|
|
157
|
-
const counts = occurrenceCounts.get(name) || new Map();
|
|
158
|
-
counts.set(file, (counts.get(file) || 0) + 1);
|
|
159
|
-
occurrenceCounts.set(name, counts);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const declarationCounts = new Map();
|
|
163
|
-
for (const node of symById.values()) {
|
|
164
|
-
const name = bareName(node.label);
|
|
165
|
-
if (!name) continue;
|
|
166
|
-
const key = `${node.source_file}\0${name}`;
|
|
167
|
-
declarationCounts.set(key, (declarationCounts.get(key) || 0) + 1);
|
|
168
|
-
}
|
|
169
|
-
const exactReferenceIds = new Set(graph.precisionReferenceSymbols || []);
|
|
170
|
-
const exactProductionReferenceIds = new Set(graph.precisionProductionReferenceSymbols || []);
|
|
171
|
-
const exactTestReferenceIds = new Set(graph.precisionTestReferenceSymbols || []);
|
|
172
|
-
|
|
173
|
-
// decorated defs (@app.route/@app.event/@pytest.fixture…) are entered by the framework: trust the
|
|
174
|
-
// builder's flag when present, else walk the source line(s) above the definition (graph-builder graphs).
|
|
175
|
-
const lineCache = new Map();
|
|
176
|
-
const linesOf = (f) => { let l = lineCache.get(f); if (!l) { l = String(sources.get(f) || "").split(/\r?\n/); lineCache.set(f, l); } return l; };
|
|
177
|
-
const symLine = (n) => { const m = /@(\d+)$/.exec(String(n.id)) || /L(\d+)/.exec(String(n.source_location || "")); return m ? Number(m[1]) : 0; };
|
|
178
|
-
const isDecorated = (n) => {
|
|
179
|
-
if (n.decorated) return true;
|
|
180
|
-
const ln = symLine(n);
|
|
181
|
-
if (!ln) return false;
|
|
182
|
-
const lines = linesOf(n.source_file);
|
|
183
|
-
for (let i = ln - 2; i >= 0 && i >= ln - 6; i--) {
|
|
184
|
-
const t = (lines[i] || "").trim();
|
|
185
|
-
if (t.startsWith("@")) return true;
|
|
186
|
-
if (t === "" || t.startsWith("#")) continue;
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
return false;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const isReferenced = (n) => {
|
|
193
|
-
if (inbound.has(n.id)) return true; // a real graph edge targets it
|
|
194
|
-
if (exactReferenceIds.has(String(n.id))) return true; // revision-bound point-query evidence found a caller
|
|
195
|
-
const name = bareName(n.label);
|
|
196
|
-
if (!name || !/^[A-Za-z_$]/.test(name)) return true; // selectors/odd labels → don't flag
|
|
197
|
-
if (/^__\w+__$/.test(name)) return true; // dunders are invoked implicitly (with/str/==/iter…), never spelled
|
|
198
|
-
if ((globalFreq.get(name) || 0) > 1) return true; // name appears beyond its single definition
|
|
199
|
-
return isDecorated(n); // framework-registered via decorator
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const sqlVerdict = createSqlDeadVerdict({ nodes, links, ep, bareName }); // gated SQL verdicts — ORM blindness stays silent
|
|
203
|
-
const deadSymbols = [];
|
|
204
|
-
for (const n of symById.values()) {
|
|
205
|
-
if (isReferenced(n) || sqlVerdict.veto(n)) continue;
|
|
206
|
-
deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file) || n.test_surface === true, reason: sqlVerdict.reason(n) || "no inbound edge and name unreferenced outside its file" });
|
|
207
|
-
}
|
|
208
|
-
const deadSet = new Set(deadSymbols.map((s) => s.id));
|
|
209
|
-
|
|
210
|
-
// A production declaration consumed only by tests is live to the raw graph but dead to production.
|
|
211
|
-
// Keep it in a separate review class: this is useful evidence, never an automatic delete verdict.
|
|
212
|
-
const testOnlySymbols = [];
|
|
213
|
-
const consumerFileOf = (id) => nodesById.get(id)?.source_file || (id.includes("#") ? id.split("#")[0] : id);
|
|
214
|
-
const isTestConsumer = (id) => nodesById.get(id)?.test_surface === true || isTestFile(consumerFileOf(id));
|
|
215
|
-
for (const n of symById.values()) {
|
|
216
|
-
if (deadSet.has(n.id) || isTestFile(n.source_file) || n.test_surface === true || isDecorated(n)) continue;
|
|
217
|
-
const sourcesForSymbol = inboundSources.get(String(n.id)) || [];
|
|
218
|
-
const hasTestInbound = sourcesForSymbol.some(isTestConsumer);
|
|
219
|
-
const hasProductionInbound = sourcesForSymbol.some((id) => consumerFileOf(id) && !isTestConsumer(id));
|
|
220
|
-
const name = bareName(n.label);
|
|
221
|
-
const occurrenceSet = occurrenceFiles.get(name) || new Set();
|
|
222
|
-
const externalOccurrences = [...occurrenceSet].filter((file) => file !== n.source_file);
|
|
223
|
-
const localOccurrences = occurrenceCounts.get(name)?.get(n.source_file) || 0;
|
|
224
|
-
const localDeclarations = declarationCounts.get(`${n.source_file}\0${name}`) || 0;
|
|
225
|
-
const hasLocalProductionUse = localOccurrences > localDeclarations;
|
|
226
|
-
const lexicalTestOnly = externalOccurrences.length > 0 && externalOccurrences.every((file) => isTestFile(file));
|
|
227
|
-
const hasExactProductionInbound = exactProductionReferenceIds.has(String(n.id));
|
|
228
|
-
const hasExactTestInbound = exactTestReferenceIds.has(String(n.id));
|
|
229
|
-
if (hasProductionInbound || hasExactProductionInbound || hasLocalProductionUse
|
|
230
|
-
|| (!hasTestInbound && !hasExactTestInbound && !lexicalTestOnly)) continue;
|
|
231
|
-
testOnlySymbols.push({
|
|
232
|
-
id: n.id,
|
|
233
|
-
file: n.source_file,
|
|
234
|
-
label: n.label,
|
|
235
|
-
test: false,
|
|
236
|
-
reason: "referenced only from test/e2e code; no production consumer was found",
|
|
237
|
-
testConsumerFiles: [...new Set(sourcesForSymbol.filter(isTestConsumer).map(consumerFileOf).filter(Boolean))].sort(),
|
|
238
|
-
evidence: hasTestInbound ? "graph" : hasExactTestInbound ? "exact-semantic" : "lexical",
|
|
239
|
-
publicApi: n.exported === true || ["public", "protected"].includes(String(n.visibility || "").toLowerCase()),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const deadFiles = [];
|
|
244
|
-
for (const n of nodes) {
|
|
245
|
-
if (String(n.id).includes("#")) continue; // file nodes only
|
|
246
|
-
if (inbound.has(n.id) || ENTRY_FILE.test(n.source_file) || isFrameworkEntryFile(n.source_file) || entrySet.has(n.source_file)) continue;
|
|
247
|
-
const syms = symsByFile.get(n.source_file) || [];
|
|
248
|
-
if (syms.length && syms.every((s) => deadSet.has(s.id))) deadFiles.push({ file: n.source_file, reason: "not imported; all symbols unreferenced" });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
deadSymbols,
|
|
253
|
-
testOnlySymbols,
|
|
254
|
-
deadFiles,
|
|
255
|
-
referencedIds: [...symById.values()].filter(isReferenced).map((n) => n.id),
|
|
256
|
-
stats: { symbols: symById.size, deadSymbols: deadSymbols.length, testOnlySymbols: testOnlySymbols.length, files: nodes.filter((n) => !String(n.id).includes("#")).length, deadFiles: deadFiles.length },
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Export-scoped unused check (knip's "unused exports"): an EXPORTED symbol (P0's exported:true flag)
|
|
261
|
-
// with no inbound non-contains edge whose bare name occurs in NO file other than its own. Same-file-only
|
|
262
|
-
// usage means the symbol is alive but the export is not — still worth surfacing. Pure like computeDead.
|
|
263
|
-
// dynamicTargets = files reached via dynamic import() — their exports are consumed at runtime, skip them.
|
|
264
|
-
export function computeUnusedExports(graph, sources, { dynamicTargets = new Set(), entrySet = new Set() } = {}) {
|
|
265
|
-
const nodes = graph.nodes || [], links = graph.links || [];
|
|
266
|
-
const ep = (v) => (v && typeof v === "object" ? v.id : v);
|
|
267
|
-
const inbound = new Set();
|
|
268
|
-
for (const l of links) if (!isStructuralRelation(l.relation)) inbound.add(ep(l.target));
|
|
269
|
-
|
|
270
|
-
const namespaceConsumed = namespaceConsumedFiles(sources, graph);
|
|
271
|
-
const candidates = [];
|
|
272
|
-
const wanted = new Set();
|
|
273
|
-
for (const n of nodes) {
|
|
274
|
-
if (!n.exported || !String(n.id).includes("#") || inbound.has(n.id)) continue;
|
|
275
|
-
if (ENTRY_FILE.test(n.source_file) || isFrameworkEntryFile(n.source_file) || entrySet.has(n.source_file) || dynamicTargets.has(n.source_file) || namespaceConsumed.has(n.source_file)) continue;
|
|
276
|
-
const nm = bareName(n.label);
|
|
277
|
-
if (!nm || !/^[A-Za-z_$]/.test(nm)) continue;
|
|
278
|
-
if (!hasModuleExport(sources.get(n.source_file), n, nm)) continue;
|
|
279
|
-
candidates.push({ n, nm });
|
|
280
|
-
wanted.add(nm);
|
|
281
|
-
}
|
|
282
|
-
if (!candidates.length) return [];
|
|
283
|
-
|
|
284
|
-
const occursIn = new Map(); // name -> Set(files whose text contains it)
|
|
285
|
-
for (const [file, text] of sources) {
|
|
286
|
-
for (const m of String(text || "").matchAll(IDENT_RE)) {
|
|
287
|
-
const w = m[0];
|
|
288
|
-
if (!wanted.has(w)) continue;
|
|
289
|
-
(occursIn.get(w) || occursIn.set(w, new Set()).get(w)).add(file);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const out = [];
|
|
294
|
-
for (const { n, nm } of candidates) {
|
|
295
|
-
const occ = occursIn.get(nm);
|
|
296
|
-
if (occ && [...occ].some((f) => f !== n.source_file)) continue; // referenced beyond its own file → alive
|
|
297
|
-
out.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file), reason: "exported but never imported or referenced outside its own file" });
|
|
298
|
-
}
|
|
299
|
-
return out;
|
|
300
|
-
}
|