weavatrix 0.3.14 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +132 -200
  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 +10 -67
  14. package/server.json +38 -0
  15. package/skill/SKILL.md +62 -374
  16. package/skill/agents/openai.yaml +4 -0
  17. package/skill/references/tool-routing.md +45 -0
  18. package/SECURITY.md +0 -101
  19. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  20. package/docs/agent-task-benchmark.md +0 -58
  21. package/docs/releases/v0.2.19.md +0 -33
  22. package/docs/releases/v0.3.0.md +0 -43
  23. package/docs/releases/v0.3.1.md +0 -30
  24. package/docs/releases/v0.3.10.md +0 -76
  25. package/docs/releases/v0.3.11.md +0 -55
  26. package/docs/releases/v0.3.12.md +0 -44
  27. package/docs/releases/v0.3.13.md +0 -34
  28. package/docs/releases/v0.3.14.md +0 -37
  29. package/docs/releases/v0.3.2.md +0 -15
  30. package/docs/releases/v0.3.3.md +0 -25
  31. package/docs/releases/v0.3.4.md +0 -40
  32. package/docs/releases/v0.3.5.md +0 -50
  33. package/docs/releases/v0.3.6.md +0 -44
  34. package/docs/releases/v0.3.7.md +0 -37
  35. package/docs/releases/v0.3.8.md +0 -21
  36. package/docs/releases/v0.3.9.md +0 -36
  37. package/scripts/run-agent-task-benchmark.mjs +0 -118
  38. package/src/analysis/allowed-test-runner.js +0 -70
  39. package/src/analysis/architecture/contract-graph.js +0 -119
  40. package/src/analysis/architecture/contract-schema.js +0 -110
  41. package/src/analysis/architecture/contract-storage.js +0 -35
  42. package/src/analysis/architecture/contract-verification.js +0 -168
  43. package/src/analysis/architecture-contract.js +0 -16
  44. package/src/analysis/audit-debt.js +0 -94
  45. package/src/analysis/audit-extensions.js +0 -60
  46. package/src/analysis/cargo-dependency-evidence.js +0 -108
  47. package/src/analysis/cargo-manifests.js +0 -81
  48. package/src/analysis/change-classification/diff-parser.js +0 -103
  49. package/src/analysis/change-classification/options.js +0 -40
  50. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  51. package/src/analysis/change-classification.js +0 -188
  52. package/src/analysis/coverage-reports-tarpaulin.js +0 -112
  53. package/src/analysis/coverage-reports.js +0 -246
  54. package/src/analysis/cycle-route.js +0 -14
  55. package/src/analysis/data-flow-evidence.js +0 -114
  56. package/src/analysis/dead-check.js +0 -300
  57. package/src/analysis/dead-code-review/candidates.js +0 -194
  58. package/src/analysis/dead-code-review/policy.js +0 -23
  59. package/src/analysis/dead-code-review.js +0 -119
  60. package/src/analysis/dep-check-ecosystems.js +0 -205
  61. package/src/analysis/dep-check.js +0 -290
  62. package/src/analysis/dep-rules.js +0 -10
  63. package/src/analysis/dependency/conventions.js +0 -16
  64. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  65. package/src/analysis/dependency/source-references.js +0 -21
  66. package/src/analysis/duplicate-groups.js +0 -104
  67. package/src/analysis/duplicates-worker.js +0 -11
  68. package/src/analysis/duplicates.compute.js +0 -269
  69. package/src/analysis/duplicates.js +0 -15
  70. package/src/analysis/duplicates.run.js +0 -52
  71. package/src/analysis/duplicates.tokenize.js +0 -199
  72. package/src/analysis/endpoints/common.js +0 -79
  73. package/src/analysis/endpoints/extract.js +0 -120
  74. package/src/analysis/endpoints/inventory.js +0 -84
  75. package/src/analysis/endpoints/mounts.js +0 -129
  76. package/src/analysis/endpoints-java.js +0 -260
  77. package/src/analysis/endpoints-rust.js +0 -124
  78. package/src/analysis/endpoints.js +0 -3
  79. package/src/analysis/findings.js +0 -70
  80. package/src/analysis/git-history/analytics.js +0 -177
  81. package/src/analysis/git-history/collector.js +0 -67
  82. package/src/analysis/git-history/options.js +0 -66
  83. package/src/analysis/git-history.js +0 -143
  84. package/src/analysis/git-ref-graph.js +0 -76
  85. package/src/analysis/go-dependency-evidence.js +0 -62
  86. package/src/analysis/graph-analysis.aggregate.js +0 -296
  87. package/src/analysis/graph-analysis.edges.js +0 -24
  88. package/src/analysis/graph-analysis.js +0 -7
  89. package/src/analysis/graph-analysis.refs.js +0 -202
  90. package/src/analysis/graph-analysis.summaries.js +0 -65
  91. package/src/analysis/health-capabilities.js +0 -83
  92. package/src/analysis/hot-path-review.js +0 -266
  93. package/src/analysis/http-contract-wrappers.js +0 -241
  94. package/src/analysis/http-contracts/analysis.js +0 -179
  95. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  96. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  97. package/src/analysis/http-contracts/graph-context.js +0 -143
  98. package/src/analysis/http-contracts/matching.js +0 -61
  99. package/src/analysis/http-contracts/shared.js +0 -85
  100. package/src/analysis/http-contracts.js +0 -6
  101. package/src/analysis/internal-audit/dependency-health.js +0 -113
  102. package/src/analysis/internal-audit/python-manifests.js +0 -78
  103. package/src/analysis/internal-audit/repo-files.js +0 -50
  104. package/src/analysis/internal-audit/supply-chain.js +0 -149
  105. package/src/analysis/internal-audit.collect.js +0 -213
  106. package/src/analysis/internal-audit.js +0 -9
  107. package/src/analysis/internal-audit.reach.js +0 -201
  108. package/src/analysis/internal-audit.run.js +0 -256
  109. package/src/analysis/java-source.js +0 -36
  110. package/src/analysis/jvm-artifact-index.js +0 -87
  111. package/src/analysis/jvm-dependency-evidence.js +0 -138
  112. package/src/analysis/jvm-manifests.js +0 -114
  113. package/src/analysis/manifests.js +0 -123
  114. package/src/analysis/package-reachability.js +0 -39
  115. package/src/analysis/source-complexity.ast.js +0 -97
  116. package/src/analysis/source-complexity.constants.js +0 -55
  117. package/src/analysis/source-complexity.js +0 -14
  118. package/src/analysis/source-complexity.report.js +0 -81
  119. package/src/analysis/source-complexity.walk.js +0 -228
  120. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  121. package/src/analysis/source-correctness.js +0 -222
  122. package/src/analysis/static-test-reachability.js +0 -150
  123. package/src/analysis/structure/dependency-graph.js +0 -151
  124. package/src/analysis/structure/findings.js +0 -162
  125. package/src/analysis/task-retrieval.js +0 -107
  126. package/src/analysis/transport-contracts.js +0 -224
  127. package/src/analysis/transport-runtime-evidence.js +0 -155
  128. package/src/analysis-kit.mjs +0 -17
  129. package/src/bounds.js +0 -4
  130. package/src/build-graph.js +0 -209
  131. package/src/child-env.js +0 -7
  132. package/src/config.js +0 -5
  133. package/src/extension/local-services.mjs +0 -75
  134. package/src/git-exec.js +0 -67
  135. package/src/graph/build-worker.js +0 -49
  136. package/src/graph/builder/go-receiver-resolution.js +0 -112
  137. package/src/graph/builder/js/dynamic-import.js +0 -26
  138. package/src/graph/builder/js/queries.js +0 -48
  139. package/src/graph/builder/lang-csharp.js +0 -40
  140. package/src/graph/builder/lang-css.js +0 -29
  141. package/src/graph/builder/lang-go.js +0 -140
  142. package/src/graph/builder/lang-html.js +0 -29
  143. package/src/graph/builder/lang-java-calls.js +0 -142
  144. package/src/graph/builder/lang-java.js +0 -215
  145. package/src/graph/builder/lang-js.js +0 -291
  146. package/src/graph/builder/lang-python.js +0 -235
  147. package/src/graph/builder/lang-rust-calls.js +0 -63
  148. package/src/graph/builder/lang-rust.js +0 -299
  149. package/src/graph/builder/lang-solidity.js +0 -126
  150. package/src/graph/builder/lang-sql.js +0 -263
  151. package/src/graph/builder/pass2-resolution.js +0 -81
  152. package/src/graph/builder/spec-pkg.js +0 -87
  153. package/src/graph/community.js +0 -27
  154. package/src/graph/edge-provenance.js +0 -31
  155. package/src/graph/file-lock.js +0 -69
  156. package/src/graph/freshness-probe.js +0 -142
  157. package/src/graph/graph-filter.js +0 -63
  158. package/src/graph/incremental-refresh.js +0 -235
  159. package/src/graph/internal-builder.barrels.js +0 -235
  160. package/src/graph/internal-builder.build.js +0 -300
  161. package/src/graph/internal-builder.java.js +0 -33
  162. package/src/graph/internal-builder.js +0 -12
  163. package/src/graph/internal-builder.langs.js +0 -157
  164. package/src/graph/internal-builder.pass2.js +0 -163
  165. package/src/graph/internal-builder.resolvers.js +0 -286
  166. package/src/graph/layout.js +0 -51
  167. package/src/graph/node-id.js +0 -7
  168. package/src/graph/parser-artifact-boundary.js +0 -55
  169. package/src/graph/relations.js +0 -4
  170. package/src/graph/repo-registry.js +0 -124
  171. package/src/graph/resolvers/rust.js +0 -117
  172. package/src/infra/infra-items.js +0 -255
  173. package/src/infra/infra-registry.js +0 -184
  174. package/src/infra/infra.detect.js +0 -119
  175. package/src/infra/infra.js +0 -38
  176. package/src/infra/infra.match.js +0 -102
  177. package/src/infra/infra.scan.js +0 -93
  178. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  179. package/src/mcp/architecture-bootstrap.mjs +0 -168
  180. package/src/mcp/architecture-starter.mjs +0 -234
  181. package/src/mcp/catalog.mjs +0 -233
  182. package/src/mcp/company-contract-page.mjs +0 -142
  183. package/src/mcp/company-contract-reconcile.mjs +0 -87
  184. package/src/mcp/company-contract-verdict.mjs +0 -42
  185. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  186. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  187. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  188. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  189. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  190. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  191. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  192. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  193. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  194. package/src/mcp/evidence-snapshot.mjs +0 -50
  195. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  196. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  197. package/src/mcp/extension-api.mjs +0 -77
  198. package/src/mcp/git-output.mjs +0 -7
  199. package/src/mcp/graph/context-core.mjs +0 -111
  200. package/src/mcp/graph/context-seeds.mjs +0 -207
  201. package/src/mcp/graph/context-state.mjs +0 -85
  202. package/src/mcp/graph/reverse-reach.mjs +0 -42
  203. package/src/mcp/graph/tools-core.mjs +0 -143
  204. package/src/mcp/graph/tools-query.mjs +0 -276
  205. package/src/mcp/graph-context.mjs +0 -5
  206. package/src/mcp/graph-diff.mjs +0 -218
  207. package/src/mcp/health/audit-format.mjs +0 -199
  208. package/src/mcp/health/audit.mjs +0 -199
  209. package/src/mcp/health/dead-code.mjs +0 -110
  210. package/src/mcp/health/duplicates.mjs +0 -121
  211. package/src/mcp/health/endpoints.mjs +0 -71
  212. package/src/mcp/health/structure.mjs +0 -220
  213. package/src/mcp/runtime-version.mjs +0 -31
  214. package/src/mcp/server/auto-refresh.mjs +0 -66
  215. package/src/mcp/server/runtime-config.mjs +0 -43
  216. package/src/mcp/server/shutdown.mjs +0 -40
  217. package/src/mcp/staleness-notice.mjs +0 -20
  218. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  219. package/src/mcp/sync/evidence-common.mjs +0 -88
  220. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  221. package/src/mcp/sync/evidence-health.mjs +0 -56
  222. package/src/mcp/sync/evidence-packages.mjs +0 -95
  223. package/src/mcp/sync/payload-common.mjs +0 -97
  224. package/src/mcp/sync/payload-v2.mjs +0 -54
  225. package/src/mcp/sync/payload-v3.mjs +0 -79
  226. package/src/mcp/sync-evidence.mjs +0 -23
  227. package/src/mcp/sync-payload.mjs +0 -70
  228. package/src/mcp/tool-result.mjs +0 -54
  229. package/src/mcp/tools-actions.mjs +0 -11
  230. package/src/mcp/tools-architecture.mjs +0 -177
  231. package/src/mcp/tools-company.mjs +0 -251
  232. package/src/mcp/tools-context.mjs +0 -222
  233. package/src/mcp/tools-endpoints.mjs +0 -207
  234. package/src/mcp/tools-graph-hubs.mjs +0 -78
  235. package/src/mcp/tools-graph.mjs +0 -17
  236. package/src/mcp/tools-health.mjs +0 -24
  237. package/src/mcp/tools-history.mjs +0 -28
  238. package/src/mcp/tools-impact-change.mjs +0 -220
  239. package/src/mcp/tools-impact-precision.mjs +0 -89
  240. package/src/mcp/tools-impact.mjs +0 -169
  241. package/src/mcp/tools-source.mjs +0 -238
  242. package/src/mcp/tools-verified-change.mjs +0 -200
  243. package/src/mcp-rg.mjs +0 -50
  244. package/src/mcp-runtime.mjs +0 -2
  245. package/src/mcp-server.mjs +0 -298
  246. package/src/mcp-source-tools.mjs +0 -163
  247. package/src/path-classification.js +0 -226
  248. package/src/path-ignore.js +0 -69
  249. package/src/precision/lsp-client/constants.js +0 -12
  250. package/src/precision/lsp-client/environment.js +0 -20
  251. package/src/precision/lsp-client/errors.js +0 -15
  252. package/src/precision/lsp-client/lifecycle.js +0 -114
  253. package/src/precision/lsp-client/message-parser.js +0 -81
  254. package/src/precision/lsp-client/protocol.js +0 -212
  255. package/src/precision/lsp-client/registry.js +0 -59
  256. package/src/precision/lsp-client/repo-uri.js +0 -60
  257. package/src/precision/lsp-client/stdio-client.js +0 -140
  258. package/src/precision/lsp-client.js +0 -10
  259. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  260. package/src/precision/lsp-overlay/build.js +0 -261
  261. package/src/precision/lsp-overlay/contract.js +0 -61
  262. package/src/precision/lsp-overlay/reference-results.js +0 -108
  263. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  264. package/src/precision/lsp-overlay/source-session.js +0 -142
  265. package/src/precision/lsp-overlay/store.js +0 -150
  266. package/src/precision/lsp-overlay/target-index.js +0 -152
  267. package/src/precision/lsp-overlay/target-query.js +0 -55
  268. package/src/precision/lsp-overlay.js +0 -11
  269. package/src/precision/symbol-query.js +0 -224
  270. package/src/precision/typescript-lsp-provider.js +0 -12
  271. package/src/precision/typescript-provider/client.js +0 -89
  272. package/src/precision/typescript-provider/discovery.js +0 -124
  273. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  274. package/src/precision/typescript-provider/project-host.js +0 -254
  275. package/src/precision/typescript-provider/project-safety.js +0 -166
  276. package/src/precision/typescript-provider/reference-usage.js +0 -53
  277. package/src/process.js +0 -95
  278. package/src/repo-path.js +0 -53
  279. package/src/scan/discover.inventory.js +0 -78
  280. package/src/scan/discover.js +0 -5
  281. package/src/scan/discover.list.js +0 -79
  282. package/src/scan/discover.stack.js +0 -227
  283. package/src/scan/search.core.js +0 -24
  284. package/src/scan/search.git.js +0 -102
  285. package/src/scan/search.js +0 -9
  286. package/src/scan/search.node.js +0 -83
  287. package/src/scan/search.preview.js +0 -49
  288. package/src/scan/search.rg.js +0 -145
  289. package/src/security/advisory-store.js +0 -172
  290. package/src/security/installed-jvm-rust.js +0 -46
  291. package/src/security/installed.js +0 -280
  292. package/src/security/malware-file-heuristics.js +0 -121
  293. package/src/security/malware-heuristics.exclusions.js +0 -134
  294. package/src/security/malware-heuristics.js +0 -15
  295. package/src/security/malware-heuristics.roots.js +0 -145
  296. package/src/security/malware-heuristics.scan.js +0 -89
  297. package/src/security/malware-heuristics.sweep.js +0 -122
  298. package/src/security/malware-scoring.js +0 -105
  299. package/src/security/match.js +0 -85
  300. package/src/security/registry-sig.classify.js +0 -137
  301. package/src/security/registry-sig.js +0 -18
  302. package/src/security/registry-sig.rules.js +0 -192
  303. package/src/security/rust-advisory-report.js +0 -60
  304. package/src/security/typosquat.js +0 -81
  305. package/src/util.js +0 -40
  306. package/src/version.js +0 -7
