weavatrix 0.3.13 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +231 -199
  3. package/bin/native/darwin-arm64/weavatrix +0 -0
  4. package/bin/native/darwin-x64/weavatrix +0 -0
  5. package/bin/native/linux-arm64/weavatrix +0 -0
  6. package/bin/native/linux-x64/weavatrix +0 -0
  7. package/bin/native/win32-arm64/weavatrix.exe +0 -0
  8. package/bin/native/win32-x64/weavatrix.exe +0 -0
  9. package/bin/resolve-binary.mjs +73 -0
  10. package/bin/run-native.mjs +26 -0
  11. package/bin/weavatrix-mcp.mjs +7 -6
  12. package/bin/weavatrix.mjs +5 -0
  13. package/package.json +8 -65
  14. package/skill/SKILL.md +62 -374
  15. package/skill/agents/openai.yaml +4 -0
  16. package/skill/references/tool-routing.md +45 -0
  17. package/SECURITY.md +0 -101
  18. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  19. package/docs/agent-task-benchmark.md +0 -58
  20. package/docs/releases/v0.2.19.md +0 -33
  21. package/docs/releases/v0.3.0.md +0 -43
  22. package/docs/releases/v0.3.1.md +0 -30
  23. package/docs/releases/v0.3.10.md +0 -76
  24. package/docs/releases/v0.3.11.md +0 -55
  25. package/docs/releases/v0.3.12.md +0 -44
  26. package/docs/releases/v0.3.13.md +0 -34
  27. package/docs/releases/v0.3.2.md +0 -15
  28. package/docs/releases/v0.3.3.md +0 -25
  29. package/docs/releases/v0.3.4.md +0 -40
  30. package/docs/releases/v0.3.5.md +0 -50
  31. package/docs/releases/v0.3.6.md +0 -44
  32. package/docs/releases/v0.3.7.md +0 -37
  33. package/docs/releases/v0.3.8.md +0 -21
  34. package/docs/releases/v0.3.9.md +0 -36
  35. package/scripts/run-agent-task-benchmark.mjs +0 -118
  36. package/src/analysis/allowed-test-runner.js +0 -70
  37. package/src/analysis/architecture/contract-graph.js +0 -119
  38. package/src/analysis/architecture/contract-schema.js +0 -110
  39. package/src/analysis/architecture/contract-storage.js +0 -35
  40. package/src/analysis/architecture/contract-verification.js +0 -168
  41. package/src/analysis/architecture-contract.js +0 -16
  42. package/src/analysis/audit-debt.js +0 -94
  43. package/src/analysis/audit-extensions.js +0 -60
  44. package/src/analysis/cargo-dependency-evidence.js +0 -108
  45. package/src/analysis/cargo-manifests.js +0 -81
  46. package/src/analysis/change-classification/diff-parser.js +0 -103
  47. package/src/analysis/change-classification/options.js +0 -40
  48. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  49. package/src/analysis/change-classification.js +0 -188
  50. package/src/analysis/coverage-reports.js +0 -235
  51. package/src/analysis/cycle-route.js +0 -14
  52. package/src/analysis/data-flow-evidence.js +0 -114
  53. package/src/analysis/dead-check.js +0 -300
  54. package/src/analysis/dead-code-review/candidates.js +0 -194
  55. package/src/analysis/dead-code-review/policy.js +0 -23
  56. package/src/analysis/dead-code-review.js +0 -119
  57. package/src/analysis/dep-check-ecosystems.js +0 -205
  58. package/src/analysis/dep-check.js +0 -290
  59. package/src/analysis/dep-rules.js +0 -10
  60. package/src/analysis/dependency/conventions.js +0 -16
  61. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  62. package/src/analysis/dependency/source-references.js +0 -21
  63. package/src/analysis/duplicate-groups.js +0 -104
  64. package/src/analysis/duplicates-worker.js +0 -11
  65. package/src/analysis/duplicates.compute.js +0 -269
  66. package/src/analysis/duplicates.js +0 -15
  67. package/src/analysis/duplicates.run.js +0 -52
  68. package/src/analysis/duplicates.tokenize.js +0 -199
  69. package/src/analysis/endpoints/common.js +0 -62
  70. package/src/analysis/endpoints/extract.js +0 -107
  71. package/src/analysis/endpoints/inventory.js +0 -84
  72. package/src/analysis/endpoints/mounts.js +0 -129
  73. package/src/analysis/endpoints-java.js +0 -260
  74. package/src/analysis/endpoints-rust.js +0 -124
  75. package/src/analysis/endpoints.js +0 -3
  76. package/src/analysis/findings.js +0 -70
  77. package/src/analysis/git-history/analytics.js +0 -177
  78. package/src/analysis/git-history/collector.js +0 -67
  79. package/src/analysis/git-history/options.js +0 -66
  80. package/src/analysis/git-history.js +0 -143
  81. package/src/analysis/git-ref-graph.js +0 -76
  82. package/src/analysis/go-dependency-evidence.js +0 -62
  83. package/src/analysis/graph-analysis.aggregate.js +0 -295
  84. package/src/analysis/graph-analysis.edges.js +0 -24
  85. package/src/analysis/graph-analysis.js +0 -7
  86. package/src/analysis/graph-analysis.refs.js +0 -154
  87. package/src/analysis/graph-analysis.summaries.js +0 -65
  88. package/src/analysis/health-capabilities.js +0 -83
  89. package/src/analysis/hot-path-review.js +0 -266
  90. package/src/analysis/http-contract-wrappers.js +0 -241
  91. package/src/analysis/http-contracts/analysis.js +0 -179
  92. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  93. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  94. package/src/analysis/http-contracts/graph-context.js +0 -143
  95. package/src/analysis/http-contracts/matching.js +0 -61
  96. package/src/analysis/http-contracts/shared.js +0 -85
  97. package/src/analysis/http-contracts.js +0 -6
  98. package/src/analysis/internal-audit/dependency-health.js +0 -113
  99. package/src/analysis/internal-audit/python-manifests.js +0 -78
  100. package/src/analysis/internal-audit/repo-files.js +0 -50
  101. package/src/analysis/internal-audit/supply-chain.js +0 -149
  102. package/src/analysis/internal-audit.collect.js +0 -213
  103. package/src/analysis/internal-audit.js +0 -9
  104. package/src/analysis/internal-audit.reach.js +0 -173
  105. package/src/analysis/internal-audit.run.js +0 -256
  106. package/src/analysis/java-source.js +0 -36
  107. package/src/analysis/jvm-artifact-index.js +0 -87
  108. package/src/analysis/jvm-dependency-evidence.js +0 -138
  109. package/src/analysis/jvm-manifests.js +0 -114
  110. package/src/analysis/manifests.js +0 -123
  111. package/src/analysis/package-reachability.js +0 -39
  112. package/src/analysis/source-complexity.ast.js +0 -97
  113. package/src/analysis/source-complexity.constants.js +0 -55
  114. package/src/analysis/source-complexity.js +0 -14
  115. package/src/analysis/source-complexity.report.js +0 -81
  116. package/src/analysis/source-complexity.walk.js +0 -228
  117. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  118. package/src/analysis/source-correctness.js +0 -222
  119. package/src/analysis/static-test-reachability.js +0 -150
  120. package/src/analysis/structure/dependency-graph.js +0 -151
  121. package/src/analysis/structure/findings.js +0 -162
  122. package/src/analysis/task-retrieval.js +0 -107
  123. package/src/analysis/transport-contracts.js +0 -224
  124. package/src/analysis/transport-runtime-evidence.js +0 -155
  125. package/src/analysis-kit.mjs +0 -17
  126. package/src/bounds.js +0 -4
  127. package/src/build-graph.js +0 -209
  128. package/src/child-env.js +0 -7
  129. package/src/config.js +0 -5
  130. package/src/extension/local-services.mjs +0 -75
  131. package/src/git-exec.js +0 -67
  132. package/src/graph/build-worker.js +0 -49
  133. package/src/graph/builder/go-receiver-resolution.js +0 -112
  134. package/src/graph/builder/js/dynamic-import.js +0 -26
  135. package/src/graph/builder/js/queries.js +0 -48
  136. package/src/graph/builder/lang-csharp.js +0 -40
  137. package/src/graph/builder/lang-css.js +0 -29
  138. package/src/graph/builder/lang-go.js +0 -140
  139. package/src/graph/builder/lang-html.js +0 -29
  140. package/src/graph/builder/lang-java-calls.js +0 -142
  141. package/src/graph/builder/lang-java.js +0 -215
  142. package/src/graph/builder/lang-js.js +0 -291
  143. package/src/graph/builder/lang-python.js +0 -235
  144. package/src/graph/builder/lang-rust-calls.js +0 -63
  145. package/src/graph/builder/lang-rust.js +0 -299
  146. package/src/graph/builder/lang-solidity.js +0 -126
  147. package/src/graph/builder/lang-sql.js +0 -263
  148. package/src/graph/builder/pass2-resolution.js +0 -81
  149. package/src/graph/builder/spec-pkg.js +0 -87
  150. package/src/graph/community.js +0 -27
  151. package/src/graph/edge-provenance.js +0 -31
  152. package/src/graph/file-lock.js +0 -69
  153. package/src/graph/freshness-probe.js +0 -142
  154. package/src/graph/graph-filter.js +0 -63
  155. package/src/graph/incremental-refresh.js +0 -235
  156. package/src/graph/internal-builder.barrels.js +0 -235
  157. package/src/graph/internal-builder.build.js +0 -300
  158. package/src/graph/internal-builder.java.js +0 -33
  159. package/src/graph/internal-builder.js +0 -12
  160. package/src/graph/internal-builder.langs.js +0 -157
  161. package/src/graph/internal-builder.pass2.js +0 -163
  162. package/src/graph/internal-builder.resolvers.js +0 -286
  163. package/src/graph/layout.js +0 -51
  164. package/src/graph/node-id.js +0 -7
  165. package/src/graph/parser-artifact-boundary.js +0 -55
  166. package/src/graph/relations.js +0 -4
  167. package/src/graph/repo-registry.js +0 -124
  168. package/src/graph/resolvers/rust.js +0 -117
  169. package/src/infra/infra-items.js +0 -255
  170. package/src/infra/infra-registry.js +0 -184
  171. package/src/infra/infra.detect.js +0 -119
  172. package/src/infra/infra.js +0 -38
  173. package/src/infra/infra.match.js +0 -102
  174. package/src/infra/infra.scan.js +0 -93
  175. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  176. package/src/mcp/architecture-bootstrap.mjs +0 -168
  177. package/src/mcp/architecture-starter.mjs +0 -234
  178. package/src/mcp/catalog.mjs +0 -233
  179. package/src/mcp/company-contract-page.mjs +0 -142
  180. package/src/mcp/company-contract-reconcile.mjs +0 -87
  181. package/src/mcp/company-contract-verdict.mjs +0 -42
  182. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  183. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  184. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  185. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  186. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  187. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  188. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  189. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  190. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  191. package/src/mcp/evidence-snapshot.mjs +0 -50
  192. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  193. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  194. package/src/mcp/extension-api.mjs +0 -77
  195. package/src/mcp/git-output.mjs +0 -7
  196. package/src/mcp/graph/context-core.mjs +0 -111
  197. package/src/mcp/graph/context-seeds.mjs +0 -207
  198. package/src/mcp/graph/context-state.mjs +0 -85
  199. package/src/mcp/graph/reverse-reach.mjs +0 -42
  200. package/src/mcp/graph/tools-core.mjs +0 -143
  201. package/src/mcp/graph/tools-query.mjs +0 -276
  202. package/src/mcp/graph-context.mjs +0 -5
  203. package/src/mcp/graph-diff.mjs +0 -218
  204. package/src/mcp/health/audit-format.mjs +0 -199
  205. package/src/mcp/health/audit.mjs +0 -174
  206. package/src/mcp/health/dead-code.mjs +0 -110
  207. package/src/mcp/health/duplicates.mjs +0 -85
  208. package/src/mcp/health/endpoints.mjs +0 -71
  209. package/src/mcp/health/structure.mjs +0 -219
  210. package/src/mcp/runtime-version.mjs +0 -31
  211. package/src/mcp/server/auto-refresh.mjs +0 -66
  212. package/src/mcp/server/runtime-config.mjs +0 -43
  213. package/src/mcp/server/shutdown.mjs +0 -40
  214. package/src/mcp/staleness-notice.mjs +0 -20
  215. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  216. package/src/mcp/sync/evidence-common.mjs +0 -88
  217. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  218. package/src/mcp/sync/evidence-health.mjs +0 -56
  219. package/src/mcp/sync/evidence-packages.mjs +0 -95
  220. package/src/mcp/sync/payload-common.mjs +0 -97
  221. package/src/mcp/sync/payload-v2.mjs +0 -54
  222. package/src/mcp/sync/payload-v3.mjs +0 -79
  223. package/src/mcp/sync-evidence.mjs +0 -23
  224. package/src/mcp/sync-payload.mjs +0 -70
  225. package/src/mcp/tool-result.mjs +0 -54
  226. package/src/mcp/tools-actions.mjs +0 -11
  227. package/src/mcp/tools-architecture.mjs +0 -177
  228. package/src/mcp/tools-company.mjs +0 -251
  229. package/src/mcp/tools-context.mjs +0 -222
  230. package/src/mcp/tools-endpoints.mjs +0 -207
  231. package/src/mcp/tools-graph-hubs.mjs +0 -78
  232. package/src/mcp/tools-graph.mjs +0 -17
  233. package/src/mcp/tools-health.mjs +0 -24
  234. package/src/mcp/tools-history.mjs +0 -28
  235. package/src/mcp/tools-impact-change.mjs +0 -220
  236. package/src/mcp/tools-impact-precision.mjs +0 -89
  237. package/src/mcp/tools-impact.mjs +0 -169
  238. package/src/mcp/tools-source.mjs +0 -238
  239. package/src/mcp/tools-verified-change.mjs +0 -200
  240. package/src/mcp-rg.mjs +0 -50
  241. package/src/mcp-runtime.mjs +0 -2
  242. package/src/mcp-server.mjs +0 -298
  243. package/src/mcp-source-tools.mjs +0 -163
  244. package/src/path-classification.js +0 -220
  245. package/src/path-ignore.js +0 -69
  246. package/src/precision/lsp-client/constants.js +0 -12
  247. package/src/precision/lsp-client/environment.js +0 -20
  248. package/src/precision/lsp-client/errors.js +0 -15
  249. package/src/precision/lsp-client/lifecycle.js +0 -114
  250. package/src/precision/lsp-client/message-parser.js +0 -81
  251. package/src/precision/lsp-client/protocol.js +0 -212
  252. package/src/precision/lsp-client/registry.js +0 -59
  253. package/src/precision/lsp-client/repo-uri.js +0 -60
  254. package/src/precision/lsp-client/stdio-client.js +0 -140
  255. package/src/precision/lsp-client.js +0 -10
  256. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  257. package/src/precision/lsp-overlay/build.js +0 -261
  258. package/src/precision/lsp-overlay/contract.js +0 -61
  259. package/src/precision/lsp-overlay/reference-results.js +0 -108
  260. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  261. package/src/precision/lsp-overlay/source-session.js +0 -142
  262. package/src/precision/lsp-overlay/store.js +0 -150
  263. package/src/precision/lsp-overlay/target-index.js +0 -152
  264. package/src/precision/lsp-overlay/target-query.js +0 -55
  265. package/src/precision/lsp-overlay.js +0 -11
  266. package/src/precision/symbol-query.js +0 -224
  267. package/src/precision/typescript-lsp-provider.js +0 -12
  268. package/src/precision/typescript-provider/client.js +0 -89
  269. package/src/precision/typescript-provider/discovery.js +0 -124
  270. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  271. package/src/precision/typescript-provider/project-host.js +0 -254
  272. package/src/precision/typescript-provider/project-safety.js +0 -166
  273. package/src/precision/typescript-provider/reference-usage.js +0 -53
  274. package/src/process.js +0 -95
  275. package/src/repo-path.js +0 -53
  276. package/src/scan/discover.inventory.js +0 -78
  277. package/src/scan/discover.js +0 -5
  278. package/src/scan/discover.list.js +0 -79
  279. package/src/scan/discover.stack.js +0 -227
  280. package/src/scan/search.core.js +0 -24
  281. package/src/scan/search.git.js +0 -102
  282. package/src/scan/search.js +0 -9
  283. package/src/scan/search.node.js +0 -83
  284. package/src/scan/search.preview.js +0 -49
  285. package/src/scan/search.rg.js +0 -145
  286. package/src/security/advisory-store.js +0 -172
  287. package/src/security/installed-jvm-rust.js +0 -46
  288. package/src/security/installed.js +0 -280
  289. package/src/security/malware-file-heuristics.js +0 -121
  290. package/src/security/malware-heuristics.exclusions.js +0 -134
  291. package/src/security/malware-heuristics.js +0 -15
  292. package/src/security/malware-heuristics.roots.js +0 -145
  293. package/src/security/malware-heuristics.scan.js +0 -89
  294. package/src/security/malware-heuristics.sweep.js +0 -122
  295. package/src/security/malware-scoring.js +0 -105
  296. package/src/security/match.js +0 -85
  297. package/src/security/registry-sig.classify.js +0 -137
  298. package/src/security/registry-sig.js +0 -18
  299. package/src/security/registry-sig.rules.js +0 -192
  300. package/src/security/rust-advisory-report.js +0 -60
  301. package/src/security/typosquat.js +0 -81
  302. package/src/util.js +0 -40
  303. package/src/version.js +0 -7
