@unerr-ai/unerr 0.2.0 → 0.2.2

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 (363) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +37236 -35793
  3. package/package.json +6 -1
  4. package/dist/behaviors/agent-llm-bridge.js +0 -166
  5. package/dist/behaviors/architecture-guard.js +0 -256
  6. package/dist/behaviors/auto-doc.js +0 -247
  7. package/dist/behaviors/cascade-guard.js +0 -289
  8. package/dist/behaviors/change-narrative.js +0 -270
  9. package/dist/behaviors/convention-drift.js +0 -290
  10. package/dist/behaviors/framework.js +0 -235
  11. package/dist/behaviors/guard-formatter.js +0 -44
  12. package/dist/behaviors/incomplete-work.js +0 -270
  13. package/dist/behaviors/loop-breaker.js +0 -300
  14. package/dist/behaviors/session-continuity.js +0 -208
  15. package/dist/commands/branches.js +0 -97
  16. package/dist/commands/check-commit.js +0 -225
  17. package/dist/commands/compress-output.js +0 -64
  18. package/dist/commands/config-verify.js +0 -243
  19. package/dist/commands/daemon.js +0 -905
  20. package/dist/commands/dashboard.js +0 -52
  21. package/dist/commands/debug.js +0 -200
  22. package/dist/commands/enrich.js +0 -184
  23. package/dist/commands/exec.js +0 -233
  24. package/dist/commands/gain.js +0 -156
  25. package/dist/commands/hook.js +0 -88
  26. package/dist/commands/index.js +0 -88
  27. package/dist/commands/init.js +0 -74
  28. package/dist/commands/install.js +0 -505
  29. package/dist/commands/learn.js +0 -116
  30. package/dist/commands/manifest.js +0 -193
  31. package/dist/commands/rewind.js +0 -103
  32. package/dist/commands/serve.js +0 -19
  33. package/dist/commands/setup-wizard.js +0 -414
  34. package/dist/commands/skills.js +0 -64
  35. package/dist/commands/stats.js +0 -20
  36. package/dist/commands/status.js +0 -654
  37. package/dist/commands/timeline.js +0 -139
  38. package/dist/commands/uninstall.js +0 -230
  39. package/dist/components/App.js +0 -109
  40. package/dist/components/Banner.js +0 -12
  41. package/dist/components/ConfirmPrompt.js +0 -25
  42. package/dist/components/DriftSummary.js +0 -23
  43. package/dist/components/GradeBadge.js +0 -15
  44. package/dist/components/HealthCard.js +0 -18
  45. package/dist/components/InkSpinner.js +0 -22
  46. package/dist/components/InputBox.js +0 -17
  47. package/dist/components/KeyValue.js +0 -13
  48. package/dist/components/MessageList.js +0 -14
  49. package/dist/components/ProgressBar.js +0 -26
  50. package/dist/components/Section.js +0 -16
  51. package/dist/components/SessionSummaryCard.js +0 -73
  52. package/dist/components/StartupDisplay.js +0 -24
  53. package/dist/components/StatusDashboard.js +0 -57
  54. package/dist/components/StatusLine.js +0 -8
  55. package/dist/components/StepLine.js +0 -22
  56. package/dist/components/Theme.js +0 -20
  57. package/dist/components/ToolProgress.js +0 -8
  58. package/dist/components/ViolationList.js +0 -21
  59. package/dist/components/render.js +0 -13
  60. package/dist/config/agent-registry.js +0 -237
  61. package/dist/config/claude-settings-hooks.js +0 -304
  62. package/dist/config/hook-installer.js +0 -65
  63. package/dist/config/instruction-writer.js +0 -388
  64. package/dist/config/mcp-config-writer.js +0 -266
  65. package/dist/config/settings.js +0 -174
  66. package/dist/config/tool-detector.js +0 -42
  67. package/dist/config/value-surfacing.js +0 -119
  68. package/dist/core/context-assembly.js +0 -108
  69. package/dist/core/conversation.js +0 -33
  70. package/dist/core/local-chat-provider.js +0 -475
  71. package/dist/core/provider-factory.js +0 -55
  72. package/dist/core/providers.js +0 -90
  73. package/dist/core/query-engine.js +0 -174
  74. package/dist/daemon/api.js +0 -312
  75. package/dist/daemon/autostart.js +0 -119
  76. package/dist/daemon/bootstrap.js +0 -39
  77. package/dist/daemon/client.js +0 -164
  78. package/dist/daemon/detect-ci.js +0 -81
  79. package/dist/daemon/platform-linux.js +0 -146
  80. package/dist/daemon/platform-macos.js +0 -134
  81. package/dist/daemon/platform-windows.js +0 -116
  82. package/dist/daemon/process-manager.js +0 -299
  83. package/dist/daemon/protocol.js +0 -23
  84. package/dist/daemon/registry.js +0 -270
  85. package/dist/daemon/settings-schema.js +0 -72
  86. package/dist/daemon/system-health.js +0 -134
  87. package/dist/daemon/version-checker.js +0 -262
  88. package/dist/daemon/warm-start.js +0 -223
  89. package/dist/entrypoints/cli.js +0 -1043
  90. package/dist/entrypoints/daemon.js +0 -380
  91. package/dist/entrypoints/repl.js +0 -147
  92. package/dist/hooks/adapters/claude-code.js +0 -90
  93. package/dist/hooks/adapters/cline.js +0 -100
  94. package/dist/hooks/adapters/cursor.js +0 -98
  95. package/dist/hooks/hook-dedup.js +0 -79
  96. package/dist/hooks/hook-runner.js +0 -113
  97. package/dist/hooks/navigation-hooks.js +0 -175
  98. package/dist/hooks/prompt-hooks.js +0 -63
  99. package/dist/hooks/shell-hooks.js +0 -47
  100. package/dist/ignore.js +0 -111
  101. package/dist/intelligence/approach-suggester.js +0 -61
  102. package/dist/intelligence/ast-extractor.js +0 -2615
  103. package/dist/intelligence/ast-worker.js +0 -34
  104. package/dist/intelligence/background-indexer.js +0 -121
  105. package/dist/intelligence/blast-radius.js +0 -200
  106. package/dist/intelligence/community-detection.js +0 -691
  107. package/dist/intelligence/community-detector.js +0 -184
  108. package/dist/intelligence/computation-scheduler.js +0 -75
  109. package/dist/intelligence/confidence-propagation.js +0 -47
  110. package/dist/intelligence/convention-detector.js +0 -242
  111. package/dist/intelligence/convention-learner.js +0 -205
  112. package/dist/intelligence/convention-matcher.js +0 -205
  113. package/dist/intelligence/cozo-schema.js +0 -376
  114. package/dist/intelligence/decision-point-detector.js +0 -90
  115. package/dist/intelligence/deep-dive-tools.js +0 -586
  116. package/dist/intelligence/durability-scorer.js +0 -84
  117. package/dist/intelligence/exploration-cost.js +0 -204
  118. package/dist/intelligence/exploration-pattern-tracker.js +0 -61
  119. package/dist/intelligence/fact-generator.js +0 -322
  120. package/dist/intelligence/facts-schema.js +0 -90
  121. package/dist/intelligence/file-intelligence.js +0 -59
  122. package/dist/intelligence/graph-holder.js +0 -220
  123. package/dist/intelligence/graph-temporal-joiner.js +0 -238
  124. package/dist/intelligence/health-grade.js +0 -423
  125. package/dist/intelligence/health-grader.js +0 -200
  126. package/dist/intelligence/health-map-data.js +0 -259
  127. package/dist/intelligence/import-symbols.js +0 -136
  128. package/dist/intelligence/incremental-indexer.js +0 -658
  129. package/dist/intelligence/indexer/centrality.js +0 -62
  130. package/dist/intelligence/indexer/cfg-context.js +0 -95
  131. package/dist/intelligence/indexer/confidence.js +0 -34
  132. package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
  133. package/dist/intelligence/indexer/edge-repair.js +0 -89
  134. package/dist/intelligence/indexer/entity-key.js +0 -17
  135. package/dist/intelligence/indexer/export-map.js +0 -132
  136. package/dist/intelligence/indexer/git-cochange.js +0 -128
  137. package/dist/intelligence/indexer/graph-patch.js +0 -147
  138. package/dist/intelligence/indexer/incremental.js +0 -78
  139. package/dist/intelligence/indexer/ingest.js +0 -160
  140. package/dist/intelligence/indexer/language-detect.js +0 -226
  141. package/dist/intelligence/indexer/metadata.js +0 -63
  142. package/dist/intelligence/indexer/mutation-tracker.js +0 -79
  143. package/dist/intelligence/indexer/orchestrator.js +0 -155
  144. package/dist/intelligence/indexer/plugin-interface.js +0 -31
  145. package/dist/intelligence/indexer/plugins/csharp.js +0 -440
  146. package/dist/intelligence/indexer/plugins/go.js +0 -335
  147. package/dist/intelligence/indexer/plugins/java.js +0 -370
  148. package/dist/intelligence/indexer/plugins/python.js +0 -358
  149. package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
  150. package/dist/intelligence/indexer/plugins/ruby.js +0 -290
  151. package/dist/intelligence/indexer/plugins/rust.js +0 -484
  152. package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
  153. package/dist/intelligence/indexer/plugins/typescript.js +0 -456
  154. package/dist/intelligence/indexer/resource-monitor.js +0 -93
  155. package/dist/intelligence/indexer/scip/decoder.js +0 -253
  156. package/dist/intelligence/indexer/scip/detector.js +0 -232
  157. package/dist/intelligence/indexer/scip/downloader.js +0 -427
  158. package/dist/intelligence/indexer/scip/fallback.js +0 -34
  159. package/dist/intelligence/indexer/scip/merger.js +0 -109
  160. package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
  161. package/dist/intelligence/indexer/scip/runner.js +0 -98
  162. package/dist/intelligence/indexer/snapshot.js +0 -66
  163. package/dist/intelligence/indexer/test-detector.js +0 -196
  164. package/dist/intelligence/indexer/watch-integration.js +0 -61
  165. package/dist/intelligence/indexer/worker.js +0 -85
  166. package/dist/intelligence/local-convention-detector.js +0 -437
  167. package/dist/intelligence/local-embeddings.js +0 -190
  168. package/dist/intelligence/local-graph.js +0 -1946
  169. package/dist/intelligence/local-indexer.js +0 -1575
  170. package/dist/intelligence/local-llm.js +0 -163
  171. package/dist/intelligence/local-rule-generator.js +0 -154
  172. package/dist/intelligence/local-snapshot.js +0 -213
  173. package/dist/intelligence/negative-knowledge.js +0 -103
  174. package/dist/intelligence/persistent-db.js +0 -85
  175. package/dist/intelligence/query-router.js +0 -2556
  176. package/dist/intelligence/risk-classifier.js +0 -116
  177. package/dist/intelligence/rule-evaluator.js +0 -380
  178. package/dist/intelligence/rule-generator.js +0 -49
  179. package/dist/intelligence/search-index.js +0 -173
  180. package/dist/intelligence/semantic/docstring-extractor.js +0 -67
  181. package/dist/intelligence/semantic/embedding-store.js +0 -52
  182. package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
  183. package/dist/intelligence/semantic/git-message-miner.js +0 -114
  184. package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
  185. package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
  186. package/dist/intelligence/semantic/node2vec-walks.js +0 -103
  187. package/dist/intelligence/semantic/path-domain-inference.js +0 -112
  188. package/dist/intelligence/semantic/similarity-engine.js +0 -60
  189. package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
  190. package/dist/intelligence/session-brief-builder.js +0 -159
  191. package/dist/intelligence/session-context.js +0 -221
  192. package/dist/intelligence/session-health-monitor.js +0 -211
  193. package/dist/intelligence/session-narrative.js +0 -197
  194. package/dist/intelligence/session-pattern-analyzer.js +0 -218
  195. package/dist/intelligence/signal-scorer.js +0 -390
  196. package/dist/intelligence/signal-show-store.js +0 -182
  197. package/dist/intelligence/smart-truncate.js +0 -158
  198. package/dist/intelligence/subgraph-cache.js +0 -88
  199. package/dist/intelligence/temporal-facts.js +0 -494
  200. package/dist/intelligence/token-estimator.js +0 -100
  201. package/dist/intelligence/tool-injector.js +0 -87
  202. package/dist/intelligence/tree-sitter-loader.js +0 -71
  203. package/dist/intelligence/worker-pool.js +0 -116
  204. package/dist/proxy/arg-validator.js +0 -79
  205. package/dist/proxy/auto-bootstrap.js +0 -167
  206. package/dist/proxy/bridge.js +0 -147
  207. package/dist/proxy/budget-enforcer.js +0 -70
  208. package/dist/proxy/compression-quality-monitor.js +0 -160
  209. package/dist/proxy/compression-stats.js +0 -51
  210. package/dist/proxy/context-rot-detector.js +0 -137
  211. package/dist/proxy/drift-detector.js +0 -139
  212. package/dist/proxy/efficiency-tracker.js +0 -79
  213. package/dist/proxy/fact-ranking.js +0 -154
  214. package/dist/proxy/format-encoder.js +0 -266
  215. package/dist/proxy/http-transport.js +0 -90
  216. package/dist/proxy/lifecycle-actor.js +0 -55
  217. package/dist/proxy/lifecycle-machine.js +0 -187
  218. package/dist/proxy/log-tailer.js +0 -265
  219. package/dist/proxy/model-pricing.js +0 -98
  220. package/dist/proxy/network-firewall.js +0 -141
  221. package/dist/proxy/nudge-state.js +0 -93
  222. package/dist/proxy/output-compressor.js +0 -185
  223. package/dist/proxy/pid-lock.js +0 -291
  224. package/dist/proxy/proxy-context.js +0 -11
  225. package/dist/proxy/proxy.js +0 -2633
  226. package/dist/proxy/response-enrichment.js +0 -32
  227. package/dist/proxy/response-envelope.js +0 -313
  228. package/dist/proxy/session-dedup.js +0 -82
  229. package/dist/proxy/session-legend.js +0 -30
  230. package/dist/proxy/session-persistence.js +0 -210
  231. package/dist/proxy/session-resume.js +0 -94
  232. package/dist/proxy/session-stats.js +0 -513
  233. package/dist/proxy/shell-classifier.js +0 -1346
  234. package/dist/proxy/shell-compression-log.js +0 -93
  235. package/dist/proxy/shell-compressor.js +0 -390
  236. package/dist/proxy/shell-graph-boost.js +0 -202
  237. package/dist/proxy/shell-monitor-map.js +0 -18
  238. package/dist/proxy/shell-stats.js +0 -54
  239. package/dist/proxy/shell-strategies/cloud.js +0 -215
  240. package/dist/proxy/shell-strategies/diff.js +0 -159
  241. package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
  242. package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
  243. package/dist/proxy/shell-strategies/git-status.js +0 -177
  244. package/dist/proxy/shell-strategies/key-value.js +0 -193
  245. package/dist/proxy/shell-strategies/log-text.js +0 -154
  246. package/dist/proxy/shell-strategies/omni.js +0 -188
  247. package/dist/proxy/shell-strategies/progress.js +0 -55
  248. package/dist/proxy/shell-strategies/redact.js +0 -76
  249. package/dist/proxy/shell-strategies/structured.js +0 -241
  250. package/dist/proxy/shell-strategies/tabular.js +0 -243
  251. package/dist/proxy/shell-strategies/test-results-types.js +0 -13
  252. package/dist/proxy/shell-strategies/test-results.js +0 -784
  253. package/dist/proxy/shell-strategies/tree-paths.js +0 -144
  254. package/dist/proxy/shell-strategies/yaml.js +0 -182
  255. package/dist/proxy/shell-tee.js +0 -111
  256. package/dist/proxy/signal-dedup.js +0 -171
  257. package/dist/proxy/startup-renderer.js +0 -158
  258. package/dist/proxy/task-token-display.js +0 -38
  259. package/dist/proxy/token-counter.js +0 -61
  260. package/dist/proxy/tool-clusters.js +0 -273
  261. package/dist/proxy/tool-definitions.js +0 -525
  262. package/dist/proxy/transport-mux.js +0 -229
  263. package/dist/proxy/wire-cap.js +0 -268
  264. package/dist/rules/developer.mozilla.org.json +0 -9
  265. package/dist/rules/github.com.json +0 -21
  266. package/dist/schemas/api/skills.js +0 -19
  267. package/dist/schemas/common/errors.js +0 -7
  268. package/dist/schemas/common/headers.js +0 -5
  269. package/dist/schemas/entities/edge.js +0 -25
  270. package/dist/schemas/entities/entity.js +0 -22
  271. package/dist/schemas/entities/rule.js +0 -18
  272. package/dist/schemas/index.js +0 -14
  273. package/dist/server/event-bus.js +0 -59
  274. package/dist/server/http.js +0 -156
  275. package/dist/server/middleware.js +0 -70
  276. package/dist/server/routes/drift.js +0 -97
  277. package/dist/server/routes/intelligence.js +0 -1217
  278. package/dist/server/routes/reasoning-quality.js +0 -444
  279. package/dist/server/routes/session.js +0 -86
  280. package/dist/server/routes/stream.js +0 -120
  281. package/dist/server/routes/system.js +0 -73
  282. package/dist/server/routes/temporal.js +0 -170
  283. package/dist/server/routes/timeline.js +0 -232
  284. package/dist/server/routes/token-flow.js +0 -403
  285. package/dist/skills/effectiveness-tracker.js +0 -93
  286. package/dist/skills/local-pack.js +0 -380
  287. package/dist/skills/resolver.js +0 -495
  288. package/dist/state-detector.js +0 -83
  289. package/dist/timeline/intent-detector.js +0 -263
  290. package/dist/timeline/loop-miner.js +0 -140
  291. package/dist/timeline/open-threads.js +0 -49
  292. package/dist/timeline/signal-reinforcer.js +0 -62
  293. package/dist/timeline/timeline-bootstrap.js +0 -151
  294. package/dist/timeline/timeline-store.js +0 -618
  295. package/dist/tools/coding/bash.js +0 -49
  296. package/dist/tools/coding/file-edit.js +0 -72
  297. package/dist/tools/coding/file-outline.js +0 -227
  298. package/dist/tools/coding/file-read-protocol.js +0 -425
  299. package/dist/tools/coding/file-read.js +0 -35
  300. package/dist/tools/coding/file-write.js +0 -43
  301. package/dist/tools/coding/glob-tool.js +0 -109
  302. package/dist/tools/coding/grep.js +0 -162
  303. package/dist/tools/coding/index.js +0 -27
  304. package/dist/tools/intelligence/index.js +0 -269
  305. package/dist/tools/intelligence/record-fact.js +0 -48
  306. package/dist/tools/intelligence/timeline-markers.js +0 -130
  307. package/dist/tools/registry.js +0 -47
  308. package/dist/tools/types.js +0 -8
  309. package/dist/tracking/auto-snapshot-triggers.js +0 -246
  310. package/dist/tracking/branch-context.js +0 -115
  311. package/dist/tracking/branch-snapshot.js +0 -217
  312. package/dist/tracking/causal-bridge.js +0 -317
  313. package/dist/tracking/circuit-breaker.js +0 -147
  314. package/dist/tracking/commit-watcher.js +0 -114
  315. package/dist/tracking/context-ledger.js +0 -119
  316. package/dist/tracking/correction-detector.js +0 -324
  317. package/dist/tracking/drift-tracker.js +0 -874
  318. package/dist/tracking/durability-tracker.js +0 -94
  319. package/dist/tracking/entity-rewind.js +0 -200
  320. package/dist/tracking/file-hash-state.js +0 -114
  321. package/dist/tracking/git-attribution.js +0 -132
  322. package/dist/tracking/git-trailers.js +0 -171
  323. package/dist/tracking/intelligence-counter.js +0 -46
  324. package/dist/tracking/intent-correlator.js +0 -202
  325. package/dist/tracking/intent-encoder.js +0 -52
  326. package/dist/tracking/intent-token-tracker.js +0 -159
  327. package/dist/tracking/ledger-archiver.js +0 -94
  328. package/dist/tracking/ledger-chains.js +0 -245
  329. package/dist/tracking/metrics-store.js +0 -361
  330. package/dist/tracking/native-watcher.js +0 -131
  331. package/dist/tracking/offline-rewind.js +0 -295
  332. package/dist/tracking/pending-violations.js +0 -74
  333. package/dist/tracking/persistence-effectiveness.js +0 -167
  334. package/dist/tracking/prompt-durability.js +0 -202
  335. package/dist/tracking/quality-signals.js +0 -213
  336. package/dist/tracking/redactor.js +0 -73
  337. package/dist/tracking/rewind-engine.js +0 -161
  338. package/dist/tracking/session-history.js +0 -128
  339. package/dist/tracking/session-receipt.js +0 -88
  340. package/dist/tracking/session-summary-writer.js +0 -157
  341. package/dist/tracking/shadow-ledger.js +0 -321
  342. package/dist/tracking/stash-manager.js +0 -258
  343. package/dist/tracking/timeline-fork.js +0 -213
  344. package/dist/tracking/timeline.js +0 -69
  345. package/dist/tracking/token-flow.js +0 -276
  346. package/dist/tracking/turn-segmenter.js +0 -122
  347. package/dist/tracking/weekly-accumulator.js +0 -179
  348. package/dist/tracking/working-snapshots.js +0 -188
  349. package/dist/tracking/workspace-manifest.js +0 -176
  350. package/dist/transport/http.js +0 -102
  351. package/dist/utils/counterfactual.js +0 -65
  352. package/dist/utils/deep-link.js +0 -34
  353. package/dist/utils/detect.js +0 -193
  354. package/dist/utils/exec.js +0 -73
  355. package/dist/utils/file-logger.js +0 -87
  356. package/dist/utils/format-error.js +0 -29
  357. package/dist/utils/git.js +0 -181
  358. package/dist/utils/log.js +0 -57
  359. package/dist/utils/logger.js +0 -35
  360. package/dist/utils/mcp-content-json.js +0 -8
  361. package/dist/utils/session-logger.js +0 -154
  362. package/dist/utils/startup-log.js +0 -512
  363. package/dist/utils/ui.js +0 -56
