weavatrix 0.3.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +231 -199
  3. package/bin/native/darwin-arm64/weavatrix +0 -0
  4. package/bin/native/darwin-x64/weavatrix +0 -0
  5. package/bin/native/linux-arm64/weavatrix +0 -0
  6. package/bin/native/linux-x64/weavatrix +0 -0
  7. package/bin/native/win32-arm64/weavatrix.exe +0 -0
  8. package/bin/native/win32-x64/weavatrix.exe +0 -0
  9. package/bin/resolve-binary.mjs +73 -0
  10. package/bin/run-native.mjs +26 -0
  11. package/bin/weavatrix-mcp.mjs +7 -6
  12. package/bin/weavatrix.mjs +5 -0
  13. package/package.json +8 -66
  14. package/skill/SKILL.md +62 -374
  15. package/skill/agents/openai.yaml +4 -0
  16. package/skill/references/tool-routing.md +45 -0
  17. package/SECURITY.md +0 -101
  18. package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
  19. package/docs/agent-task-benchmark.md +0 -58
  20. package/docs/releases/v0.2.19.md +0 -33
  21. package/docs/releases/v0.3.0.md +0 -43
  22. package/docs/releases/v0.3.1.md +0 -30
  23. package/docs/releases/v0.3.10.md +0 -76
  24. package/docs/releases/v0.3.11.md +0 -55
  25. package/docs/releases/v0.3.12.md +0 -44
  26. package/docs/releases/v0.3.13.md +0 -34
  27. package/docs/releases/v0.3.14.md +0 -37
  28. package/docs/releases/v0.3.2.md +0 -15
  29. package/docs/releases/v0.3.3.md +0 -25
  30. package/docs/releases/v0.3.4.md +0 -40
  31. package/docs/releases/v0.3.5.md +0 -50
  32. package/docs/releases/v0.3.6.md +0 -44
  33. package/docs/releases/v0.3.7.md +0 -37
  34. package/docs/releases/v0.3.8.md +0 -21
  35. package/docs/releases/v0.3.9.md +0 -36
  36. package/scripts/run-agent-task-benchmark.mjs +0 -118
  37. package/src/analysis/allowed-test-runner.js +0 -70
  38. package/src/analysis/architecture/contract-graph.js +0 -119
  39. package/src/analysis/architecture/contract-schema.js +0 -110
  40. package/src/analysis/architecture/contract-storage.js +0 -35
  41. package/src/analysis/architecture/contract-verification.js +0 -168
  42. package/src/analysis/architecture-contract.js +0 -16
  43. package/src/analysis/audit-debt.js +0 -94
  44. package/src/analysis/audit-extensions.js +0 -60
  45. package/src/analysis/cargo-dependency-evidence.js +0 -108
  46. package/src/analysis/cargo-manifests.js +0 -81
  47. package/src/analysis/change-classification/diff-parser.js +0 -103
  48. package/src/analysis/change-classification/options.js +0 -40
  49. package/src/analysis/change-classification/symbol-classifier.js +0 -177
  50. package/src/analysis/change-classification.js +0 -188
  51. package/src/analysis/coverage-reports-tarpaulin.js +0 -112
  52. package/src/analysis/coverage-reports.js +0 -246
  53. package/src/analysis/cycle-route.js +0 -14
  54. package/src/analysis/data-flow-evidence.js +0 -114
  55. package/src/analysis/dead-check.js +0 -300
  56. package/src/analysis/dead-code-review/candidates.js +0 -194
  57. package/src/analysis/dead-code-review/policy.js +0 -23
  58. package/src/analysis/dead-code-review.js +0 -119
  59. package/src/analysis/dep-check-ecosystems.js +0 -205
  60. package/src/analysis/dep-check.js +0 -290
  61. package/src/analysis/dep-rules.js +0 -10
  62. package/src/analysis/dependency/conventions.js +0 -16
  63. package/src/analysis/dependency/scoped-dependencies.js +0 -51
  64. package/src/analysis/dependency/source-references.js +0 -21
  65. package/src/analysis/duplicate-groups.js +0 -104
  66. package/src/analysis/duplicates-worker.js +0 -11
  67. package/src/analysis/duplicates.compute.js +0 -269
  68. package/src/analysis/duplicates.js +0 -15
  69. package/src/analysis/duplicates.run.js +0 -52
  70. package/src/analysis/duplicates.tokenize.js +0 -199
  71. package/src/analysis/endpoints/common.js +0 -79
  72. package/src/analysis/endpoints/extract.js +0 -120
  73. package/src/analysis/endpoints/inventory.js +0 -84
  74. package/src/analysis/endpoints/mounts.js +0 -129
  75. package/src/analysis/endpoints-java.js +0 -260
  76. package/src/analysis/endpoints-rust.js +0 -124
  77. package/src/analysis/endpoints.js +0 -3
  78. package/src/analysis/findings.js +0 -70
  79. package/src/analysis/git-history/analytics.js +0 -177
  80. package/src/analysis/git-history/collector.js +0 -67
  81. package/src/analysis/git-history/options.js +0 -66
  82. package/src/analysis/git-history.js +0 -143
  83. package/src/analysis/git-ref-graph.js +0 -76
  84. package/src/analysis/go-dependency-evidence.js +0 -62
  85. package/src/analysis/graph-analysis.aggregate.js +0 -296
  86. package/src/analysis/graph-analysis.edges.js +0 -24
  87. package/src/analysis/graph-analysis.js +0 -7
  88. package/src/analysis/graph-analysis.refs.js +0 -202
  89. package/src/analysis/graph-analysis.summaries.js +0 -65
  90. package/src/analysis/health-capabilities.js +0 -83
  91. package/src/analysis/hot-path-review.js +0 -266
  92. package/src/analysis/http-contract-wrappers.js +0 -241
  93. package/src/analysis/http-contracts/analysis.js +0 -179
  94. package/src/analysis/http-contracts/client-call-detection.js +0 -82
  95. package/src/analysis/http-contracts/client-call-parser.js +0 -273
  96. package/src/analysis/http-contracts/graph-context.js +0 -143
  97. package/src/analysis/http-contracts/matching.js +0 -61
  98. package/src/analysis/http-contracts/shared.js +0 -85
  99. package/src/analysis/http-contracts.js +0 -6
  100. package/src/analysis/internal-audit/dependency-health.js +0 -113
  101. package/src/analysis/internal-audit/python-manifests.js +0 -78
  102. package/src/analysis/internal-audit/repo-files.js +0 -50
  103. package/src/analysis/internal-audit/supply-chain.js +0 -149
  104. package/src/analysis/internal-audit.collect.js +0 -213
  105. package/src/analysis/internal-audit.js +0 -9
  106. package/src/analysis/internal-audit.reach.js +0 -201
  107. package/src/analysis/internal-audit.run.js +0 -256
  108. package/src/analysis/java-source.js +0 -36
  109. package/src/analysis/jvm-artifact-index.js +0 -87
  110. package/src/analysis/jvm-dependency-evidence.js +0 -138
  111. package/src/analysis/jvm-manifests.js +0 -114
  112. package/src/analysis/manifests.js +0 -123
  113. package/src/analysis/package-reachability.js +0 -39
  114. package/src/analysis/source-complexity.ast.js +0 -97
  115. package/src/analysis/source-complexity.constants.js +0 -55
  116. package/src/analysis/source-complexity.js +0 -14
  117. package/src/analysis/source-complexity.report.js +0 -81
  118. package/src/analysis/source-complexity.walk.js +0 -228
  119. package/src/analysis/source-correctness/retry-patterns.js +0 -93
  120. package/src/analysis/source-correctness.js +0 -222
  121. package/src/analysis/static-test-reachability.js +0 -150
  122. package/src/analysis/structure/dependency-graph.js +0 -151
  123. package/src/analysis/structure/findings.js +0 -162
  124. package/src/analysis/task-retrieval.js +0 -107
  125. package/src/analysis/transport-contracts.js +0 -224
  126. package/src/analysis/transport-runtime-evidence.js +0 -155
  127. package/src/analysis-kit.mjs +0 -17
  128. package/src/bounds.js +0 -4
  129. package/src/build-graph.js +0 -209
  130. package/src/child-env.js +0 -7
  131. package/src/config.js +0 -5
  132. package/src/extension/local-services.mjs +0 -75
  133. package/src/git-exec.js +0 -67
  134. package/src/graph/build-worker.js +0 -49
  135. package/src/graph/builder/go-receiver-resolution.js +0 -112
  136. package/src/graph/builder/js/dynamic-import.js +0 -26
  137. package/src/graph/builder/js/queries.js +0 -48
  138. package/src/graph/builder/lang-csharp.js +0 -40
  139. package/src/graph/builder/lang-css.js +0 -29
  140. package/src/graph/builder/lang-go.js +0 -140
  141. package/src/graph/builder/lang-html.js +0 -29
  142. package/src/graph/builder/lang-java-calls.js +0 -142
  143. package/src/graph/builder/lang-java.js +0 -215
  144. package/src/graph/builder/lang-js.js +0 -291
  145. package/src/graph/builder/lang-python.js +0 -235
  146. package/src/graph/builder/lang-rust-calls.js +0 -63
  147. package/src/graph/builder/lang-rust.js +0 -299
  148. package/src/graph/builder/lang-solidity.js +0 -126
  149. package/src/graph/builder/lang-sql.js +0 -263
  150. package/src/graph/builder/pass2-resolution.js +0 -81
  151. package/src/graph/builder/spec-pkg.js +0 -87
  152. package/src/graph/community.js +0 -27
  153. package/src/graph/edge-provenance.js +0 -31
  154. package/src/graph/file-lock.js +0 -69
  155. package/src/graph/freshness-probe.js +0 -142
  156. package/src/graph/graph-filter.js +0 -63
  157. package/src/graph/incremental-refresh.js +0 -235
  158. package/src/graph/internal-builder.barrels.js +0 -235
  159. package/src/graph/internal-builder.build.js +0 -300
  160. package/src/graph/internal-builder.java.js +0 -33
  161. package/src/graph/internal-builder.js +0 -12
  162. package/src/graph/internal-builder.langs.js +0 -157
  163. package/src/graph/internal-builder.pass2.js +0 -163
  164. package/src/graph/internal-builder.resolvers.js +0 -286
  165. package/src/graph/layout.js +0 -51
  166. package/src/graph/node-id.js +0 -7
  167. package/src/graph/parser-artifact-boundary.js +0 -55
  168. package/src/graph/relations.js +0 -4
  169. package/src/graph/repo-registry.js +0 -124
  170. package/src/graph/resolvers/rust.js +0 -117
  171. package/src/infra/infra-items.js +0 -255
  172. package/src/infra/infra-registry.js +0 -184
  173. package/src/infra/infra.detect.js +0 -119
  174. package/src/infra/infra.js +0 -38
  175. package/src/infra/infra.match.js +0 -102
  176. package/src/infra/infra.scan.js +0 -93
  177. package/src/mcp/actions/graph-lifecycle.mjs +0 -148
  178. package/src/mcp/architecture-bootstrap.mjs +0 -168
  179. package/src/mcp/architecture-starter.mjs +0 -234
  180. package/src/mcp/catalog.mjs +0 -233
  181. package/src/mcp/company-contract-page.mjs +0 -142
  182. package/src/mcp/company-contract-reconcile.mjs +0 -87
  183. package/src/mcp/company-contract-verdict.mjs +0 -42
  184. package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
  185. package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
  186. package/src/mcp/evidence/package-graph-common.mjs +0 -115
  187. package/src/mcp/evidence/package-lock-graph.mjs +0 -92
  188. package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
  189. package/src/mcp/evidence-snapshot.common.mjs +0 -158
  190. package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
  191. package/src/mcp/evidence-snapshot.health.mjs +0 -102
  192. package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
  193. package/src/mcp/evidence-snapshot.mjs +0 -50
  194. package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
  195. package/src/mcp/evidence-snapshot.structure.mjs +0 -84
  196. package/src/mcp/extension-api.mjs +0 -77
  197. package/src/mcp/git-output.mjs +0 -7
  198. package/src/mcp/graph/context-core.mjs +0 -111
  199. package/src/mcp/graph/context-seeds.mjs +0 -207
  200. package/src/mcp/graph/context-state.mjs +0 -85
  201. package/src/mcp/graph/reverse-reach.mjs +0 -42
  202. package/src/mcp/graph/tools-core.mjs +0 -143
  203. package/src/mcp/graph/tools-query.mjs +0 -276
  204. package/src/mcp/graph-context.mjs +0 -5
  205. package/src/mcp/graph-diff.mjs +0 -218
  206. package/src/mcp/health/audit-format.mjs +0 -199
  207. package/src/mcp/health/audit.mjs +0 -199
  208. package/src/mcp/health/dead-code.mjs +0 -110
  209. package/src/mcp/health/duplicates.mjs +0 -121
  210. package/src/mcp/health/endpoints.mjs +0 -71
  211. package/src/mcp/health/structure.mjs +0 -220
  212. package/src/mcp/runtime-version.mjs +0 -31
  213. package/src/mcp/server/auto-refresh.mjs +0 -66
  214. package/src/mcp/server/runtime-config.mjs +0 -43
  215. package/src/mcp/server/shutdown.mjs +0 -40
  216. package/src/mcp/staleness-notice.mjs +0 -20
  217. package/src/mcp/sync/evidence-architecture.mjs +0 -54
  218. package/src/mcp/sync/evidence-common.mjs +0 -88
  219. package/src/mcp/sync/evidence-duplicates.mjs +0 -97
  220. package/src/mcp/sync/evidence-health.mjs +0 -56
  221. package/src/mcp/sync/evidence-packages.mjs +0 -95
  222. package/src/mcp/sync/payload-common.mjs +0 -97
  223. package/src/mcp/sync/payload-v2.mjs +0 -54
  224. package/src/mcp/sync/payload-v3.mjs +0 -79
  225. package/src/mcp/sync-evidence.mjs +0 -23
  226. package/src/mcp/sync-payload.mjs +0 -70
  227. package/src/mcp/tool-result.mjs +0 -54
  228. package/src/mcp/tools-actions.mjs +0 -11
  229. package/src/mcp/tools-architecture.mjs +0 -177
  230. package/src/mcp/tools-company.mjs +0 -251
  231. package/src/mcp/tools-context.mjs +0 -222
  232. package/src/mcp/tools-endpoints.mjs +0 -207
  233. package/src/mcp/tools-graph-hubs.mjs +0 -78
  234. package/src/mcp/tools-graph.mjs +0 -17
  235. package/src/mcp/tools-health.mjs +0 -24
  236. package/src/mcp/tools-history.mjs +0 -28
  237. package/src/mcp/tools-impact-change.mjs +0 -220
  238. package/src/mcp/tools-impact-precision.mjs +0 -89
  239. package/src/mcp/tools-impact.mjs +0 -169
  240. package/src/mcp/tools-source.mjs +0 -238
  241. package/src/mcp/tools-verified-change.mjs +0 -200
  242. package/src/mcp-rg.mjs +0 -50
  243. package/src/mcp-runtime.mjs +0 -2
  244. package/src/mcp-server.mjs +0 -298
  245. package/src/mcp-source-tools.mjs +0 -163
  246. package/src/path-classification.js +0 -226
  247. package/src/path-ignore.js +0 -69
  248. package/src/precision/lsp-client/constants.js +0 -12
  249. package/src/precision/lsp-client/environment.js +0 -20
  250. package/src/precision/lsp-client/errors.js +0 -15
  251. package/src/precision/lsp-client/lifecycle.js +0 -114
  252. package/src/precision/lsp-client/message-parser.js +0 -81
  253. package/src/precision/lsp-client/protocol.js +0 -212
  254. package/src/precision/lsp-client/registry.js +0 -59
  255. package/src/precision/lsp-client/repo-uri.js +0 -60
  256. package/src/precision/lsp-client/stdio-client.js +0 -140
  257. package/src/precision/lsp-client.js +0 -10
  258. package/src/precision/lsp-overlay/budget-policy.js +0 -78
  259. package/src/precision/lsp-overlay/build.js +0 -261
  260. package/src/precision/lsp-overlay/contract.js +0 -61
  261. package/src/precision/lsp-overlay/reference-results.js +0 -108
  262. package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
  263. package/src/precision/lsp-overlay/source-session.js +0 -142
  264. package/src/precision/lsp-overlay/store.js +0 -150
  265. package/src/precision/lsp-overlay/target-index.js +0 -152
  266. package/src/precision/lsp-overlay/target-query.js +0 -55
  267. package/src/precision/lsp-overlay.js +0 -11
  268. package/src/precision/symbol-query.js +0 -224
  269. package/src/precision/typescript-lsp-provider.js +0 -12
  270. package/src/precision/typescript-provider/client.js +0 -89
  271. package/src/precision/typescript-provider/discovery.js +0 -124
  272. package/src/precision/typescript-provider/isolated-runtime.js +0 -39
  273. package/src/precision/typescript-provider/project-host.js +0 -254
  274. package/src/precision/typescript-provider/project-safety.js +0 -166
  275. package/src/precision/typescript-provider/reference-usage.js +0 -53
  276. package/src/process.js +0 -95
  277. package/src/repo-path.js +0 -53
  278. package/src/scan/discover.inventory.js +0 -78
  279. package/src/scan/discover.js +0 -5
  280. package/src/scan/discover.list.js +0 -79
  281. package/src/scan/discover.stack.js +0 -227
  282. package/src/scan/search.core.js +0 -24
  283. package/src/scan/search.git.js +0 -102
  284. package/src/scan/search.js +0 -9
  285. package/src/scan/search.node.js +0 -83
  286. package/src/scan/search.preview.js +0 -49
  287. package/src/scan/search.rg.js +0 -145
  288. package/src/security/advisory-store.js +0 -172
  289. package/src/security/installed-jvm-rust.js +0 -46
  290. package/src/security/installed.js +0 -280
  291. package/src/security/malware-file-heuristics.js +0 -121
  292. package/src/security/malware-heuristics.exclusions.js +0 -134
  293. package/src/security/malware-heuristics.js +0 -15
  294. package/src/security/malware-heuristics.roots.js +0 -145
  295. package/src/security/malware-heuristics.scan.js +0 -89
  296. package/src/security/malware-heuristics.sweep.js +0 -122
  297. package/src/security/malware-scoring.js +0 -105
  298. package/src/security/match.js +0 -85
  299. package/src/security/registry-sig.classify.js +0 -137
  300. package/src/security/registry-sig.js +0 -18
  301. package/src/security/registry-sig.rules.js +0 -192
  302. package/src/security/rust-advisory-report.js +0 -60
  303. package/src/security/typosquat.js +0 -81
  304. package/src/util.js +0 -40
  305. package/src/version.js +0 -7
@@ -1,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
- }