@@ -1,295 +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, countLocalRefsOutsideOwnRange, 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
- for (let i = 0; i < sorted.length; i++) {
227
- const start = sorted[i].start;
228
- const next = i + 1 < sorted.length ? sorted[i + 1].start : total > start ? total + 1 : start + 1;
229
- const exactEnd = Number.isFinite(sorted[i].end) && sorted[i].end >= start ? sorted[i].end : 0;
230
- const end = exactEnd || Math.max(start, next - 1);
231
- const loc = Math.max(1, end - start + 1);
232
- symbolLoc.set(sorted[i].id, loc);
233
- sorted[i].ref.loc = loc;
234
- sorted[i].ref.endLine = end;
235
- const cov = coverageForRange(fid, start, end);
236
- if (cov != null) symbolCoverage.set(sorted[i].id, cov);
237
- const refs = countLocalRefsOutsideOwnRange(txt, bareSymbolName(sorted[i].label), start, end);
238
- if (refs > 0) symbolLocalRefs.set(sorted[i].id, refs);
239
- }
240
- }
241
- for (const s of symbols) {
242
- const fid = id2file.get(s.id);
243
- s.loc = symbolLoc.get(s.id) || 0;
244
- s.coverage = symbolCoverage.get(s.id) ?? coverageForFile(fid)?.pct ?? null;
245
- }
246
-
247
- return {
248
- complexityV: Number(graph.complexityV) || 0,
249
- modules: [...modules.values()]
250
- .map((mod) => ({
251
- name: mod.name,
252
- repo: mod.repo || null,
253
- fileCount: mod.files.length,
254
- nodeCount: moduleNodeCount.get(mod.name) || 0,
255
- symbolCount: mod.files.reduce((sum, f) => sum + f.symbolCount, 0),
256
- files: mod.files
257
- .map((f) => {
258
- const cov = coverageForFile(f.file);
259
- return { ...f, loc: fileLoc.get(f.file) || 0, coverage: cov?.pct ?? null, coverageSource: cov?.source || "" };
260
- })
261
- .sort((a, b) => b.symbolCount - a.symbolCount)
262
- }))
263
- .sort((a, b) => b.fileCount - a.fileCount),
264
- moduleEdges: edgeList(moduleEdges),
265
- typeOnlyModuleEdges: edgeList(typeOnlyModuleEdges),
266
- compileOnlyModuleEdges: edgeList(compileOnlyModuleEdges),
267
- compileTimeModuleEdges: edgeList(compileTimeModuleEdges),
268
- fileEdges: edgeList(fileEdges),
269
- typeOnlyFileEdges: edgeList(typeOnlyFileEdges),
270
- compileOnlyFileEdges: edgeList(compileOnlyFileEdges),
271
- compileTimeFileEdges: edgeList(compileTimeFileEdges),
272
- symbols,
273
- symbolEdges,
274
- symbolRefs: [...new Set([...symbolLocalRefs.keys(), ...symbolExternalRefs.keys()])].map((id) => {
275
- const sourceRefs = symbolLocalRefs.get(id) || 0;
276
- const externalRefs = symbolExternalRefs.get(id) || 0;
277
- return { id, localRefs: sourceRefs + externalRefs, sourceRefs, externalRefs };
278
- }),
279
- folderLoc,
280
- totals: {
281
- files: fileModule.size,
282
- nodes: nodes.filter((n) => n.file_type === "code").length,
283
- fileEdges: fileEdges.size,
284
- typeOnlyFileEdges: typeOnlyFileEdges.size,
285
- compileOnlyFileEdges: compileOnlyFileEdges.size,
286
- compileTimeFileEdges: compileTimeFileEdges.size,
287
- moduleEdges: moduleEdges.size,
288
- typeOnlyModuleEdges: typeOnlyModuleEdges.size,
289
- compileOnlyModuleEdges: compileOnlyModuleEdges.size,
290
- compileTimeModuleEdges: compileTimeModuleEdges.size,
291
- symbols: symbols.length,
292
- symbolEdges: symbolEdges.length
293
- }
294
- };
295
- }
@@ -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,154 +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
- function importCandidates(fromFile, spec) {
45
- const raw = String(spec || "");
46
- if (!raw.startsWith(".")) return [];
47
- const base = normalizeRepoParts(`${dirOfRepoPath(fromFile)}/${raw}`);
48
- return [
49
- base,
50
- `${base}.js`,
51
- `${base}.ts`,
52
- `${base}.jsx`,
53
- `${base}.tsx`,
54
- `${base}.mjs`,
55
- `${base}.cjs`,
56
- `${base}/index.js`,
57
- `${base}/index.ts`,
58
- `${base}/index.jsx`,
59
- `${base}/index.tsx`
60
- ];
61
- }
62
-
63
- function stripModuleStatements(text) {
64
- return String(text || "")
65
- .replace(/\bimport\s+[\s\S]*?\s+from\s*['"][^'"]+['"]\s*;?/g, "")
66
- .replace(/\bexport\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s*['"][^'"]+['"]\s*;?/g, "")
67
- .replace(/\b(?:const|let|var)\s+\{[\s\S]*?\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)\s*;?/g, "");
68
- }
69
-
70
- function parseNamedSpecifiers(raw) {
71
- return String(raw || "")
72
- .split(",")
73
- .map((part) => part.trim())
74
- .filter(Boolean)
75
- .map((part) => {
76
- const typeOnly = /^type\s+/.test(part);
77
- const clean = part.replace(/^type\s+/, "").trim();
78
- const m = clean.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
79
- return m ? { imported: m[1], local: m[2] || m[1], typeOnly } : null;
80
- })
81
- .filter(Boolean);
82
- }
83
-
84
- // Import-driven external references: scan each file's import/export/require statements and count how
85
- // often the imported names are used in the importer, attributing counts to the target file's symbol
86
- // ids. Moved whole out of aggregateGraph; parameters are its `filePath` (fid → repo path),
87
- // `fileSymbols` (fid → symbol list) and `fileText` (fid → source text) maps.
88
- export function computeSymbolExternalRefs(filePath, fileSymbols, fileText) {
89
- const fidByPath = new Map();
90
- for (const [fid, p] of filePath) fidByPath.set(normRepoPath(p), fid);
91
- const symbolIdsByFileAndName = new Map();
92
- for (const [fid, list] of fileSymbols) {
93
- const byName = symbolIdsByFileAndName.get(fid) || new Map();
94
- for (const sym of list || []) {
95
- const name = bareSymbolName(sym.label);
96
- if (!isIdentifierName(name)) continue;
97
- const ids = byName.get(name) || [];
98
- ids.push({id: sym.id, space: sym.symbolSpace || "value"});
99
- byName.set(name, ids);
100
- }
101
- symbolIdsByFileAndName.set(fid, byName);
102
- }
103
-
104
- const symbolExternalRefs = new Map();
105
- const addExternalRefs = (targetFid, importedName, refs, typeOnly = false) => {
106
- if (!targetFid || refs <= 0 || !isIdentifierName(importedName)) return;
107
- const ids = symbolIdsByFileAndName.get(targetFid)?.get(importedName) || [];
108
- for (const entry of ids) {
109
- const matches = entry.space === "both" || (typeOnly ? entry.space === "type" : entry.space !== "type");
110
- if (matches) symbolExternalRefs.set(entry.id, (symbolExternalRefs.get(entry.id) || 0) + refs);
111
- }
112
- };
113
- const resolveImportedFid = (fromPath, spec) => {
114
- for (const candidate of importCandidates(fromPath, spec)) {
115
- const fid = fidByPath.get(candidate);
116
- if (fid) return fid;
117
- }
118
- return "";
119
- };
120
- for (const [importerFid, txt] of fileText) {
121
- const importerPath = filePath.get(importerFid) || importerFid;
122
- const scrubbed = stripModuleStatements(txt);
123
- const seenStatements = [
124
- ...String(txt).matchAll(/\bimport\s+([\s\S]*?)\s+from\s*['"]([^'"]+)['"]\s*;?/g)
125
- ];
126
- for (const m of seenStatements) {
127
- const targetFid = resolveImportedFid(importerPath, m[2]);
128
- if (!targetFid) continue;
129
- const named = String(m[1] || "").match(/\{([\s\S]*?)\}/);
130
- if (named) {
131
- const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
132
- for (const spec of parseNamedSpecifiers(named[1])) addExternalRefs(targetFid, spec.imported, countIdentifierInText(scrubbed, spec.local), statementTypeOnly || spec.typeOnly);
133
- }
134
- const ns = String(m[1] || "").match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
135
- if (ns) {
136
- const byName = symbolIdsByFileAndName.get(targetFid) || new Map();
137
- const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
138
- for (const name of byName.keys()) addExternalRefs(targetFid, name, countMemberAccess(scrubbed, ns[1], name), statementTypeOnly);
139
- }
140
- }
141
- for (const m of String(txt).matchAll(/\bexport\s+(type\s+)?\{([\s\S]*?)\}\s+from\s*['"]([^'"]+)['"]\s*;?/g)) {
142
- const targetFid = resolveImportedFid(importerPath, m[3]);
143
- if (!targetFid) continue;
144
- const statementTypeOnly = Boolean(m[1]);
145
- for (const spec of parseNamedSpecifiers(m[2])) addExternalRefs(targetFid, spec.imported, 1, statementTypeOnly || spec.typeOnly);
146
- }
147
- for (const m of String(txt).matchAll(/\b(?:const|let|var)\s+\{([\s\S]*?)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)\s*;?/g)) {
148
- const targetFid = resolveImportedFid(importerPath, m[2]);
149
- if (!targetFid) continue;
150
- for (const spec of parseNamedSpecifiers(m[1])) addExternalRefs(targetFid, spec.imported, countIdentifierInText(scrubbed, spec.local));
151
- }
152
- }
153
- return symbolExternalRefs;
154
- }
@@ -1,65 +0,0 @@
1
- // Reading a built graph.json and summarizing it for the UI cards: named communities and degree
2
- // hotspots. Both read the graph file from disk and return [] on any failure.
3
- import { readFileSync } from "node:fs";
4
- import { communityTerritoryOf } from "../graph/community.js";
5
- import { isStructuralRelation } from "../graph/relations.js";
6
-
7
- // graph-builder labels communities "Community N" without an LLM. Derive a real name from each
8
- // community's dominant folder + sample files so the UI shows modules, not bare numbers.
9
- export function summarizeCommunities(graphJsonPath, max = 40) {
10
- try {
11
- const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
12
- const byCommunity = new Map();
13
- for (const node of graph.nodes || []) {
14
- if (node.file_type !== "code") continue;
15
- const community = node.community;
16
- if (community === undefined || community === null) continue;
17
- if (!byCommunity.has(community)) byCommunity.set(community, { id: community, size: 0, dirs: new Map(), files: [] });
18
- const entry = byCommunity.get(community);
19
- entry.size += 1;
20
- const parts = String(node.source_file || "").split(/[\\/]/).filter(Boolean);
21
- const dir = communityTerritoryOf(node.source_file);
22
- entry.dirs.set(dir, (entry.dirs.get(dir) || 0) + 1);
23
- if (entry.files.length < 4) entry.files.push(parts[parts.length - 1] || node.source_file || "");
24
- }
25
- return [...byCommunity.values()]
26
- .map((entry) => {
27
- const dominant = [...entry.dirs.entries()].sort((left, right) => right[1] - left[1])[0];
28
- return { id: entry.id, size: entry.size, name: dominant ? dominant[0] : "(mixed)", files: entry.files };
29
- })
30
- .sort((left, right) => right.size - left.size)
31
- .slice(0, max);
32
- } catch {
33
- return [];
34
- }
35
- }
36
-
37
- // Top nodes by total degree (in+out) — the "load-bearing" / refactor-candidate hotspots.
38
- export function summarizeHotspots(graphJsonPath, max = 15) {
39
- try {
40
- const graph = JSON.parse(readFileSync(graphJsonPath, "utf8"));
41
- const nodes = graph.nodes || [];
42
- const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
43
- const inDeg = new Map();
44
- const outDeg = new Map();
45
- for (const link of graph.links || []) {
46
- if (isStructuralRelation(link.relation) || link.barrelProxy === true) continue;
47
- const s = endpoint(link.source);
48
- const t = endpoint(link.target);
49
- outDeg.set(s, (outDeg.get(s) || 0) + 1);
50
- inDeg.set(t, (inDeg.get(t) || 0) + 1);
51
- }
52
- return nodes
53
- .filter((node) => node.file_type === "code")
54
- .map((node) => {
55
- const inbound = inDeg.get(node.id) || 0;
56
- const outbound = outDeg.get(node.id) || 0;
57
- return { label: node.label || node.norm_label || node.id, file: node.source_file || "", in: inbound, out: outbound, degree: inbound + outbound };
58
- })
59
- .filter((node) => node.degree > 0)
60
- .sort((left, right) => right.degree - left.degree)
61
- .slice(0, max);
62
- } catch {
63
- return [];
64
- }
65
- }
@@ -1,83 +0,0 @@
1
- // Honest Health capability coverage. `status` says whether a check actually ran for this repository;
2
- // `completeness` says whether its evidence can support a repository-wide conclusion.
3
- const capability = (status, completeness, detail, extra = {}) => ({ status, completeness, detail, ...extra });
4
-
5
- const sourceLanguages = (files) => {
6
- const languages = new Set();
7
- for (const file of files || []) {
8
- if (/\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/i.test(file)) languages.add("javascript/typescript");
9
- else if (/\.py$/i.test(file)) languages.add("python");
10
- else if (/\.go$/i.test(file)) languages.add("go");
11
- else if (/\.java$/i.test(file)) languages.add("java");
12
- else if (/\.rs$/i.test(file)) languages.add("rust");
13
- else if (/\.cs$/i.test(file)) languages.add("csharp");
14
- }
15
- return languages;
16
- };
17
-
18
- const checkCapability = (check, supported, label) => {
19
- if (!supported) return capability("NOT_SUPPORTED", "PARTIAL", `${label} is not supported for the discovered package ecosystem.`);
20
- if (check?.status === "OK") return capability("CHECKED", "COMPLETE", check.detail || `${label} completed.`);
21
- if (check?.status === "PARTIAL") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} evidence is partial.`);
22
- if (check?.status === "ERROR") return capability("NOT_CHECKED", "PARTIAL", check.detail || `${label} failed.`);
23
- return capability("NOT_CHECKED", "PARTIAL", check?.detail || `${label} was not requested.`);
24
- };
25
-
26
- export function buildHealthCapabilityMatrix({
27
- graphComplete,
28
- dependencyStatus,
29
- dependencyEcosystems = {},
30
- checks = {},
31
- sourceFiles = [],
32
- correctnessCoverage = {},
33
- measuredCoverageFiles = 0,
34
- } = {}) {
35
- const languages = sourceLanguages(sourceFiles);
36
- const ecosystemRows = Object.values(dependencyEcosystems).filter((item) => item?.present);
37
- const supportedDependencyRows = ecosystemRows.filter((item) => item.status === "CHECKED");
38
- const unsupportedDependencyRows = ecosystemRows.filter((item) => item.status === "NOT_SUPPORTED");
39
- const noDependencyEvidence = ecosystemRows.length === 0;
40
- const onlyUnsupportedDependencies = unsupportedDependencyRows.length > 0 && supportedDependencyRows.length === 0;
41
- const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python", "rust", "maven", "gradle"].includes(item.ecosystem));
42
- const onlyRustAdvisories = supportedDependencyRows.length > 0 && supportedDependencyRows.every((item) => item.ecosystem === "rust");
43
- const malwareSupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
44
- const coverageSupported = [...languages].some((language) => ["javascript/typescript", "python", "go"].includes(language));
45
- const runtimeFiles = Number(correctnessCoverage.runtimeCorrectnessFiles || 0);
46
- const concurrencyFiles = Number(correctnessCoverage.concurrencyFiles || 0);
47
-
48
- return {
49
- capabilityMatrixV: 1,
50
- structure: capability(
51
- "CHECKED",
52
- graphComplete ? "COMPLETE" : "PARTIAL",
53
- graphComplete
54
- ? "Graph cycles, orphans and configured boundaries were checked over the complete graph."
55
- : "Structure checks ran, but the graph is scoped or excludes part of the repository.",
56
- ),
57
- dependencies: capability(
58
- noDependencyEvidence ? "NOT_CHECKED" : onlyUnsupportedDependencies ? "NOT_SUPPORTED" : "CHECKED",
59
- !noDependencyEvidence && dependencyStatus === "COMPLETE" ? "COMPLETE" : "PARTIAL",
60
- noDependencyEvidence
61
- ? "No dependency manifest was discovered, so manifest-to-import verification did not run."
62
- : onlyUnsupportedDependencies
63
- ? "Dependency manifests were detected, but every discovered build ecosystem lacks import-to-artifact verification."
64
- : unsupportedDependencyRows.length
65
- ? `Supported ecosystems were checked; ${unsupportedDependencyRows.map((item) => item.ecosystem).join(", ")} remains NOT_SUPPORTED.`
66
- : "Discovered supported manifests were compared with indexed imports at their documented evidence level.",
67
- { ecosystems: dependencyEcosystems },
68
- ),
69
- runtimeCorrectness: runtimeFiles
70
- ? capability("CHECKED", "PARTIAL", `Checked ${runtimeFiles} product source file(s) for the bounded Go/Java/retry correctness patterns. This is not compiler or runtime proof.`, { checks: correctnessCoverage.checks || {} })
71
- : capability("NOT_SUPPORTED", "PARTIAL", "No source file matched the currently supported bounded correctness patterns."),
72
- concurrency: concurrencyFiles
73
- ? capability("CHECKED", "PARTIAL", `Checked ${concurrencyFiles} Java source file(s) for direct InterruptedException restore/rethrow evidence. No race detector ran; race freedom is not claimed.`)
74
- : capability("NOT_SUPPORTED", "PARTIAL", "No supported Java interruption check applied. No race detector ran; race freedom is not claimed."),
75
- advisories: checkCapability(onlyRustAdvisories && checks.rustsec?.status === "OK" ? checks.rustsec : checks.osv, advisorySupported, onlyRustAdvisories ? "RustSec advisory matching" : "OSV advisory matching"),
76
- malware: checkCapability(checks.malware, malwareSupported, "Installed-package malware scanning"),
77
- coverage: measuredCoverageFiles > 0
78
- ? capability("CHECKED", "COMPLETE", `Mapped an existing supported coverage report to ${measuredCoverageFiles} file(s).`)
79
- : coverageSupported
80
- ? capability("NOT_CHECKED", "PARTIAL", "No supported measured coverage report was found; static test reachability is not coverage.")
81
- : capability("NOT_SUPPORTED", "PARTIAL", `No supported coverage format applies to the discovered language set${languages.size ? ` (${[...languages].join(", ")})` : ""}.`),
82
- };
83
- }