weavatrix 0.3.13 → 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 -65
- 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.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.js +0 -235
- 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 -62
- package/src/analysis/endpoints/extract.js +0 -107
- 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 -295
- 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 -154
- 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 -173
- 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 -174
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -85
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -219
- 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 -220
- 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,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
|
-
}
|
|
@@ -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', '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
|
-
}
|