@@ -1,76 +0,0 @@
1
- // Build a source graph for an immutable Git commit without mutating the user's worktree.
2
- // The commit is first resolved to a full object id, then exported into a private temporary
3
- // directory. No hooks are run and no branch/worktree metadata is changed.
4
- import {mkdtempSync, mkdirSync, rmSync} from 'node:fs'
5
- import {join} from 'node:path'
6
- import {tmpdir} from 'node:os'
7
- import {runGit} from '../git-exec.js'
8
- import {runCommandSync} from '../process.js'
9
- import {buildInternalGraph} from '../graph/internal-builder.js'
10
- import {filterGraphForMode} from '../graph/graph-filter.js'
11
-
12
- const SAFE_REF = /^(?!-)[A-Za-z0-9][A-Za-z0-9._\/@{}+~^-]{0,199}$/
13
-
14
- function git(repoRoot, args, timeout = 15_000) {
15
- return runGit(repoRoot, args, {timeout, maxBuffer: 4 * 1024 * 1024})
16
- }
17
-
18
- export function resolveGitCommit(repoRoot, requestedRef) {
19
- const ref = String(requestedRef || '').trim()
20
- if (!SAFE_REF.test(ref)) return {ok: false, error: 'base_ref contains unsupported characters'}
21
- const result = git(repoRoot, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`])
22
- const commit = String(result.stdout || '').trim()
23
- if (result.status !== 0 || !/^[a-f0-9]{40,64}$/i.test(commit)) {
24
- return {ok: false, error: `could not resolve Git ref "${ref}" to a commit`}
25
- }
26
- return {ok: true, ref, commit}
27
- }
28
-
29
- export async function withGitRefCheckout(repoRoot, requestedRef, operation) {
30
- const resolved = resolveGitCommit(repoRoot, requestedRef)
31
- if (!resolved.ok) return resolved
32
- const temp = mkdtempSync(join(tmpdir(), 'weavatrix-git-ref-'))
33
- const checkout = join(temp, 'repo')
34
- const archive = join(temp, 'source.tar')
35
- mkdirSync(checkout, {recursive: true})
36
- try {
37
- const archived = git(repoRoot, ['archive', '--format=tar', `--output=${archive}`, resolved.commit], 60_000)
38
- if (archived.status !== 0) {
39
- return {ok: false, error: `git archive failed for ${resolved.ref}: ${String(archived.stderr || '').trim() || 'unknown error'}`}
40
- }
41
- const extracted = runCommandSync('tar', ['-xf', archive, '-C', checkout], {timeout: 60_000, maxBuffer: 4 * 1024 * 1024})
42
- if (extracted.status !== 0) {
43
- return {ok: false, error: `temporary Git archive extraction failed: ${String(extracted.stderr || '').trim() || 'tar unavailable'}`}
44
- }
45
- // A normal build asks Git for tracked + non-ignored files. A plain archive has no .git
46
- // directory, so the fallback walker deliberately skips tracked dot paths such as
47
- // .github/workflows. Recreate only an index over the immutable archive (no commit, hooks or
48
- // checkout) so live and baseline graphs use the same file-universe semantics.
49
- const initialized = git(checkout, ['init', '--quiet'])
50
- if (initialized.status !== 0) {
51
- return {ok: false, error: `temporary Git index initialization failed: ${String(initialized.stderr || '').trim() || 'unknown error'}`}
52
- }
53
- const indexed = git(checkout, ['add', '--all', '--force'], 60_000)
54
- if (indexed.status !== 0) {
55
- return {ok: false, error: `temporary Git index population failed: ${String(indexed.stderr || '').trim() || 'unknown error'}`}
56
- }
57
- const value = await operation(checkout, resolved)
58
- return {ok: true, ref: resolved.ref, commit: resolved.commit, value}
59
- } catch (error) {
60
- return {ok: false, error: error instanceof Error ? error.message : String(error)}
61
- } finally {
62
- rmSync(temp, {recursive: true, force: true})
63
- }
64
- }
65
-
66
- export async function buildGraphAtGitRef(repoRoot, requestedRef, {mode = 'full'} = {}) {
67
- const buildMode = ['full', 'no-tests', 'tests-only'].includes(mode) ? mode : 'full'
68
- const result = await withGitRefCheckout(repoRoot, requestedRef, async (checkout) => {
69
- const fullGraph = await buildInternalGraph(checkout)
70
- return {
71
- ...filterGraphForMode(fullGraph, buildMode, {repoRoot: checkout}),
72
- graphBuildMode: buildMode,
73
- }
74
- })
75
- return result.ok ? {...result, graph: result.value} : result
76
- }
@@ -1,62 +0,0 @@
1
- import { createRepoBoundary } from "../repo-path.js";
2
- import { computeGoDepFindings } from "./dep-check-ecosystems.js";
3
- import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
4
- import { parseGoMod } from "./manifests.js";
5
- import { makeFinding } from "./findings.js";
6
- import { dependencyScopeOwnsFile, dependencyScopeRoot } from "./dependency/scoped-dependencies.js";
7
-
8
- export function collectGoDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [], nonRuntimeRoots = [] } = {}) {
9
- const boundary = createRepoBoundary(repoRoot);
10
- const manifests = files.filter((file) => /(^|\/)go\.mod$/i.test(file));
11
- const scopes = manifests.map((file) => ({ file, root: dependencyScopeRoot(file), parsed: parseGoMod(readRepoText(boundary, file)) }))
12
- .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
13
- const importsByScope = new Map(scopes.map((scope) => [scope, []]));
14
- for (const entry of externalImports) {
15
- if (entry.ecosystem !== "Go") continue;
16
- const owner = scopes.find((scope) => dependencyScopeOwnsFile(scope.root, entry.file));
17
- if (owner) importsByScope.get(owner).push(entry);
18
- }
19
- const findings = [], declared = new Set(), issues = [];
20
- if (!scopes.length) {
21
- const missing = new Map();
22
- for (const entry of externalImports.filter((item) => item.ecosystem === "Go" && item.pkg && !item.builtin && !item.unresolved)) {
23
- if (!missing.has(entry.pkg)) missing.set(entry.pkg, entry);
24
- }
25
- for (const [name, entry] of missing) findings.push(makeFinding({
26
- category: "unused", rule: "missing-dep", severity: "low", confidence: "high",
27
- title: `Go import without a module manifest: ${name}`,
28
- reason: "An indexed external Go import exists, but no go.mod was discovered.",
29
- detail: `"${entry.spec || name}" is imported by ${entry.file}, but no go.mod exists in the repository.`,
30
- package: name, file: entry.file, line: entry.line || 0, source: "internal",
31
- verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_PRESENT" }, indexedSourceImports: { status: "FOUND", count: 1, files: [entry.file] }, mapping: "Go module prefix" },
32
- fixHint: "initialize or restore the owning Go module manifest",
33
- }));
34
- }
35
- for (const scope of scopes) {
36
- if (!scope.parsed.module) issues.push(`${scope.file}: module directive is missing or unreadable`);
37
- const result = computeGoDepFindings({ externalImports: importsByScope.get(scope), goMod: scope.parsed, nonRuntimeRoots });
38
- findings.push(...result.findings.map((finding) => ({
39
- ...finding,
40
- manifest: scope.file,
41
- verification: finding.rule === "missing-dep"
42
- ? { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", file: scope.file }, indexedSourceImports: { status: "FOUND", count: finding.evidence?.length || 1, files: (finding.evidence || []).map((item) => item.file) }, mapping: "longest go.mod module prefix" }
43
- : { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "REVIEW_REQUIRED", manifestDeclaration: { status: "FOUND", file: scope.file }, indexedSourceImports: { status: "ZERO_FOUND", completeness: "COMPLETE_FOR_GRAPH_SCOPE", count: 0, files: [] }, mapping: "longest go.mod module prefix" },
44
- })));
45
- for (const name of result.declared) declared.add(`${scope.root || "."}:${name}`);
46
- }
47
- const present = manifests.length > 0;
48
- return {
49
- present,
50
- status: present ? "CHECKED" : "NOT_PRESENT",
51
- completeness: present ? (issues.length ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
52
- manifests,
53
- declared,
54
- findings,
55
- reasons: issues,
56
- reason: !present
57
- ? "No go.mod was discovered."
58
- : issues.length
59
- ? `Go imports and requirements were checked across ${manifests.length} module(s), but ${issues.length} module descriptor(s) were incomplete.`
60
- : `Every discovered go.mod scope (${manifests.length}) was compared with indexed Go imports, including direct/indirect requirements and replace directives.`,
61
- };
62
- }
@@ -1,296 +0,0 @@
1
- // Aggregating a built graph.json into the file/module/symbol rollup the UI needs. `aggregateGraph`
2
- // is pure over the parsed graph (covered by tests) apart from optional repoRoot file reads;
3
- // `analyzeGraph` reads the graph file from disk first.
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { normalizeRepoParts, readCoverageForRepo, pctFromCounts } from "./coverage-reports.js";
7
- import { bareSymbolName, computeLocalSymbolRefs, computeSymbolExternalRefs } from "./graph-analysis.refs.js";
8
- import { createRepoBoundary } from "../repo-path.js";
9
- import { edgeList, folderModuleOf } from "./graph-analysis.edges.js";
10
- export { folderModuleOf } from "./graph-analysis.edges.js";
11
-
12
- // Aggregate a built graph.json into the file- and module-level view the UI needs:
13
- // - graph-builder nodes are FILES *and* their symbols (functions/methods), linked file→symbol by the
14
- // "contains" relation. So a community's raw node count (what the cards show) is NOT a file count.
15
- // - Each file is assigned to ONE module via its file-node's community (symbols may cluster apart,
16
- // but the file itself belongs where its file-node sits) → exact, non-overlapping file counts.
17
- // - Real edges (everything except "contains") roll up to file→file and module→module relations.
18
- export function analyzeGraph(graphJsonPath, repoRoot) {
19
- let graph;
20
- try {
21
- graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
22
- } catch {
23
- return null;
24
- }
25
- return aggregateGraph(graph, repoRoot);
26
- }
27
-
28
- // Pure aggregation over an already-parsed graph object — split out of analyzeGraph so it can be
29
- // unit-tested without a graph.json on disk. Touches the filesystem only when repoRoot is provided
30
- // (to read each source file's line count for the size colouring).
31
- export function aggregateGraph(graph, repoRoot) {
32
- const nodes = graph.nodes || [];
33
- const links = graph.links || [];
34
- const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
35
-
36
- // symbol nodes = targets of a "contains" edge (a file "contains" its functions/methods)
37
- const symbolIds = new Set();
38
- for (const link of links) if (link.relation === "contains") symbolIds.add(endpoint(link.target));
39
-
40
- // community → dominant folder name (over all code nodes — same rule as summarizeCommunities)
41
- // MODULE = the file's own top FOLDER (its "territory"), up to 2 path levels — NOT a dependency
42
- // cluster. e.g. src/widget/foo.js → "src/widget"; benchmarks/cleanup.py → "benchmarks" (so all
43
- // benchmarks files are one module); a top-level file like index.js → "(root)".
44
- // In a merged (cross-repo) graph every node carries a `repo`; qualify file & module identity by it
45
- // so same-named folders/files in different repos never merge. Single-repo graphs have no `repo`,
46
- // so identity stays the bare path (unchanged behavior).
47
- const fileIdOf = (node) => (node.repo ? `${node.repo}::${node.source_file}` : node.source_file);
48
- const moduleOfNode = (node) => (node.repo ? `${node.repo}/` : "") + folderModuleOf(node.source_file);
49
-
50
- // file → module (folder) + symbols + display path, all keyed by repo-qualified file id
51
- const fileModule = new Map();
52
- const fileSymbols = new Map();
53
- const filePath = new Map();
54
- const moduleRepo = new Map();
55
- for (const node of nodes) {
56
- if (node.file_type !== "code" || !node.source_file) continue;
57
- const fid = fileIdOf(node);
58
- if (symbolIds.has(node.id)) {
59
- const list = fileSymbols.get(fid) || [];
60
- list.push({
61
- id: node.id,
62
- label: node.label || node.norm_label || node.id,
63
- line: String(node.source_location || "").replace(/^L/, ""),
64
- endLine: String(node.source_end || "").replace(/^L/, ""),
65
- ...(node.complexity ? { complexity: node.complexity } : {}),
66
- ...(node.decorated ? { decorated: true } : {}),
67
- ...(node.symbol_kind ? {symbolKind: node.symbol_kind} : {}),
68
- ...(node.symbol_space ? {symbolSpace: node.symbol_space} : {})
69
- });
70
- fileSymbols.set(fid, list);
71
- }
72
- if (!fileModule.has(fid)) {
73
- const mod = moduleOfNode(node);
74
- fileModule.set(fid, mod);
75
- filePath.set(fid, node.source_file);
76
- if (node.repo) moduleRepo.set(mod, node.repo);
77
- }
78
- }
79
-
80
- // raw node count per module (files + their symbols), for the honest "X files · Y nodes"
81
- const moduleNodeCount = new Map();
82
- for (const node of nodes) {
83
- if (node.file_type !== "code" || !node.source_file) continue;
84
- const name = moduleOfNode(node);
85
- moduleNodeCount.set(name, (moduleNodeCount.get(name) || 0) + 1);
86
- }
87
-
88
- // modules: folder → its files (+ symbol breakdown). `repo` is set for cross-repo graphs.
89
- const modules = new Map();
90
- for (const [fid, name] of fileModule) {
91
- const mod = modules.get(name) || { name, repo: moduleRepo.get(name) || null, files: [] };
92
- const symbols = fileSymbols.get(fid) || [];
93
- mod.files.push({ file: fid, path: filePath.get(fid) || fid, symbolCount: symbols.length, symbols: symbols.slice(0, 300) });
94
- modules.set(name, mod);
95
- }
96
-
97
- // file→file and module→module edges (skip "contains"; map endpoints by repo-qualified file / module)
98
- const id2file = new Map();
99
- for (const node of nodes) if (node.source_file) id2file.set(node.id, fileIdOf(node));
100
- const fileEdges = new Map(); // key → { count, rels:{relation→n} } so we can emit the DOMINANT relation (call/import/inherit)
101
- const moduleEdges = new Map();
102
- const typeOnlyFileEdges = new Map();
103
- const typeOnlyModuleEdges = new Map();
104
- const compileOnlyFileEdges = new Map();
105
- const compileOnlyModuleEdges = new Map();
106
- const compileTimeFileEdges = new Map();
107
- const compileTimeModuleEdges = new Map();
108
- const addFileEdge = (map, key, link) => {
109
- let edge = map.get(key);
110
- if (!edge) map.set(key, (edge = { count: 0, rels: {} }));
111
- edge.count++;
112
- if (link.relation) edge.rels[link.relation] = (edge.rels[link.relation] || 0) + 1;
113
- };
114
- for (const link of links) {
115
- if (link.relation === "contains" || link.barrelProxy === true) continue;
116
- const fromFile = id2file.get(endpoint(link.source));
117
- const toFile = id2file.get(endpoint(link.target));
118
- if (fromFile && toFile && fromFile !== toFile) {
119
- const key = `${fromFile} ${toFile}`;
120
- const compileTime = link.typeOnly === true || link.compileOnly === true;
121
- const targetFileEdges = link.typeOnly === true ? typeOnlyFileEdges : link.compileOnly === true ? compileOnlyFileEdges : fileEdges;
122
- addFileEdge(targetFileEdges, key, link);
123
- if (compileTime) addFileEdge(compileTimeFileEdges, key, link);
124
- const fromMod = fileModule.get(fromFile);
125
- const toMod = fileModule.get(toFile);
126
- if (fromMod && toMod && fromMod !== toMod) {
127
- const mkey = `${fromMod} ${toMod}`;
128
- const targetModuleEdges = link.typeOnly === true ? typeOnlyModuleEdges : link.compileOnly === true ? compileOnlyModuleEdges : moduleEdges;
129
- targetModuleEdges.set(mkey, (targetModuleEdges.get(mkey) || 0) + 1);
130
- if (compileTime) compileTimeModuleEdges.set(mkey, (compileTimeModuleEdges.get(mkey) || 0) + 1);
131
- }
132
- }
133
- }
134
- // symbol-level (function/method) call graph: edges between symbol nodes (calls/method), each symbol
135
- // tagged with its file's module so the Symbols view can still cluster into module regions.
136
- const symEdges = new Map();
137
- for (const link of links) {
138
- if (link.relation === "contains" || link.barrelProxy === true) continue;
139
- const s = endpoint(link.source);
140
- const t = endpoint(link.target);
141
- if (s === t || !symbolIds.has(s) || !symbolIds.has(t)) continue;
142
- const key = `${s}\t${t}`;
143
- symEdges.set(key, (symEdges.get(key) || 0) + 1);
144
- }
145
- const connectedSyms = new Set();
146
- for (const key of symEdges.keys()) {
147
- const tab = key.indexOf("\t");
148
- connectedSyms.add(key.slice(0, tab));
149
- connectedSyms.add(key.slice(tab + 1));
150
- }
151
- const symbols = [];
152
- for (const node of nodes) {
153
- if (!symbolIds.has(node.id) || !connectedSyms.has(node.id)) continue;
154
- const fid = fileIdOf(node);
155
- symbols.push({
156
- id: node.id,
157
- label: node.label || node.norm_label || node.id,
158
- file: node.source_file || "",
159
- module: fileModule.get(fid) || moduleOfNode(node),
160
- line: String(node.source_location || "").replace(/^L/, ""),
161
- endLine: String(node.source_end || "").replace(/^L/, ""),
162
- ...(node.complexity ? { complexity: node.complexity } : {})
163
- });
164
- }
165
- const symbolEdges = [...symEdges.entries()]
166
- .map(([key, count]) => {
167
- const tab = key.indexOf("\t");
168
- return { from: key.slice(0, tab), to: key.slice(tab + 1), count };
169
- })
170
- .sort((a, b) => b.count - a.count);
171
-
172
- // lines of code per folder (best-effort — needs the repo on disk; absent for merged combos) so the
173
- // UI can color folders blue→red by size.
174
- let folderLoc = null;
175
- const fileLoc = new Map();
176
- const fileText = new Map();
177
- if (repoRoot) {
178
- const boundary = createRepoBoundary(repoRoot);
179
- folderLoc = {};
180
- for (const [sourceFile, mod] of fileModule) {
181
- let loc = 0;
182
- try {
183
- const resolved = boundary.resolve(sourceFile);
184
- if (!resolved.ok) throw new Error("source path is outside the repository");
185
- const txt = readFileSync(resolved.path, "utf8");
186
- loc = txt ? txt.split("\n").length : 0;
187
- fileText.set(sourceFile, txt || "");
188
- } catch {
189
- /* file may be gone — skip */
190
- }
191
- fileLoc.set(sourceFile, loc);
192
- folderLoc[mod] = (folderLoc[mod] || 0) + loc;
193
- }
194
- }
195
- const coverageByFile = repoRoot ? readCoverageForRepo(repoRoot, [...filePath.values()]) : new Map();
196
- const coverageForFile = (fid) => coverageByFile.get(normalizeRepoParts(filePath.get(fid) || fid)) || null;
197
- const coverageForRange = (fid, startLine, endLine) => {
198
- const cov = coverageForFile(fid);
199
- if (!cov) return null;
200
- if (!(cov.lines instanceof Map) || !cov.lines.size || !Number.isFinite(startLine) || !Number.isFinite(endLine)) return cov.pct ?? null;
201
- let total = 0;
202
- let covered = 0;
203
- for (let line = Math.max(1, startLine); line <= Math.max(startLine, endLine); line++) {
204
- if (!cov.lines.has(line)) continue;
205
- total++;
206
- if (Number(cov.lines.get(line)) > 0) covered++;
207
- }
208
- return total ? pctFromCounts(covered, total) : cov.pct ?? null;
209
- };
210
-
211
- const symbolExternalRefs = computeSymbolExternalRefs(filePath, fileSymbols, fileText);
212
-
213
- // Prefer the exact AST end line emitted by the internal builder. Older graph.json files only carry a
214
- // start line, so keep the historical next-declaration/EOF approximation as a compatibility fallback.
215
- const symbolLoc = new Map();
216
- const symbolCoverage = new Map();
217
- // Source-level refs catch local value/constants usage that graph-builder's call edges can miss.
218
- const symbolLocalRefs = new Map();
219
- for (const [fid, list] of fileSymbols) {
220
- const total = fileLoc.get(fid) || 0;
221
- const txt = fileText.get(fid) || "";
222
- const sorted = list
223
- .map((s) => ({ id: s.id, label: s.label, start: parseInt(s.line, 10), end: parseInt(s.endLine, 10), ref: s }))
224
- .filter((s) => Number.isFinite(s.start) && s.start > 0)
225
- .sort((a, b) => a.start - b.start);
226
- const ranges = [];
227
- for (let i = 0; i < sorted.length; i++) {
228
- const start = sorted[i].start;
229
- const next = i + 1 < sorted.length ? sorted[i + 1].start : total > start ? total + 1 : start + 1;
230
- const exactEnd = Number.isFinite(sorted[i].end) && sorted[i].end >= start ? sorted[i].end : 0;
231
- const end = exactEnd || Math.max(start, next - 1);
232
- const loc = Math.max(1, end - start + 1);
233
- symbolLoc.set(sorted[i].id, loc);
234
- sorted[i].ref.loc = loc;
235
- sorted[i].ref.endLine = end;
236
- const cov = coverageForRange(fid, start, end);
237
- if (cov != null) symbolCoverage.set(sorted[i].id, cov);
238
- ranges.push({id: sorted[i].id, name: bareSymbolName(sorted[i].label), startLine: start, endLine: end});
239
- }
240
- for (const [id, refs] of computeLocalSymbolRefs(txt, ranges)) symbolLocalRefs.set(id, refs);
241
- }
242
- for (const s of symbols) {
243
- const fid = id2file.get(s.id);
244
- s.loc = symbolLoc.get(s.id) || 0;
245
- s.coverage = symbolCoverage.get(s.id) ?? coverageForFile(fid)?.pct ?? null;
246
- }
247
-
248
- return {
249
- complexityV: Number(graph.complexityV) || 0,
250
- modules: [...modules.values()]
251
- .map((mod) => ({
252
- name: mod.name,
253
- repo: mod.repo || null,
254
- fileCount: mod.files.length,
255
- nodeCount: moduleNodeCount.get(mod.name) || 0,
256
- symbolCount: mod.files.reduce((sum, f) => sum + f.symbolCount, 0),
257
- files: mod.files
258
- .map((f) => {
259
- const cov = coverageForFile(f.file);
260
- return { ...f, loc: fileLoc.get(f.file) || 0, coverage: cov?.pct ?? null, coverageSource: cov?.source || "" };
261
- })
262
- .sort((a, b) => b.symbolCount - a.symbolCount)
263
- }))
264
- .sort((a, b) => b.fileCount - a.fileCount),
265
- moduleEdges: edgeList(moduleEdges),
266
- typeOnlyModuleEdges: edgeList(typeOnlyModuleEdges),
267
- compileOnlyModuleEdges: edgeList(compileOnlyModuleEdges),
268
- compileTimeModuleEdges: edgeList(compileTimeModuleEdges),
269
- fileEdges: edgeList(fileEdges),
270
- typeOnlyFileEdges: edgeList(typeOnlyFileEdges),
271
- compileOnlyFileEdges: edgeList(compileOnlyFileEdges),
272
- compileTimeFileEdges: edgeList(compileTimeFileEdges),
273
- symbols,
274
- symbolEdges,
275
- symbolRefs: [...new Set([...symbolLocalRefs.keys(), ...symbolExternalRefs.keys()])].map((id) => {
276
- const sourceRefs = symbolLocalRefs.get(id) || 0;
277
- const externalRefs = symbolExternalRefs.get(id) || 0;
278
- return { id, localRefs: sourceRefs + externalRefs, sourceRefs, externalRefs };
279
- }),
280
- folderLoc,
281
- totals: {
282
- files: fileModule.size,
283
- nodes: nodes.filter((n) => n.file_type === "code").length,
284
- fileEdges: fileEdges.size,
285
- typeOnlyFileEdges: typeOnlyFileEdges.size,
286
- compileOnlyFileEdges: compileOnlyFileEdges.size,
287
- compileTimeFileEdges: compileTimeFileEdges.size,
288
- moduleEdges: moduleEdges.size,
289
- typeOnlyModuleEdges: typeOnlyModuleEdges.size,
290
- compileOnlyModuleEdges: compileOnlyModuleEdges.size,
291
- compileTimeModuleEdges: compileTimeModuleEdges.size,
292
- symbols: symbols.length,
293
- symbolEdges: symbolEdges.length
294
- }
295
- };
296
- }
@@ -1,24 +0,0 @@
1
- import { communityTerritoryOf } from "../graph/community.js";
2
-
3
- // Stable folder territories used by module_map. Nested package source roots keep the first directory below
4
- // `src`; otherwise a monorepo's entire crate/app would collapse into one module with no visible dependencies.
5
- export function folderModuleOf(file) {
6
- if (/\.java$/i.test(String(file || ""))) return communityTerritoryOf(file);
7
- const dirs = String(file || "").split(/[\\/]/).filter(Boolean).slice(0, -1);
8
- if (!dirs.length) return "(root)";
9
- const sourceRoot = dirs.lastIndexOf("src");
10
- if (sourceRoot >= 0 && dirs.length > sourceRoot + 1) return dirs.slice(0, sourceRoot + 2).join("/");
11
- return dirs.slice(0, 2).join("/");
12
- }
13
-
14
- export function edgeList(map) {
15
- return [...map.entries()]
16
- .map(([key, value]) => {
17
- const splitAt = key.indexOf(" ");
18
- const from = key.slice(0, splitAt), to = key.slice(splitAt + 1);
19
- if (typeof value === "number") return { from, to, count: value };
20
- const dominant = Object.entries(value.rels).sort((a, b) => b[1] - a[1])[0];
21
- return { from, to, count: value.count, relation: dominant ? dominant[0] : null };
22
- })
23
- .sort((a, b) => b.count - a.count);
24
- }
@@ -1,7 +0,0 @@
1
- // Reading a built graph.json and aggregating it into the views the UI needs: named communities,
2
- // degree hotspots, and the file/module/symbol rollup. `aggregateGraph` is pure (covered by tests);
3
- // the rest read the graph file from disk.
4
- // Facade: implementation lives in graph-analysis.summaries.js and graph-analysis.aggregate.js
5
- // (with internal helpers in graph-analysis.refs.js).
6
- export { summarizeCommunities, summarizeHotspots } from "./graph-analysis.summaries.js";
7
- export { analyzeGraph, aggregateGraph, folderModuleOf } from "./graph-analysis.aggregate.js";
@@ -1,202 +0,0 @@
1
- // Internal helpers for graph-analysis: identifier reference counting inside source text,
2
- // relative-import candidate resolution, and the import-driven external-reference scan that feeds
3
- // aggregateGraph's symbolRefs output. Not part of the public graph-analysis facade.
4
- import { normRepoPath, normalizeRepoParts, dirOfRepoPath } from "./coverage-reports.js";
5
-
6
- const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
- export const bareSymbolName = (label) => String(label || "").replace(/\s*\(.*$/, "").trim();
8
- const isIdentifierName = (name) => /^[A-Za-z_$][\w$]*$/.test(name);
9
-
10
- function countIdentifierInLine(line, name) {
11
- const re = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
12
- let count = 0;
13
- while (re.exec(line)) count++;
14
- return count;
15
- }
16
-
17
- function countIdentifierInText(text, name) {
18
- if (!text || !isIdentifierName(name)) return 0;
19
- return String(text).split(/\r?\n/).reduce((sum, line) => sum + countIdentifierInLine(line, name), 0);
20
- }
21
-
22
- function countMemberAccess(text, objectName, memberName) {
23
- if (!text || !isIdentifierName(objectName) || !isIdentifierName(memberName)) return 0;
24
- const re = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(objectName)}\\s*\\.\\s*${escapeRegExp(memberName)}(?![A-Za-z0-9_$])`, "g");
25
- let count = 0;
26
- while (re.exec(String(text))) count++;
27
- return count;
28
- }
29
-
30
- export function countLocalRefsOutsideOwnRange(text, name, startLine, endLine) {
31
- if (!text || !isIdentifierName(name)) return 0;
32
- const start = Number.isFinite(startLine) && startLine > 0 ? startLine : 0;
33
- const end = Number.isFinite(endLine) && endLine >= start ? endLine : start;
34
- let refs = 0;
35
- const lines = String(text).split(/\r?\n/);
36
- for (let i = 0; i < lines.length; i++) {
37
- const lineNo = i + 1;
38
- if (start && lineNo >= start && lineNo <= end) continue;
39
- refs += countIdentifierInLine(lines[i], name);
40
- }
41
- return refs;
42
- }
43
-
44
- const lowerBound = (values, target) => {
45
- let low = 0, high = values.length;
46
- while (low < high) {
47
- const middle = (low + high) >>> 1;
48
- if (values[middle] < target) low = middle + 1;
49
- else high = middle;
50
- }
51
- return low;
52
- };
53
-
54
- // Count every wanted identifier in a file once, then subtract occurrences inside each declaration
55
- // with binary searches over occurrence line numbers. The former per-symbol full-file scan became
56
- // quadratic on large vendored files (thousands of symbols over tens of thousands of lines).
57
- export function computeLocalSymbolRefs(text, symbols) {
58
- const candidates = (symbols || []).map((symbol) => ({
59
- id: symbol.id,
60
- name: bareSymbolName(symbol.name ?? symbol.label),
61
- start: Number(symbol.startLine ?? symbol.start),
62
- end: Number(symbol.endLine ?? symbol.end),
63
- })).filter((symbol) => symbol.id && isIdentifierName(symbol.name));
64
- if (!text || !candidates.length) return new Map();
65
- const wanted = new Set(candidates.map((symbol) => symbol.name));
66
- const occurrenceLines = new Map();
67
- const identifier = /[A-Za-z_$][\w$]*/g;
68
- const lines = String(text).split(/\r?\n/);
69
- for (let index = 0; index < lines.length; index++) {
70
- identifier.lastIndex = 0;
71
- let match;
72
- while ((match = identifier.exec(lines[index]))) {
73
- const name = match[0];
74
- if (!wanted.has(name)) continue;
75
- const positions = occurrenceLines.get(name) || [];
76
- positions.push(index + 1);
77
- occurrenceLines.set(name, positions);
78
- }
79
- }
80
- const refs = new Map();
81
- for (const symbol of candidates) {
82
- const positions = occurrenceLines.get(symbol.name) || [];
83
- const start = Number.isFinite(symbol.start) && symbol.start > 0 ? symbol.start : 0;
84
- const end = Number.isFinite(symbol.end) && symbol.end >= start ? symbol.end : start;
85
- const inside = start ? lowerBound(positions, end + 1) - lowerBound(positions, start) : 0;
86
- const outside = Math.max(0, positions.length - inside);
87
- if (outside > 0) refs.set(symbol.id, outside);
88
- }
89
- return refs;
90
- }
91
-
92
- function importCandidates(fromFile, spec) {
93
- const raw = String(spec || "");
94
- if (!raw.startsWith(".")) return [];
95
- const base = normalizeRepoParts(`${dirOfRepoPath(fromFile)}/${raw}`);
96
- return [
97
- base,
98
- `${base}.js`,
99
- `${base}.ts`,
100
- `${base}.jsx`,
101
- `${base}.tsx`,
102
- `${base}.mjs`,
103
- `${base}.cjs`,
104
- `${base}/index.js`,
105
- `${base}/index.ts`,
106
- `${base}/index.jsx`,
107
- `${base}/index.tsx`
108
- ];
109
- }
110
-
111
- function stripModuleStatements(text) {
112
- return String(text || "")
113
- .replace(/\bimport\s+[\s\S]*?\s+from\s*['"][^'"]+['"]\s*;?/g, "")
114
- .replace(/\bexport\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s*['"][^'"]+['"]\s*;?/g, "")
115
- .replace(/\b(?:const|let|var)\s+\{[\s\S]*?\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)\s*;?/g, "");
116
- }
117
-
118
- function parseNamedSpecifiers(raw) {
119
- return String(raw || "")
120
- .split(",")
121
- .map((part) => part.trim())
122
- .filter(Boolean)
123
- .map((part) => {
124
- const typeOnly = /^type\s+/.test(part);
125
- const clean = part.replace(/^type\s+/, "").trim();
126
- const m = clean.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
127
- return m ? { imported: m[1], local: m[2] || m[1], typeOnly } : null;
128
- })
129
- .filter(Boolean);
130
- }
131
-
132
- // Import-driven external references: scan each file's import/export/require statements and count how
133
- // often the imported names are used in the importer, attributing counts to the target file's symbol
134
- // ids. Moved whole out of aggregateGraph; parameters are its `filePath` (fid → repo path),
135
- // `fileSymbols` (fid → symbol list) and `fileText` (fid → source text) maps.
136
- export function computeSymbolExternalRefs(filePath, fileSymbols, fileText) {
137
- const fidByPath = new Map();
138
- for (const [fid, p] of filePath) fidByPath.set(normRepoPath(p), fid);
139
- const symbolIdsByFileAndName = new Map();
140
- for (const [fid, list] of fileSymbols) {
141
- const byName = symbolIdsByFileAndName.get(fid) || new Map();
142
- for (const sym of list || []) {
143
- const name = bareSymbolName(sym.label);
144
- if (!isIdentifierName(name)) continue;
145
- const ids = byName.get(name) || [];
146
- ids.push({id: sym.id, space: sym.symbolSpace || "value"});
147
- byName.set(name, ids);
148
- }
149
- symbolIdsByFileAndName.set(fid, byName);
150
- }
151
-
152
- const symbolExternalRefs = new Map();
153
- const addExternalRefs = (targetFid, importedName, refs, typeOnly = false) => {
154
- if (!targetFid || refs <= 0 || !isIdentifierName(importedName)) return;
155
- const ids = symbolIdsByFileAndName.get(targetFid)?.get(importedName) || [];
156
- for (const entry of ids) {
157
- const matches = entry.space === "both" || (typeOnly ? entry.space === "type" : entry.space !== "type");
158
- if (matches) symbolExternalRefs.set(entry.id, (symbolExternalRefs.get(entry.id) || 0) + refs);
159
- }
160
- };
161
- const resolveImportedFid = (fromPath, spec) => {
162
- for (const candidate of importCandidates(fromPath, spec)) {
163
- const fid = fidByPath.get(candidate);
164
- if (fid) return fid;
165
- }
166
- return "";
167
- };
168
- for (const [importerFid, txt] of fileText) {
169
- const importerPath = filePath.get(importerFid) || importerFid;
170
- const scrubbed = stripModuleStatements(txt);
171
- const seenStatements = [
172
- ...String(txt).matchAll(/\bimport\s+([\s\S]*?)\s+from\s*['"]([^'"]+)['"]\s*;?/g)
173
- ];
174
- for (const m of seenStatements) {
175
- const targetFid = resolveImportedFid(importerPath, m[2]);
176
- if (!targetFid) continue;
177
- const named = String(m[1] || "").match(/\{([\s\S]*?)\}/);
178
- if (named) {
179
- const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
180
- for (const spec of parseNamedSpecifiers(named[1])) addExternalRefs(targetFid, spec.imported, countIdentifierInText(scrubbed, spec.local), statementTypeOnly || spec.typeOnly);
181
- }
182
- const ns = String(m[1] || "").match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
183
- if (ns) {
184
- const byName = symbolIdsByFileAndName.get(targetFid) || new Map();
185
- const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
186
- for (const name of byName.keys()) addExternalRefs(targetFid, name, countMemberAccess(scrubbed, ns[1], name), statementTypeOnly);
187
- }
188
- }
189
- for (const m of String(txt).matchAll(/\bexport\s+(type\s+)?\{([\s\S]*?)\}\s+from\s*['"]([^'"]+)['"]\s*;?/g)) {
190
- const targetFid = resolveImportedFid(importerPath, m[3]);
191
- if (!targetFid) continue;
192
- const statementTypeOnly = Boolean(m[1]);
193
- for (const spec of parseNamedSpecifiers(m[2])) addExternalRefs(targetFid, spec.imported, 1, statementTypeOnly || spec.typeOnly);
194
- }
195
- for (const m of String(txt).matchAll(/\b(?:const|let|var)\s+\{([\s\S]*?)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)\s*;?/g)) {
196
- const targetFid = resolveImportedFid(importerPath, m[2]);
197
- if (!targetFid) continue;
198
- for (const spec of parseNamedSpecifiers(m[1])) addExternalRefs(targetFid, spec.imported, countIdentifierInText(scrubbed, spec.local));
199
- }
200
- }
201
- return symbolExternalRefs;
202
- }