weavatrix 0.3.14 → 1.0.0

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