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,143 +0,0 @@
1
- import {
2
- isSymbol, degreeOf, labelOf, connList, resolveNodeInfo, ambiguityNote,
3
- graphStaleness, fileStalenessNote,
4
- } from '../graph-context.mjs'
5
- import {summarizeEdgeProvenance} from '../../graph/edge-provenance.js'
6
-
7
- const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
8
-
9
- export function tGraphStats(g, ctx) {
10
- const files = g.nodes.filter((node) => !isSymbol(node.id)).length
11
- const symbols = g.nodes.length - files
12
- const relCount = {}
13
- const confCount = {}
14
- let typeOnlyEdges = 0
15
- let compileOnlyEdges = 0
16
- for (const edge of g.links) {
17
- relCount[edge.relation ?? '?'] = (relCount[edge.relation ?? '?'] || 0) + 1
18
- if (edge.confidence != null) confCount[edge.confidence] = (confCount[edge.confidence] || 0) + 1
19
- if (edge.typeOnly === true) typeOnlyEdges++
20
- if (edge.compileOnly === true) compileOnlyEdges++
21
- }
22
- const communities = new Map()
23
- for (const node of g.nodes) {
24
- const community = node.community ?? 'none'
25
- communities.set(community, (communities.get(community) || 0) + 1)
26
- }
27
- const topCommunities = [...communities.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
28
- const formatCounts = (counts) => Object.entries(counts)
29
- .sort((a, b) => b[1] - a[1])
30
- .map(([key, value]) => `${key}: ${value}`)
31
- .join(', ')
32
- const freshness = ctx ? graphStaleness(ctx) : null
33
- const provenance = summarizeEdgeProvenance(g.links)
34
- const precision = g.precision || {state: 'UNAVAILABLE', verifiedEdges: 0, candidates: 0, queried: 0, reason: 'no revision-matched precision overlay'}
35
- return [
36
- 'Graph summary',
37
- ctx?.runtime ? `- Weavatrix runtime: v${ctx.runtime.version}; disk v${ctx.runtime.diskVersion || 'unavailable'}; stale ${ctx.runtime.staleRuntime ? 'YES' : 'no'}; profile ${ctx.runtime.profile}; ${ctx.runtime.toolCount} registered tools; capabilities: ${ctx.runtime.capabilities.join(',') || '(none)'}` : null,
38
- ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
39
- ctx?.graphPath ? `- Graph: ${ctx.graphPath}` : null,
40
- `- Build mode: ${g.graphBuildMode || 'full'}`,
41
- `- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
42
- `- Edges: ${g.links.length}`,
43
- g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : '- Typed-edge metadata: unavailable (rebuild_graph required)',
44
- g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${formatCounts(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : '- Edge provenance: unavailable (rebuild_graph required)',
45
- `- Semantic precision: ${precision.state}${precision.provider ? ` via ${precision.provider}${precision.providerVersion ? ` ${precision.providerVersion}` : ''}${precision.typescriptVersion ? ` (TypeScript ${precision.typescriptVersion})` : ''}` : ''}; ${precision.verifiedEdges || 0} EXACT_LSP edge(s), ${precision.queried || 0}/${precision.candidates || 0} bounded target(s) queried${precision.truncated ? ' (partial/truncated)' : ''}${precision.reason ? `; ${precision.reason}` : ''}`,
46
- g.barrelResolutionV ? `- Barrel resolution: v${g.barrelResolutionV} (semantic tools look through JS/TS re-export facades)` : '- Barrel resolution: unavailable (rebuild_graph required for JS/TS barrel transparency)',
47
- g.reExportOccurrencesV ? `- Re-export occurrences: v${g.reExportOccurrencesV} (${g.reExportOccurrences.length} exact site(s))` : '- Re-export occurrences: unavailable (rebuild_graph required)',
48
- g.symbolSpacesV ? `- TypeScript symbol spaces: v${g.symbolSpacesV} (type/value identities separated)` : '- TypeScript symbol spaces: unavailable (rebuild_graph required)',
49
- `- Relations: ${formatCounts(relCount)}`,
50
- Object.keys(confCount).length ? `- Legacy confidence: ${formatCounts(confCount)}` : null,
51
- `- Communities: ${communities.size} (top by size: ${topCommunities.map(([community, count]) => `#${community}=${count}`).join(', ')})`,
52
- freshness?.builtAt ? `- Built: ${freshness.builtAt.toISOString()}${freshness.headAt ? ` (repo HEAD committed ${freshness.headAt.toISOString()})` : ''}` : null,
53
- ].filter(Boolean).join('\n')
54
- }
55
-
56
- export function tGetNode(g, {label} = {}, ctx) {
57
- const info = resolveNodeInfo(g, label)
58
- const node = info.node
59
- if (!node) return `No node found matching "${label}".`
60
- const note = ambiguityNote(label, info)
61
- const id = String(node.id)
62
- const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
63
- const outgoing = g.out.get(id) || []
64
- const incoming = g.inn.get(id) || []
65
- const semanticOutgoing = connList(outgoing)
66
- const semanticIncoming = connList(incoming)
67
- const sample = (edges, direction) => edges.slice(0, 12)
68
- .map((edge) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${edge.provenance || 'UNKNOWN'}] ${labelOf(g, edge.id)} [${edge.id}]`)
69
- .join('\n') || ' (none)'
70
- return [
71
- note,
72
- `Node: ${node.label ?? id}`,
73
- `- id: ${id}`,
74
- `- kind: ${isSymbol(id) ? 'symbol' : 'file'}${node.file_type ? ` (${node.file_type})` : ''}`,
75
- node.source_file ? `- source: ${node.source_file}${node.source_location ? ` ${node.source_location}` : ''}` : null,
76
- node.community != null ? `- community: ${node.community}` : null,
77
- `- semantic degree: ${semanticOutgoing.length + semanticIncoming.length} (out ${semanticOutgoing.length}, in ${semanticIncoming.length})${outgoing.length + incoming.length !== semanticOutgoing.length + semanticIncoming.length ? `; ${outgoing.length + incoming.length} physical/structural edges retained` : ''}`,
78
- `Outgoing:\n${sample(outgoing, 'out')}`,
79
- `Incoming:\n${sample(incoming, 'in')}`,
80
- drift,
81
- ].filter(Boolean).join('\n')
82
- }
83
-
84
- function dedupeEdges(edges) {
85
- const grouped = new Map()
86
- for (const edge of edges) {
87
- const key = `${edge.relation || 'rel'}|${compileKind(edge) || 'runtime'}|${edge.id}`
88
- const current = grouped.get(key)
89
- if (current) {
90
- current.count += 1
91
- current.provenance.add(edge.provenance || 'UNKNOWN')
92
- } else grouped.set(key, {
93
- id: edge.id, relation: edge.relation, typeOnly: edge.typeOnly === true,
94
- compileOnly: edge.compileOnly === true, provenance: new Set([edge.provenance || 'UNKNOWN']), count: 1,
95
- })
96
- }
97
- return [...grouped.values()]
98
- }
99
-
100
- export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
101
- const info = resolveNodeInfo(g, label)
102
- const node = info.node
103
- if (!node) return `No node found matching "${label}".`
104
- const note = ambiguityNote(label, info)
105
- const id = String(node.id)
106
- const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
107
- const filter = relation_filter ? String(relation_filter).toLowerCase() : null
108
- const matches = (edge) => !filter || String(edge.relation ?? '').toLowerCase() === filter
109
- const outgoingRaw = (g.out.get(id) || []).filter(matches)
110
- const incomingRaw = (g.inn.get(id) || []).filter(matches)
111
- const outgoing = dedupeEdges(outgoingRaw)
112
- const incoming = dedupeEdges(incomingRaw)
113
- const line = (edge, direction) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${[...edge.provenance].sort().join('+')}] ${labelOf(g, edge.id)} [${edge.id}]${edge.count > 1 ? ` (${edge.count} sites)` : ''}`
114
- return [
115
- note,
116
- `Neighbors of ${node.label ?? id}${filter ? ` (relation=${filter})` : ''}: ${outgoing.length + incoming.length} unique (${outgoingRaw.length + incomingRaw.length} edges)`,
117
- `Outgoing (${outgoing.length}):`,
118
- ...outgoing.slice(0, 60).map((edge) => line(edge, 'out')),
119
- `Incoming (${incoming.length}):`,
120
- ...incoming.slice(0, 60).map((edge) => line(edge, 'in')),
121
- drift,
122
- ].filter(Boolean).join('\n')
123
- }
124
-
125
- export function tGetCommunity(g, {community_id} = {}) {
126
- const groups = new Map()
127
- for (const node of g.nodes) {
128
- if (node.community == null) continue
129
- if (!groups.has(node.community)) groups.set(node.community, [])
130
- groups.get(node.community).push(node)
131
- }
132
- const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length)
133
- const index = Number(community_id)
134
- if (!Number.isInteger(index) || index < 0 || index >= ranked.length) return `Invalid community_id ${community_id}. Valid range 0..${ranked.length - 1} (0 = largest).`
135
- const [rawId, members] = ranked[index]
136
- const files = members.filter((member) => !isSymbol(member.id))
137
- return [
138
- `Community #${index} (raw id ${rawId}) — ${members.length} nodes, ${files.length} files:`,
139
- ...members.slice().sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id)).slice(0, 80)
140
- .map((member) => ` ${member.label ?? member.id} [${member.id}]`),
141
- members.length > 80 ? ` … +${members.length - 80} more` : null,
142
- ].filter(Boolean).join('\n')
143
- }
@@ -1,276 +0,0 @@
1
- import {
2
- isSymbol, degreeOf, labelOf, resolveNode, findSeeds, resolveSeedFiles,
3
- requestedPathClasses,
4
- } from '../graph-context.mjs'
5
- import {createPathClassifier, hasPathClass} from '../../path-classification.js'
6
-
7
- const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
8
- const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
9
- const AMBIGUOUS_FILE_BASENAME_RE = /^(mod|lib|main)\.rs$|^index\.[a-z0-9]+$|^__init__\.py$/
10
- const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
11
- const queryWords = (value) => new Set(String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean))
12
- const exactSymbolName = (node) => {
13
- const match = String(node?.id || '').match(/#([^@]+)@/)
14
- return match ? match[1] : String(node?.label || '').replace(/\(\)$/, '')
15
- }
16
-
17
- function resolveExactSeedSymbols(g, requested, limit = 12) {
18
- const values = Array.isArray(requested) ? requested.slice(0, limit) : []
19
- const seeds = []
20
- const missing = []
21
- const ambiguous = []
22
- for (const raw of values) {
23
- const wanted = String(raw || '').trim()
24
- if (!wanted) continue
25
- let matches = []
26
- const byId = g.byId.get(wanted)
27
- if (byId && isSymbol(byId.id)) matches = [byId]
28
- else matches = g.nodes.filter((node) => isSymbol(node.id)
29
- && (String(node.label || '') === wanted || exactSymbolName(node) === wanted))
30
- if (matches.length === 1) {
31
- if (!seeds.some((seed) => String(seed.id) === String(matches[0].id))) seeds.push(matches[0])
32
- } else if (matches.length > 1) ambiguous.push(`${wanted} (${matches.length} exact matches; pass a symbol id)`)
33
- else missing.push(wanted)
34
- }
35
- return {seeds, missing, ambiguous}
36
- }
37
-
38
- // Edge/hop lines print bare labels without the [id] suffix node lines carry, so repeated or
39
- // conventionally-generic file basenames (mod.rs, index.ts, __init__.py) become indistinguishable —
40
- // widen those, scoped to the shown ids, to the last two path segments.
41
- const makeEdgeLabel = (g, shownIds) => {
42
- const counts = new Map()
43
- for (const id of shownIds) {
44
- const label = labelOf(g, id)
45
- counts.set(label, (counts.get(label) || 0) + 1)
46
- }
47
- return (id) => {
48
- const label = labelOf(g, id)
49
- const isFile = String(id).includes('/') && !String(id).includes('#')
50
- if (isFile && ((counts.get(label) || 0) > 1 || AMBIGUOUS_FILE_BASENAME_RE.test(label))) return String(id).split('/').slice(-2).join('/')
51
- return label
52
- }
53
- }
54
-
55
- // shortest_path is intentionally a connectivity query, so it may walk an edge in either
56
- // direction. Keep the stored direction on every hop so the rendered path never turns a reverse
57
- // traversal into a false caller/importer direction.
58
- const connectivityNeighbors = (g, id) => {
59
- const seen = new Map()
60
- for (const edge of g.out.get(id) || []) {
61
- if (edge.barrelProxy !== true) seen.set(String(edge.id), {relation: edge.relation, direction: 'forward'})
62
- }
63
- for (const edge of g.inn.get(id) || []) {
64
- if (edge.barrelProxy !== true && !seen.has(String(edge.id))) {
65
- seen.set(String(edge.id), {relation: edge.relation, direction: 'backward'})
66
- }
67
- }
68
- return seen
69
- }
70
-
71
- const relationSet = (relationFilter, legacyFilter) => {
72
- const raw = relationFilter ?? legacyFilter
73
- const values = Array.isArray(raw) ? raw : (raw == null ? [] : String(raw).split(','))
74
- const normalized = values.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
75
- return normalized.length ? new Set(normalized) : null
76
- }
77
-
78
- export function tQueryGraph(g, {
79
- question, mode = 'bfs', depth = 3, context_filter, relation_filter, seed_files, seed_symbols, augment_seeds = false,
80
- flow_direction = 'both', include_classified = false, include_low_signal = false, token_budget = 2000,
81
- } = {}, toolCtx = {}) {
82
- const pinned = resolveSeedFiles(g, seed_files)
83
- const exactSymbols = resolveExactSeedSymbols(g, seed_symbols)
84
- const pinnedSeeds = [...pinned.seeds, ...exactSymbols.seeds.filter((node) => !pinned.seeds.some((seed) => String(seed.id) === String(node.id)))]
85
- const automatic = pinnedSeeds.length && augment_seeds !== true
86
- ? []
87
- : findSeeds(g, question, Math.max(0, 6 - pinnedSeeds.length), {repoRoot: toolCtx.repoRoot || null})
88
- const seeds = [...pinnedSeeds, ...automatic.filter((node) => !pinnedSeeds.some((seed) => String(seed.id) === String(node.id)))]
89
- if (!seeds.length) {
90
- const details = [
91
- pinned.missing.length ? `Unresolved seed files: ${pinned.missing.join(', ')}` : null,
92
- exactSymbols.missing.length ? `Unresolved exact symbols: ${exactSymbols.missing.join(', ')}` : null,
93
- exactSymbols.ambiguous.length ? `Ambiguous exact symbols: ${exactSymbols.ambiguous.join(', ')}` : null,
94
- ].filter(Boolean)
95
- return [`No nodes matched "${question || ''}".`, ...details].join('\n')
96
- }
97
-
98
- const maxDepth = Math.max(1, Math.min(6, Number(depth) || 3))
99
- const relations = relationSet(relation_filter, context_filter)
100
- const relationAllowed = (relation) => !relations || relations.has(String(relation ?? '').toLowerCase())
101
- const direction = ['forward', 'backward', 'both'].includes(String(flow_direction).toLowerCase())
102
- ? String(flow_direction).toLowerCase() : 'both'
103
- const neighbors = (id) => {
104
- const found = new Map()
105
- const add = (edges) => {
106
- for (const edge of edges || []) {
107
- const neighbor = String(edge.id)
108
- if (edge.barrelProxy === true || !relationAllowed(edge.relation) || found.has(neighbor)) continue
109
- found.set(neighbor, edge.relation)
110
- }
111
- }
112
- if (direction !== 'backward') add(g.out.get(id))
113
- if (direction !== 'forward') add(g.inn.get(id))
114
- return found
115
- }
116
-
117
- const requestedClasses = requestedPathClasses(question)
118
- const classifier = createPathClassifier(toolCtx.repoRoot || null)
119
- const classificationCache = new Map()
120
- const pinnedFiles = new Set(pinnedSeeds.map(querySourceFile))
121
- const classifiedSuppressed = new Set()
122
- const pathPolicy = (id) => {
123
- const node = g.byId.get(String(id))
124
- const file = querySourceFile(node)
125
- if (!file || pinnedFiles.has(file) || include_classified === true) return {ok: true}
126
- // Extractor-proven test symbols (Rust #[cfg(test)]) live in production files; suppress them
127
- // under the same policy as path-classified tests unless the question asks about tests.
128
- if (node?.test_surface === true && !requestedClasses.has('test')) {
129
- classifiedSuppressed.add(String(id))
130
- return {ok: false, bucket: 'classified'}
131
- }
132
- if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
133
- const info = classificationCache.get(file)
134
- const classes = QUERY_NON_PRODUCT.filter((name) => hasPathClass(info, name))
135
- if (!classes.length && !info?.excluded) return {ok: true}
136
- if (classes.some((name) => requestedClasses.has(name))) return {ok: true}
137
- classifiedSuppressed.add(String(id))
138
- return {ok: false, bucket: 'classified'}
139
- }
140
-
141
- const questionTerms = queryWords(question)
142
- const start = seeds.map((seed) => String(seed.id))
143
- const isLowSignal = (id) => {
144
- if (include_low_signal === true || start.includes(String(id))) return false
145
- const node = g.byId.get(String(id))
146
- if (!node || !isSymbol(node.id) || !LOW_SIGNAL_SYMBOL_RE.test(String(node.symbol_kind || ''))) return false
147
- const labelTerms = queryWords(node.label || String(node.id || '').split('#').pop() || '')
148
- if ([...questionTerms].some((term) => labelTerms.has(term))) return false
149
- return degreeOf(g, id) === 0
150
- }
151
- const charBudget = Math.max(400, (Number(token_budget) || 2000) * 4)
152
- const nodeBudget = Math.max(20, Math.min(120, Math.round((Number(token_budget) || 2000) / 40)))
153
- const depthOf = new Map()
154
-
155
- if (mode === 'dfs') {
156
- const stack = start.map((id) => ({id, depth: 0}))
157
- const seen = new Set()
158
- while (stack.length) {
159
- const {id, depth: currentDepth} = stack.pop()
160
- if (!depthOf.has(id) || currentDepth < depthOf.get(id)) depthOf.set(id, currentDepth)
161
- if (seen.has(id)) continue
162
- seen.add(id)
163
- if (currentDepth >= maxDepth) continue
164
- for (const [neighbor, relation] of neighbors(id)) {
165
- if (!relationAllowed(relation) || !pathPolicy(neighbor).ok) continue
166
- if (!seen.has(neighbor)) stack.push({id: neighbor, depth: currentDepth + 1})
167
- }
168
- }
169
- } else {
170
- let frontier = start.slice()
171
- start.forEach((id) => depthOf.set(id, 0))
172
- for (let currentDepth = 0; currentDepth < maxDepth && frontier.length; currentDepth++) {
173
- const next = []
174
- for (const id of frontier) {
175
- for (const [neighbor, relation] of neighbors(id)) {
176
- if (!relationAllowed(relation) || !pathPolicy(neighbor).ok) continue
177
- if (!depthOf.has(neighbor)) {
178
- depthOf.set(neighbor, currentDepth + 1)
179
- next.push(neighbor)
180
- }
181
- }
182
- }
183
- frontier = next
184
- }
185
- }
186
-
187
- const reachedBeforeSignalFilter = depthOf.size
188
- const lowSignalSuppressed = [...depthOf.keys()].filter(isLowSignal).length
189
- const ranked = [...depthOf.entries()]
190
- .filter(([id]) => !isLowSignal(id))
191
- .map(([id, distance]) => ({id, distance, degree: degreeOf(g, id)}))
192
- .sort((a, b) => a.distance - b.distance || b.degree - a.degree)
193
- const shown = ranked.slice(0, nodeBudget)
194
- const shownIds = new Set(shown.map((node) => node.id))
195
- const edgeSeen = new Set()
196
- const shownEdges = []
197
- for (const source of shownIds) {
198
- for (const edge of g.out.get(source) || []) {
199
- const target = String(edge.id)
200
- if (edge.barrelProxy === true || !shownIds.has(target) || !relationAllowed(edge.relation)) continue
201
- const key = `${source}|${edge.relation}|${target}`
202
- if (edgeSeen.has(key)) continue
203
- edgeSeen.add(key)
204
- shownEdges.push([source, edge.relation, target])
205
- if (shownEdges.length >= 160) break
206
- }
207
- if (shownEdges.length >= 160) break
208
- }
209
- const head = [
210
- `Query: "${question || ''}" (${mode}, depth ${maxDepth}, flow ${direction}${relations ? `, relations ${[...relations].join('/')}` : ''})`,
211
- `Seeds: ${seeds.map((seed) => seed.label ?? seed.id).join(', ')}`,
212
- pinned.missing.length ? `Unresolved pinned seed files: ${pinned.missing.join(', ')}` : null,
213
- exactSymbols.missing.length ? `Unresolved exact seed symbols: ${exactSymbols.missing.join(', ')}` : null,
214
- exactSymbols.ambiguous.length ? `Ambiguous exact seed symbols: ${exactSymbols.ambiguous.join(', ')}` : null,
215
- `Reached ${reachedBeforeSignalFilter} policy-eligible nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
216
- classifiedSuppressed.size ? `Suppressed ${classifiedSuppressed.size} classified/non-product traversal node(s); ask for that class or pass include_classified:true.` : null,
217
- lowSignalSuppressed ? `Suppressed ${lowSignalSuppressed} unreferenced constant/field node(s) with no query-term match; pass include_low_signal:true to inspect them.` : null,
218
- include_classified === true ? 'Path policy: classified/non-product traversal explicitly enabled.' : `Path policy: production-first${requestedClasses.size ? `; explicit question classes enabled: ${[...requestedClasses].join(', ')}` : ''}.`,
219
- '',
220
- 'Nodes:',
221
- ]
222
- const nodeLines = shown.map((node) => ` [d${node.distance}] ${labelOf(g, node.id)} (deg ${node.degree}) [${node.id}]`)
223
- const edgeLabel = makeEdgeLabel(g, shownIds)
224
- const edgeLines = ['', 'Edges:', ...shownEdges.map(([source, relation, target]) => ` ${edgeLabel(source)} --${relation || 'rel'}--> ${edgeLabel(target)}`)]
225
- let text = [...head.filter(Boolean), ...nodeLines, ...edgeLines].join('\n')
226
- if (text.length > charBudget) text = text.slice(0, charBudget) + `\n... (truncated to ~${token_budget} tokens)`
227
- return text
228
- }
229
-
230
- export function tShortestPath(g, {source, target, max_hops = 8} = {}) {
231
- const sourceNode = resolveNode(g, source)
232
- const targetNode = resolveNode(g, target)
233
- if (!sourceNode) return `Source "${source}" not found.`
234
- if (!targetNode) return `Target "${target}" not found.`
235
- const sourceId = String(sourceNode.id)
236
- const targetId = String(targetNode.id)
237
- if (sourceId === targetId) return `Source and target are the same node: ${sourceNode.label ?? sourceId}.`
238
- const limit = Math.max(1, Math.min(20, Number(max_hops) || 8))
239
- const previous = new Map([[sourceId, null]])
240
- const edgeTo = new Map()
241
- let frontier = [sourceId]
242
- let hops = 0
243
- let found = false
244
- while (frontier.length && hops < limit && !found) {
245
- const next = []
246
- for (const id of frontier) {
247
- for (const [neighbor, edge] of connectivityNeighbors(g, id)) {
248
- if (previous.has(neighbor)) continue
249
- previous.set(neighbor, id)
250
- edgeTo.set(neighbor, edge)
251
- if (neighbor === targetId) { found = true; break }
252
- next.push(neighbor)
253
- }
254
- if (found) break
255
- }
256
- frontier = next
257
- hops++
258
- }
259
- if (!previous.has(targetId)) return `No path found between "${sourceNode.label ?? sourceId}" and "${targetNode.label ?? targetId}" within ${limit} hops.`
260
- const path = []
261
- for (let current = targetId; current != null; current = previous.get(current)) path.unshift(current)
262
- const edgeLabel = makeEdgeLabel(g, path)
263
- const hopCount = path.length - 1
264
- const lines = path.map((id, index) => {
265
- if (index === 0) return ` ${edgeLabel(id)}`
266
- const edge = edgeTo.get(id) || {}
267
- return edge.direction === 'backward'
268
- ? ` <--${edge.relation || 'rel'}-- ${edgeLabel(id)}`
269
- : ` --${edge.relation || 'rel'}--> ${edgeLabel(id)}`
270
- })
271
- return [
272
- `Shortest path (${hopCount} ${hopCount === 1 ? 'hop' : 'hops'}; undirected connectivity) from ${edgeLabel(sourceId)} to ${edgeLabel(targetId)}`,
273
- 'Edge arrows preserve stored graph direction; traversal may move with or against them.',
274
- ...lines,
275
- ].join('\n')
276
- }
@@ -1,5 +0,0 @@
1
- // Stable public facade for graph loading, query seeding and graph state helpers.
2
- export * from './graph/context-core.mjs'
3
- export * from './graph/context-seeds.mjs'
4
- export * from './graph/context-state.mjs'
5
- export {prevGraphPathFor, edgeEndpoint, fileOfId, diffGraphs, formatGraphDiff} from './graph-diff.mjs'
@@ -1,218 +0,0 @@
1
- // Structural graph delta helpers split from graph-context so the process-lifetime graph indexes stay small.
2
- import {buildFileImportGraph, findSccs} from '../analysis/dep-rules.js'
3
- import {folderModuleOf} from '../analysis/graph-analysis.aggregate.js'
4
- import {isStructuralRelation} from '../graph/relations.js'
5
- import {fileOfId} from '../graph/node-id.js'
6
-
7
- // ---- graph diff ----------------------------------------------------------------------------------
8
- // One previous state is enough (4-13 MB per repo — cheap): rebuild_graph snapshots the outgoing
9
- // graph.json as graph.prev.json and reports the structural delta inline, at the exact moment the fix
10
- // is being verified. graph_diff re-queries the same pair later. Raw node/edge dumps would be noise —
11
- // the signal is aggregated: module-dependency drift, cycle count changes, newly orphaned symbols.
12
- export const prevGraphPathFor = (graphPath) => String(graphPath).replace(/\.json$/, '.prev.json')
13
- export const edgeEndpoint = (v) => String(v && typeof v === 'object' ? v.id : v)
14
- export {fileOfId}
15
- const folderOfFile = folderModuleOf
16
-
17
- const terminalLineSuffix = (id) => {
18
- const raw = String(id)
19
- return raw.includes('#') ? raw.replace(/@\d+$/, '') : raw
20
- }
21
-
22
- const sourceLine = (node) => {
23
- const explicit = String(node?.source_location || '').match(/^L(\d+)/i)
24
- if (explicit) return Number(explicit[1])
25
- const suffix = String(node?.id || '').match(/@(\d+)$/)
26
- return suffix ? Number(suffix[1]) : Number.MAX_SAFE_INTEGER
27
- }
28
-
29
- // Node ids retain source lines for precise read_source jumps, but location is not identity. This
30
- // comparison-only index keeps file/name/kind/export/signature shape stable across line shifts and
31
- // uses an ordinal only for true same-name overloads. Raw ids remain the user-facing evidence.
32
- function stableNodeIndex(graph) {
33
- const groups = new Map()
34
- for (const node of graph.nodes || []) {
35
- const raw = String(node?.id ?? '')
36
- if (!raw) continue
37
- const stem = terminalLineSuffix(raw)
38
- const params = Number.isInteger(node?.complexity?.params) ? node.complexity.params : ''
39
- const signature = [stem, node?.symbol_kind || '', node?.symbol_space || '', node?.exported === true ? 'exported' : '', params].join('\u0000')
40
- if (!groups.has(signature)) groups.set(signature, [])
41
- groups.get(signature).push(node)
42
- }
43
- const byRaw = new Map()
44
- const rawByStable = new Map()
45
- for (const [signature, nodes] of groups) {
46
- nodes.sort((a, b) => sourceLine(a) - sourceLine(b) || String(a.id).localeCompare(String(b.id)))
47
- nodes.forEach((node, ordinal) => {
48
- const stable = `${signature}\u0000${ordinal}`
49
- const raw = String(node.id)
50
- byRaw.set(raw, stable)
51
- rawByStable.set(stable, raw)
52
- })
53
- }
54
- return {byRaw, rawByStable, keys: new Set(rawByStable.keys())}
55
- }
56
-
57
- // Works on anything with {nodes, links}: the raw graph.json shape and the loadGraph struct alike.
58
- export function diffGraphs(oldG, newG) {
59
- const oldEdgeTypesV = Number(oldG.edgeTypesV) || 0
60
- const newEdgeTypesV = Number(newG.edgeTypesV) || 0
61
- const oldBarrelResolutionV = Number(oldG.barrelResolutionV) || 0
62
- const newBarrelResolutionV = Number(newG.barrelResolutionV) || 0
63
- const oldExtractorSchemaV = Number(oldG.extractorSchemaV) || 0
64
- const newExtractorSchemaV = Number(newG.extractorSchemaV) || 0
65
- const schemaChanges = [
66
- oldEdgeTypesV !== newEdgeTypesV ? `edges v${oldEdgeTypesV}→v${newEdgeTypesV}` : null,
67
- oldBarrelResolutionV !== newBarrelResolutionV ? `barrels v${oldBarrelResolutionV}→v${newBarrelResolutionV}` : null,
68
- oldExtractorSchemaV !== newExtractorSchemaV ? `extractor v${oldExtractorSchemaV}→v${newExtractorSchemaV}` : null,
69
- ].filter(Boolean)
70
- const schemaMigration = schemaChanges.length > 0
71
- const oldNodeIndex = stableNodeIndex(oldG)
72
- const newNodeIndex = stableNodeIndex(newG)
73
-
74
- const edgeClass = (l) => l.typeOnly === true ? 'type' : l.compileOnly === true ? 'compile' : 'runtime'
75
- const stableEndpoint = (index, value) => {
76
- const raw = edgeEndpoint(value)
77
- return index.byRaw.get(raw) || raw
78
- }
79
- const edgeKey = (l, index) => `${stableEndpoint(index, l.source)}|${l.relation || ''}|${schemaMigration ? 'untyped' : edgeClass(l)}|${stableEndpoint(index, l.target)}`
80
- const edgeSet = (graph, index) => new Set((graph.links || []).map((l) => edgeKey(l, index)))
81
- const oldEdges = edgeSet(oldG, oldNodeIndex)
82
- const newEdges = edgeSet(newG, newNodeIndex)
83
-
84
- const moduleEdges = (graph, classification) => {
85
- const set = new Set()
86
- for (const l of graph.links || []) {
87
- if (isStructuralRelation(l.relation) || l.barrelProxy === true) continue
88
- if (edgeClass(l) !== classification) continue
89
- const a = folderOfFile(fileOfId(edgeEndpoint(l.source)))
90
- const b = folderOfFile(fileOfId(edgeEndpoint(l.target)))
91
- if (a !== b) set.add(`${a} → ${b}`)
92
- }
93
- return set
94
- }
95
- // A parser/schema upgrade can discover edges that the old graph could not represent (Rust use/mod in
96
- // edgeTypesV2). Do not call that architecture drift: establish a fresh classification baseline.
97
- const oldMods = schemaMigration ? new Set() : moduleEdges(oldG, 'runtime')
98
- const newMods = schemaMigration ? new Set() : moduleEdges(newG, 'runtime')
99
- const oldTypeMods = schemaMigration ? new Set() : moduleEdges(oldG, 'type')
100
- const newTypeMods = schemaMigration ? new Set() : moduleEdges(newG, 'type')
101
- const oldCompileMods = schemaMigration ? new Set() : moduleEdges(oldG, 'compile')
102
- const newCompileMods = schemaMigration ? new Set() : moduleEdges(newG, 'compile')
103
-
104
- const incoming = (graph, index) => {
105
- const m = new Map()
106
- for (const l of graph.links || []) {
107
- if (isStructuralRelation(l.relation) || l.barrelProxy === true) continue
108
- const t = stableEndpoint(index, l.target)
109
- m.set(t, (m.get(t) || 0) + 1)
110
- }
111
- return m
112
- }
113
- const oldIn = incoming(oldG, oldNodeIndex)
114
- const newIn = incoming(newG, newNodeIndex)
115
-
116
- const cycles = (graph, includeTypeOnly) => {
117
- try {
118
- const sccs = findSccs(buildFileImportGraph(graph, {includeTypeOnly}).adj)
119
- .map((members) => members.map(String).sort())
120
- .sort((a, b) => b.length - a.length || a.join('\n').localeCompare(b.join('\n')))
121
- return {
122
- count: sccs.length,
123
- largest: sccs[0]?.length || 0,
124
- groups: sccs,
125
- }
126
- } catch {
127
- return null
128
- }
129
- }
130
- const cycleDelta = (before, after) => {
131
- if (!before || !after) return null
132
- const key = (group) => group.join('|')
133
- const beforeKeys = new Set(before.groups.map(key))
134
- const afterKeys = new Set(after.groups.map(key))
135
- const overlap = (a, b) => {
136
- const bSet = new Set(b)
137
- return a.reduce((n, member) => n + (bSet.has(member) ? 1 : 0), 0)
138
- }
139
- const unmatchedBefore = before.groups.filter((group) => !afterKeys.has(key(group)))
140
- const unmatchedAfter = after.groups.filter((group) => !beforeKeys.has(key(group)))
141
- const changed = unmatchedAfter.filter((group) => unmatchedBefore.some((old) => overlap(group, old) >= 2))
142
- const introduced = unmatchedAfter.filter((group) => !unmatchedBefore.some((old) => overlap(group, old) >= 2))
143
- const resolved = unmatchedBefore.filter((group) => !unmatchedAfter.some((next) => overlap(group, next) >= 2))
144
- return {
145
- before: before.count,
146
- after: after.count,
147
- largestBefore: before.largest,
148
- largestAfter: after.largest,
149
- introduced: introduced.map(key),
150
- resolved: resolved.map(key),
151
- membershipChanged: changed.length,
152
- }
153
- }
154
-
155
- return {
156
- schemaMigration: schemaMigration ? {from: oldEdgeTypesV, to: newEdgeTypesV, changes: schemaChanges} : null,
157
- nodes: {
158
- added: [...newNodeIndex.keys].filter((key) => !oldNodeIndex.keys.has(key)).map((key) => newNodeIndex.rawByStable.get(key)),
159
- removed: [...oldNodeIndex.keys].filter((key) => !newNodeIndex.keys.has(key)).map((key) => oldNodeIndex.rawByStable.get(key))
160
- },
161
- edges: {
162
- added: [...newEdges].filter((k) => !oldEdges.has(k)).length,
163
- removed: [...oldEdges].filter((k) => !newEdges.has(k)).length
164
- },
165
- moduleEdges: {
166
- added: [...newMods].filter((k) => !oldMods.has(k)),
167
- removed: [...oldMods].filter((k) => !newMods.has(k)),
168
- typeAdded: [...newTypeMods].filter((k) => !oldTypeMods.has(k)),
169
- typeRemoved: [...oldTypeMods].filter((k) => !newTypeMods.has(k)),
170
- compileAdded: [...newCompileMods].filter((k) => !oldCompileMods.has(k)),
171
- compileRemoved: [...oldCompileMods].filter((k) => !newCompileMods.has(k)),
172
- },
173
- // survived the rebuild but lost every caller/importer — likely made dead by the change
174
- orphaned: [...oldIn.keys()]
175
- .filter((key) => newNodeIndex.keys.has(key) && !newIn.has(key))
176
- .map((key) => newNodeIndex.rawByStable.get(key) || oldNodeIndex.rawByStable.get(key)),
177
- cycles: {
178
- runtime: schemaMigration ? null : cycleDelta(cycles(oldG, false), cycles(newG, false)),
179
- // Backward-compatible field name: since edgeTypesV 2 this includes typeOnly and compileOnly.
180
- typeInclusive: schemaMigration ? null : cycleDelta(cycles(oldG, true), cycles(newG, true)),
181
- }
182
- }
183
- }
184
-
185
- export function formatGraphDiff(d) {
186
- if (!d.nodes.added.length && !d.nodes.removed.length && !d.edges.added && !d.edges.removed) {
187
- return d.schemaMigration
188
- ? `Graph extractor/schema upgraded (${d.schemaMigration.changes.join(', ')}); compile-time baseline established. Runtime/compile-time cycle and module classifications are intentionally not compared on this rebuild.`
189
- : 'No structural change between the two graph states.'
190
- }
191
- const cap = (list, n) => list.slice(0, n).map((x) => ` ${x}`).concat(list.length > n ? [` … +${list.length - n} more`] : [])
192
- const lines = [`Structural delta: nodes +${d.nodes.added.length}/−${d.nodes.removed.length}, edges +${d.edges.added}/−${d.edges.removed}.`]
193
- if (d.schemaMigration) lines.push(`Graph extractor/schema upgraded (${d.schemaMigration.changes.join(', ')}); runtime/compile-time cycle and module classifications are intentionally not compared until the next rebuild.`)
194
- const runtime = d.cycles?.runtime
195
- if (runtime && (runtime.before !== runtime.after || runtime.largestBefore !== runtime.largestAfter || runtime.introduced.length || runtime.resolved.length || runtime.membershipChanged)) {
196
- const changes = []
197
- if (runtime.introduced.length) changes.push(`${runtime.introduced.length} genuinely new runtime SCC(s) — review`)
198
- if (runtime.resolved.length) changes.push(`${runtime.resolved.length} runtime SCC(s) resolved`)
199
- if (runtime.membershipChanged) changes.push(`${runtime.membershipChanged} SCC membership change(s)`)
200
- const verdict = changes.length ? `; ${changes.join('; ')}` : ''
201
- lines.push(`Runtime import cycles: count ${runtime.before} → ${runtime.after}, largest SCC ${runtime.largestBefore} → ${runtime.largestAfter}${verdict}.`)
202
- }
203
- const all = d.cycles?.typeInclusive
204
- if (all && (all.before !== all.after || all.largestBefore !== all.largestAfter) &&
205
- (!runtime || all.before !== runtime.before || all.after !== runtime.after || all.largestBefore !== runtime.largestBefore || all.largestAfter !== runtime.largestAfter)) {
206
- lines.push(`Compile-time-inclusive dependency SCCs: count ${all.before} → ${all.after}, largest ${all.largestBefore} → ${all.largestAfter} (compile-time coupling, not necessarily a runtime cycle).`)
207
- }
208
- if (d.moduleEdges.added.length) lines.push('NEW module dependencies (architecture drift — review):', ...cap(d.moduleEdges.added, 12))
209
- if (d.moduleEdges.removed.length) lines.push('Removed module dependencies (decoupling confirmed):', ...cap(d.moduleEdges.removed, 12))
210
- if (d.moduleEdges.typeAdded.length) lines.push('New type-only module dependencies (compile-time coupling):', ...cap(d.moduleEdges.typeAdded, 12))
211
- if (d.moduleEdges.typeRemoved.length) lines.push('Removed type-only module dependencies:', ...cap(d.moduleEdges.typeRemoved, 12))
212
- if (d.moduleEdges.compileAdded.length) lines.push('New compile-only module dependencies (compile-time coupling):', ...cap(d.moduleEdges.compileAdded, 12))
213
- if (d.moduleEdges.compileRemoved.length) lines.push('Removed compile-only module dependencies:', ...cap(d.moduleEdges.compileRemoved, 12))
214
- if (d.orphaned.length) lines.push('Symbols that lost their last caller/importer (now dead?):', ...cap(d.orphaned, 10))
215
- if (d.nodes.added.length) lines.push('Added nodes:', ...cap(d.nodes.added, 12))
216
- if (d.nodes.removed.length) lines.push('Removed nodes:', ...cap(d.nodes.removed, 12))
217
- return lines.join('\n')
218
- }