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,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', '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
- }