@unerr-ai/unerr 0.1.0 → 0.1.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 (553) hide show
  1. package/dist/__tests__/architecture-guard.test.js +122 -0
  2. package/dist/__tests__/arg-validator.test.js +205 -0
  3. package/dist/__tests__/ast-extractor.test.js +203 -0
  4. package/dist/__tests__/auto-bootstrap.test.js +280 -0
  5. package/dist/__tests__/background-indexer.test.js +228 -0
  6. package/dist/__tests__/blast-radius-engine.test.js +200 -0
  7. package/dist/__tests__/bridge-isolation.test.js +37 -0
  8. package/dist/__tests__/budget-enforcer.test.js +53 -0
  9. package/dist/__tests__/cfg-test-detection-perf.test.js +82 -0
  10. package/dist/__tests__/change-narrative.test.js +190 -0
  11. package/dist/__tests__/check-commit.test.js +258 -0
  12. package/dist/__tests__/checksum.test.js +34 -0
  13. package/dist/__tests__/commit-watcher.test.js +154 -0
  14. package/dist/__tests__/community-detection.test.js +179 -0
  15. package/dist/__tests__/community-tools.test.js +299 -0
  16. package/dist/__tests__/components.test.js +449 -0
  17. package/dist/__tests__/compression-log.test.js +174 -0
  18. package/dist/__tests__/compression-quality-monitor.test.js +40 -0
  19. package/dist/__tests__/config-healer.test.js +165 -0
  20. package/dist/__tests__/context-ledger.test.js +58 -0
  21. package/dist/__tests__/convention-detector.test.js +99 -0
  22. package/dist/__tests__/convention-learner.test.js +86 -0
  23. package/dist/__tests__/correction-detector.test.js +330 -0
  24. package/dist/__tests__/daemon-autostart-install.test.js +283 -0
  25. package/dist/__tests__/daemon-bridge.test.js +222 -0
  26. package/dist/__tests__/daemon-dashboard.test.js +202 -0
  27. package/dist/__tests__/daemon-registry.test.js +240 -0
  28. package/dist/__tests__/daemon-supervisor.test.js +318 -0
  29. package/dist/__tests__/daemon-version-check.test.js +275 -0
  30. package/dist/__tests__/decision-point-detector.test.js +98 -0
  31. package/dist/__tests__/deep-link.test.js +143 -0
  32. package/dist/__tests__/disallowed-tools.test.js +115 -0
  33. package/dist/__tests__/drift-tracker.test.js +582 -0
  34. package/dist/__tests__/durability-scorer.test.js +152 -0
  35. package/dist/__tests__/efficiency-tracker.test.js +65 -0
  36. package/dist/__tests__/enrich.test.js +144 -0
  37. package/dist/__tests__/entity-rewind.test.js +248 -0
  38. package/dist/__tests__/ephemeral.test.js +111 -0
  39. package/dist/__tests__/exploration-cost.test.js +93 -0
  40. package/dist/__tests__/fact-generator.test.js +197 -0
  41. package/dist/__tests__/file-l0-graph.test.js +244 -0
  42. package/dist/__tests__/file-logger.test.js +82 -0
  43. package/dist/__tests__/file-outline.test.js +141 -0
  44. package/dist/__tests__/file-read-protocol.test.js +188 -0
  45. package/dist/__tests__/format-encoder.test.js +233 -0
  46. package/dist/__tests__/git-attribution.test.js +259 -0
  47. package/dist/__tests__/graph-temporal-joiner.test.js +219 -0
  48. package/dist/__tests__/health-grade-enhanced.test.js +138 -0
  49. package/dist/__tests__/health-map-data.test.js +173 -0
  50. package/dist/__tests__/helpers/mcp-harness.js +45 -0
  51. package/dist/__tests__/helpers/mcp-harness.test.js +68 -0
  52. package/dist/__tests__/hook-dedup.test.js +112 -0
  53. package/dist/__tests__/hook-runner.test.js +253 -0
  54. package/dist/__tests__/indexer-cfg.test.js +185 -0
  55. package/dist/__tests__/indexer-cross-file.test.js +172 -0
  56. package/dist/__tests__/indexer-extraction.test.js +245 -0
  57. package/dist/__tests__/indexer-incremental.test.js +232 -0
  58. package/dist/__tests__/indexer-language-expansion.test.js +165 -0
  59. package/dist/__tests__/init-push.test.js +131 -0
  60. package/dist/__tests__/instruction-writer.test.js +179 -0
  61. package/dist/__tests__/intelligence-integration.test.js +217 -0
  62. package/dist/__tests__/intent-correlator.test.js +175 -0
  63. package/dist/__tests__/intent-detector.test.js +235 -0
  64. package/dist/__tests__/intent-encoder.test.js +167 -0
  65. package/dist/__tests__/java-build-tool-detection.test.js +174 -0
  66. package/dist/__tests__/layer3-sprint-q.test.js +160 -0
  67. package/dist/__tests__/layer3-sprint-r.test.js +91 -0
  68. package/dist/__tests__/layer3-sprint-s.test.js +183 -0
  69. package/dist/__tests__/layer3-sprint-t.test.js +201 -0
  70. package/dist/__tests__/layer3-sprint-u.test.js +174 -0
  71. package/dist/__tests__/layer4-sprint-ba2.test.js +354 -0
  72. package/dist/__tests__/layer4-sprint-ba4.test.js +84 -0
  73. package/dist/__tests__/layer4-sprint-vs.test.js +105 -0
  74. package/dist/__tests__/ledger-chains.test.js +162 -0
  75. package/dist/__tests__/lifecycle-machine.test.js +226 -0
  76. package/dist/__tests__/local-chat-provider.test.js +170 -0
  77. package/dist/__tests__/local-convention-detector.test.js +308 -0
  78. package/dist/__tests__/local-embeddings.test.js +422 -0
  79. package/dist/__tests__/local-graph.test.js +540 -0
  80. package/dist/__tests__/local-indexer.test.js +228 -0
  81. package/dist/__tests__/local-intelligence-l3.test.js +332 -0
  82. package/dist/__tests__/local-llm.test.js +253 -0
  83. package/dist/__tests__/local-mode-offline.test.js +187 -0
  84. package/dist/__tests__/local-mode-stats.test.js +273 -0
  85. package/dist/__tests__/local-mode-tui.test.js +343 -0
  86. package/dist/__tests__/local-parse.test.js +199 -0
  87. package/dist/__tests__/log-tailer.test.js +208 -0
  88. package/dist/__tests__/loop-breaker.test.js +276 -0
  89. package/dist/__tests__/loop-miner.test.js +226 -0
  90. package/dist/__tests__/mcp-config.test.js +126 -0
  91. package/dist/__tests__/mcp-content-json.test.js +10 -0
  92. package/dist/__tests__/mcp-envelope.test.js +124 -0
  93. package/dist/__tests__/metrics-store.test.js +223 -0
  94. package/dist/__tests__/native-watcher.test.js +191 -0
  95. package/dist/__tests__/navigation-hooks-agent-aware.test.js +145 -0
  96. package/dist/__tests__/negative-knowledge.test.js +116 -0
  97. package/dist/__tests__/network-boundary.test.js +190 -0
  98. package/dist/__tests__/network-firewall.test.js +112 -0
  99. package/dist/__tests__/nudge-invariants.test.js +160 -0
  100. package/dist/__tests__/nudge-v2.test.js +225 -0
  101. package/dist/__tests__/offline-rewind.test.js +251 -0
  102. package/dist/__tests__/open-threads.test.js +89 -0
  103. package/dist/__tests__/output-compressor.test.js +93 -0
  104. package/dist/__tests__/pending-violations.test.js +112 -0
  105. package/dist/__tests__/persistence-effectiveness.test.js +143 -0
  106. package/dist/__tests__/provider-factory.test.js +42 -0
  107. package/dist/__tests__/providers.test.js +24 -0
  108. package/dist/__tests__/proxy.test.js +314 -0
  109. package/dist/__tests__/query-router.test.js +1018 -0
  110. package/dist/__tests__/reasoning-quality-route.test.js +138 -0
  111. package/dist/__tests__/redactor.test.js +120 -0
  112. package/dist/__tests__/resource-monitor.test.js +57 -0
  113. package/dist/__tests__/response-envelope.test.js +100 -0
  114. package/dist/__tests__/risk-classifier.test.js +101 -0
  115. package/dist/__tests__/risk-signal-scope.test.js +75 -0
  116. package/dist/__tests__/rule-evaluator.test.js +280 -0
  117. package/dist/__tests__/scip-decoder.test.js +49 -0
  118. package/dist/__tests__/scip-downloader.test.js +201 -0
  119. package/dist/__tests__/scip-merger.test.js +103 -0
  120. package/dist/__tests__/search-index.test.js +422 -0
  121. package/dist/__tests__/semantic-enrichment.test.js +360 -0
  122. package/dist/__tests__/session-brief-builder.test.js +187 -0
  123. package/dist/__tests__/session-context.test.js +221 -0
  124. package/dist/__tests__/session-continuity.test.js +144 -0
  125. package/dist/__tests__/session-dedup.test.js +74 -0
  126. package/dist/__tests__/session-event-wiring.test.js +206 -0
  127. package/dist/__tests__/session-events.test.js +149 -0
  128. package/dist/__tests__/session-legend.test.js +20 -0
  129. package/dist/__tests__/session-persistence.test.js +131 -0
  130. package/dist/__tests__/session-resume-block.test.js +107 -0
  131. package/dist/__tests__/session-resume.test.js +97 -0
  132. package/dist/__tests__/session-summary-writer.test.js +134 -0
  133. package/dist/__tests__/shadow-ledger.test.js +203 -0
  134. package/dist/__tests__/shell-classifier.test.js +151 -0
  135. package/dist/__tests__/shell-compression-floor.test.js +189 -0
  136. package/dist/__tests__/shell-compression-v2.test.js +339 -0
  137. package/dist/__tests__/shell-compressor.test.js +35 -0
  138. package/dist/__tests__/shell-hooks.test.js +128 -0
  139. package/dist/__tests__/shell-strategies.test.js +644 -0
  140. package/dist/__tests__/shell-tee.test.js +133 -0
  141. package/dist/__tests__/signal-dedup.test.js +158 -0
  142. package/dist/__tests__/signal-reinforcer.test.js +77 -0
  143. package/dist/__tests__/signal-scorer.test.js +251 -0
  144. package/dist/__tests__/signal-show-store.test.js +108 -0
  145. package/dist/__tests__/smart-truncate.test.js +215 -0
  146. package/dist/__tests__/snapshot-v2.test.js +113 -0
  147. package/dist/__tests__/sprint-l1-local-mode.test.js +130 -0
  148. package/dist/__tests__/sprint-l10-boot.test.js +220 -0
  149. package/dist/__tests__/sprint-l9-offline-commands.test.js +189 -0
  150. package/dist/__tests__/sprint-q-persistent-context.test.js +198 -0
  151. package/dist/__tests__/sprint-s1-wiring.test.js +215 -0
  152. package/dist/__tests__/sprint-s2-wiring.test.js +256 -0
  153. package/dist/__tests__/sprint-s3-wiring.test.js +195 -0
  154. package/dist/__tests__/sprint-s4-wiring.test.js +213 -0
  155. package/dist/__tests__/sprint-s6-hooks.test.js +222 -0
  156. package/dist/__tests__/sprint-s7-persistent.test.js +263 -0
  157. package/dist/__tests__/sprint-s8-value.test.js +167 -0
  158. package/dist/__tests__/sprint-s9-behavioral.test.js +179 -0
  159. package/dist/__tests__/sprint3-intelligence.test.js +297 -0
  160. package/dist/__tests__/sprint5-mcp-server.test.js +136 -0
  161. package/dist/__tests__/startup-display.test.js +302 -0
  162. package/dist/__tests__/startup-log-file.test.js +97 -0
  163. package/dist/__tests__/stash-manager.test.js +229 -0
  164. package/dist/__tests__/state-detector.test.js +92 -0
  165. package/dist/__tests__/status-dashboard.test.js +142 -0
  166. package/dist/__tests__/temporal-facts.test.js +292 -0
  167. package/dist/__tests__/temporal-routes.test.js +142 -0
  168. package/dist/__tests__/test-detector.test.js +174 -0
  169. package/dist/__tests__/theme.test.js +72 -0
  170. package/dist/__tests__/timeline-agents.test.js +122 -0
  171. package/dist/__tests__/timeline-bootstrap.test.js +176 -0
  172. package/dist/__tests__/timeline-filters.test.js +193 -0
  173. package/dist/__tests__/timeline-markers.test.js +151 -0
  174. package/dist/__tests__/timeline-routes.test.js +156 -0
  175. package/dist/__tests__/timeline-store.test.js +171 -0
  176. package/dist/__tests__/token-counter.test.js +86 -0
  177. package/dist/__tests__/token-estimator.test.js +96 -0
  178. package/dist/__tests__/token-flow-api.test.js +239 -0
  179. package/dist/__tests__/token-flow-instrumentation.test.js +437 -0
  180. package/dist/__tests__/token-flow-persistence.test.js +356 -0
  181. package/dist/__tests__/token-flow-routes.test.js +199 -0
  182. package/dist/__tests__/token-flow.test.js +695 -0
  183. package/dist/__tests__/tool-clusters.test.js +177 -0
  184. package/dist/__tests__/transport-mux.test.js +283 -0
  185. package/dist/__tests__/turn-segmenter.test.js +166 -0
  186. package/dist/__tests__/uninstall.test.js +141 -0
  187. package/dist/__tests__/warm-start-policy.test.js +271 -0
  188. package/dist/__tests__/wire-cap-nudge.test.js +77 -0
  189. package/dist/__tests__/worker-pool.test.js +101 -0
  190. package/dist/behaviors/agent-llm-bridge.js +166 -0
  191. package/dist/behaviors/architecture-guard.js +256 -0
  192. package/dist/behaviors/auto-doc.js +247 -0
  193. package/dist/behaviors/cascade-guard.js +289 -0
  194. package/dist/behaviors/change-narrative.js +270 -0
  195. package/dist/behaviors/convention-drift.js +290 -0
  196. package/dist/behaviors/framework.js +235 -0
  197. package/dist/behaviors/guard-formatter.js +44 -0
  198. package/dist/behaviors/incomplete-work.js +270 -0
  199. package/dist/behaviors/loop-breaker.js +300 -0
  200. package/dist/behaviors/session-continuity.js +208 -0
  201. package/dist/cli.js +996 -710
  202. package/dist/commands/branches.js +97 -0
  203. package/dist/commands/check-commit.js +225 -0
  204. package/dist/commands/compress-output.js +64 -0
  205. package/dist/commands/config-verify.js +243 -0
  206. package/dist/commands/daemon.js +905 -0
  207. package/dist/commands/dashboard.js +52 -0
  208. package/dist/commands/debug.js +200 -0
  209. package/dist/commands/enrich.js +184 -0
  210. package/dist/commands/exec.js +233 -0
  211. package/dist/commands/gain.js +156 -0
  212. package/dist/commands/hook.js +88 -0
  213. package/dist/commands/index.js +88 -0
  214. package/dist/commands/init.js +74 -0
  215. package/dist/commands/install.js +505 -0
  216. package/dist/commands/learn.js +116 -0
  217. package/dist/commands/manifest.js +193 -0
  218. package/dist/commands/rewind.js +103 -0
  219. package/dist/commands/serve.js +19 -0
  220. package/dist/commands/setup-wizard.js +414 -0
  221. package/dist/commands/skills.js +64 -0
  222. package/dist/commands/stats.js +20 -0
  223. package/dist/commands/status.js +654 -0
  224. package/dist/commands/timeline.js +139 -0
  225. package/dist/commands/uninstall.js +230 -0
  226. package/dist/components/App.js +109 -0
  227. package/dist/components/Banner.js +12 -0
  228. package/dist/components/ConfirmPrompt.js +25 -0
  229. package/dist/components/DriftSummary.js +23 -0
  230. package/dist/components/GradeBadge.js +15 -0
  231. package/dist/components/HealthCard.js +18 -0
  232. package/dist/components/InkSpinner.js +22 -0
  233. package/dist/components/InputBox.js +17 -0
  234. package/dist/components/KeyValue.js +13 -0
  235. package/dist/components/MessageList.js +14 -0
  236. package/dist/components/ProgressBar.js +26 -0
  237. package/dist/components/Section.js +16 -0
  238. package/dist/components/SessionSummaryCard.js +73 -0
  239. package/dist/components/StartupDisplay.js +24 -0
  240. package/dist/components/StatusDashboard.js +57 -0
  241. package/dist/components/StatusLine.js +8 -0
  242. package/dist/components/StepLine.js +22 -0
  243. package/dist/components/Theme.js +20 -0
  244. package/dist/components/ToolProgress.js +8 -0
  245. package/dist/components/ViolationList.js +21 -0
  246. package/dist/components/render.js +13 -0
  247. package/dist/config/agent-registry.js +237 -0
  248. package/dist/config/claude-settings-hooks.js +304 -0
  249. package/dist/config/hook-installer.js +65 -0
  250. package/dist/config/instruction-writer.js +388 -0
  251. package/dist/config/mcp-config-writer.js +266 -0
  252. package/dist/config/settings.js +174 -0
  253. package/dist/config/tool-detector.js +42 -0
  254. package/dist/config/value-surfacing.js +119 -0
  255. package/dist/core/context-assembly.js +108 -0
  256. package/dist/core/conversation.js +33 -0
  257. package/dist/core/local-chat-provider.js +475 -0
  258. package/dist/core/provider-factory.js +55 -0
  259. package/dist/core/providers.js +90 -0
  260. package/dist/core/query-engine.js +174 -0
  261. package/dist/daemon/api.js +312 -0
  262. package/dist/daemon/autostart.js +119 -0
  263. package/dist/daemon/bootstrap.js +39 -0
  264. package/dist/daemon/client.js +164 -0
  265. package/dist/daemon/detect-ci.js +81 -0
  266. package/dist/daemon/platform-linux.js +146 -0
  267. package/dist/daemon/platform-macos.js +134 -0
  268. package/dist/daemon/platform-windows.js +116 -0
  269. package/dist/daemon/process-manager.js +299 -0
  270. package/dist/daemon/protocol.js +23 -0
  271. package/dist/daemon/registry.js +270 -0
  272. package/dist/daemon/settings-schema.js +72 -0
  273. package/dist/daemon/system-health.js +134 -0
  274. package/dist/daemon/version-checker.js +262 -0
  275. package/dist/daemon/warm-start.js +223 -0
  276. package/dist/entrypoints/cli.js +1043 -0
  277. package/dist/entrypoints/daemon.js +380 -0
  278. package/dist/entrypoints/repl.js +147 -0
  279. package/dist/hooks/adapters/claude-code.js +90 -0
  280. package/dist/hooks/adapters/cline.js +100 -0
  281. package/dist/hooks/adapters/cursor.js +98 -0
  282. package/dist/hooks/hook-dedup.js +79 -0
  283. package/dist/hooks/hook-runner.js +113 -0
  284. package/dist/hooks/navigation-hooks.js +175 -0
  285. package/dist/hooks/prompt-hooks.js +63 -0
  286. package/dist/hooks/shell-hooks.js +47 -0
  287. package/dist/ignore.js +111 -0
  288. package/dist/intelligence/approach-suggester.js +61 -0
  289. package/dist/intelligence/ast-extractor.js +2615 -0
  290. package/dist/intelligence/ast-worker.js +34 -0
  291. package/dist/intelligence/background-indexer.js +121 -0
  292. package/dist/intelligence/blast-radius.js +200 -0
  293. package/dist/intelligence/community-detection.js +691 -0
  294. package/dist/intelligence/community-detector.js +184 -0
  295. package/dist/intelligence/computation-scheduler.js +75 -0
  296. package/dist/intelligence/confidence-propagation.js +47 -0
  297. package/dist/intelligence/convention-detector.js +242 -0
  298. package/dist/intelligence/convention-learner.js +205 -0
  299. package/dist/intelligence/convention-matcher.js +205 -0
  300. package/dist/intelligence/cozo-schema.js +376 -0
  301. package/dist/intelligence/decision-point-detector.js +90 -0
  302. package/dist/intelligence/deep-dive-tools.js +586 -0
  303. package/dist/intelligence/durability-scorer.js +84 -0
  304. package/dist/intelligence/exploration-cost.js +204 -0
  305. package/dist/intelligence/exploration-pattern-tracker.js +61 -0
  306. package/dist/intelligence/fact-generator.js +322 -0
  307. package/dist/intelligence/facts-schema.js +90 -0
  308. package/dist/intelligence/file-intelligence.js +59 -0
  309. package/dist/intelligence/graph-holder.js +220 -0
  310. package/dist/intelligence/graph-temporal-joiner.js +238 -0
  311. package/dist/intelligence/health-grade.js +423 -0
  312. package/dist/intelligence/health-grader.js +200 -0
  313. package/dist/intelligence/health-map-data.js +259 -0
  314. package/dist/intelligence/import-symbols.js +136 -0
  315. package/dist/intelligence/incremental-indexer.js +658 -0
  316. package/dist/intelligence/indexer/centrality.js +62 -0
  317. package/dist/intelligence/indexer/cfg-context.js +95 -0
  318. package/dist/intelligence/indexer/confidence.js +34 -0
  319. package/dist/intelligence/indexer/cross-file-resolver.js +104 -0
  320. package/dist/intelligence/indexer/edge-repair.js +89 -0
  321. package/dist/intelligence/indexer/entity-key.js +17 -0
  322. package/dist/intelligence/indexer/export-map.js +132 -0
  323. package/dist/intelligence/indexer/git-cochange.js +128 -0
  324. package/dist/intelligence/indexer/graph-patch.js +147 -0
  325. package/dist/intelligence/indexer/incremental.js +78 -0
  326. package/dist/intelligence/indexer/ingest.js +160 -0
  327. package/dist/intelligence/indexer/language-detect.js +226 -0
  328. package/dist/intelligence/indexer/metadata.js +63 -0
  329. package/dist/intelligence/indexer/mutation-tracker.js +79 -0
  330. package/dist/intelligence/indexer/orchestrator.js +155 -0
  331. package/dist/intelligence/indexer/plugin-interface.js +31 -0
  332. package/dist/intelligence/indexer/plugins/csharp.js +440 -0
  333. package/dist/intelligence/indexer/plugins/go.js +335 -0
  334. package/dist/intelligence/indexer/plugins/java.js +370 -0
  335. package/dist/intelligence/indexer/plugins/python.js +358 -0
  336. package/dist/intelligence/indexer/plugins/regex-fallback.js +82 -0
  337. package/dist/intelligence/indexer/plugins/ruby.js +290 -0
  338. package/dist/intelligence/indexer/plugins/rust.js +484 -0
  339. package/dist/intelligence/indexer/plugins/tier2-generic.js +310 -0
  340. package/dist/intelligence/indexer/plugins/typescript.js +456 -0
  341. package/dist/intelligence/indexer/resource-monitor.js +93 -0
  342. package/dist/intelligence/indexer/scip/decoder.js +253 -0
  343. package/dist/intelligence/indexer/scip/detector.js +232 -0
  344. package/dist/intelligence/indexer/scip/downloader.js +427 -0
  345. package/dist/intelligence/indexer/scip/fallback.js +34 -0
  346. package/dist/intelligence/indexer/scip/merger.js +109 -0
  347. package/dist/intelligence/indexer/scip/orchestrator.js +433 -0
  348. package/dist/intelligence/indexer/scip/runner.js +98 -0
  349. package/dist/intelligence/indexer/snapshot.js +66 -0
  350. package/dist/intelligence/indexer/test-detector.js +196 -0
  351. package/dist/intelligence/indexer/watch-integration.js +61 -0
  352. package/dist/intelligence/indexer/worker.js +85 -0
  353. package/dist/intelligence/local-convention-detector.js +437 -0
  354. package/dist/intelligence/local-embeddings.js +190 -0
  355. package/dist/intelligence/local-graph.js +1946 -0
  356. package/dist/intelligence/local-indexer.js +1575 -0
  357. package/dist/intelligence/local-llm.js +163 -0
  358. package/dist/intelligence/local-rule-generator.js +154 -0
  359. package/dist/intelligence/local-snapshot.js +213 -0
  360. package/dist/intelligence/negative-knowledge.js +103 -0
  361. package/dist/intelligence/persistent-db.js +85 -0
  362. package/dist/intelligence/query-router.js +2556 -0
  363. package/dist/intelligence/risk-classifier.js +116 -0
  364. package/dist/intelligence/rule-evaluator.js +380 -0
  365. package/dist/intelligence/rule-generator.js +49 -0
  366. package/dist/intelligence/search-index.js +173 -0
  367. package/dist/intelligence/semantic/docstring-extractor.js +67 -0
  368. package/dist/intelligence/semantic/embedding-store.js +52 -0
  369. package/dist/intelligence/semantic/enrichment-orchestrator.js +48 -0
  370. package/dist/intelligence/semantic/git-message-miner.js +114 -0
  371. package/dist/intelligence/semantic/identifier-tokenizer.js +51 -0
  372. package/dist/intelligence/semantic/node2vec-embeddings.js +71 -0
  373. package/dist/intelligence/semantic/node2vec-walks.js +103 -0
  374. package/dist/intelligence/semantic/path-domain-inference.js +112 -0
  375. package/dist/intelligence/semantic/similarity-engine.js +60 -0
  376. package/dist/intelligence/semantic/tfidf-vectors.js +88 -0
  377. package/dist/intelligence/session-brief-builder.js +159 -0
  378. package/dist/intelligence/session-context.js +221 -0
  379. package/dist/intelligence/session-health-monitor.js +211 -0
  380. package/dist/intelligence/session-narrative.js +197 -0
  381. package/dist/intelligence/session-pattern-analyzer.js +218 -0
  382. package/dist/intelligence/signal-scorer.js +390 -0
  383. package/dist/intelligence/signal-show-store.js +182 -0
  384. package/dist/intelligence/smart-truncate.js +158 -0
  385. package/dist/intelligence/subgraph-cache.js +88 -0
  386. package/dist/intelligence/temporal-facts.js +494 -0
  387. package/dist/intelligence/token-estimator.js +100 -0
  388. package/dist/intelligence/tool-injector.js +87 -0
  389. package/dist/intelligence/tree-sitter-loader.js +71 -0
  390. package/dist/intelligence/worker-pool.js +116 -0
  391. package/dist/proxy/arg-validator.js +79 -0
  392. package/dist/proxy/auto-bootstrap.js +167 -0
  393. package/dist/proxy/bridge.js +147 -0
  394. package/dist/proxy/budget-enforcer.js +70 -0
  395. package/dist/proxy/compression-quality-monitor.js +160 -0
  396. package/dist/proxy/compression-stats.js +51 -0
  397. package/dist/proxy/context-rot-detector.js +137 -0
  398. package/dist/proxy/drift-detector.js +139 -0
  399. package/dist/proxy/efficiency-tracker.js +79 -0
  400. package/dist/proxy/fact-ranking.js +154 -0
  401. package/dist/proxy/format-encoder.js +266 -0
  402. package/dist/proxy/http-transport.js +90 -0
  403. package/dist/proxy/lifecycle-actor.js +55 -0
  404. package/dist/proxy/lifecycle-machine.js +187 -0
  405. package/dist/proxy/log-tailer.js +265 -0
  406. package/dist/proxy/model-pricing.js +98 -0
  407. package/dist/proxy/network-firewall.js +141 -0
  408. package/dist/proxy/nudge-state.js +93 -0
  409. package/dist/proxy/output-compressor.js +185 -0
  410. package/dist/proxy/pid-lock.js +291 -0
  411. package/dist/proxy/proxy-context.js +11 -0
  412. package/dist/proxy/proxy.js +2633 -0
  413. package/dist/proxy/response-enrichment.js +32 -0
  414. package/dist/proxy/response-envelope.js +313 -0
  415. package/dist/proxy/session-dedup.js +82 -0
  416. package/dist/proxy/session-legend.js +30 -0
  417. package/dist/proxy/session-persistence.js +210 -0
  418. package/dist/proxy/session-resume.js +94 -0
  419. package/dist/proxy/session-stats.js +513 -0
  420. package/dist/proxy/shell-classifier.js +1346 -0
  421. package/dist/proxy/shell-compression-log.js +93 -0
  422. package/dist/proxy/shell-compressor.js +390 -0
  423. package/dist/proxy/shell-graph-boost.js +202 -0
  424. package/dist/proxy/shell-monitor-map.js +18 -0
  425. package/dist/proxy/shell-stats.js +54 -0
  426. package/dist/proxy/shell-strategies/cloud.js +215 -0
  427. package/dist/proxy/shell-strategies/diff.js +159 -0
  428. package/dist/proxy/shell-strategies/error-diagnostic.js +796 -0
  429. package/dist/proxy/shell-strategies/filter-dsl.js +358 -0
  430. package/dist/proxy/shell-strategies/git-status.js +177 -0
  431. package/dist/proxy/shell-strategies/key-value.js +193 -0
  432. package/dist/proxy/shell-strategies/log-text.js +154 -0
  433. package/dist/proxy/shell-strategies/omni.js +188 -0
  434. package/dist/proxy/shell-strategies/progress.js +55 -0
  435. package/dist/proxy/shell-strategies/redact.js +76 -0
  436. package/dist/proxy/shell-strategies/structured.js +241 -0
  437. package/dist/proxy/shell-strategies/tabular.js +243 -0
  438. package/dist/proxy/shell-strategies/test-results-types.js +13 -0
  439. package/dist/proxy/shell-strategies/test-results.js +784 -0
  440. package/dist/proxy/shell-strategies/tree-paths.js +144 -0
  441. package/dist/proxy/shell-strategies/yaml.js +182 -0
  442. package/dist/proxy/shell-tee.js +111 -0
  443. package/dist/proxy/signal-dedup.js +171 -0
  444. package/dist/proxy/startup-renderer.js +158 -0
  445. package/dist/proxy/task-token-display.js +38 -0
  446. package/dist/proxy/token-counter.js +61 -0
  447. package/dist/proxy/tool-clusters.js +273 -0
  448. package/dist/proxy/tool-definitions.js +525 -0
  449. package/dist/proxy/transport-mux.js +229 -0
  450. package/dist/proxy/wire-cap.js +268 -0
  451. package/dist/schemas/api/skills.js +19 -0
  452. package/dist/schemas/common/errors.js +7 -0
  453. package/dist/schemas/common/headers.js +5 -0
  454. package/dist/schemas/entities/edge.js +25 -0
  455. package/dist/schemas/entities/entity.js +22 -0
  456. package/dist/schemas/entities/rule.js +18 -0
  457. package/dist/schemas/index.js +14 -0
  458. package/dist/server/event-bus.js +59 -0
  459. package/dist/server/http.js +156 -0
  460. package/dist/server/middleware.js +70 -0
  461. package/dist/server/routes/drift.js +97 -0
  462. package/dist/server/routes/intelligence.js +1217 -0
  463. package/dist/server/routes/reasoning-quality.js +444 -0
  464. package/dist/server/routes/session.js +86 -0
  465. package/dist/server/routes/stream.js +120 -0
  466. package/dist/server/routes/system.js +73 -0
  467. package/dist/server/routes/temporal.js +170 -0
  468. package/dist/server/routes/timeline.js +232 -0
  469. package/dist/server/routes/token-flow.js +403 -0
  470. package/dist/skills/effectiveness-tracker.js +93 -0
  471. package/dist/skills/local-pack.js +380 -0
  472. package/dist/skills/resolver.js +495 -0
  473. package/dist/state-detector.js +83 -0
  474. package/dist/timeline/intent-detector.js +263 -0
  475. package/dist/timeline/loop-miner.js +140 -0
  476. package/dist/timeline/open-threads.js +49 -0
  477. package/dist/timeline/signal-reinforcer.js +62 -0
  478. package/dist/timeline/timeline-bootstrap.js +151 -0
  479. package/dist/timeline/timeline-store.js +618 -0
  480. package/dist/tools/coding/bash.js +49 -0
  481. package/dist/tools/coding/file-edit.js +72 -0
  482. package/dist/tools/coding/file-outline.js +227 -0
  483. package/dist/tools/coding/file-read-protocol.js +425 -0
  484. package/dist/tools/coding/file-read.js +35 -0
  485. package/dist/tools/coding/file-write.js +43 -0
  486. package/dist/tools/coding/glob-tool.js +109 -0
  487. package/dist/tools/coding/grep.js +162 -0
  488. package/dist/tools/coding/index.js +27 -0
  489. package/dist/tools/intelligence/index.js +269 -0
  490. package/dist/tools/intelligence/record-fact.js +48 -0
  491. package/dist/tools/intelligence/timeline-markers.js +130 -0
  492. package/dist/tools/registry.js +47 -0
  493. package/dist/tools/types.js +8 -0
  494. package/dist/tracking/auto-snapshot-triggers.js +246 -0
  495. package/dist/tracking/branch-context.js +115 -0
  496. package/dist/tracking/branch-snapshot.js +217 -0
  497. package/dist/tracking/causal-bridge.js +317 -0
  498. package/dist/tracking/circuit-breaker.js +147 -0
  499. package/dist/tracking/commit-watcher.js +114 -0
  500. package/dist/tracking/context-ledger.js +119 -0
  501. package/dist/tracking/correction-detector.js +324 -0
  502. package/dist/tracking/drift-tracker.js +874 -0
  503. package/dist/tracking/durability-tracker.js +94 -0
  504. package/dist/tracking/entity-rewind.js +200 -0
  505. package/dist/tracking/file-hash-state.js +114 -0
  506. package/dist/tracking/git-attribution.js +132 -0
  507. package/dist/tracking/git-trailers.js +171 -0
  508. package/dist/tracking/intelligence-counter.js +46 -0
  509. package/dist/tracking/intent-correlator.js +202 -0
  510. package/dist/tracking/intent-encoder.js +52 -0
  511. package/dist/tracking/intent-token-tracker.js +159 -0
  512. package/dist/tracking/ledger-archiver.js +94 -0
  513. package/dist/tracking/ledger-chains.js +245 -0
  514. package/dist/tracking/metrics-store.js +361 -0
  515. package/dist/tracking/native-watcher.js +131 -0
  516. package/dist/tracking/offline-rewind.js +295 -0
  517. package/dist/tracking/pending-violations.js +74 -0
  518. package/dist/tracking/persistence-effectiveness.js +167 -0
  519. package/dist/tracking/prompt-durability.js +202 -0
  520. package/dist/tracking/quality-signals.js +213 -0
  521. package/dist/tracking/redactor.js +73 -0
  522. package/dist/tracking/rewind-engine.js +161 -0
  523. package/dist/tracking/session-history.js +128 -0
  524. package/dist/tracking/session-receipt.js +88 -0
  525. package/dist/tracking/session-summary-writer.js +157 -0
  526. package/dist/tracking/shadow-ledger.js +321 -0
  527. package/dist/tracking/stash-manager.js +258 -0
  528. package/dist/tracking/timeline-fork.js +213 -0
  529. package/dist/tracking/timeline.js +69 -0
  530. package/dist/tracking/token-flow.js +276 -0
  531. package/dist/tracking/turn-segmenter.js +122 -0
  532. package/dist/tracking/weekly-accumulator.js +179 -0
  533. package/dist/tracking/working-snapshots.js +188 -0
  534. package/dist/tracking/workspace-manifest.js +176 -0
  535. package/dist/transport/http.js +102 -0
  536. package/dist/ui/assets/index-BsMTQdhX.js +10 -0
  537. package/dist/ui/index.html +1 -1
  538. package/dist/utils/counterfactual.js +65 -0
  539. package/dist/utils/deep-link.js +34 -0
  540. package/dist/utils/detect.js +193 -0
  541. package/dist/utils/exec.js +73 -0
  542. package/dist/utils/file-logger.js +87 -0
  543. package/dist/utils/format-error.js +29 -0
  544. package/dist/utils/git.js +181 -0
  545. package/dist/utils/log.js +57 -0
  546. package/dist/utils/logger.js +35 -0
  547. package/dist/utils/mcp-content-json.js +8 -0
  548. package/dist/utils/session-logger.js +154 -0
  549. package/dist/utils/startup-log.js +512 -0
  550. package/dist/utils/ui.js +56 -0
  551. package/package.json +5 -3
  552. package/scripts/postinstall.mjs +312 -0
  553. package/dist/ui/assets/index-B-0HTtUR.js +0 -10
@@ -0,0 +1,1217 @@
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
+ }