@@ -1,1217 +0,0 @@
1
- /**
2
- * Layer 7: Intelligence API — graph stats, health, entity detail, ledger-derived insight.
3
- *
4
- * All GET handlers. Data is read from in-process CozoDB and tracking modules.
5
- */
6
- import { Hono } from "hono";
7
- import { isTestFile } from "../../intelligence/indexer/test-detector.js";
8
- import { computeOverallDurability, computePromptDurabilityProfiles, getMostFragile, } from "../../tracking/prompt-durability.js";
9
- const ENTITY_BODY_MAX = 24_000;
10
- function slimCallerCallee(e) {
11
- return {
12
- key: e.key,
13
- name: e.name,
14
- kind: e.kind,
15
- file_path: e.file_path,
16
- fan_in: e.fan_in,
17
- fan_out: e.fan_out,
18
- risk_level: e.risk_level,
19
- };
20
- }
21
- function compactEntityBody(e) {
22
- if (e.body.length <= ENTITY_BODY_MAX) {
23
- return { ...e, body_truncated: false };
24
- }
25
- return {
26
- ...e,
27
- body: `${e.body.slice(0, ENTITY_BODY_MAX)}…`,
28
- body_truncated: true,
29
- };
30
- }
31
- function parseLimit(raw, fallback, max) {
32
- const n = Number.parseInt(raw ?? "", 10);
33
- if (Number.isNaN(n) || n < 1)
34
- return fallback;
35
- return Math.min(n, max);
36
- }
37
- export function createIntelligenceRoutes(deps) {
38
- const app = new Hono();
39
- app.get("/search", async (c) => {
40
- const start = performance.now();
41
- const q = (c.req.query("q") ?? "").trim();
42
- const limit = parseLimit(c.req.query("limit"), 20, 50);
43
- if (!deps.localGraph) {
44
- return c.json({
45
- data: [],
46
- _meta: {
47
- source: "local",
48
- graph: "unavailable",
49
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
50
- },
51
- }, 503);
52
- }
53
- if (q.length === 0) {
54
- return c.json({
55
- data: [],
56
- _meta: {
57
- source: "local",
58
- empty_query: true,
59
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
60
- },
61
- });
62
- }
63
- const results = await deps.localGraph.searchEntities(q, limit);
64
- return c.json({
65
- data: results,
66
- _meta: {
67
- source: "local",
68
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
69
- },
70
- });
71
- });
72
- app.get("/graph-stats", async (c) => {
73
- const start = performance.now();
74
- if (!deps.localGraph) {
75
- return c.json({
76
- data: null,
77
- _meta: {
78
- source: "local",
79
- graph: "unavailable",
80
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
81
- },
82
- }, 503);
83
- }
84
- const stats = await deps.localGraph.getLocalProjectStats();
85
- return c.json({
86
- data: stats,
87
- _meta: {
88
- source: "local",
89
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
90
- },
91
- });
92
- });
93
- app.get("/health", async (c) => {
94
- const start = performance.now();
95
- const grade = await deps.getHealthGrade();
96
- if (!grade) {
97
- return c.json({
98
- data: null,
99
- _meta: {
100
- source: "local",
101
- graph: "unavailable",
102
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
103
- },
104
- });
105
- }
106
- return c.json({
107
- data: grade,
108
- _meta: {
109
- source: "local",
110
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
111
- },
112
- });
113
- });
114
- app.get("/top-entities", async (c) => {
115
- const start = performance.now();
116
- const limit = parseLimit(c.req.query("limit"), 10, 50);
117
- const communityRaw = c.req.query("community");
118
- const communityId = communityRaw !== undefined && communityRaw !== ""
119
- ? Number.parseInt(communityRaw, 10)
120
- : undefined;
121
- if (!deps.localGraph) {
122
- return c.json({
123
- data: [],
124
- _meta: {
125
- source: "local",
126
- graph: "unavailable",
127
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
128
- },
129
- }, 503);
130
- }
131
- const nodes = communityId !== undefined && !Number.isNaN(communityId)
132
- ? await deps.localGraph.getCriticalNodes(limit, communityId)
133
- : await deps.localGraph.getCriticalNodes(limit);
134
- return c.json({
135
- data: nodes,
136
- _meta: {
137
- source: "local",
138
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
139
- },
140
- });
141
- });
142
- // R2: Reading Tour — top backbone files for an unfamiliar repo.
143
- // Picks the top-N most-depended-on entities, dedupes by file, generates a "why" line.
144
- app.get("/reading-tour", async (c) => {
145
- const start = performance.now();
146
- const limit = parseLimit(c.req.query("limit"), 5, 10);
147
- if (!deps.localGraph) {
148
- return c.json({
149
- data: { stops: [], estimatedReadingTimeMin: 0 },
150
- _meta: {
151
- source: "local",
152
- graph: "unavailable",
153
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
154
- },
155
- }, 503);
156
- }
157
- // Pull a generous candidate pool so we can dedupe by file and still hit limit.
158
- const candidates = await deps.localGraph.getCriticalNodes(limit * 6);
159
- const seenFiles = new Set();
160
- const stops = [];
161
- for (const node of candidates) {
162
- if (stops.length >= limit)
163
- break;
164
- if (seenFiles.has(node.file_path))
165
- continue;
166
- seenFiles.add(node.file_path);
167
- const why = node.fan_in >= 30
168
- ? `Called from ${node.fan_in} places — load-bearing.`
169
- : node.fan_in >= 15
170
- ? `${node.fan_in} dependents — touched by most of the codebase.`
171
- : `${node.fan_in} dependents — a structural anchor.`;
172
- stops.push({
173
- rank: stops.length + 1,
174
- key: node.key,
175
- name: node.name,
176
- file_path: node.file_path,
177
- kind: node.kind,
178
- fan_in: node.fan_in,
179
- community_label: node.community_label,
180
- why,
181
- });
182
- }
183
- return c.json({
184
- data: {
185
- stops,
186
- estimatedReadingTimeMin: Math.max(10, stops.length * 5),
187
- },
188
- _meta: {
189
- source: "local",
190
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
191
- },
192
- });
193
- });
194
- app.get("/entity/:key", async (c) => {
195
- const start = performance.now();
196
- const key = decodeURIComponent(c.req.param("key"));
197
- if (!deps.localGraph) {
198
- return c.json({
199
- data: null,
200
- _meta: {
201
- source: "local",
202
- graph: "unavailable",
203
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
204
- },
205
- }, 503);
206
- }
207
- const entity = await deps.localGraph.getEntity(key);
208
- if (!entity) {
209
- return c.json({
210
- data: null,
211
- _meta: {
212
- source: "local",
213
- not_found: key,
214
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
215
- },
216
- }, 404);
217
- }
218
- const [callers, callees] = await Promise.all([
219
- deps.localGraph.getCallersOf(key),
220
- deps.localGraph.getCalleesOf(key),
221
- ]);
222
- const slimmed = callers.map(slimCallerCallee);
223
- const productionCallers = slimmed.filter((c) => !isTestFile(c.file_path));
224
- const testCallers = slimmed.filter((c) => isTestFile(c.file_path));
225
- return c.json({
226
- data: {
227
- entity: compactEntityBody(entity),
228
- callers: slimmed,
229
- production_callers: productionCallers,
230
- test_callers: testCallers,
231
- callees: callees.map(slimCallerCallee),
232
- },
233
- _meta: {
234
- source: "local",
235
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
236
- },
237
- });
238
- });
239
- app.get("/test-coverage/:key", async (c) => {
240
- const start = performance.now();
241
- const key = decodeURIComponent(c.req.param("key"));
242
- const includeTransitive = c.req.query("transitive") !== "false";
243
- if (!deps.localGraph) {
244
- return c.json({
245
- data: [],
246
- _meta: {
247
- source: "local",
248
- graph: "unavailable",
249
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
250
- },
251
- }, 503);
252
- }
253
- const coverage = await deps.localGraph.getTestCoverage(key, includeTransitive);
254
- return c.json({
255
- data: coverage,
256
- _meta: {
257
- source: "local",
258
- entity_key: key,
259
- include_transitive: includeTransitive,
260
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
261
- },
262
- });
263
- });
264
- app.get("/conventions", async (c) => {
265
- const start = performance.now();
266
- const { learnConventions } = await import("../../intelligence/convention-learner.js");
267
- const entries = deps.getRecentLedgerEntries(500);
268
- const conventions = learnConventions(entries);
269
- return c.json({
270
- data: conventions,
271
- _meta: {
272
- source: "local",
273
- ledger_entries_sampled: entries.length,
274
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
275
- },
276
- });
277
- });
278
- app.get("/causal/:key", async (c) => {
279
- const start = performance.now();
280
- const key = decodeURIComponent(c.req.param("key"));
281
- const { CausalBridge } = await import("../../tracking/causal-bridge.js");
282
- const bridge = new CausalBridge(deps.unerrDir, deps.cwd);
283
- const chain = await bridge.buildCausalChain(key);
284
- return c.json({
285
- data: chain,
286
- _meta: {
287
- source: "local",
288
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
289
- },
290
- });
291
- });
292
- app.get("/graph-visual", async (c) => {
293
- const start = performance.now();
294
- if (!deps.localGraph) {
295
- return c.json({
296
- data: { nodes: [], edges: [], communities: [] },
297
- _meta: {
298
- source: "local",
299
- graph: "unavailable",
300
- view_mode: "flat",
301
- node_count: 0,
302
- edge_count: 0,
303
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
304
- },
305
- }, 503);
306
- }
307
- // Fetch ALL entities and edges — no community filter (file-as-L0 approach)
308
- const [entityResult, edgeResult] = await Promise.all([
309
- deps.localGraph.db.run(`?[key, name, fp, fi, fo, community, rl, kind] :=
310
- *entities{key, name, file_path: fp, fan_in: fi, fan_out: fo, community, risk_level: rl, kind},
311
- kind != "file", kind != "module"`),
312
- deps.localGraph.db.run("?[from_key, to_key, type] := *edges{from_key, to_key, type}"),
313
- ]);
314
- // Use single source of truth for test detection (8-language support)
315
- // Separate "contains" edges (parent→child) from relationship edges
316
- const containsChildren = new Map(); // parent → Set<child>
317
- const childToParent = new Map(); // child → parent
318
- const relationshipEdges = [];
319
- for (const row of edgeResult.rows) {
320
- const from = row[0];
321
- const to = row[1];
322
- const type = row[2];
323
- if (type === "contains") {
324
- if (!containsChildren.has(from))
325
- containsChildren.set(from, new Set());
326
- containsChildren.get(from)?.add(to);
327
- childToParent.set(to, from);
328
- }
329
- else {
330
- relationshipEdges.push({ from, to, type });
331
- }
332
- }
333
- // Build entity map for quick lookup
334
- const entityMap = new Map();
335
- for (const row of entityResult.rows) {
336
- const [key, name, fp, fi, fo, community, rl, kind] = row;
337
- entityMap.set(key, { key, name, fp, fi, fo, community, rl, kind });
338
- }
339
- // Kinds that should be collapsed into their parent node
340
- const COLLAPSIBLE_KINDS = new Set([
341
- "method",
342
- "constructor",
343
- "type",
344
- "interface",
345
- "enum",
346
- "property",
347
- ]);
348
- // Determine which entities are "child" entities that should collapse into parent
349
- const collapsedInto = new Map(); // child → visual parent
350
- const memberCounts = new Map(); // parent → member count
351
- for (const [child, parent] of childToParent) {
352
- const childEntity = entityMap.get(child);
353
- if (!childEntity)
354
- continue;
355
- if (entityMap.has(parent) && COLLAPSIBLE_KINDS.has(childEntity.kind)) {
356
- collapsedInto.set(child, parent);
357
- memberCounts.set(parent, (memberCounts.get(parent) ?? 0) + 1);
358
- }
359
- }
360
- // Redirect edges that point to/from collapsed children to their parent
361
- const redirectedEdges = [];
362
- const edgeDedup = new Set();
363
- for (const e of relationshipEdges) {
364
- const from = collapsedInto.get(e.from) ?? e.from;
365
- const to = collapsedInto.get(e.to) ?? e.to;
366
- if (from === to)
367
- continue;
368
- const dedupKey = `${from}→${to}→${e.type}`;
369
- if (edgeDedup.has(dedupKey))
370
- continue;
371
- edgeDedup.add(dedupKey);
372
- redirectedEdges.push({ from, to, type: e.type });
373
- }
374
- // Build final node list (excluding collapsed children)
375
- const nodeKeys = new Set();
376
- let collapsedCount = 0;
377
- const nodes = [];
378
- for (const [key, ent] of entityMap) {
379
- if (collapsedInto.has(key)) {
380
- collapsedCount++;
381
- continue;
382
- }
383
- nodeKeys.add(key);
384
- nodes.push({
385
- id: key,
386
- label: ent.name,
387
- kind: ent.kind,
388
- file: ent.fp,
389
- fileGroup: ent.fp,
390
- fanIn: ent.fi,
391
- fanOut: ent.fo,
392
- community: ent.community,
393
- risk: ent.rl,
394
- isTest: isTestFile(ent.fp),
395
- members: memberCounts.get(key) ?? 0,
396
- externalOut: 0,
397
- externalIn: 0,
398
- });
399
- }
400
- // Final edge filter: only edges where both endpoints are visible
401
- const edges = redirectedEdges.filter((e) => nodeKeys.has(e.from) && nodeKeys.has(e.to));
402
- // Compute external (cross-community) edge counts per node
403
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
404
- for (const e of edges) {
405
- const fromNode = nodeById.get(e.from);
406
- const toNode = nodeById.get(e.to);
407
- if (fromNode && toNode && fromNode.community !== toNode.community) {
408
- fromNode.externalOut++;
409
- toNode.externalIn++;
410
- }
411
- }
412
- // ─── File-as-L0: Read materialized file-level data ─────────────────────
413
- // Group entities by file_path
414
- const fileEntityMap = new Map(); // filePath → entities in that file
415
- for (const n of nodes) {
416
- const list = fileEntityMap.get(n.file) ?? [];
417
- list.push(n);
418
- fileEntityMap.set(n.file, list);
419
- }
420
- // Read materialized file edges from CozoDB (computed at index time)
421
- const fileEdges = [];
422
- const fileEdgeWeights = new Map(); // "from→to" → weight for position computation
423
- try {
424
- const feResult = await deps.localGraph.db.run("?[from_file, to_file, weight] := *file_edges{from_file, to_file, weight}");
425
- for (const row of feResult.rows) {
426
- const from = row[0];
427
- const to = row[1];
428
- const weight = row[2];
429
- fileEdges.push({ from, to, weight });
430
- const key = `${from}→${to}`;
431
- fileEdgeWeights.set(key, (fileEdgeWeights.get(key) ?? 0) + weight);
432
- }
433
- }
434
- catch {
435
- // File edges not yet materialized — empty
436
- }
437
- // Read materialized file communities from CozoDB
438
- const fileCommunityAssignment = new Map();
439
- const fileCommunityLabels = new Map();
440
- const fileCommunityCohesion = new Map();
441
- try {
442
- const fcResult = await deps.localGraph.db.run("?[file_path, community, label, cohesion] := *file_communities{file_path, community, label, cohesion}");
443
- for (const row of fcResult.rows) {
444
- const fp = row[0];
445
- const cid = row[1];
446
- fileCommunityAssignment.set(fp, cid);
447
- fileCommunityLabels.set(cid, row[2]);
448
- fileCommunityCohesion.set(cid, row[3]);
449
- }
450
- }
451
- catch {
452
- // File communities not yet materialized — fallback: all in community 0
453
- }
454
- // Fallback: assign files without a community
455
- for (const fp of fileEntityMap.keys()) {
456
- if (!fileCommunityAssignment.has(fp)) {
457
- fileCommunityAssignment.set(fp, 0);
458
- }
459
- }
460
- // Build file nodes with aggregated metrics
461
- const fileNodes = [];
462
- const RISK_ORDER = { high: 3, medium: 2, low: 1 };
463
- for (const [fp, ents] of fileEntityMap) {
464
- const kinds = {};
465
- let totalFanIn = 0;
466
- let totalFanOut = 0;
467
- let maxRiskLevel = "low";
468
- for (const ent of ents) {
469
- kinds[ent.kind] = (kinds[ent.kind] ?? 0) + 1;
470
- totalFanIn += ent.fanIn;
471
- totalFanOut += ent.fanOut;
472
- if ((RISK_ORDER[ent.risk] ?? 0) > (RISK_ORDER[maxRiskLevel] ?? 0)) {
473
- maxRiskLevel = ent.risk;
474
- }
475
- }
476
- const myComm = fileCommunityAssignment.get(fp) ?? 0;
477
- let extOut = 0;
478
- let extIn = 0;
479
- for (const fe of fileEdges) {
480
- if (fe.from === fp) {
481
- const targetComm = fileCommunityAssignment.get(fe.to) ?? 0;
482
- if (targetComm !== myComm)
483
- extOut += fe.weight;
484
- }
485
- if (fe.to === fp) {
486
- const sourceComm = fileCommunityAssignment.get(fe.from) ?? 0;
487
- if (sourceComm !== myComm)
488
- extIn += fe.weight;
489
- }
490
- }
491
- const filename = fp.split("/").pop() ?? fp;
492
- fileNodes.push({
493
- id: fp,
494
- label: filename,
495
- filePath: fp,
496
- entityCount: ents.length,
497
- totalFanIn,
498
- totalFanOut,
499
- fileCommunity: myComm,
500
- maxRisk: maxRiskLevel,
501
- isTest: isTestFile(fp),
502
- kinds,
503
- externalOut: extOut,
504
- externalIn: extIn,
505
- });
506
- }
507
- // Build file communities from materialized data
508
- const fileCommunityGroups = new Map();
509
- for (const [fp, comm] of fileCommunityAssignment) {
510
- const list = fileCommunityGroups.get(comm) ?? [];
511
- list.push(fp);
512
- fileCommunityGroups.set(comm, list);
513
- }
514
- const fileCommunities = [];
515
- for (const [commId, files] of fileCommunityGroups) {
516
- const label = fileCommunityLabels.get(commId) ?? `cluster-${commId}`;
517
- const cohesion = fileCommunityCohesion.get(commId) ?? 1.0;
518
- let entityCount = 0;
519
- for (const fp of files) {
520
- entityCount += fileEntityMap.get(fp)?.length ?? 0;
521
- }
522
- // Inter-community edges from materialized file edges
523
- const interEdgeMap = new Map();
524
- for (const fe of fileEdges) {
525
- const fromComm = fileCommunityAssignment.get(fe.from) ?? -1;
526
- const toComm = fileCommunityAssignment.get(fe.to) ?? -1;
527
- if (fromComm === commId && toComm !== commId && toComm >= 0) {
528
- interEdgeMap.set(toComm, (interEdgeMap.get(toComm) ?? 0) + fe.weight);
529
- }
530
- }
531
- fileCommunities.push({
532
- id: commId,
533
- label,
534
- fileCount: files.length,
535
- entityCount,
536
- cohesion: Math.round(cohesion * 100) / 100,
537
- files,
538
- inter_edges: Array.from(interEdgeMap.entries()).map(([target, weight]) => ({ target, weight })),
539
- });
540
- }
541
- // Compute cross-community external counts on entity nodes
542
- for (const n of nodes) {
543
- const myFileComm = fileCommunityAssignment.get(n.file) ?? 0;
544
- n.externalOut = 0;
545
- n.externalIn = 0;
546
- for (const e of edges) {
547
- if (e.from === n.id) {
548
- const targetNode = nodeById.get(e.to);
549
- if (targetNode &&
550
- (fileCommunityAssignment.get(targetNode.file) ?? 0) !== myFileComm) {
551
- n.externalOut++;
552
- }
553
- }
554
- if (e.to === n.id) {
555
- const sourceNode = nodeById.get(e.from);
556
- if (sourceNode &&
557
- (fileCommunityAssignment.get(sourceNode.file) ?? 0) !== myFileComm) {
558
- n.externalIn++;
559
- }
560
- }
561
- }
562
- }
563
- // Read class edges for L1 class-level visualization
564
- let classEdges = [];
565
- try {
566
- const ceResult = await deps.localGraph.db.run("?[from_class, to_class, edge_type, weight] := *class_edges{from_class, to_class, edge_type, weight}");
567
- classEdges = ceResult.rows.map((row) => ({
568
- from: row[0],
569
- to: row[1],
570
- type: row[2],
571
- weight: row[3],
572
- }));
573
- }
574
- catch {
575
- // Class edges not materialized — empty
576
- }
577
- // Determine view_mode based on file count and file community count
578
- const totalFileCount = fileNodes.length;
579
- const totalFileCommunities = fileCommunities.length;
580
- let viewMode;
581
- if (totalFileCommunities <= 2 && totalFileCount < 15) {
582
- viewMode = "flat";
583
- }
584
- else if (totalFileCommunities <= 3) {
585
- viewMode = "file-clusters";
586
- }
587
- else {
588
- viewMode = "hierarchical";
589
- }
590
- // ─── Server-side position pre-computation ──────────────────────────────
591
- const positions = { communities: {}, files: {}, entities: {} };
592
- try {
593
- const graphologyMod = (await import("graphology"));
594
- const fa2Mod = (await import("graphology-layout-forceatlas2"));
595
- const Graph = graphologyMod.default ?? graphologyMod;
596
- const forceAtlas2 = fa2Mod.default ?? fa2Mod;
597
- // Community-level positions
598
- if (fileCommunities.length > 1) {
599
- const commGraph = new Graph({ type: "undirected" });
600
- for (const comm of fileCommunities) {
601
- commGraph.addNode(String(comm.id), {
602
- x: Math.random() * 100 - 50,
603
- y: Math.random() * 100 - 50,
604
- size: comm.entityCount,
605
- });
606
- }
607
- for (const comm of fileCommunities) {
608
- for (const ie of comm.inter_edges) {
609
- if (commGraph.hasNode(String(ie.target)) &&
610
- !commGraph.hasEdge(String(comm.id), String(ie.target))) {
611
- commGraph.addEdge(String(comm.id), String(ie.target), {
612
- weight: ie.weight,
613
- });
614
- }
615
- }
616
- }
617
- forceAtlas2.assign(commGraph, {
618
- iterations: 100,
619
- settings: {
620
- gravity: 1,
621
- scalingRatio: 10,
622
- strongGravityMode: true,
623
- barnesHutOptimize: true,
624
- },
625
- });
626
- commGraph.forEachNode((node, attrs) => {
627
- positions.communities[Number(node)] = {
628
- x: Math.round(attrs.x),
629
- y: Math.round(attrs.y),
630
- };
631
- });
632
- }
633
- // File-level positions (one layout per community)
634
- for (const comm of fileCommunities) {
635
- const filesInComm = comm.files;
636
- if (filesInComm.length === 0)
637
- continue;
638
- const fileGraph = new Graph({ type: "undirected" });
639
- for (const fp of filesInComm) {
640
- fileGraph.addNode(fp, {
641
- x: Math.random() * 100 - 50,
642
- y: Math.random() * 100 - 50,
643
- size: fileEntityMap.get(fp)?.length ?? 1,
644
- });
645
- }
646
- for (const [key, weight] of fileEdgeWeights) {
647
- const parts = key.split("→");
648
- const from = parts[0];
649
- const to = parts[1];
650
- if (fileGraph.hasNode(from) &&
651
- fileGraph.hasNode(to) &&
652
- !fileGraph.hasEdge(from, to)) {
653
- fileGraph.addEdge(from, to, { weight });
654
- }
655
- }
656
- if (fileGraph.order > 1) {
657
- forceAtlas2.assign(fileGraph, {
658
- iterations: 80,
659
- settings: {
660
- gravity: 2,
661
- scalingRatio: 5,
662
- barnesHutOptimize: fileGraph.order > 30,
663
- },
664
- });
665
- }
666
- fileGraph.forEachNode((node, attrs) => {
667
- positions.files[node] = {
668
- x: Math.round(attrs.x),
669
- y: Math.round(attrs.y),
670
- };
671
- });
672
- }
673
- // Entity-level positions (vertical code-order layout per file)
674
- for (const [, ents] of fileEntityMap) {
675
- if (ents.length <= 1) {
676
- if (ents.length === 1) {
677
- const ent0 = ents[0];
678
- if (ent0)
679
- positions.entities[ent0.id] = { x: 0, y: 0 };
680
- }
681
- continue;
682
- }
683
- const sorted = [...ents].sort((a, b) => {
684
- const kindOrder = {
685
- class: 0,
686
- function: 1,
687
- variable: 2,
688
- };
689
- const ka = kindOrder[a.kind] ?? 3;
690
- const kb = kindOrder[b.kind] ?? 3;
691
- if (ka !== kb)
692
- return ka - kb;
693
- return a.label.localeCompare(b.label);
694
- });
695
- sorted.forEach((ent, i) => {
696
- positions.entities[ent.id] = { x: 0, y: i * 60 };
697
- });
698
- }
699
- }
700
- catch {
701
- // Position computation is non-critical — frontend falls back to force-directed
702
- }
703
- return c.json({
704
- data: {
705
- nodes,
706
- edges,
707
- fileNodes,
708
- fileEdges,
709
- fileCommunities,
710
- classEdges,
711
- positions,
712
- },
713
- _meta: {
714
- source: "local",
715
- view_mode: viewMode,
716
- node_count: nodes.length,
717
- edge_count: edges.length,
718
- file_count: totalFileCount,
719
- file_edge_count: fileEdges.length,
720
- class_edge_count: classEdges.length,
721
- collapsed_count: collapsedCount,
722
- total_entities: entityResult.rows.length,
723
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
724
- },
725
- });
726
- });
727
- app.get("/risk-hotspots", async (c) => {
728
- const start = performance.now();
729
- const limit = parseLimit(c.req.query("limit"), 20, 50);
730
- if (!deps.localGraph) {
731
- return c.json({
732
- data: [],
733
- _meta: {
734
- source: "local",
735
- graph: "unavailable",
736
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
737
- },
738
- }, 503);
739
- }
740
- // Get top entities by degree
741
- const topNodes = await deps.localGraph.getCriticalNodes(limit);
742
- // Batch test coverage check: for each entity, count direct + transitive test edges
743
- // Single Datalog query to get test counts for all keys at once
744
- const keys = topNodes.map((n) => n.key);
745
- const testCountMap = new Map();
746
- if (keys.length > 0) {
747
- try {
748
- // Direct tests
749
- const directResult = await deps.localGraph.db.run(`?[target, count(tk)] := *edges{from_key: tk, to_key: target, type: "tests"},
750
- target in $keys`, { keys });
751
- for (const row of directResult.rows) {
752
- testCountMap.set(row[0], row[1]);
753
- }
754
- // Transitive tests (depth 2: test → caller → target)
755
- const transitiveResult = await deps.localGraph.db.run(`?[target, count(tk)] := *edges{from_key: mid, to_key: target, type: "calls"},
756
- *edges{from_key: tk, to_key: mid, type: "tests"},
757
- target in $keys`, { keys });
758
- for (const row of transitiveResult.rows) {
759
- const key = row[0];
760
- const existing = testCountMap.get(key) ?? 0;
761
- testCountMap.set(key, existing + row[1]);
762
- }
763
- }
764
- catch {
765
- // Test coverage query failed — leave counts at 0
766
- }
767
- }
768
- // Also get caller counts for blast radius context
769
- const callerCountMap = new Map();
770
- if (keys.length > 0) {
771
- try {
772
- const callerResult = await deps.localGraph.db.run(`?[target, count(caller)] := *edges{from_key: caller, to_key: target, type: "calls"},
773
- target in $keys`, { keys });
774
- for (const row of callerResult.rows) {
775
- callerCountMap.set(row[0], row[1]);
776
- }
777
- }
778
- catch {
779
- // Caller count query failed — leave counts at 0
780
- }
781
- }
782
- const hotspots = topNodes.map((n) => ({
783
- key: n.key,
784
- name: n.name,
785
- file_path: n.file_path,
786
- kind: n.kind,
787
- fan_in: n.fan_in,
788
- fan_out: n.fan_out,
789
- degree: n.degree,
790
- risk_level: n.risk_level,
791
- community_label: n.community_label,
792
- test_count: testCountMap.get(n.key) ?? 0,
793
- caller_count: callerCountMap.get(n.key) ?? 0,
794
- }));
795
- return c.json({
796
- data: hotspots,
797
- _meta: {
798
- source: "local",
799
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
800
- },
801
- });
802
- });
803
- app.get("/insights", async (c) => {
804
- const start = performance.now();
805
- if (!deps.localGraph) {
806
- return c.json({
807
- data: null,
808
- _meta: {
809
- source: "local",
810
- graph: "unavailable",
811
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
812
- },
813
- }, 503);
814
- }
815
- // ── Gather raw data with parallel queries ──────────────────────────
816
- const topN = 50; // Analyze more than we show for accurate metrics
817
- const topNodes = await deps.localGraph.getCriticalNodes(topN);
818
- const keys = topNodes.map((n) => n.key);
819
- // Batch test coverage for all top entities
820
- const testCountMap = new Map();
821
- if (keys.length > 0) {
822
- try {
823
- const directResult = await deps.localGraph.db.run(`?[target, count(tk)] := *edges{from_key: tk, to_key: target, type: "tests"},
824
- target in $keys`, { keys });
825
- for (const row of directResult.rows) {
826
- testCountMap.set(row[0], row[1]);
827
- }
828
- const transitiveResult = await deps.localGraph.db.run(`?[target, count(tk)] := *edges{from_key: mid, to_key: target, type: "calls"},
829
- *edges{from_key: tk, to_key: mid, type: "tests"},
830
- target in $keys`, { keys });
831
- for (const row of transitiveResult.rows) {
832
- const k = row[0];
833
- testCountMap.set(k, (testCountMap.get(k) ?? 0) + row[1]);
834
- }
835
- }
836
- catch {
837
- // Test coverage unavailable
838
- }
839
- }
840
- // ── Compute blast-radius-weighted coverage ────────────────────────
841
- // Weight each entity by its fan_in (how many things depend on it)
842
- let totalBlastRadius = 0;
843
- let testedBlastRadius = 0;
844
- let untestedBlastRadius = 0;
845
- for (const n of topNodes) {
846
- const weight = Math.max(n.fan_in, 1); // min 1 so isolated entities still count
847
- totalBlastRadius += weight;
848
- if ((testCountMap.get(n.key) ?? 0) > 0) {
849
- testedBlastRadius += weight;
850
- }
851
- else {
852
- untestedBlastRadius += weight;
853
- }
854
- }
855
- const blastRadiusCoverage = totalBlastRadius > 0
856
- ? Math.round((testedBlastRadius / totalBlastRadius) * 100)
857
- : 0;
858
- // ── Identify bottlenecks ──────────────────────────────────────────
859
- // Bottleneck = high fan_in AND high fan_out AND zero tests
860
- // These are structural chokepoints — everything flows through them
861
- const bottlenecks = topNodes
862
- .filter((n) => n.fan_in >= 3 &&
863
- n.fan_out >= 2 &&
864
- (testCountMap.get(n.key) ?? 0) === 0)
865
- .map((n) => ({
866
- key: n.key,
867
- name: n.name,
868
- file_path: n.file_path,
869
- kind: n.kind,
870
- fan_in: n.fan_in,
871
- fan_out: n.fan_out,
872
- degree: n.degree,
873
- risk_level: n.risk_level,
874
- }));
875
- // ── Risk distribution ─────────────────────────────────────────────
876
- const riskDistribution = { high: 0, medium: 0, low: 0 };
877
- for (const n of topNodes) {
878
- const level = n.risk_level;
879
- if (level in riskDistribution)
880
- riskDistribution[level]++;
881
- }
882
- // ── Risk concentration ────────────────────────────────────────────
883
- // What % of total degree sits in the top 5 entities?
884
- const sortedByDegree = [...topNodes].sort((a, b) => b.degree - a.degree);
885
- const totalDegree = topNodes.reduce((s, n) => s + n.degree, 0);
886
- const top5Degree = sortedByDegree
887
- .slice(0, 5)
888
- .reduce((s, n) => s + n.degree, 0);
889
- const riskConcentration = totalDegree > 0 ? Math.round((top5Degree / totalDegree) * 100) : 0;
890
- // ── Community health ──────────────────────────────────────────────
891
- const communityMap = new Map();
892
- for (const n of topNodes) {
893
- const label = n.community_label || `cluster-${n.community}`;
894
- let comm = communityMap.get(label);
895
- if (!comm) {
896
- comm = {
897
- label,
898
- entities: 0,
899
- totalDegree: 0,
900
- riskHigh: 0,
901
- riskMedium: 0,
902
- riskLow: 0,
903
- untested: 0,
904
- tested: 0,
905
- totalFanIn: 0,
906
- untestedFanIn: 0,
907
- };
908
- communityMap.set(label, comm);
909
- }
910
- comm.entities++;
911
- comm.totalDegree += n.degree;
912
- comm.totalFanIn += n.fan_in;
913
- const hasCoverage = (testCountMap.get(n.key) ?? 0) > 0;
914
- if (hasCoverage)
915
- comm.tested++;
916
- else {
917
- comm.untested++;
918
- comm.untestedFanIn += n.fan_in;
919
- }
920
- if (n.risk_level === "high")
921
- comm.riskHigh++;
922
- else if (n.risk_level === "medium")
923
- comm.riskMedium++;
924
- else
925
- comm.riskLow++;
926
- }
927
- // Read cohesion data from file communities if available
928
- const communityHealth = [];
929
- for (const [, comm] of communityMap) {
930
- const total = comm.tested + comm.untested;
931
- communityHealth.push({
932
- label: comm.label,
933
- entities: comm.entities,
934
- tested: comm.tested,
935
- untested: comm.untested,
936
- coveragePct: total > 0 ? Math.round((comm.tested / total) * 100) : 0,
937
- blastRadiusCoveragePct: comm.totalFanIn > 0
938
- ? Math.round(((comm.totalFanIn - comm.untestedFanIn) / comm.totalFanIn) * 100)
939
- : 0,
940
- riskHigh: comm.riskHigh,
941
- riskMedium: comm.riskMedium,
942
- riskLow: comm.riskLow,
943
- totalDegree: comm.totalDegree,
944
- });
945
- }
946
- // Sort communities: worst blast-radius coverage first
947
- communityHealth.sort((a, b) => a.blastRadiusCoveragePct - b.blastRadiusCoveragePct);
948
- // ── Most coupled community pair ───────────────────────────────────
949
- let mostCoupledPair = null;
950
- try {
951
- const fcResult = await deps.localGraph.db.run(`?[from_label, to_label, w] :=
952
- *file_edges{from_file, to_file, weight: w},
953
- *file_communities{file_path: from_file, label: from_label},
954
- *file_communities{file_path: to_file, label: to_label},
955
- from_label != to_label
956
- :order -w
957
- :limit 1`);
958
- if (fcResult.rows.length > 0) {
959
- const [from, to, weight] = fcResult.rows[0];
960
- mostCoupledPair = { from, to, weight };
961
- }
962
- }
963
- catch {
964
- // File communities not available
965
- }
966
- // ── Generate narrative insights ───────────────────────────────────
967
- const insights = [];
968
- // Insight 1: Blast-radius-weighted coverage gap
969
- if (blastRadiusCoverage < 50) {
970
- insights.push({
971
- id: "blast-radius-coverage",
972
- severity: "critical",
973
- title: `Blast-radius coverage is only ${blastRadiusCoverage}%`,
974
- description: `${untestedBlastRadius} dependency-weighted risk sits in untested code. Your entity-count coverage looks better but hides that your most-depended-on code is unprotected.`,
975
- metric: blastRadiusCoverage,
976
- metricLabel: "blast-radius coverage",
977
- });
978
- }
979
- else if (blastRadiusCoverage < 75) {
980
- insights.push({
981
- id: "blast-radius-coverage",
982
- severity: "warning",
983
- title: `Blast-radius coverage at ${blastRadiusCoverage}%`,
984
- description: `Your most critical code paths are partially covered, but ${untestedBlastRadius} dependency-weight remains untested.`,
985
- metric: blastRadiusCoverage,
986
- metricLabel: "blast-radius coverage",
987
- });
988
- }
989
- else {
990
- insights.push({
991
- id: "blast-radius-coverage",
992
- severity: "positive",
993
- title: `Strong blast-radius coverage: ${blastRadiusCoverage}%`,
994
- description: "Your highest-impact code paths are well tested. Regressions are unlikely to cascade silently.",
995
- metric: blastRadiusCoverage,
996
- metricLabel: "blast-radius coverage",
997
- });
998
- }
999
- // Insight 2: Bottleneck alert
1000
- if (bottlenecks.length > 0) {
1001
- const totalBottleneckFanIn = bottlenecks.reduce((s, b) => s + b.fan_in, 0);
1002
- insights.push({
1003
- id: "bottlenecks",
1004
- severity: bottlenecks.length >= 5 ? "critical" : "warning",
1005
- title: `${bottlenecks.length} structural bottleneck${bottlenecks.length !== 1 ? "s" : ""} with zero tests`,
1006
- description: `These functions have high fan-in AND fan-out — all dependency traffic flows through them. Combined, ${totalBottleneckFanIn} dependents are exposed. A failure in any one cascades in both directions.`,
1007
- metric: bottlenecks.length,
1008
- metricLabel: "bottlenecks",
1009
- });
1010
- }
1011
- // Insight 3: Risk concentration
1012
- if (riskConcentration > 40) {
1013
- insights.push({
1014
- id: "risk-concentration",
1015
- severity: riskConcentration > 60 ? "warning" : "info",
1016
- title: `${riskConcentration}% of structural risk concentrated in top 5 entities`,
1017
- description: `Your risk isn't spread evenly — a small number of entities carry most of the dependency weight. Focus testing and review here for maximum impact.`,
1018
- metric: riskConcentration,
1019
- metricLabel: "risk in top 5",
1020
- });
1021
- }
1022
- // Insight 4: Most coupled pair
1023
- if (mostCoupledPair && mostCoupledPair.weight >= 3) {
1024
- insights.push({
1025
- id: "coupling-hotspot",
1026
- severity: mostCoupledPair.weight >= 10 ? "warning" : "info",
1027
- title: `"${mostCoupledPair.from}" and "${mostCoupledPair.to}" are tightly coupled`,
1028
- description: `${mostCoupledPair.weight} cross-boundary calls between these modules. Changes in one are likely to require changes in the other.`,
1029
- metric: mostCoupledPair.weight,
1030
- metricLabel: "cross-boundary calls",
1031
- });
1032
- }
1033
- // Insight 5: Positive — low risk distribution
1034
- if (riskDistribution.high === 0 && topNodes.length > 0) {
1035
- insights.push({
1036
- id: "no-high-risk",
1037
- severity: "positive",
1038
- title: "No high-risk entities detected",
1039
- description: "Your top entities are well-balanced with moderate dependency counts. The codebase structure is healthy.",
1040
- });
1041
- }
1042
- // ── Health score (0-100) ──────────────────────────────────────────
1043
- // Weighted: 40% blast-radius coverage, 25% bottleneck penalty, 20% risk spread, 15% high-risk penalty
1044
- const bottleneckPenalty = Math.min(bottlenecks.length * 5, 25);
1045
- const highRiskPenalty = Math.min(riskDistribution.high * 3, 15);
1046
- const concentrationPenalty = riskConcentration > 50 ? (riskConcentration - 50) * 0.4 : 0;
1047
- const healthScore = Math.max(0, Math.min(100, Math.round(blastRadiusCoverage * 0.4 +
1048
- (100 - bottleneckPenalty) * 0.25 +
1049
- (100 - concentrationPenalty) * 0.2 +
1050
- (100 - highRiskPenalty) * 0.15)));
1051
- const healthGrade = healthScore >= 90
1052
- ? "A"
1053
- : healthScore >= 80
1054
- ? "B"
1055
- : healthScore >= 65
1056
- ? "C"
1057
- : healthScore >= 50
1058
- ? "D"
1059
- : "F";
1060
- return c.json({
1061
- data: {
1062
- healthScore,
1063
- healthGrade,
1064
- blastRadiusCoverage,
1065
- untestedBlastRadius,
1066
- testedBlastRadius,
1067
- totalBlastRadius,
1068
- bottlenecks,
1069
- riskDistribution,
1070
- riskConcentration,
1071
- communityHealth,
1072
- mostCoupledPair,
1073
- insights,
1074
- },
1075
- _meta: {
1076
- source: "local",
1077
- entities_analyzed: topNodes.length,
1078
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
1079
- },
1080
- });
1081
- });
1082
- app.get("/durability", async (c) => {
1083
- const start = performance.now();
1084
- const limit = parseLimit(c.req.query("ledger_limit"), 800, 5000);
1085
- const raw = deps.getRecentLedgerEntries(limit);
1086
- const ledgerLike = raw.map((entry) => ({
1087
- prompt: entry.plan_summary ?? "",
1088
- files: extractFilesHint(entry),
1089
- survived: undefined,
1090
- riskLevel: undefined,
1091
- }));
1092
- const profiles = computePromptDurabilityProfiles(ledgerLike);
1093
- return c.json({
1094
- data: {
1095
- profiles,
1096
- overall: computeOverallDurability(profiles),
1097
- most_fragile: getMostFragile(profiles, 5),
1098
- },
1099
- _meta: {
1100
- source: "local",
1101
- ledger_entries_sampled: raw.length,
1102
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
1103
- },
1104
- });
1105
- });
1106
- // ── Sprint 8: Health Map ──
1107
- app.get("/health-map", async (c) => {
1108
- if (!deps.localGraph) {
1109
- return c.json({ error: "Graph not available" }, 503);
1110
- }
1111
- const start = performance.now();
1112
- const root = c.req.query("root") || undefined;
1113
- try {
1114
- const { HealthMapData } = await import("../../intelligence/health-map-data.js");
1115
- const healthMap = new HealthMapData(deps.localGraph, deps.factStore ?? null);
1116
- const tree = await healthMap.buildTree(root);
1117
- return c.json({
1118
- data: tree,
1119
- _meta: {
1120
- source: "local",
1121
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
1122
- },
1123
- });
1124
- }
1125
- catch (err) {
1126
- return c.json({ error: err instanceof Error ? err.message : "Health map failed" }, 500);
1127
- }
1128
- });
1129
- app.get("/health-map/file", async (c) => {
1130
- if (!deps.localGraph) {
1131
- return c.json({ error: "Graph not available" }, 503);
1132
- }
1133
- const start = performance.now();
1134
- const filePath = c.req.query("path");
1135
- if (!filePath) {
1136
- return c.json({ error: "Missing ?path= query parameter" }, 400);
1137
- }
1138
- try {
1139
- const { HealthMapData } = await import("../../intelligence/health-map-data.js");
1140
- const healthMap = new HealthMapData(deps.localGraph, deps.factStore ?? null);
1141
- const node = await healthMap.getFileHealth(filePath);
1142
- if (!node) {
1143
- return c.json({ error: "No entities found for file" }, 404);
1144
- }
1145
- // R4: Recent activity — count ledger entries that touched this file
1146
- // within the last 30 days. Conservative signal of "this file is hot".
1147
- const days = 30;
1148
- const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
1149
- const entries = deps.getRecentLedgerEntries(500);
1150
- const matchEnds = filePath.toLowerCase();
1151
- let editCount = 0;
1152
- let mostRecentTs;
1153
- const sessions = new Set();
1154
- for (const entry of entries) {
1155
- const ts = Date.parse(entry.ts);
1156
- if (Number.isNaN(ts) || ts < sinceMs)
1157
- continue;
1158
- const files = extractFilesHint(entry);
1159
- const hit = files.some((f) => f.toLowerCase().endsWith(matchEnds));
1160
- if (!hit)
1161
- continue;
1162
- editCount += 1;
1163
- sessions.add(entry.session_id);
1164
- if (!mostRecentTs || entry.ts > mostRecentTs)
1165
- mostRecentTs = entry.ts;
1166
- }
1167
- return c.json({
1168
- data: {
1169
- ...node,
1170
- recent_activity: {
1171
- edit_count: editCount,
1172
- session_count: sessions.size,
1173
- most_recent_ts: mostRecentTs ?? null,
1174
- window_days: days,
1175
- },
1176
- },
1177
- _meta: {
1178
- source: "local",
1179
- latency_ms: Math.round((performance.now() - start) * 100) / 100,
1180
- },
1181
- });
1182
- }
1183
- catch (err) {
1184
- return c.json({ error: err instanceof Error ? err.message : "File health failed" }, 500);
1185
- }
1186
- });
1187
- // Sprint 9.3: Signal delivery stats endpoint
1188
- app.get("/signal-stats", (c) => {
1189
- if (!deps.getSignalStats) {
1190
- return c.json({
1191
- data: { total_delivered: 0, by_type: {}, coverage_pct: 0 },
1192
- });
1193
- }
1194
- return c.json({ data: deps.getSignalStats() });
1195
- });
1196
- return app;
1197
- }
1198
- function extractFilesHint(entry) {
1199
- const files = [];
1200
- const args = entry.args_summary;
1201
- if (args && typeof args === "object") {
1202
- for (const v of Object.values(args)) {
1203
- if (typeof v === "string" && (v.includes("/") || v.includes("\\"))) {
1204
- files.push(v);
1205
- }
1206
- if (Array.isArray(v)) {
1207
- for (const item of v) {
1208
- if (typeof item === "string" &&
1209
- (item.includes("/") || item.includes("\\"))) {
1210
- files.push(item);
1211
- }
1212
- }
1213
- }
1214
- }
1215
- }
1216
- return files;
1217
- }