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.
Files changed (303) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +231 -199
  3. package/bin/native/darwin-arm64/weavatrix +0 -0
  4. package/bin/native/darwin-x64/weavatrix +0 -0
  5. package/bin/native/linux-arm64/weavatrix +0 -0
  6. package/bin/native/linux-x64/weavatrix +0 -0
  7. package/bin/native/win32-arm64/weavatrix.exe +0 -0
  8. package/bin/native/win32-x64/weavatrix.exe +0 -0
  9. package/bin/resolve-binary.mjs +73 -0
  10. package/bin/run-native.mjs +26 -0
  11. package/bin/weavatrix-mcp.mjs +7 -6
  12. package/bin/weavatrix.mjs +5 -0
  13. package/package.json +8 -65
  14. package/skill/SKILL.md +62 -374
  15. package/skill/agents/openai.yaml +4 -0
  16. package/skill/references/tool-routing.md +45 -0
  17. package/SECURITY.md +0 -101
  18. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  19. package/docs/agent-task-benchmark.md +0 -58
  20. package/docs/releases/v0.2.19.md +0 -33
  21. package/docs/releases/v0.3.0.md +0 -43
  22. package/docs/releases/v0.3.1.md +0 -30
  23. package/docs/releases/v0.3.10.md +0 -76
  24. package/docs/releases/v0.3.11.md +0 -55
  25. package/docs/releases/v0.3.12.md +0 -44
  26. package/docs/releases/v0.3.13.md +0 -34
  27. package/docs/releases/v0.3.2.md +0 -15
  28. package/docs/releases/v0.3.3.md +0 -25
  29. package/docs/releases/v0.3.4.md +0 -40
  30. package/docs/releases/v0.3.5.md +0 -50
  31. package/docs/releases/v0.3.6.md +0 -44
  32. package/docs/releases/v0.3.7.md +0 -37
  33. package/docs/releases/v0.3.8.md +0 -21
  34. package/docs/releases/v0.3.9.md +0 -36
  35. package/scripts/run-agent-task-benchmark.mjs +0 -118
  36. package/src/analysis/allowed-test-runner.js +0 -70
  37. package/src/analysis/architecture/contract-graph.js +0 -119
  38. package/src/analysis/architecture/contract-schema.js +0 -110
  39. package/src/analysis/architecture/contract-storage.js +0 -35
  40. package/src/analysis/architecture/contract-verification.js +0 -168
  41. package/src/analysis/architecture-contract.js +0 -16
  42. package/src/analysis/audit-debt.js +0 -94
  43. package/src/analysis/audit-extensions.js +0 -60
  44. package/src/analysis/cargo-dependency-evidence.js +0 -108
  45. package/src/analysis/cargo-manifests.js +0 -81
  46. package/src/analysis/change-classification/diff-parser.js +0 -103
  47. package/src/analysis/change-classification/options.js +0 -40
  48. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  49. package/src/analysis/change-classification.js +0 -188
  50. package/src/analysis/coverage-reports.js +0 -235
  51. package/src/analysis/cycle-route.js +0 -14
  52. package/src/analysis/data-flow-evidence.js +0 -114
  53. package/src/analysis/dead-check.js +0 -300
  54. package/src/analysis/dead-code-review/candidates.js +0 -194
  55. package/src/analysis/dead-code-review/policy.js +0 -23
  56. package/src/analysis/dead-code-review.js +0 -119
  57. package/src/analysis/dep-check-ecosystems.js +0 -205
  58. package/src/analysis/dep-check.js +0 -290
  59. package/src/analysis/dep-rules.js +0 -10
  60. package/src/analysis/dependency/conventions.js +0 -16
  61. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  62. package/src/analysis/dependency/source-references.js +0 -21
  63. package/src/analysis/duplicate-groups.js +0 -104
  64. package/src/analysis/duplicates-worker.js +0 -11
  65. package/src/analysis/duplicates.compute.js +0 -269
  66. package/src/analysis/duplicates.js +0 -15
  67. package/src/analysis/duplicates.run.js +0 -52
  68. package/src/analysis/duplicates.tokenize.js +0 -199
  69. package/src/analysis/endpoints/common.js +0 -62
  70. package/src/analysis/endpoints/extract.js +0 -107
  71. package/src/analysis/endpoints/inventory.js +0 -84
  72. package/src/analysis/endpoints/mounts.js +0 -129
  73. package/src/analysis/endpoints-java.js +0 -260
  74. package/src/analysis/endpoints-rust.js +0 -124
  75. package/src/analysis/endpoints.js +0 -3
  76. package/src/analysis/findings.js +0 -70
  77. package/src/analysis/git-history/analytics.js +0 -177
  78. package/src/analysis/git-history/collector.js +0 -67
  79. package/src/analysis/git-history/options.js +0 -66
  80. package/src/analysis/git-history.js +0 -143
  81. package/src/analysis/git-ref-graph.js +0 -76
  82. package/src/analysis/go-dependency-evidence.js +0 -62
  83. package/src/analysis/graph-analysis.aggregate.js +0 -295
  84. package/src/analysis/graph-analysis.edges.js +0 -24
  85. package/src/analysis/graph-analysis.js +0 -7
  86. package/src/analysis/graph-analysis.refs.js +0 -154
  87. package/src/analysis/graph-analysis.summaries.js +0 -65
  88. package/src/analysis/health-capabilities.js +0 -83
  89. package/src/analysis/hot-path-review.js +0 -266
  90. package/src/analysis/http-contract-wrappers.js +0 -241
  91. package/src/analysis/http-contracts/analysis.js +0 -179
  92. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  93. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  94. package/src/analysis/http-contracts/graph-context.js +0 -143
  95. package/src/analysis/http-contracts/matching.js +0 -61
  96. package/src/analysis/http-contracts/shared.js +0 -85
  97. package/src/analysis/http-contracts.js +0 -6
  98. package/src/analysis/internal-audit/dependency-health.js +0 -113
  99. package/src/analysis/internal-audit/python-manifests.js +0 -78
  100. package/src/analysis/internal-audit/repo-files.js +0 -50
  101. package/src/analysis/internal-audit/supply-chain.js +0 -149
  102. package/src/analysis/internal-audit.collect.js +0 -213
  103. package/src/analysis/internal-audit.js +0 -9
  104. package/src/analysis/internal-audit.reach.js +0 -173
  105. package/src/analysis/internal-audit.run.js +0 -256
  106. package/src/analysis/java-source.js +0 -36
  107. package/src/analysis/jvm-artifact-index.js +0 -87
  108. package/src/analysis/jvm-dependency-evidence.js +0 -138
  109. package/src/analysis/jvm-manifests.js +0 -114
  110. package/src/analysis/manifests.js +0 -123
  111. package/src/analysis/package-reachability.js +0 -39
  112. package/src/analysis/source-complexity.ast.js +0 -97
  113. package/src/analysis/source-complexity.constants.js +0 -55
  114. package/src/analysis/source-complexity.js +0 -14
  115. package/src/analysis/source-complexity.report.js +0 -81
  116. package/src/analysis/source-complexity.walk.js +0 -228
  117. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  118. package/src/analysis/source-correctness.js +0 -222
  119. package/src/analysis/static-test-reachability.js +0 -150
  120. package/src/analysis/structure/dependency-graph.js +0 -151
  121. package/src/analysis/structure/findings.js +0 -162
  122. package/src/analysis/task-retrieval.js +0 -107
  123. package/src/analysis/transport-contracts.js +0 -224
  124. package/src/analysis/transport-runtime-evidence.js +0 -155
  125. package/src/analysis-kit.mjs +0 -17
  126. package/src/bounds.js +0 -4
  127. package/src/build-graph.js +0 -209
  128. package/src/child-env.js +0 -7
  129. package/src/config.js +0 -5
  130. package/src/extension/local-services.mjs +0 -75
  131. package/src/git-exec.js +0 -67
  132. package/src/graph/build-worker.js +0 -49
  133. package/src/graph/builder/go-receiver-resolution.js +0 -112
  134. package/src/graph/builder/js/dynamic-import.js +0 -26
  135. package/src/graph/builder/js/queries.js +0 -48
  136. package/src/graph/builder/lang-csharp.js +0 -40
  137. package/src/graph/builder/lang-css.js +0 -29
  138. package/src/graph/builder/lang-go.js +0 -140
  139. package/src/graph/builder/lang-html.js +0 -29
  140. package/src/graph/builder/lang-java-calls.js +0 -142
  141. package/src/graph/builder/lang-java.js +0 -215
  142. package/src/graph/builder/lang-js.js +0 -291
  143. package/src/graph/builder/lang-python.js +0 -235
  144. package/src/graph/builder/lang-rust-calls.js +0 -63
  145. package/src/graph/builder/lang-rust.js +0 -299
  146. package/src/graph/builder/lang-solidity.js +0 -126
  147. package/src/graph/builder/lang-sql.js +0 -263
  148. package/src/graph/builder/pass2-resolution.js +0 -81
  149. package/src/graph/builder/spec-pkg.js +0 -87
  150. package/src/graph/community.js +0 -27
  151. package/src/graph/edge-provenance.js +0 -31
  152. package/src/graph/file-lock.js +0 -69
  153. package/src/graph/freshness-probe.js +0 -142
  154. package/src/graph/graph-filter.js +0 -63
  155. package/src/graph/incremental-refresh.js +0 -235
  156. package/src/graph/internal-builder.barrels.js +0 -235
  157. package/src/graph/internal-builder.build.js +0 -300
  158. package/src/graph/internal-builder.java.js +0 -33
  159. package/src/graph/internal-builder.js +0 -12
  160. package/src/graph/internal-builder.langs.js +0 -157
  161. package/src/graph/internal-builder.pass2.js +0 -163
  162. package/src/graph/internal-builder.resolvers.js +0 -286
  163. package/src/graph/layout.js +0 -51
  164. package/src/graph/node-id.js +0 -7
  165. package/src/graph/parser-artifact-boundary.js +0 -55
  166. package/src/graph/relations.js +0 -4
  167. package/src/graph/repo-registry.js +0 -124
  168. package/src/graph/resolvers/rust.js +0 -117
  169. package/src/infra/infra-items.js +0 -255
  170. package/src/infra/infra-registry.js +0 -184
  171. package/src/infra/infra.detect.js +0 -119
  172. package/src/infra/infra.js +0 -38
  173. package/src/infra/infra.match.js +0 -102
  174. package/src/infra/infra.scan.js +0 -93
  175. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  176. package/src/mcp/architecture-bootstrap.mjs +0 -168
  177. package/src/mcp/architecture-starter.mjs +0 -234
  178. package/src/mcp/catalog.mjs +0 -233
  179. package/src/mcp/company-contract-page.mjs +0 -142
  180. package/src/mcp/company-contract-reconcile.mjs +0 -87
  181. package/src/mcp/company-contract-verdict.mjs +0 -42
  182. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  183. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  184. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  185. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  186. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  187. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  188. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  189. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  190. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  191. package/src/mcp/evidence-snapshot.mjs +0 -50
  192. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  193. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  194. package/src/mcp/extension-api.mjs +0 -77
  195. package/src/mcp/git-output.mjs +0 -7
  196. package/src/mcp/graph/context-core.mjs +0 -111
  197. package/src/mcp/graph/context-seeds.mjs +0 -207
  198. package/src/mcp/graph/context-state.mjs +0 -85
  199. package/src/mcp/graph/reverse-reach.mjs +0 -42
  200. package/src/mcp/graph/tools-core.mjs +0 -143
  201. package/src/mcp/graph/tools-query.mjs +0 -276
  202. package/src/mcp/graph-context.mjs +0 -5
  203. package/src/mcp/graph-diff.mjs +0 -218
  204. package/src/mcp/health/audit-format.mjs +0 -199
  205. package/src/mcp/health/audit.mjs +0 -174
  206. package/src/mcp/health/dead-code.mjs +0 -110
  207. package/src/mcp/health/duplicates.mjs +0 -85
  208. package/src/mcp/health/endpoints.mjs +0 -71
  209. package/src/mcp/health/structure.mjs +0 -219
  210. package/src/mcp/runtime-version.mjs +0 -31
  211. package/src/mcp/server/auto-refresh.mjs +0 -66
  212. package/src/mcp/server/runtime-config.mjs +0 -43
  213. package/src/mcp/server/shutdown.mjs +0 -40
  214. package/src/mcp/staleness-notice.mjs +0 -20
  215. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  216. package/src/mcp/sync/evidence-common.mjs +0 -88
  217. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  218. package/src/mcp/sync/evidence-health.mjs +0 -56
  219. package/src/mcp/sync/evidence-packages.mjs +0 -95
  220. package/src/mcp/sync/payload-common.mjs +0 -97
  221. package/src/mcp/sync/payload-v2.mjs +0 -54
  222. package/src/mcp/sync/payload-v3.mjs +0 -79
  223. package/src/mcp/sync-evidence.mjs +0 -23
  224. package/src/mcp/sync-payload.mjs +0 -70
  225. package/src/mcp/tool-result.mjs +0 -54
  226. package/src/mcp/tools-actions.mjs +0 -11
  227. package/src/mcp/tools-architecture.mjs +0 -177
  228. package/src/mcp/tools-company.mjs +0 -251
  229. package/src/mcp/tools-context.mjs +0 -222
  230. package/src/mcp/tools-endpoints.mjs +0 -207
  231. package/src/mcp/tools-graph-hubs.mjs +0 -78
  232. package/src/mcp/tools-graph.mjs +0 -17
  233. package/src/mcp/tools-health.mjs +0 -24
  234. package/src/mcp/tools-history.mjs +0 -28
  235. package/src/mcp/tools-impact-change.mjs +0 -220
  236. package/src/mcp/tools-impact-precision.mjs +0 -89
  237. package/src/mcp/tools-impact.mjs +0 -169
  238. package/src/mcp/tools-source.mjs +0 -238
  239. package/src/mcp/tools-verified-change.mjs +0 -200
  240. package/src/mcp-rg.mjs +0 -50
  241. package/src/mcp-runtime.mjs +0 -2
  242. package/src/mcp-server.mjs +0 -298
  243. package/src/mcp-source-tools.mjs +0 -163
  244. package/src/path-classification.js +0 -220
  245. package/src/path-ignore.js +0 -69
  246. package/src/precision/lsp-client/constants.js +0 -12
  247. package/src/precision/lsp-client/environment.js +0 -20
  248. package/src/precision/lsp-client/errors.js +0 -15
  249. package/src/precision/lsp-client/lifecycle.js +0 -114
  250. package/src/precision/lsp-client/message-parser.js +0 -81
  251. package/src/precision/lsp-client/protocol.js +0 -212
  252. package/src/precision/lsp-client/registry.js +0 -59
  253. package/src/precision/lsp-client/repo-uri.js +0 -60
  254. package/src/precision/lsp-client/stdio-client.js +0 -140
  255. package/src/precision/lsp-client.js +0 -10
  256. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  257. package/src/precision/lsp-overlay/build.js +0 -261
  258. package/src/precision/lsp-overlay/contract.js +0 -61
  259. package/src/precision/lsp-overlay/reference-results.js +0 -108
  260. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  261. package/src/precision/lsp-overlay/source-session.js +0 -142
  262. package/src/precision/lsp-overlay/store.js +0 -150
  263. package/src/precision/lsp-overlay/target-index.js +0 -152
  264. package/src/precision/lsp-overlay/target-query.js +0 -55
  265. package/src/precision/lsp-overlay.js +0 -11
  266. package/src/precision/symbol-query.js +0 -224
  267. package/src/precision/typescript-lsp-provider.js +0 -12
  268. package/src/precision/typescript-provider/client.js +0 -89
  269. package/src/precision/typescript-provider/discovery.js +0 -124
  270. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  271. package/src/precision/typescript-provider/project-host.js +0 -254
  272. package/src/precision/typescript-provider/project-safety.js +0 -166
  273. package/src/precision/typescript-provider/reference-usage.js +0 -53
  274. package/src/process.js +0 -95
  275. package/src/repo-path.js +0 -53
  276. package/src/scan/discover.inventory.js +0 -78
  277. package/src/scan/discover.js +0 -5
  278. package/src/scan/discover.list.js +0 -79
  279. package/src/scan/discover.stack.js +0 -227
  280. package/src/scan/search.core.js +0 -24
  281. package/src/scan/search.git.js +0 -102
  282. package/src/scan/search.js +0 -9
  283. package/src/scan/search.node.js +0 -83
  284. package/src/scan/search.preview.js +0 -49
  285. package/src/scan/search.rg.js +0 -145
  286. package/src/security/advisory-store.js +0 -172
  287. package/src/security/installed-jvm-rust.js +0 -46
  288. package/src/security/installed.js +0 -280
  289. package/src/security/malware-file-heuristics.js +0 -121
  290. package/src/security/malware-heuristics.exclusions.js +0 -134
  291. package/src/security/malware-heuristics.js +0 -15
  292. package/src/security/malware-heuristics.roots.js +0 -145
  293. package/src/security/malware-heuristics.scan.js +0 -89
  294. package/src/security/malware-heuristics.sweep.js +0 -122
  295. package/src/security/malware-scoring.js +0 -105
  296. package/src/security/match.js +0 -85
  297. package/src/security/registry-sig.classify.js +0 -137
  298. package/src/security/registry-sig.js +0 -18
  299. package/src/security/registry-sig.rules.js +0 -192
  300. package/src/security/rust-advisory-report.js +0 -60
  301. package/src/security/typosquat.js +0 -81
  302. package/src/util.js +0 -40
  303. 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
- }