@unerr-ai/unerr 0.0.1 → 0.1.1

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 (556) hide show
  1. package/README.md +215 -35
  2. package/dist/__tests__/architecture-guard.test.js +122 -0
  3. package/dist/__tests__/arg-validator.test.js +205 -0
  4. package/dist/__tests__/ast-extractor.test.js +203 -0
  5. package/dist/__tests__/auto-bootstrap.test.js +280 -0
  6. package/dist/__tests__/background-indexer.test.js +228 -0
  7. package/dist/__tests__/blast-radius-engine.test.js +200 -0
  8. package/dist/__tests__/bridge-isolation.test.js +37 -0
  9. package/dist/__tests__/budget-enforcer.test.js +53 -0
  10. package/dist/__tests__/cfg-test-detection-perf.test.js +82 -0
  11. package/dist/__tests__/change-narrative.test.js +190 -0
  12. package/dist/__tests__/check-commit.test.js +258 -0
  13. package/dist/__tests__/checksum.test.js +34 -0
  14. package/dist/__tests__/commit-watcher.test.js +154 -0
  15. package/dist/__tests__/community-detection.test.js +179 -0
  16. package/dist/__tests__/community-tools.test.js +299 -0
  17. package/dist/__tests__/components.test.js +449 -0
  18. package/dist/__tests__/compression-log.test.js +174 -0
  19. package/dist/__tests__/compression-quality-monitor.test.js +40 -0
  20. package/dist/__tests__/config-healer.test.js +165 -0
  21. package/dist/__tests__/context-ledger.test.js +58 -0
  22. package/dist/__tests__/convention-detector.test.js +99 -0
  23. package/dist/__tests__/convention-learner.test.js +86 -0
  24. package/dist/__tests__/correction-detector.test.js +330 -0
  25. package/dist/__tests__/daemon-autostart-install.test.js +283 -0
  26. package/dist/__tests__/daemon-bridge.test.js +222 -0
  27. package/dist/__tests__/daemon-dashboard.test.js +202 -0
  28. package/dist/__tests__/daemon-registry.test.js +240 -0
  29. package/dist/__tests__/daemon-supervisor.test.js +318 -0
  30. package/dist/__tests__/daemon-version-check.test.js +275 -0
  31. package/dist/__tests__/decision-point-detector.test.js +98 -0
  32. package/dist/__tests__/deep-link.test.js +143 -0
  33. package/dist/__tests__/disallowed-tools.test.js +115 -0
  34. package/dist/__tests__/drift-tracker.test.js +582 -0
  35. package/dist/__tests__/durability-scorer.test.js +152 -0
  36. package/dist/__tests__/efficiency-tracker.test.js +65 -0
  37. package/dist/__tests__/enrich.test.js +144 -0
  38. package/dist/__tests__/entity-rewind.test.js +248 -0
  39. package/dist/__tests__/ephemeral.test.js +111 -0
  40. package/dist/__tests__/exploration-cost.test.js +93 -0
  41. package/dist/__tests__/fact-generator.test.js +197 -0
  42. package/dist/__tests__/file-l0-graph.test.js +244 -0
  43. package/dist/__tests__/file-logger.test.js +82 -0
  44. package/dist/__tests__/file-outline.test.js +141 -0
  45. package/dist/__tests__/file-read-protocol.test.js +188 -0
  46. package/dist/__tests__/format-encoder.test.js +233 -0
  47. package/dist/__tests__/git-attribution.test.js +259 -0
  48. package/dist/__tests__/graph-temporal-joiner.test.js +219 -0
  49. package/dist/__tests__/health-grade-enhanced.test.js +138 -0
  50. package/dist/__tests__/health-map-data.test.js +173 -0
  51. package/dist/__tests__/helpers/mcp-harness.js +45 -0
  52. package/dist/__tests__/helpers/mcp-harness.test.js +68 -0
  53. package/dist/__tests__/hook-dedup.test.js +112 -0
  54. package/dist/__tests__/hook-runner.test.js +253 -0
  55. package/dist/__tests__/indexer-cfg.test.js +185 -0
  56. package/dist/__tests__/indexer-cross-file.test.js +172 -0
  57. package/dist/__tests__/indexer-extraction.test.js +245 -0
  58. package/dist/__tests__/indexer-incremental.test.js +232 -0
  59. package/dist/__tests__/indexer-language-expansion.test.js +165 -0
  60. package/dist/__tests__/init-push.test.js +131 -0
  61. package/dist/__tests__/instruction-writer.test.js +179 -0
  62. package/dist/__tests__/intelligence-integration.test.js +217 -0
  63. package/dist/__tests__/intent-correlator.test.js +175 -0
  64. package/dist/__tests__/intent-detector.test.js +235 -0
  65. package/dist/__tests__/intent-encoder.test.js +167 -0
  66. package/dist/__tests__/java-build-tool-detection.test.js +174 -0
  67. package/dist/__tests__/layer3-sprint-q.test.js +160 -0
  68. package/dist/__tests__/layer3-sprint-r.test.js +91 -0
  69. package/dist/__tests__/layer3-sprint-s.test.js +183 -0
  70. package/dist/__tests__/layer3-sprint-t.test.js +201 -0
  71. package/dist/__tests__/layer3-sprint-u.test.js +174 -0
  72. package/dist/__tests__/layer4-sprint-ba2.test.js +354 -0
  73. package/dist/__tests__/layer4-sprint-ba4.test.js +84 -0
  74. package/dist/__tests__/layer4-sprint-vs.test.js +105 -0
  75. package/dist/__tests__/ledger-chains.test.js +162 -0
  76. package/dist/__tests__/lifecycle-machine.test.js +226 -0
  77. package/dist/__tests__/local-chat-provider.test.js +170 -0
  78. package/dist/__tests__/local-convention-detector.test.js +308 -0
  79. package/dist/__tests__/local-embeddings.test.js +422 -0
  80. package/dist/__tests__/local-graph.test.js +540 -0
  81. package/dist/__tests__/local-indexer.test.js +228 -0
  82. package/dist/__tests__/local-intelligence-l3.test.js +332 -0
  83. package/dist/__tests__/local-llm.test.js +253 -0
  84. package/dist/__tests__/local-mode-offline.test.js +187 -0
  85. package/dist/__tests__/local-mode-stats.test.js +273 -0
  86. package/dist/__tests__/local-mode-tui.test.js +343 -0
  87. package/dist/__tests__/local-parse.test.js +199 -0
  88. package/dist/__tests__/log-tailer.test.js +208 -0
  89. package/dist/__tests__/loop-breaker.test.js +276 -0
  90. package/dist/__tests__/loop-miner.test.js +226 -0
  91. package/dist/__tests__/mcp-config.test.js +126 -0
  92. package/dist/__tests__/mcp-content-json.test.js +10 -0
  93. package/dist/__tests__/mcp-envelope.test.js +124 -0
  94. package/dist/__tests__/metrics-store.test.js +223 -0
  95. package/dist/__tests__/native-watcher.test.js +191 -0
  96. package/dist/__tests__/navigation-hooks-agent-aware.test.js +145 -0
  97. package/dist/__tests__/negative-knowledge.test.js +116 -0
  98. package/dist/__tests__/network-boundary.test.js +190 -0
  99. package/dist/__tests__/network-firewall.test.js +112 -0
  100. package/dist/__tests__/nudge-invariants.test.js +160 -0
  101. package/dist/__tests__/nudge-v2.test.js +225 -0
  102. package/dist/__tests__/offline-rewind.test.js +251 -0
  103. package/dist/__tests__/open-threads.test.js +89 -0
  104. package/dist/__tests__/output-compressor.test.js +93 -0
  105. package/dist/__tests__/pending-violations.test.js +112 -0
  106. package/dist/__tests__/persistence-effectiveness.test.js +143 -0
  107. package/dist/__tests__/provider-factory.test.js +42 -0
  108. package/dist/__tests__/providers.test.js +24 -0
  109. package/dist/__tests__/proxy.test.js +314 -0
  110. package/dist/__tests__/query-router.test.js +1018 -0
  111. package/dist/__tests__/reasoning-quality-route.test.js +138 -0
  112. package/dist/__tests__/redactor.test.js +120 -0
  113. package/dist/__tests__/resource-monitor.test.js +57 -0
  114. package/dist/__tests__/response-envelope.test.js +100 -0
  115. package/dist/__tests__/risk-classifier.test.js +101 -0
  116. package/dist/__tests__/risk-signal-scope.test.js +75 -0
  117. package/dist/__tests__/rule-evaluator.test.js +280 -0
  118. package/dist/__tests__/scip-decoder.test.js +49 -0
  119. package/dist/__tests__/scip-downloader.test.js +201 -0
  120. package/dist/__tests__/scip-merger.test.js +103 -0
  121. package/dist/__tests__/search-index.test.js +422 -0
  122. package/dist/__tests__/semantic-enrichment.test.js +360 -0
  123. package/dist/__tests__/session-brief-builder.test.js +187 -0
  124. package/dist/__tests__/session-context.test.js +221 -0
  125. package/dist/__tests__/session-continuity.test.js +144 -0
  126. package/dist/__tests__/session-dedup.test.js +74 -0
  127. package/dist/__tests__/session-event-wiring.test.js +206 -0
  128. package/dist/__tests__/session-events.test.js +149 -0
  129. package/dist/__tests__/session-legend.test.js +20 -0
  130. package/dist/__tests__/session-persistence.test.js +131 -0
  131. package/dist/__tests__/session-resume-block.test.js +107 -0
  132. package/dist/__tests__/session-resume.test.js +97 -0
  133. package/dist/__tests__/session-summary-writer.test.js +134 -0
  134. package/dist/__tests__/shadow-ledger.test.js +203 -0
  135. package/dist/__tests__/shell-classifier.test.js +151 -0
  136. package/dist/__tests__/shell-compression-floor.test.js +189 -0
  137. package/dist/__tests__/shell-compression-v2.test.js +339 -0
  138. package/dist/__tests__/shell-compressor.test.js +35 -0
  139. package/dist/__tests__/shell-hooks.test.js +128 -0
  140. package/dist/__tests__/shell-strategies.test.js +644 -0
  141. package/dist/__tests__/shell-tee.test.js +133 -0
  142. package/dist/__tests__/signal-dedup.test.js +158 -0
  143. package/dist/__tests__/signal-reinforcer.test.js +77 -0
  144. package/dist/__tests__/signal-scorer.test.js +251 -0
  145. package/dist/__tests__/signal-show-store.test.js +108 -0
  146. package/dist/__tests__/smart-truncate.test.js +215 -0
  147. package/dist/__tests__/snapshot-v2.test.js +113 -0
  148. package/dist/__tests__/sprint-l1-local-mode.test.js +130 -0
  149. package/dist/__tests__/sprint-l10-boot.test.js +220 -0
  150. package/dist/__tests__/sprint-l9-offline-commands.test.js +189 -0
  151. package/dist/__tests__/sprint-q-persistent-context.test.js +198 -0
  152. package/dist/__tests__/sprint-s1-wiring.test.js +215 -0
  153. package/dist/__tests__/sprint-s2-wiring.test.js +256 -0
  154. package/dist/__tests__/sprint-s3-wiring.test.js +195 -0
  155. package/dist/__tests__/sprint-s4-wiring.test.js +213 -0
  156. package/dist/__tests__/sprint-s6-hooks.test.js +222 -0
  157. package/dist/__tests__/sprint-s7-persistent.test.js +263 -0
  158. package/dist/__tests__/sprint-s8-value.test.js +167 -0
  159. package/dist/__tests__/sprint-s9-behavioral.test.js +179 -0
  160. package/dist/__tests__/sprint3-intelligence.test.js +297 -0
  161. package/dist/__tests__/sprint5-mcp-server.test.js +136 -0
  162. package/dist/__tests__/startup-display.test.js +302 -0
  163. package/dist/__tests__/startup-log-file.test.js +97 -0
  164. package/dist/__tests__/stash-manager.test.js +229 -0
  165. package/dist/__tests__/state-detector.test.js +92 -0
  166. package/dist/__tests__/status-dashboard.test.js +142 -0
  167. package/dist/__tests__/temporal-facts.test.js +292 -0
  168. package/dist/__tests__/temporal-routes.test.js +142 -0
  169. package/dist/__tests__/test-detector.test.js +174 -0
  170. package/dist/__tests__/theme.test.js +72 -0
  171. package/dist/__tests__/timeline-agents.test.js +122 -0
  172. package/dist/__tests__/timeline-bootstrap.test.js +176 -0
  173. package/dist/__tests__/timeline-filters.test.js +193 -0
  174. package/dist/__tests__/timeline-markers.test.js +151 -0
  175. package/dist/__tests__/timeline-routes.test.js +156 -0
  176. package/dist/__tests__/timeline-store.test.js +171 -0
  177. package/dist/__tests__/token-counter.test.js +86 -0
  178. package/dist/__tests__/token-estimator.test.js +96 -0
  179. package/dist/__tests__/token-flow-api.test.js +239 -0
  180. package/dist/__tests__/token-flow-instrumentation.test.js +437 -0
  181. package/dist/__tests__/token-flow-persistence.test.js +356 -0
  182. package/dist/__tests__/token-flow-routes.test.js +199 -0
  183. package/dist/__tests__/token-flow.test.js +695 -0
  184. package/dist/__tests__/tool-clusters.test.js +177 -0
  185. package/dist/__tests__/transport-mux.test.js +283 -0
  186. package/dist/__tests__/turn-segmenter.test.js +166 -0
  187. package/dist/__tests__/uninstall.test.js +141 -0
  188. package/dist/__tests__/warm-start-policy.test.js +271 -0
  189. package/dist/__tests__/wire-cap-nudge.test.js +77 -0
  190. package/dist/__tests__/worker-pool.test.js +101 -0
  191. package/dist/behaviors/agent-llm-bridge.js +166 -0
  192. package/dist/behaviors/architecture-guard.js +256 -0
  193. package/dist/behaviors/auto-doc.js +247 -0
  194. package/dist/behaviors/cascade-guard.js +289 -0
  195. package/dist/behaviors/change-narrative.js +270 -0
  196. package/dist/behaviors/convention-drift.js +290 -0
  197. package/dist/behaviors/framework.js +235 -0
  198. package/dist/behaviors/guard-formatter.js +44 -0
  199. package/dist/behaviors/incomplete-work.js +270 -0
  200. package/dist/behaviors/loop-breaker.js +300 -0
  201. package/dist/behaviors/session-continuity.js +208 -0
  202. package/dist/cli.js +6446 -2227
  203. package/dist/commands/branches.js +97 -0
  204. package/dist/commands/check-commit.js +225 -0
  205. package/dist/commands/compress-output.js +64 -0
  206. package/dist/commands/config-verify.js +243 -0
  207. package/dist/commands/daemon.js +905 -0
  208. package/dist/commands/dashboard.js +52 -0
  209. package/dist/commands/debug.js +200 -0
  210. package/dist/commands/enrich.js +184 -0
  211. package/dist/commands/exec.js +233 -0
  212. package/dist/commands/gain.js +156 -0
  213. package/dist/commands/hook.js +88 -0
  214. package/dist/commands/index.js +88 -0
  215. package/dist/commands/init.js +74 -0
  216. package/dist/commands/install.js +505 -0
  217. package/dist/commands/learn.js +116 -0
  218. package/dist/commands/manifest.js +193 -0
  219. package/dist/commands/rewind.js +103 -0
  220. package/dist/commands/serve.js +19 -0
  221. package/dist/commands/setup-wizard.js +414 -0
  222. package/dist/commands/skills.js +64 -0
  223. package/dist/commands/stats.js +20 -0
  224. package/dist/commands/status.js +654 -0
  225. package/dist/commands/timeline.js +139 -0
  226. package/dist/commands/uninstall.js +230 -0
  227. package/dist/components/App.js +109 -0
  228. package/dist/components/Banner.js +12 -0
  229. package/dist/components/ConfirmPrompt.js +25 -0
  230. package/dist/components/DriftSummary.js +23 -0
  231. package/dist/components/GradeBadge.js +15 -0
  232. package/dist/components/HealthCard.js +18 -0
  233. package/dist/components/InkSpinner.js +22 -0
  234. package/dist/components/InputBox.js +17 -0
  235. package/dist/components/KeyValue.js +13 -0
  236. package/dist/components/MessageList.js +14 -0
  237. package/dist/components/ProgressBar.js +26 -0
  238. package/dist/components/Section.js +16 -0
  239. package/dist/components/SessionSummaryCard.js +73 -0
  240. package/dist/components/StartupDisplay.js +24 -0
  241. package/dist/components/StatusDashboard.js +57 -0
  242. package/dist/components/StatusLine.js +8 -0
  243. package/dist/components/StepLine.js +22 -0
  244. package/dist/components/Theme.js +20 -0
  245. package/dist/components/ToolProgress.js +8 -0
  246. package/dist/components/ViolationList.js +21 -0
  247. package/dist/components/render.js +13 -0
  248. package/dist/config/agent-registry.js +237 -0
  249. package/dist/config/claude-settings-hooks.js +304 -0
  250. package/dist/config/hook-installer.js +65 -0
  251. package/dist/config/instruction-writer.js +388 -0
  252. package/dist/config/mcp-config-writer.js +266 -0
  253. package/dist/config/settings.js +174 -0
  254. package/dist/config/tool-detector.js +42 -0
  255. package/dist/config/value-surfacing.js +119 -0
  256. package/dist/core/context-assembly.js +108 -0
  257. package/dist/core/conversation.js +33 -0
  258. package/dist/core/local-chat-provider.js +475 -0
  259. package/dist/core/provider-factory.js +55 -0
  260. package/dist/core/providers.js +90 -0
  261. package/dist/core/query-engine.js +174 -0
  262. package/dist/daemon/api.js +312 -0
  263. package/dist/daemon/autostart.js +119 -0
  264. package/dist/daemon/bootstrap.js +39 -0
  265. package/dist/daemon/client.js +164 -0
  266. package/dist/daemon/detect-ci.js +81 -0
  267. package/dist/daemon/platform-linux.js +146 -0
  268. package/dist/daemon/platform-macos.js +134 -0
  269. package/dist/daemon/platform-windows.js +116 -0
  270. package/dist/daemon/process-manager.js +299 -0
  271. package/dist/daemon/protocol.js +23 -0
  272. package/dist/daemon/registry.js +270 -0
  273. package/dist/daemon/settings-schema.js +72 -0
  274. package/dist/daemon/system-health.js +134 -0
  275. package/dist/daemon/version-checker.js +262 -0
  276. package/dist/daemon/warm-start.js +223 -0
  277. package/dist/entrypoints/cli.js +1043 -0
  278. package/dist/entrypoints/daemon.js +380 -0
  279. package/dist/entrypoints/repl.js +147 -0
  280. package/dist/hooks/adapters/claude-code.js +90 -0
  281. package/dist/hooks/adapters/cline.js +100 -0
  282. package/dist/hooks/adapters/cursor.js +98 -0
  283. package/dist/hooks/hook-dedup.js +79 -0
  284. package/dist/hooks/hook-runner.js +113 -0
  285. package/dist/hooks/navigation-hooks.js +175 -0
  286. package/dist/hooks/prompt-hooks.js +63 -0
  287. package/dist/hooks/shell-hooks.js +47 -0
  288. package/dist/ignore.js +111 -0
  289. package/dist/intelligence/approach-suggester.js +61 -0
  290. package/dist/intelligence/ast-extractor.js +2615 -0
  291. package/dist/intelligence/ast-worker.js +34 -0
  292. package/dist/intelligence/background-indexer.js +121 -0
  293. package/dist/intelligence/blast-radius.js +200 -0
  294. package/dist/intelligence/community-detection.js +691 -0
  295. package/dist/intelligence/community-detector.js +184 -0
  296. package/dist/intelligence/computation-scheduler.js +75 -0
  297. package/dist/intelligence/confidence-propagation.js +47 -0
  298. package/dist/intelligence/convention-detector.js +242 -0
  299. package/dist/intelligence/convention-learner.js +205 -0
  300. package/dist/intelligence/convention-matcher.js +205 -0
  301. package/dist/intelligence/cozo-schema.js +376 -0
  302. package/dist/intelligence/decision-point-detector.js +90 -0
  303. package/dist/intelligence/deep-dive-tools.js +586 -0
  304. package/dist/intelligence/durability-scorer.js +84 -0
  305. package/dist/intelligence/exploration-cost.js +204 -0
  306. package/dist/intelligence/exploration-pattern-tracker.js +61 -0
  307. package/dist/intelligence/fact-generator.js +322 -0
  308. package/dist/intelligence/facts-schema.js +90 -0
  309. package/dist/intelligence/file-intelligence.js +59 -0
  310. package/dist/intelligence/graph-holder.js +220 -0
  311. package/dist/intelligence/graph-temporal-joiner.js +238 -0
  312. package/dist/intelligence/health-grade.js +423 -0
  313. package/dist/intelligence/health-grader.js +200 -0
  314. package/dist/intelligence/health-map-data.js +259 -0
  315. package/dist/intelligence/import-symbols.js +136 -0
  316. package/dist/intelligence/incremental-indexer.js +658 -0
  317. package/dist/intelligence/indexer/centrality.js +62 -0
  318. package/dist/intelligence/indexer/cfg-context.js +95 -0
  319. package/dist/intelligence/indexer/confidence.js +34 -0
  320. package/dist/intelligence/indexer/cross-file-resolver.js +104 -0
  321. package/dist/intelligence/indexer/edge-repair.js +89 -0
  322. package/dist/intelligence/indexer/entity-key.js +17 -0
  323. package/dist/intelligence/indexer/export-map.js +132 -0
  324. package/dist/intelligence/indexer/git-cochange.js +128 -0
  325. package/dist/intelligence/indexer/graph-patch.js +147 -0
  326. package/dist/intelligence/indexer/incremental.js +78 -0
  327. package/dist/intelligence/indexer/ingest.js +160 -0
  328. package/dist/intelligence/indexer/language-detect.js +226 -0
  329. package/dist/intelligence/indexer/metadata.js +63 -0
  330. package/dist/intelligence/indexer/mutation-tracker.js +79 -0
  331. package/dist/intelligence/indexer/orchestrator.js +155 -0
  332. package/dist/intelligence/indexer/plugin-interface.js +31 -0
  333. package/dist/intelligence/indexer/plugins/csharp.js +440 -0
  334. package/dist/intelligence/indexer/plugins/go.js +335 -0
  335. package/dist/intelligence/indexer/plugins/java.js +370 -0
  336. package/dist/intelligence/indexer/plugins/python.js +358 -0
  337. package/dist/intelligence/indexer/plugins/regex-fallback.js +82 -0
  338. package/dist/intelligence/indexer/plugins/ruby.js +290 -0
  339. package/dist/intelligence/indexer/plugins/rust.js +484 -0
  340. package/dist/intelligence/indexer/plugins/tier2-generic.js +310 -0
  341. package/dist/intelligence/indexer/plugins/typescript.js +456 -0
  342. package/dist/intelligence/indexer/resource-monitor.js +93 -0
  343. package/dist/intelligence/indexer/scip/decoder.js +253 -0
  344. package/dist/intelligence/indexer/scip/detector.js +232 -0
  345. package/dist/intelligence/indexer/scip/downloader.js +427 -0
  346. package/dist/intelligence/indexer/scip/fallback.js +34 -0
  347. package/dist/intelligence/indexer/scip/merger.js +109 -0
  348. package/dist/intelligence/indexer/scip/orchestrator.js +433 -0
  349. package/dist/intelligence/indexer/scip/runner.js +98 -0
  350. package/dist/intelligence/indexer/snapshot.js +66 -0
  351. package/dist/intelligence/indexer/test-detector.js +196 -0
  352. package/dist/intelligence/indexer/watch-integration.js +61 -0
  353. package/dist/intelligence/indexer/worker.js +85 -0
  354. package/dist/intelligence/local-convention-detector.js +437 -0
  355. package/dist/intelligence/local-embeddings.js +190 -0
  356. package/dist/intelligence/local-graph.js +1946 -0
  357. package/dist/intelligence/local-indexer.js +1575 -0
  358. package/dist/intelligence/local-llm.js +163 -0
  359. package/dist/intelligence/local-rule-generator.js +154 -0
  360. package/dist/intelligence/local-snapshot.js +213 -0
  361. package/dist/intelligence/negative-knowledge.js +103 -0
  362. package/dist/intelligence/persistent-db.js +85 -0
  363. package/dist/intelligence/query-router.js +2556 -0
  364. package/dist/intelligence/risk-classifier.js +116 -0
  365. package/dist/intelligence/rule-evaluator.js +380 -0
  366. package/dist/intelligence/rule-generator.js +49 -0
  367. package/dist/intelligence/search-index.js +173 -0
  368. package/dist/intelligence/semantic/docstring-extractor.js +67 -0
  369. package/dist/intelligence/semantic/embedding-store.js +52 -0
  370. package/dist/intelligence/semantic/enrichment-orchestrator.js +48 -0
  371. package/dist/intelligence/semantic/git-message-miner.js +114 -0
  372. package/dist/intelligence/semantic/identifier-tokenizer.js +51 -0
  373. package/dist/intelligence/semantic/node2vec-embeddings.js +71 -0
  374. package/dist/intelligence/semantic/node2vec-walks.js +103 -0
  375. package/dist/intelligence/semantic/path-domain-inference.js +112 -0
  376. package/dist/intelligence/semantic/similarity-engine.js +60 -0
  377. package/dist/intelligence/semantic/tfidf-vectors.js +88 -0
  378. package/dist/intelligence/session-brief-builder.js +159 -0
  379. package/dist/intelligence/session-context.js +221 -0
  380. package/dist/intelligence/session-health-monitor.js +211 -0
  381. package/dist/intelligence/session-narrative.js +197 -0
  382. package/dist/intelligence/session-pattern-analyzer.js +218 -0
  383. package/dist/intelligence/signal-scorer.js +390 -0
  384. package/dist/intelligence/signal-show-store.js +182 -0
  385. package/dist/intelligence/smart-truncate.js +158 -0
  386. package/dist/intelligence/subgraph-cache.js +88 -0
  387. package/dist/intelligence/temporal-facts.js +494 -0
  388. package/dist/intelligence/token-estimator.js +100 -0
  389. package/dist/intelligence/tool-injector.js +87 -0
  390. package/dist/intelligence/tree-sitter-loader.js +71 -0
  391. package/dist/intelligence/worker-pool.js +116 -0
  392. package/dist/proxy/arg-validator.js +79 -0
  393. package/dist/proxy/auto-bootstrap.js +167 -0
  394. package/dist/proxy/bridge.js +147 -0
  395. package/dist/proxy/budget-enforcer.js +70 -0
  396. package/dist/proxy/compression-quality-monitor.js +160 -0
  397. package/dist/proxy/compression-stats.js +51 -0
  398. package/dist/proxy/context-rot-detector.js +137 -0
  399. package/dist/proxy/drift-detector.js +139 -0
  400. package/dist/proxy/efficiency-tracker.js +79 -0
  401. package/dist/proxy/fact-ranking.js +154 -0
  402. package/dist/proxy/format-encoder.js +266 -0
  403. package/dist/proxy/http-transport.js +90 -0
  404. package/dist/proxy/lifecycle-actor.js +55 -0
  405. package/dist/proxy/lifecycle-machine.js +187 -0
  406. package/dist/proxy/log-tailer.js +265 -0
  407. package/dist/proxy/model-pricing.js +98 -0
  408. package/dist/proxy/network-firewall.js +141 -0
  409. package/dist/proxy/nudge-state.js +93 -0
  410. package/dist/proxy/output-compressor.js +185 -0
  411. package/dist/proxy/pid-lock.js +291 -0
  412. package/dist/proxy/proxy-context.js +11 -0
  413. package/dist/proxy/proxy.js +2633 -0
  414. package/dist/proxy/response-enrichment.js +32 -0
  415. package/dist/proxy/response-envelope.js +313 -0
  416. package/dist/proxy/session-dedup.js +82 -0
  417. package/dist/proxy/session-legend.js +30 -0
  418. package/dist/proxy/session-persistence.js +210 -0
  419. package/dist/proxy/session-resume.js +94 -0
  420. package/dist/proxy/session-stats.js +513 -0
  421. package/dist/proxy/shell-classifier.js +1346 -0
  422. package/dist/proxy/shell-compression-log.js +93 -0
  423. package/dist/proxy/shell-compressor.js +390 -0
  424. package/dist/proxy/shell-graph-boost.js +202 -0
  425. package/dist/proxy/shell-monitor-map.js +18 -0
  426. package/dist/proxy/shell-stats.js +54 -0
  427. package/dist/proxy/shell-strategies/cloud.js +215 -0
  428. package/dist/proxy/shell-strategies/diff.js +159 -0
  429. package/dist/proxy/shell-strategies/error-diagnostic.js +796 -0
  430. package/dist/proxy/shell-strategies/filter-dsl.js +358 -0
  431. package/dist/proxy/shell-strategies/git-status.js +177 -0
  432. package/dist/proxy/shell-strategies/key-value.js +193 -0
  433. package/dist/proxy/shell-strategies/log-text.js +154 -0
  434. package/dist/proxy/shell-strategies/omni.js +188 -0
  435. package/dist/proxy/shell-strategies/progress.js +55 -0
  436. package/dist/proxy/shell-strategies/redact.js +76 -0
  437. package/dist/proxy/shell-strategies/structured.js +241 -0
  438. package/dist/proxy/shell-strategies/tabular.js +243 -0
  439. package/dist/proxy/shell-strategies/test-results-types.js +13 -0
  440. package/dist/proxy/shell-strategies/test-results.js +784 -0
  441. package/dist/proxy/shell-strategies/tree-paths.js +144 -0
  442. package/dist/proxy/shell-strategies/yaml.js +182 -0
  443. package/dist/proxy/shell-tee.js +111 -0
  444. package/dist/proxy/signal-dedup.js +171 -0
  445. package/dist/proxy/startup-renderer.js +158 -0
  446. package/dist/proxy/task-token-display.js +38 -0
  447. package/dist/proxy/token-counter.js +61 -0
  448. package/dist/proxy/tool-clusters.js +273 -0
  449. package/dist/proxy/tool-definitions.js +525 -0
  450. package/dist/proxy/transport-mux.js +229 -0
  451. package/dist/proxy/wire-cap.js +268 -0
  452. package/dist/schemas/api/skills.js +19 -0
  453. package/dist/schemas/common/errors.js +7 -0
  454. package/dist/schemas/common/headers.js +5 -0
  455. package/dist/schemas/entities/edge.js +25 -0
  456. package/dist/schemas/entities/entity.js +22 -0
  457. package/dist/schemas/entities/rule.js +18 -0
  458. package/dist/schemas/index.js +14 -0
  459. package/dist/server/event-bus.js +59 -0
  460. package/dist/server/http.js +156 -0
  461. package/dist/server/middleware.js +70 -0
  462. package/dist/server/routes/drift.js +97 -0
  463. package/dist/server/routes/intelligence.js +1217 -0
  464. package/dist/server/routes/reasoning-quality.js +444 -0
  465. package/dist/server/routes/session.js +86 -0
  466. package/dist/server/routes/stream.js +120 -0
  467. package/dist/server/routes/system.js +73 -0
  468. package/dist/server/routes/temporal.js +170 -0
  469. package/dist/server/routes/timeline.js +232 -0
  470. package/dist/server/routes/token-flow.js +403 -0
  471. package/dist/skills/effectiveness-tracker.js +93 -0
  472. package/dist/skills/local-pack.js +380 -0
  473. package/dist/skills/resolver.js +495 -0
  474. package/dist/state-detector.js +83 -0
  475. package/dist/timeline/intent-detector.js +263 -0
  476. package/dist/timeline/loop-miner.js +140 -0
  477. package/dist/timeline/open-threads.js +49 -0
  478. package/dist/timeline/signal-reinforcer.js +62 -0
  479. package/dist/timeline/timeline-bootstrap.js +151 -0
  480. package/dist/timeline/timeline-store.js +618 -0
  481. package/dist/tools/coding/bash.js +49 -0
  482. package/dist/tools/coding/file-edit.js +72 -0
  483. package/dist/tools/coding/file-outline.js +227 -0
  484. package/dist/tools/coding/file-read-protocol.js +425 -0
  485. package/dist/tools/coding/file-read.js +35 -0
  486. package/dist/tools/coding/file-write.js +43 -0
  487. package/dist/tools/coding/glob-tool.js +109 -0
  488. package/dist/tools/coding/grep.js +162 -0
  489. package/dist/tools/coding/index.js +27 -0
  490. package/dist/tools/intelligence/index.js +269 -0
  491. package/dist/tools/intelligence/record-fact.js +48 -0
  492. package/dist/tools/intelligence/timeline-markers.js +130 -0
  493. package/dist/tools/registry.js +47 -0
  494. package/dist/tools/types.js +8 -0
  495. package/dist/tracking/auto-snapshot-triggers.js +246 -0
  496. package/dist/tracking/branch-context.js +115 -0
  497. package/dist/tracking/branch-snapshot.js +217 -0
  498. package/dist/tracking/causal-bridge.js +317 -0
  499. package/dist/tracking/circuit-breaker.js +147 -0
  500. package/dist/tracking/commit-watcher.js +114 -0
  501. package/dist/tracking/context-ledger.js +119 -0
  502. package/dist/tracking/correction-detector.js +324 -0
  503. package/dist/tracking/drift-tracker.js +874 -0
  504. package/dist/tracking/durability-tracker.js +94 -0
  505. package/dist/tracking/entity-rewind.js +200 -0
  506. package/dist/tracking/file-hash-state.js +114 -0
  507. package/dist/tracking/git-attribution.js +132 -0
  508. package/dist/tracking/git-trailers.js +171 -0
  509. package/dist/tracking/intelligence-counter.js +46 -0
  510. package/dist/tracking/intent-correlator.js +202 -0
  511. package/dist/tracking/intent-encoder.js +52 -0
  512. package/dist/tracking/intent-token-tracker.js +159 -0
  513. package/dist/tracking/ledger-archiver.js +94 -0
  514. package/dist/tracking/ledger-chains.js +245 -0
  515. package/dist/tracking/metrics-store.js +361 -0
  516. package/dist/tracking/native-watcher.js +131 -0
  517. package/dist/tracking/offline-rewind.js +295 -0
  518. package/dist/tracking/pending-violations.js +74 -0
  519. package/dist/tracking/persistence-effectiveness.js +167 -0
  520. package/dist/tracking/prompt-durability.js +202 -0
  521. package/dist/tracking/quality-signals.js +213 -0
  522. package/dist/tracking/redactor.js +73 -0
  523. package/dist/tracking/rewind-engine.js +161 -0
  524. package/dist/tracking/session-history.js +128 -0
  525. package/dist/tracking/session-receipt.js +88 -0
  526. package/dist/tracking/session-summary-writer.js +157 -0
  527. package/dist/tracking/shadow-ledger.js +321 -0
  528. package/dist/tracking/stash-manager.js +258 -0
  529. package/dist/tracking/timeline-fork.js +213 -0
  530. package/dist/tracking/timeline.js +69 -0
  531. package/dist/tracking/token-flow.js +276 -0
  532. package/dist/tracking/turn-segmenter.js +122 -0
  533. package/dist/tracking/weekly-accumulator.js +179 -0
  534. package/dist/tracking/working-snapshots.js +188 -0
  535. package/dist/tracking/workspace-manifest.js +176 -0
  536. package/dist/transport/http.js +102 -0
  537. package/dist/ui/assets/index-7gl3mIuY.css +1 -0
  538. package/dist/ui/assets/index-BsMTQdhX.js +10 -0
  539. package/dist/ui/index.html +2 -2
  540. package/dist/utils/counterfactual.js +65 -0
  541. package/dist/utils/deep-link.js +34 -0
  542. package/dist/utils/detect.js +193 -0
  543. package/dist/utils/exec.js +73 -0
  544. package/dist/utils/file-logger.js +87 -0
  545. package/dist/utils/format-error.js +29 -0
  546. package/dist/utils/git.js +181 -0
  547. package/dist/utils/log.js +57 -0
  548. package/dist/utils/logger.js +35 -0
  549. package/dist/utils/mcp-content-json.js +8 -0
  550. package/dist/utils/session-logger.js +154 -0
  551. package/dist/utils/startup-log.js +512 -0
  552. package/dist/utils/ui.js +56 -0
  553. package/package.json +5 -3
  554. package/scripts/postinstall.mjs +299 -0
  555. package/dist/ui/assets/index-BISLlJyc.js +0 -10
  556. package/dist/ui/assets/index-BUChTv4H.css +0 -1
@@ -0,0 +1,1946 @@
1
+ /**
2
+ * CozoGraphStore — Local graph store backed by CozoDB.
3
+ *
4
+ * Provides read-only IGraphStore-like interface for local graph queries.
5
+ * Loaded from msgpack snapshots loaded from local snapshots.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { detectCommunities } from "./community-detection.js";
10
+ import { initSchema } from "./cozo-schema.js";
11
+ import { buildSearchIndex, searchLocal, tokenize } from "./search-index.js";
12
+ /** Default timeout for read queries (ms). Prevents reads from hanging behind long writes. */
13
+ const QUERY_TIMEOUT_MS = 2000;
14
+ /** Timeout for write operations (ms). Prevents indefinite hangs on CozoDB lock contention. */
15
+ const WRITE_TIMEOUT_MS = 10_000;
16
+ export class CozoGraphStore {
17
+ db;
18
+ loaded = false;
19
+ /** Serialized write queue — prevents concurrent CozoDB write contention. */
20
+ writeChain = Promise.resolve();
21
+ constructor(db) {
22
+ this.db = db;
23
+ }
24
+ /**
25
+ * Timeout-protected read query. Returns empty rows on timeout instead of hanging.
26
+ * Use this for all tool-facing read paths to prevent stuck MCP calls.
27
+ */
28
+ async query(script, params, timeoutMs = QUERY_TIMEOUT_MS) {
29
+ return Promise.race([
30
+ this.db.run(script, params),
31
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`CozoDB query timeout after ${timeoutMs}ms`)), timeoutMs)),
32
+ ]);
33
+ }
34
+ /**
35
+ * Serialized, timeout-protected write operation.
36
+ * Writes are queued so they don't contend with each other,
37
+ * and reads (via query()) can detect contention via timeout.
38
+ */
39
+ async write(script, params) {
40
+ let result;
41
+ const op = this.writeChain.then(async () => {
42
+ result = await Promise.race([
43
+ this.db.run(script, params),
44
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`CozoDB write timeout after ${WRITE_TIMEOUT_MS}ms`)), WRITE_TIMEOUT_MS)),
45
+ ]);
46
+ });
47
+ this.writeChain = op.catch(() => { }); // keep chain alive on failure
48
+ await op;
49
+ return result;
50
+ }
51
+ /**
52
+ * Async factory — ensures CozoDB schema exists before returning the store.
53
+ * Safe for both fresh and persistent databases.
54
+ *
55
+ * RC-6: Checks for concurrent DB access. If a daemon process already
56
+ * owns the DB (detected via PID lock file), returns null so callers
57
+ * can fall back to parse-only mode.
58
+ */
59
+ static async create(db, projectRoot) {
60
+ // RC-6: Check if another process owns the DB
61
+ if (projectRoot) {
62
+ const pidPath = join(projectRoot, ".unerr", "state", "proxy.pid");
63
+ if (existsSync(pidPath)) {
64
+ try {
65
+ const raw = readFileSync(pidPath, "utf-8").trim();
66
+ let ownerPid;
67
+ if (raw.startsWith("{")) {
68
+ const data = JSON.parse(raw);
69
+ ownerPid = typeof data.pid === "number" ? data.pid : undefined;
70
+ }
71
+ else {
72
+ ownerPid = Number.parseInt(raw, 10);
73
+ if (Number.isNaN(ownerPid))
74
+ ownerPid = undefined;
75
+ }
76
+ if (ownerPid !== undefined && ownerPid !== process.pid) {
77
+ try {
78
+ process.kill(ownerPid, 0); // Check if alive (signal 0)
79
+ throw new Error(`DB owned by daemon (PID ${ownerPid}). Use parse-only mode to avoid lock contention.`);
80
+ }
81
+ catch (killErr) {
82
+ if (killErr instanceof Error &&
83
+ killErr.message.startsWith("DB owned by daemon")) {
84
+ throw killErr;
85
+ }
86
+ // Process not alive — stale PID file, safe to proceed
87
+ process.stderr.write(`[unerr] Stale PID file detected (PID ${ownerPid} not alive). Proceeding with DB access.\n`);
88
+ }
89
+ }
90
+ }
91
+ catch (outerErr) {
92
+ if (outerErr instanceof Error &&
93
+ outerErr.message.startsWith("DB owned by daemon")) {
94
+ throw outerErr;
95
+ }
96
+ // PID file parse error — proceed with DB access
97
+ }
98
+ }
99
+ }
100
+ const store = new CozoGraphStore(db);
101
+ await initSchema(db);
102
+ return store;
103
+ }
104
+ /**
105
+ * Check if the graph has indexed data (entities relation is populated).
106
+ * Used to determine if a persistent DB needs initial indexing.
107
+ */
108
+ async isPopulated() {
109
+ const result = await this.query("?[count(key)] := *entities{key}");
110
+ const count = result.rows[0]?.[0] ?? 0;
111
+ return count > 0;
112
+ }
113
+ /**
114
+ * Get the entity count without loading full data.
115
+ */
116
+ async getEntityCount() {
117
+ const result = await this.query("?[count(key)] := *entities{key}");
118
+ return result.rows[0]?.[0] ?? 0;
119
+ }
120
+ /**
121
+ * Load a deserialized snapshot into CozoDB.
122
+ */
123
+ async loadSnapshot(envelope) {
124
+ // Bulk insert entities (v3: includes risk fields)
125
+ for (const entity of envelope.entities) {
126
+ await this.write(`?[key, kind, name, file_path, start_line, end_line, signature, body, fan_in, fan_out, risk_level] <- [[$key, $kind, $name, $fp, $sl, $el, $sig, $body, $fi, $fo, $rl]]
127
+ :put entities { key => kind, name, file_path, start_line, end_line, signature, body, fan_in, fan_out, risk_level }`, {
128
+ key: entity.key,
129
+ kind: entity.kind,
130
+ name: entity.name,
131
+ fp: entity.file_path,
132
+ sl: entity.start_line ?? 0,
133
+ el: entity.end_line ?? 0,
134
+ sig: entity.signature ?? "",
135
+ body: entity.body ?? "",
136
+ fi: entity.fan_in ?? 0,
137
+ fo: entity.fan_out ?? 0,
138
+ rl: entity.risk_level ?? "normal",
139
+ });
140
+ // Build file index
141
+ await this.write("?[file_path, entity_key] <- [[$fp, $key]] :put file_index { file_path, entity_key }", { fp: entity.file_path, key: entity.key });
142
+ }
143
+ // Bulk insert edges (v2: includes CFG control flow fields)
144
+ for (const edge of envelope.edges) {
145
+ await this.write("?[from_key, to_key, type, sequence_order, condition, branch_kind, is_loop, loop_kind, nesting_depth, is_try_guarded, is_error_handler, mutation_target, mutation_mode] <- [[$from, $to, $type, $seq, $cond, $br, $lp, $lk, $nd, $tg, $eh, $mt, $mm]] :put edges { from_key, to_key, type => sequence_order, condition, branch_kind, is_loop, loop_kind, nesting_depth, is_try_guarded, is_error_handler, mutation_target, mutation_mode }", {
146
+ from: edge.from_key,
147
+ to: edge.to_key,
148
+ type: edge.type,
149
+ seq: edge.seq ?? -1,
150
+ cond: edge.cond ?? "",
151
+ br: edge.br ?? "",
152
+ lp: edge.lp ?? false,
153
+ lk: edge.lk ?? "",
154
+ nd: edge.nd ?? 0,
155
+ tg: edge.tg ?? false,
156
+ eh: edge.eh ?? false,
157
+ mt: edge.mt ?? "",
158
+ mm: edge.mm ?? "",
159
+ });
160
+ }
161
+ // Load rules if present (v2 envelope)
162
+ if (envelope.rules && envelope.rules.length > 0) {
163
+ await this.loadRules(envelope.rules);
164
+ }
165
+ // Load patterns if present (v2 envelope)
166
+ if (envelope.patterns && envelope.patterns.length > 0) {
167
+ await this.loadPatterns(envelope.patterns);
168
+ }
169
+ // Load rule exceptions if present (Sprint 9.6)
170
+ if (envelope.rule_exceptions && envelope.rule_exceptions.length > 0) {
171
+ await this.loadRuleExceptions(envelope.rule_exceptions);
172
+ }
173
+ // Load justifications if present (v4 envelope / MV-03)
174
+ if (envelope.justifications && envelope.justifications.length > 0) {
175
+ await this.loadJustifications(envelope.justifications);
176
+ }
177
+ // Load inline justifications from entities (alternative path)
178
+ for (const entity of envelope.entities) {
179
+ if (entity.purpose || entity.taxonomy || entity.feature_area) {
180
+ await this.write(`?[entity_key, purpose, taxonomy, feature_area, confidence] <-
181
+ [[$ek, $purpose, $taxonomy, $fa, $conf]]
182
+ :put justifications { entity_key => purpose, taxonomy, feature_area, confidence }`, {
183
+ ek: entity.key,
184
+ purpose: entity.purpose ?? "",
185
+ taxonomy: entity.taxonomy ?? "",
186
+ fa: entity.feature_area ?? "",
187
+ conf: entity.justification_confidence ?? 0,
188
+ });
189
+ }
190
+ }
191
+ // Build search index
192
+ await buildSearchIndex(this.db);
193
+ // Leapfrog Sprint A: Community detection via Louvain
194
+ await this.detectAndStoreCommunities();
195
+ this.loaded = true;
196
+ }
197
+ /**
198
+ * Leapfrog Sprint A.2: Run community detection and store results in CozoDB.
199
+ *
200
+ * Extracts all entities and edges from CozoDB, runs Louvain community detection,
201
+ * then writes community assignments back to entities.community and the communities relation.
202
+ * Called once at the end of loadSnapshot(). <200ms for 5K entities.
203
+ */
204
+ async detectAndStoreCommunities() {
205
+ // Extract minimal entity data for community detection
206
+ const entityResult = await this.query("?[key, file_path] := *entities{key, file_path}");
207
+ const entities = entityResult.rows.map((row) => ({
208
+ key: row[0],
209
+ file_path: row[1],
210
+ }));
211
+ if (entities.length === 0)
212
+ return;
213
+ // Extract edges (all types contribute to community structure)
214
+ // R.5: Include edge type so community detection can weight contains edges at 0.3
215
+ const edgeResult = await this.query("?[from_key, to_key, type] := *edges{from_key, to_key, type}");
216
+ const edges = edgeResult.rows.map((row) => ({
217
+ from_key: row[0],
218
+ to_key: row[1],
219
+ type: row[2],
220
+ }));
221
+ // Run community detection
222
+ const result = detectCommunities(entities, edges);
223
+ // Write community assignments back to entities using :update (only changes community column)
224
+ for (const [key, communityId] of result.assignments) {
225
+ await this.write("?[key, community] <- [[$key, $cid]] :update entities { key => community }", { key, cid: communityId });
226
+ }
227
+ // Write community metadata
228
+ for (const c of result.communities) {
229
+ await this.write("?[id, label, size, cohesion] <- [[$id, $label, $size, $cohesion]] :put communities { id => label, size, cohesion }", { id: c.id, label: c.label, size: c.size, cohesion: c.cohesion });
230
+ }
231
+ }
232
+ /**
233
+ * Get a single entity by key.
234
+ */
235
+ async getEntity(key) {
236
+ const result = await this.query("?[key, kind, name, fp, sl, el, sig, body, fi, fo, rl, community] := *entities{key, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community}, key = $key", { key });
237
+ if (result.rows.length === 0)
238
+ return null;
239
+ const [k, kind, name, fp, sl, el, sig, body, fi, fo, rl, community] = result
240
+ .rows[0];
241
+ return {
242
+ key: k,
243
+ kind,
244
+ name,
245
+ file_path: fp,
246
+ start_line: sl,
247
+ end_line: el,
248
+ signature: sig,
249
+ body,
250
+ fan_in: fi,
251
+ fan_out: fo,
252
+ risk_level: rl,
253
+ community,
254
+ };
255
+ }
256
+ /**
257
+ * Get all entities that call the given entity.
258
+ * Merges base edges with drift_edges (Task 6.4).
259
+ */
260
+ async getCallersOf(key) {
261
+ // Defensive kind filter: only callable kinds (function, method, class) can be
262
+ // callers. Pre-fix drift_edges may contain variable/interface/type rows from
263
+ // the historical extractor bug that scanned full-file content per-entity.
264
+ const result = await this.query(`base[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] := *edges{from_key, to_key: $key, type: "calls"},
265
+ *entities{key: from_key, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community: comm},
266
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace",
267
+ k = from_key
268
+ drift[k] := *drift_edges[k, $key, "calls", ds, _], ds != "removed"
269
+ drift_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
270
+ drift[k], *entities{key: k, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community: comm},
271
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace"
272
+ drift_overlay_entity[k, kind, name, fp, ls, el, sig, body, fi, fo, rl, comm] :=
273
+ drift[k], *drift_overlay{key: k, name, kind, signature: sig, body, file_path: fp, line_start: ls},
274
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace",
275
+ not *entities{key: k}, el = 0, fi = 0, fo = 0, rl = "normal", comm = -1
276
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
277
+ base[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm]
278
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
279
+ drift_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm],
280
+ not base[k, _, _, _, _, _, _, _, _, _, _, _]
281
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
282
+ drift_overlay_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm],
283
+ not base[k, _, _, _, _, _, _, _, _, _, _, _],
284
+ not drift_entity[k, _, _, _, _, _, _, _, _, _, _, _]`, { key });
285
+ return result.rows.map((row) => {
286
+ const [k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] = row;
287
+ return {
288
+ key: k,
289
+ kind,
290
+ name,
291
+ file_path: fp,
292
+ start_line: sl,
293
+ end_line: el,
294
+ signature: sig,
295
+ body,
296
+ fan_in: fi,
297
+ fan_out: fo,
298
+ risk_level: rl,
299
+ community: comm,
300
+ };
301
+ });
302
+ }
303
+ /**
304
+ * Get all entities called by the given entity.
305
+ * Merges base edges with drift_edges (Task 6.4).
306
+ */
307
+ async getCalleesOf(key) {
308
+ const result = await this.query(`base[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] := *edges{from_key: $key, to_key, type: "calls"},
309
+ *entities{key: to_key, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community: comm},
310
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace",
311
+ k = to_key
312
+ drift[k] := *drift_edges[$key, k, "calls", ds, _], ds != "removed"
313
+ drift_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
314
+ drift[k], *entities{key: k, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community: comm},
315
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace"
316
+ drift_overlay_entity[k, kind, name, fp, ls, el, sig, body, fi, fo, rl, comm] :=
317
+ drift[k], *drift_overlay{key: k, name, kind, signature: sig, body, file_path: fp, line_start: ls},
318
+ kind != "variable", kind != "interface", kind != "type", kind != "enum", kind != "namespace",
319
+ not *entities{key: k}, el = 0, fi = 0, fo = 0, rl = "normal", comm = -1
320
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
321
+ base[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm]
322
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
323
+ drift_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm],
324
+ not base[k, _, _, _, _, _, _, _, _, _, _, _]
325
+ ?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] :=
326
+ drift_overlay_entity[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm],
327
+ not base[k, _, _, _, _, _, _, _, _, _, _, _],
328
+ not drift_entity[k, _, _, _, _, _, _, _, _, _, _, _]`, { key });
329
+ return result.rows.map((row) => {
330
+ const [k, kind, name, fp, sl, el, sig, body, fi, fo, rl, comm] = row;
331
+ return {
332
+ key: k,
333
+ kind,
334
+ name,
335
+ file_path: fp,
336
+ start_line: sl,
337
+ end_line: el,
338
+ signature: sig,
339
+ body,
340
+ fan_in: fi,
341
+ fan_out: fo,
342
+ risk_level: rl,
343
+ community: comm,
344
+ };
345
+ });
346
+ }
347
+ /**
348
+ * Compute blast radius for an entity using recursive Datalog traversal.
349
+ *
350
+ * Returns direct callers/callees counts, transitive dependent count at
351
+ * the specified depth, and chokepoint detection (fan_in > 5 AND fan_out > 5).
352
+ * Designed to complete in <5ms on graphs with 10K+ entities.
353
+ */
354
+ async getBlastRadius(entityKey, maxDepth = 2) {
355
+ // Direct callers and callees (depth 1)
356
+ const callersResult = await this.query(`?[k] := *edges{from_key, to_key: $key, type: "calls"}, k = from_key`, { key: entityKey });
357
+ const calleesResult = await this.query(`?[k] := *edges{from_key: $key, to_key, type: "calls"}, k = to_key`, { key: entityKey });
358
+ const directCallers = callersResult.rows.length;
359
+ const directCallees = calleesResult.rows.length;
360
+ // Split callers into production vs test
361
+ const callerTestSplit = await this.query(`?[k, it] := *edges{from_key, to_key: $key, type: "calls"},
362
+ *entities{key: from_key, is_test: it}, k = from_key`, { key: entityKey });
363
+ let productionCallers = 0;
364
+ let testCallers = 0;
365
+ for (const row of callerTestSplit.rows) {
366
+ if (row[1])
367
+ testCallers++;
368
+ else
369
+ productionCallers++;
370
+ }
371
+ // Direct caller count IS the blast radius for signal purposes. Earlier
372
+ // versions ran a recursive transitive walk (callers-of-callers up to
373
+ // maxDepth) but: (a) the only `_meta.blast_radius.transitive_depth2`
374
+ // consumer was dropped when MCP `_meta` was removed, and (b) the recursive
375
+ // form was broken (two `?[]` heads). Keep `transitive_count` in the result
376
+ // shape for back-compat, set to `directCallers` — depth-1 truth.
377
+ const transitiveCount = directCallers;
378
+ void maxDepth; // retained as a stable parameter for callers; one-hop now.
379
+ // Chokepoint: fan_in > 5 AND fan_out > 5
380
+ const entity = await this.getEntity(entityKey);
381
+ const isChokepoint = entity !== null && entity.fan_in > 5 && entity.fan_out > 5;
382
+ // Build summary string for _context
383
+ const parts = [];
384
+ if (productionCallers > 0)
385
+ parts.push(`${productionCallers} production caller${productionCallers !== 1 ? "s" : ""}`);
386
+ if (testCallers > 0)
387
+ parts.push(`${testCallers} test caller${testCallers !== 1 ? "s" : ""}`);
388
+ if (directCallees > 0)
389
+ parts.push(`${directCallees} direct callee${directCallees !== 1 ? "s" : ""}`);
390
+ if (isChokepoint)
391
+ parts.push("CHOKEPOINT");
392
+ const summary = parts.length > 0 ? parts.join(", ") : "No dependencies";
393
+ return {
394
+ direct_callers: directCallers,
395
+ direct_callees: directCallees,
396
+ production_callers: productionCallers,
397
+ test_callers: testCallers,
398
+ transitive_count: transitiveCount,
399
+ transitive_depth: 1,
400
+ is_chokepoint: isChokepoint,
401
+ summary,
402
+ };
403
+ }
404
+ /**
405
+ * Sprint 3.1: Full N-hop blast radius returning entity details with depth.
406
+ *
407
+ * Traverses callers recursively up to maxDepth using CozoDB Datalog recursion,
408
+ * then joins entity details (name, file_path) for each affected node.
409
+ * Configurable max depth (default 2). <10ms on 10K-entity graphs at depth 3.
410
+ */
411
+ async getBlastRadiusEntities(entityKey, maxDepth = 2) {
412
+ // Single-hop direct callers — same data, no broken recursion. The
413
+ // consumer (`_meta.blast_radius.affected_entities.slice(0, 20)`) was
414
+ // removed when MCP `_meta` was filtered out; this method survives for
415
+ // backwards-compat and the file-siblings fallback below.
416
+ void maxDepth; // depth-N walk would re-introduce the parser bug; depth-1 truth is sufficient.
417
+ const directResult = await this.query(`?[target] := *edges{from_key, to_key: $root, type: "calls"}, target = from_key`, { root: entityKey });
418
+ const depthMap = new Map();
419
+ for (const row of directResult.rows) {
420
+ const key = row[0];
421
+ if (key === entityKey)
422
+ continue;
423
+ if (!depthMap.has(key))
424
+ depthMap.set(key, 1);
425
+ }
426
+ if (depthMap.size === 0) {
427
+ // R.6: File-level fallback — return sibling entities from the same file
428
+ return this.getFileSiblings(entityKey);
429
+ }
430
+ // Batch-fetch entity details for all affected keys
431
+ const entities = [];
432
+ for (const [key, depth] of depthMap) {
433
+ const entityResult = await this.query("?[k, name, fp] := *entities{key: k, name, file_path: fp}, k = $key", { key });
434
+ if (entityResult.rows.length > 0) {
435
+ const [, name, file] = entityResult.rows[0];
436
+ entities.push({ key, name, file, depth });
437
+ }
438
+ }
439
+ // Sort by depth ascending, then by key for deterministic output
440
+ entities.sort((a, b) => a.depth - b.depth || a.key.localeCompare(b.key));
441
+ return entities;
442
+ }
443
+ /**
444
+ * R.6: Get sibling entities from the same file as the given entity.
445
+ * Used as blast radius fallback when no callers exist.
446
+ */
447
+ async getFileSiblings(entityKey) {
448
+ // Find the file path for this entity
449
+ const fileResult = await this.query("?[fp] := *entities{key: $key, file_path: fp}", { key: entityKey });
450
+ if (fileResult.rows.length === 0)
451
+ return [];
452
+ const filePath = fileResult.rows[0]?.[0];
453
+ // Get all entities in the same file (via contains edges from the file entity)
454
+ const siblingsResult = await this.query(`?[key, name, fp] := *edges{from_key: $fileKey, to_key: key, type: "contains"}, *entities{key, name, file_path: fp}, key != $entityKey`, { fileKey: `file:${filePath}`, entityKey });
455
+ return siblingsResult.rows.map((row) => ({
456
+ key: row[0],
457
+ name: row[1],
458
+ file: row[2],
459
+ depth: 0, // Same file = depth 0 (adjacent)
460
+ }));
461
+ }
462
+ // ── Sprint R.7: File-Level Queries ──────────────────────────────────
463
+ /**
464
+ * R.7: Get all entities contained within a file.
465
+ */
466
+ async getFileEntities(filePath) {
467
+ const result = await this.query(`?[key, kind, name] := *edges{from_key: $fileKey, to_key: key, type: "contains"}, *entities{key, kind, name}`, { fileKey: `file:${filePath}` });
468
+ return result.rows.map((row) => ({
469
+ key: row[0],
470
+ kind: row[1],
471
+ name: row[2],
472
+ }));
473
+ }
474
+ /**
475
+ * R.7: Get files connected to the given file via imports or co-change edges.
476
+ * Returns connected files with edge type and direction.
477
+ */
478
+ async getFileNeighbors(filePath) {
479
+ const fileKey = `file:${filePath}`;
480
+ // Outbound: this file imports others
481
+ const outResult = await this.query("?[to_key, type] := *edges{from_key: $fk, to_key, type}, to_key != $fk", { fk: fileKey });
482
+ // Inbound: others import this file
483
+ const inResult = await this.query("?[from_key, type] := *edges{from_key, to_key: $fk, type}, from_key != $fk", { fk: fileKey });
484
+ const outbound = [];
485
+ const inbound = [];
486
+ for (const row of outResult.rows) {
487
+ const toKey = row[0];
488
+ const edgeType = row[1];
489
+ // Only include file→file edges (skip contains edges to entities)
490
+ if (toKey.startsWith("file:")) {
491
+ outbound.push({ file: toKey.slice(5), edgeType, direction: "out" });
492
+ }
493
+ }
494
+ for (const row of inResult.rows) {
495
+ const fromKey = row[0];
496
+ const edgeType = row[1];
497
+ if (fromKey.startsWith("file:")) {
498
+ inbound.push({ file: fromKey.slice(5), edgeType, direction: "in" });
499
+ }
500
+ }
501
+ // Interleave so both directions survive a small `limit` at the wire cap.
502
+ // Without this, in/out are emitted as two contiguous runs and the default
503
+ // 20-item cap can hide one side entirely.
504
+ const neighbors = [];
505
+ const maxLen = Math.max(outbound.length, inbound.length);
506
+ for (let i = 0; i < maxLen; i++) {
507
+ const o = outbound[i];
508
+ if (o)
509
+ neighbors.push(o);
510
+ const inb = inbound[i];
511
+ if (inb)
512
+ neighbors.push(inb);
513
+ }
514
+ return neighbors;
515
+ }
516
+ // ── Sprint R.13: Test Coverage Query ──────────────────────────────
517
+ /**
518
+ * Find test entities that cover a given source entity.
519
+ * Uses "tests" edges (direct) and optionally traverses callers for transitive coverage.
520
+ */
521
+ async getTestCoverage(entityKey, includeTransitive = true) {
522
+ // Direct: tests edges pointing to this entity
523
+ const directResult = await this.query(`?[k, name, fp] := *edges{from_key: k, to_key: $target, type: "tests"},
524
+ *entities{key: k, name, file_path: fp}`, { target: entityKey });
525
+ const results = [];
526
+ const seenKeys = new Set();
527
+ // Coverage is fundamentally a FILE-level signal — a test file either
528
+ // exercises this entity or it doesn't, and which particular helper
529
+ // function inside the file routes the call isn't actionable info for
530
+ // the agent. Dedup by file_path to keep one (closest-depth) row per
531
+ // covering test file.
532
+ const seenFiles = new Set();
533
+ for (const row of directResult.rows) {
534
+ const key = row[0];
535
+ const file = row[2];
536
+ if (seenKeys.has(key) || seenFiles.has(file))
537
+ continue;
538
+ seenKeys.add(key);
539
+ seenFiles.add(file);
540
+ results.push({
541
+ key,
542
+ name: row[1],
543
+ file,
544
+ depth: 1,
545
+ });
546
+ }
547
+ // Transitive: test entities that call something that calls the target
548
+ if (includeTransitive) {
549
+ const transitiveResult = await this.query(`?[k, name, fp] := *edges{from_key: mid, to_key: $target, type: "calls"},
550
+ *edges{from_key: k, to_key: mid, type: "tests"},
551
+ *entities{key: k, name, file_path: fp}`, { target: entityKey });
552
+ for (const row of transitiveResult.rows) {
553
+ const key = row[0];
554
+ const file = row[2];
555
+ if (seenKeys.has(key) || seenFiles.has(file))
556
+ continue;
557
+ seenKeys.add(key);
558
+ seenFiles.add(file);
559
+ results.push({
560
+ key,
561
+ name: row[1],
562
+ file,
563
+ depth: 2,
564
+ });
565
+ }
566
+ }
567
+ return results;
568
+ }
569
+ // ── Leapfrog Sprint A: Community Query Methods ─────────────────────
570
+ /**
571
+ * Get community metadata for an entity's community.
572
+ * Returns null if entity has no community assignment (community == -1).
573
+ */
574
+ async getCommunityForEntity(entityKey) {
575
+ const result = await this.query(`?[id, label, size, cohesion] :=
576
+ *entities{key, community},
577
+ key = $key, community >= 0,
578
+ *communities[community, label, size, cohesion],
579
+ id = community`, { key: entityKey });
580
+ if (result.rows.length === 0)
581
+ return null;
582
+ const [id, label, size, cohesion] = result.rows[0];
583
+ return { id, label, size, cohesion };
584
+ }
585
+ /**
586
+ * Get cross-community edges for an entity.
587
+ * Returns edges where the entity connects to entities in different communities.
588
+ */
589
+ async getCrossCommunityEdges(entityKey) {
590
+ // Outbound cross-community edges
591
+ const outbound = await this.query(`?[to_name, to_key, to_community, to_label, edge_type] :=
592
+ *edges{from_key: $key, to_key: tk, type: edge_type},
593
+ *entities{key: $key, community: c1},
594
+ *entities{key: tk, name: to_name, community: to_community},
595
+ c1 >= 0, to_community >= 0, c1 != to_community,
596
+ *communities{id: to_community, label: to_label},
597
+ to_key = tk`, { key: entityKey });
598
+ // Inbound cross-community edges
599
+ const inbound = await this.query(`?[from_name, from_key, from_community, from_label, edge_type] :=
600
+ *edges{from_key: fk, to_key: $key, type: edge_type},
601
+ *entities{key: $key, community: c1},
602
+ *entities{key: fk, name: from_name, community: from_community},
603
+ c1 >= 0, from_community >= 0, c1 != from_community,
604
+ *communities{id: from_community, label: from_label},
605
+ from_key = fk`, { key: entityKey });
606
+ const edges = [];
607
+ for (const row of outbound.rows) {
608
+ const [name, key, cid, label, rel] = row;
609
+ edges.push({
610
+ entity_name: name,
611
+ entity_key: key,
612
+ entity_community_id: cid,
613
+ entity_community_label: label,
614
+ relation: rel,
615
+ });
616
+ }
617
+ for (const row of inbound.rows) {
618
+ const [name, key, cid, label, rel] = row;
619
+ edges.push({
620
+ entity_name: name,
621
+ entity_key: key,
622
+ entity_community_id: cid,
623
+ entity_community_label: label,
624
+ relation: `${rel} (inbound)`,
625
+ });
626
+ }
627
+ return edges;
628
+ }
629
+ /**
630
+ * Get all cross-community edges in the graph, sorted by surprise score.
631
+ * Surprise = inverse of inter-community edge density between the two communities.
632
+ */
633
+ async getCrossBoundaryLinks(communityId, topN = 10) {
634
+ // Get all cross-community edges
635
+ const query = communityId !== undefined
636
+ ? `?[fn, ff, fc, fl, tn, tf, tc, tl, et] :=
637
+ *edges{from_key: fk, to_key: tk, type: et},
638
+ *entities{key: fk, name: fn, file_path: ff, community: fc},
639
+ *entities{key: tk, name: tn, file_path: tf, community: tc},
640
+ fc >= 0, tc >= 0, fc != tc,
641
+ (fc = $cid or tc = $cid),
642
+ *communities{id: fc, label: fl},
643
+ *communities{id: tc, label: tl}`
644
+ : `?[fn, ff, fc, fl, tn, tf, tc, tl, et] :=
645
+ *edges{from_key: fk, to_key: tk, type: et},
646
+ *entities{key: fk, name: fn, file_path: ff, community: fc},
647
+ *entities{key: tk, name: tn, file_path: tf, community: tc},
648
+ fc >= 0, tc >= 0, fc != tc,
649
+ *communities{id: fc, label: fl},
650
+ *communities{id: tc, label: tl}`;
651
+ const result = await this.query(query, communityId !== undefined ? { cid: communityId } : {});
652
+ // Count edges per community pair for density calculation
653
+ const pairCounts = new Map();
654
+ for (const row of result.rows) {
655
+ const fc = row[2];
656
+ const tc = row[6];
657
+ const pairKey = `${Math.min(fc, tc)}-${Math.max(fc, tc)}`;
658
+ pairCounts.set(pairKey, (pairCounts.get(pairKey) ?? 0) + 1);
659
+ }
660
+ // Get max pair count for normalization
661
+ const maxPairCount = Math.max(1, ...pairCounts.values());
662
+ // Score and sort
663
+ const scored = result.rows.map((row) => {
664
+ const [fn, ff, fc, fl, tn, tf, tc, tl, et] = row;
665
+ const pairKey = `${Math.min(fc, tc)}-${Math.max(fc, tc)}`;
666
+ const density = (pairCounts.get(pairKey) ?? 0) / maxPairCount;
667
+ return {
668
+ from_name: fn,
669
+ from_file: ff,
670
+ from_community: fc,
671
+ from_community_label: fl,
672
+ to_name: tn,
673
+ to_file: tf,
674
+ to_community: tc,
675
+ to_community_label: tl,
676
+ edge_type: et,
677
+ surprise_score: Math.round((1.0 - density) * 1000) / 1000,
678
+ };
679
+ });
680
+ scored.sort((a, b) => b.surprise_score - a.surprise_score);
681
+ return scored.slice(0, topN);
682
+ }
683
+ /**
684
+ * Get critical nodes — highest degree entities excluding file-level hubs.
685
+ * Degree ranking excluding kind=="file" and kind=="module".
686
+ */
687
+ async getCriticalNodes(topN = 10, communityId) {
688
+ const query = communityId !== undefined
689
+ ? `?[key, name, fp, fi, fo, degree, community, label, rl, kind] :=
690
+ *entities{key, name, file_path: fp, fan_in: fi, fan_out: fo, community, risk_level: rl, kind},
691
+ kind != "file", kind != "module",
692
+ community = $cid, community >= 0,
693
+ *communities{id: community, label},
694
+ degree = fi + fo
695
+ :order -degree
696
+ :limit $top_n`
697
+ : `?[key, name, fp, fi, fo, degree, community, label, rl, kind] :=
698
+ *entities{key, name, file_path: fp, fan_in: fi, fan_out: fo, community, risk_level: rl, kind},
699
+ kind != "file", kind != "module",
700
+ community >= 0,
701
+ *communities{id: community, label},
702
+ degree = fi + fo
703
+ :order -degree
704
+ :limit $top_n`;
705
+ const params = { top_n: topN };
706
+ if (communityId !== undefined)
707
+ params.cid = communityId;
708
+ const result = await this.query(query, params);
709
+ return result.rows.map((row) => {
710
+ const [key, name, fp, fi, fo, degree, community, label, rl, kind] = row;
711
+ return {
712
+ key,
713
+ name,
714
+ file_path: fp,
715
+ kind,
716
+ fan_in: fi,
717
+ fan_out: fo,
718
+ degree,
719
+ community,
720
+ community_label: label,
721
+ risk_level: rl,
722
+ };
723
+ });
724
+ }
725
+ /**
726
+ * Get all communities with metadata.
727
+ */
728
+ async getAllCommunities() {
729
+ const result = await this.query("?[id, label, size, cohesion] := *communities[id, label, size, cohesion] :order -size");
730
+ return result.rows.map((row) => {
731
+ const [id, label, size, cohesion] = row;
732
+ return { id, label, size, cohesion };
733
+ });
734
+ }
735
+ /**
736
+ * Get all entities in a given file.
737
+ */
738
+ async getEntitiesByFile(filePath) {
739
+ const result = await this.query(`?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, community] := *file_index[$fp, ek],
740
+ *entities{key: ek, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community},
741
+ k = ek`, { fp: filePath });
742
+ return result.rows.map((row) => {
743
+ const [k, kind, name, fp, sl, el, sig, body, fi, fo, rl, community] = row;
744
+ return {
745
+ key: k,
746
+ kind,
747
+ name,
748
+ file_path: fp,
749
+ start_line: sl,
750
+ end_line: el,
751
+ signature: sig,
752
+ body,
753
+ fan_in: fi,
754
+ fan_out: fo,
755
+ risk_level: rl,
756
+ community,
757
+ };
758
+ });
759
+ }
760
+ /**
761
+ * Find a single entity by exact name match (first match across all files).
762
+ * Used by drift edge resolution (Task 6.4).
763
+ */
764
+ async findEntityByName(name) {
765
+ const result = await this.query("?[k, kind, name, fp, sl, el, sig, body, fi, fo, rl, community] := *entities{key: k, kind, name, file_path: fp, start_line: sl, end_line: el, signature: sig, body, fan_in: fi, fan_out: fo, risk_level: rl, community}, name = $name :limit 1", { name });
766
+ if (result.rows.length === 0)
767
+ return null;
768
+ const [k, kind, n, fp, sl, el, sig, body, fi, fo, rl, community] = result
769
+ .rows[0];
770
+ return {
771
+ key: k,
772
+ kind,
773
+ name: n,
774
+ file_path: fp,
775
+ start_line: sl,
776
+ end_line: el,
777
+ signature: sig,
778
+ body,
779
+ fan_in: fi,
780
+ fan_out: fo,
781
+ risk_level: rl,
782
+ community,
783
+ };
784
+ }
785
+ /**
786
+ * Search entities by name query.
787
+ */
788
+ async searchEntities(query, limit = 20) {
789
+ return await searchLocal(this.db, query, limit);
790
+ }
791
+ /**
792
+ * Get import edges for a file.
793
+ */
794
+ async getImports(filePath) {
795
+ // File-level import edges use "file:<path>" keys (created by local-indexer R.3).
796
+ const fileKey = `file:${filePath}`;
797
+ const result = await this.query(`?[to_key] := *edges{from_key: $fk, to_key, type: "imports"}`, { fk: fileKey });
798
+ return result.rows.map((row) => {
799
+ const raw = row[0];
800
+ // Strip "file:" prefix from file-level keys
801
+ const imported_file = raw.startsWith("file:") ? raw.slice(5) : raw;
802
+ return { imported_file };
803
+ });
804
+ }
805
+ /**
806
+ * Bulk insert rules into CozoDB.
807
+ */
808
+ async loadRules(rules) {
809
+ for (const rule of rules) {
810
+ await this.write(`?[key, name, scope, severity, engine, query, message, file_glob, enabled, repo_id, status, target_kinds, ast_grep_fix, example, decay_score, evaluations, overrides] <- [[$key, $name, $scope, $severity, $engine, $query, $message, $fg, $enabled, $rid, $status, $tk, $agf, $ex, $ds, $evals, $ov]]
811
+ :put rules { key => name, scope, severity, engine, query, message, file_glob, enabled, repo_id, status, target_kinds, ast_grep_fix, example, decay_score, evaluations, overrides }`, {
812
+ key: rule.key,
813
+ name: rule.name,
814
+ scope: rule.scope,
815
+ severity: rule.severity,
816
+ engine: rule.engine,
817
+ query: rule.query,
818
+ message: rule.message,
819
+ fg: rule.file_glob,
820
+ enabled: rule.enabled,
821
+ rid: rule.repo_id,
822
+ status: rule.status ?? "active",
823
+ tk: rule.target_kinds ?? "",
824
+ agf: rule.ast_grep_fix ?? "",
825
+ ex: rule.example ?? "",
826
+ ds: rule.decay_score ?? 0.0,
827
+ evals: rule.evaluations ?? 0,
828
+ ov: rule.overrides ?? 0,
829
+ });
830
+ }
831
+ }
832
+ /**
833
+ * Bulk insert patterns into CozoDB.
834
+ */
835
+ async loadPatterns(patterns) {
836
+ for (const pattern of patterns) {
837
+ await this.write(`?[key, name, kind, frequency, confidence, exemplar_keys, promoted_rule_key] <- [[$key, $name, $kind, $freq, $conf, $ek, $prk]]
838
+ :put patterns { key => name, kind, frequency, confidence, exemplar_keys, promoted_rule_key }`, {
839
+ key: pattern.key,
840
+ name: pattern.name,
841
+ kind: pattern.kind,
842
+ freq: pattern.frequency,
843
+ conf: pattern.confidence,
844
+ ek: pattern.exemplar_keys.join(","),
845
+ prk: pattern.promoted_rule_key,
846
+ });
847
+ }
848
+ }
849
+ /**
850
+ * Check if rules exist in the local store.
851
+ */
852
+ async hasRules() {
853
+ try {
854
+ const result = await this.query("?[key] := *rules[key, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] :limit 1");
855
+ return result?.rows?.length > 0;
856
+ }
857
+ catch {
858
+ return false;
859
+ }
860
+ }
861
+ /**
862
+ * Get rules, optionally filtered by file path glob matching.
863
+ * Returns rules sorted by scope priority (workspace > branch > path > repo > org).
864
+ */
865
+ async getRules(filePath) {
866
+ let result;
867
+ try {
868
+ result = await this.query("?[key, name, scope, severity, engine, query, message, fg, enabled, rid, status, tk, agf, ex, ds, evals, ov] := *rules[key, name, scope, severity, engine, query, message, fg, enabled, rid, status, tk, agf, ex, ds, evals, ov], enabled = true");
869
+ }
870
+ catch {
871
+ return [];
872
+ }
873
+ if (!result?.rows)
874
+ return [];
875
+ const rules = result.rows.map((row) => {
876
+ const [key, name, scope, severity, engine, query, message, file_glob, enabled, repo_id, status, target_kinds, ast_grep_fix, example, decay_score, evaluations, overrides,] = row;
877
+ return {
878
+ key,
879
+ name,
880
+ scope,
881
+ severity,
882
+ engine,
883
+ query,
884
+ message,
885
+ file_glob,
886
+ enabled,
887
+ repo_id,
888
+ status,
889
+ target_kinds,
890
+ ast_grep_fix,
891
+ example,
892
+ decay_score,
893
+ evaluations,
894
+ overrides,
895
+ };
896
+ });
897
+ // Filter by file path glob if provided
898
+ if (filePath) {
899
+ return rules.filter((rule) => {
900
+ if (!rule.file_glob)
901
+ return true;
902
+ return matchGlob(filePath, rule.file_glob);
903
+ });
904
+ }
905
+ // Sort by scope priority
906
+ const scopePriority = {
907
+ workspace: 5,
908
+ branch: 4,
909
+ path: 3,
910
+ repo: 2,
911
+ org: 1,
912
+ };
913
+ rules.sort((a, b) => (scopePriority[b.scope] ?? 0) - (scopePriority[a.scope] ?? 0));
914
+ return rules;
915
+ }
916
+ /**
917
+ * Sprint 9.9: Get rule health summary for status display.
918
+ *
919
+ * Categories based on decay_score:
920
+ * - healthy: decay_score < 0.3 (or no score)
921
+ * - aging: 0.3 <= decay_score < 0.6
922
+ * - decayed: decay_score >= 0.6
923
+ * - dormant: evaluations == 0
924
+ */
925
+ async getRuleHealthSummary() {
926
+ const rules = await this.getRules();
927
+ let healthy = 0;
928
+ let aging = 0;
929
+ let decayed = 0;
930
+ let dormant = 0;
931
+ const warnings = [];
932
+ for (const rule of rules) {
933
+ const evals = rule.evaluations ?? 0;
934
+ const overrides = rule.overrides ?? 0;
935
+ const ds = rule.decay_score ?? 0;
936
+ if (evals === 0) {
937
+ dormant++;
938
+ }
939
+ else if (ds >= 0.6) {
940
+ decayed++;
941
+ const overrideRate = evals > 0 ? Math.round((overrides / evals) * 100) : 0;
942
+ warnings.push({
943
+ key: rule.key,
944
+ name: rule.name,
945
+ decayScore: ds,
946
+ overrideRate,
947
+ });
948
+ }
949
+ else if (ds >= 0.3) {
950
+ aging++;
951
+ }
952
+ else {
953
+ healthy++;
954
+ }
955
+ }
956
+ // Sort warnings by decay score descending
957
+ warnings.sort((a, b) => b.decayScore - a.decayScore);
958
+ return {
959
+ total: rules.length,
960
+ healthy,
961
+ aging,
962
+ decayed,
963
+ dormant,
964
+ warnings: warnings.slice(0, 3),
965
+ };
966
+ }
967
+ /**
968
+ * Get all patterns.
969
+ */
970
+ async getPatterns() {
971
+ const result = await this.query("?[key, name, kind, freq, conf, ek, prk] := *patterns[key, name, kind, freq, conf, ek, prk]");
972
+ return result.rows.map((row) => {
973
+ const [key, name, kind, frequency, confidence, exemplarKeysStr, promoted_rule_key,] = row;
974
+ return {
975
+ key,
976
+ name,
977
+ kind,
978
+ frequency,
979
+ confidence,
980
+ exemplar_keys: exemplarKeysStr
981
+ ? exemplarKeysStr.split(",").filter(Boolean)
982
+ : [],
983
+ promoted_rule_key,
984
+ };
985
+ });
986
+ }
987
+ /**
988
+ * Health check — always up for local store.
989
+ */
990
+ healthCheck() {
991
+ return { status: "up", latencyMs: 0 };
992
+ }
993
+ isLoaded() {
994
+ return this.loaded;
995
+ }
996
+ // ── Justification Queries (MV-03) ──────────────────────────────
997
+ /**
998
+ * Bulk insert justifications into CozoDB.
999
+ */
1000
+ async loadJustifications(justifications) {
1001
+ for (const j of justifications) {
1002
+ await this.write(`?[entity_key, purpose, taxonomy, feature_area, confidence] <-
1003
+ [[$ek, $purpose, $taxonomy, $fa, $conf]]
1004
+ :put justifications { entity_key => purpose, taxonomy, feature_area, confidence }`, {
1005
+ ek: j.entity_key,
1006
+ purpose: j.purpose,
1007
+ taxonomy: j.taxonomy,
1008
+ fa: j.feature_area,
1009
+ conf: j.confidence,
1010
+ });
1011
+ }
1012
+ }
1013
+ /**
1014
+ * Get business context (justification) for an entity.
1015
+ */
1016
+ async getBusinessContext(entityKey) {
1017
+ const result = await this.query(`?[ek, purpose, taxonomy, fa, conf] :=
1018
+ *justifications[ek, purpose, taxonomy, fa, conf], ek = $ek`, { ek: entityKey });
1019
+ if (result.rows.length === 0)
1020
+ return null;
1021
+ const [, purpose, taxonomy, feature_area, confidence] = result.rows[0];
1022
+ const entity = await this.getEntity(entityKey);
1023
+ return { purpose, taxonomy, feature_area, confidence, entity };
1024
+ }
1025
+ /**
1026
+ * Get conventions: patterns with adherence rates computed from entity coverage.
1027
+ */
1028
+ async getConventions() {
1029
+ const patterns = await this.getPatterns();
1030
+ if (patterns.length === 0)
1031
+ return [];
1032
+ // Per-kind entity counts for accurate adherence computation
1033
+ const kindResult = await this.query("?[kind, count(key)] := *entities{key, kind}");
1034
+ const kindCounts = new Map();
1035
+ for (const row of kindResult.rows) {
1036
+ kindCounts.set(row[0], row[1]);
1037
+ }
1038
+ return patterns.map((p) => {
1039
+ // Use the count of entities of the same kind for adherence
1040
+ const kindCount = Math.max(kindCounts.get(p.kind) ?? 1, 1);
1041
+ return {
1042
+ name: p.name,
1043
+ kind: p.kind,
1044
+ frequency: p.frequency,
1045
+ confidence: p.confidence,
1046
+ // Adherence = (frequency / kindCount) capped at 1.0
1047
+ adherence_rate: Math.min(1, p.frequency / kindCount),
1048
+ };
1049
+ });
1050
+ }
1051
+ /**
1052
+ * Get top N conventions applicable to a specific entity's file path.
1053
+ * Conventions are patterns + rules filtered by file_glob match,
1054
+ * sorted by confidence descending, limited to top N.
1055
+ */
1056
+ async getConventionsForEntity(entityFilePath, limit = 3) {
1057
+ // Get rules that apply to this file
1058
+ const rules = await this.getRules(entityFilePath);
1059
+ const conventions = [];
1060
+ // Patterns as conventions (naming, structure)
1061
+ const patterns = await this.getPatterns();
1062
+ // Per-kind entity counts for accurate adherence
1063
+ const kindResult = await this.query("?[kind, count(key)] := *entities{key, kind}");
1064
+ const kindCounts = new Map();
1065
+ for (const row of kindResult.rows) {
1066
+ kindCounts.set(row[0], row[1]);
1067
+ }
1068
+ for (const p of patterns) {
1069
+ const kindCount = Math.max(kindCounts.get(p.kind) ?? 1, 1);
1070
+ conventions.push({
1071
+ id: `pattern:${p.key}`,
1072
+ name: p.name,
1073
+ adherence_pct: Math.round(Math.min(1, p.frequency / kindCount) * 100),
1074
+ rule: `${p.kind} pattern (${p.frequency} occurrences, ${Math.round(p.confidence * 100)}% confidence)`,
1075
+ });
1076
+ }
1077
+ // Rules as conventions (severity-based ordering)
1078
+ for (const r of rules) {
1079
+ conventions.push({
1080
+ id: `rule:${r.key}`,
1081
+ name: r.name,
1082
+ adherence_pct: 100, // Rules are prescriptive, not measured
1083
+ rule: r.message || `${r.severity} rule: ${r.name}`,
1084
+ });
1085
+ }
1086
+ // Sort by adherence_pct ascending (lowest adherence = most relevant to surface)
1087
+ // then take top N
1088
+ conventions.sort((a, b) => a.adherence_pct - b.adherence_pct);
1089
+ return conventions.slice(0, limit);
1090
+ }
1091
+ /**
1092
+ * Check if justifications exist in the local store.
1093
+ */
1094
+ async hasJustifications() {
1095
+ const result = await this.query("?[ek] := *justifications[ek, _, _, _, _] :limit 1");
1096
+ return result.rows.length > 0;
1097
+ }
1098
+ // ── Rule Exceptions (Sprint 9.6) ────────────────────────────────
1099
+ /**
1100
+ * Bulk insert rule exceptions into CozoDB.
1101
+ */
1102
+ async loadRuleExceptions(exceptions) {
1103
+ for (const ex of exceptions) {
1104
+ await this.write(`?[key, entity_key, rule_key, reason, granted_by, granted_at, expires_at, status, jira_ticket] <- [[$key, $ek, $rk, $reason, $gb, $ga, $ea, $status, $jt]]
1105
+ :put rule_exceptions { key => entity_key, rule_key, reason, granted_by, granted_at, expires_at, status, jira_ticket }`, {
1106
+ key: ex.key,
1107
+ ek: ex.entity_key,
1108
+ rk: ex.rule_key,
1109
+ reason: ex.reason,
1110
+ gb: ex.granted_by,
1111
+ ga: ex.granted_at,
1112
+ ea: ex.expires_at,
1113
+ status: ex.status ?? "active",
1114
+ jt: ex.jira_ticket ?? "",
1115
+ });
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Get active rule exceptions for an entity, optionally filtered by rule key.
1120
+ * Returns only non-expired, active exceptions.
1121
+ */
1122
+ async getRuleExceptions(entityKey, ruleKey) {
1123
+ const result = await this.query("?[key, ek, rk, reason, gb, ga, ea, status, jt] := *rule_exceptions[key, ek, rk, reason, gb, ga, ea, status, jt], ek = $ek, status = 'active'", { ek: entityKey });
1124
+ const now = new Date().toISOString();
1125
+ const exceptions = result.rows
1126
+ .map((row) => {
1127
+ const [key, entity_key, rule_key, reason, granted_by, granted_at, expires_at, status, jira_ticket,] = row;
1128
+ return {
1129
+ key,
1130
+ entity_key,
1131
+ rule_key,
1132
+ reason,
1133
+ granted_by,
1134
+ granted_at,
1135
+ expires_at,
1136
+ status,
1137
+ jira_ticket,
1138
+ };
1139
+ })
1140
+ .filter((ex) => !ex.expires_at || ex.expires_at > now);
1141
+ if (ruleKey) {
1142
+ return exceptions.filter((ex) => ex.rule_key === ruleKey);
1143
+ }
1144
+ return exceptions;
1145
+ }
1146
+ /**
1147
+ * Get all rule exceptions that are expiring within the given window (milliseconds).
1148
+ * Used by _meta to surface "exception expiring soon" warnings.
1149
+ */
1150
+ async getExpiringExceptions(windowMs) {
1151
+ const result = await this.query("?[key, ek, rk, reason, gb, ga, ea, status, jt] := *rule_exceptions[key, ek, rk, reason, gb, ga, ea, status, jt], status = 'active'");
1152
+ const now = Date.now();
1153
+ const threshold = new Date(now + windowMs).toISOString();
1154
+ const nowIso = new Date(now).toISOString();
1155
+ return result.rows
1156
+ .map((row) => {
1157
+ const [key, entity_key, rule_key, reason, granted_by, granted_at, expires_at, status, jira_ticket,] = row;
1158
+ return {
1159
+ key,
1160
+ entity_key,
1161
+ rule_key,
1162
+ reason,
1163
+ granted_by,
1164
+ granted_at,
1165
+ expires_at,
1166
+ status,
1167
+ jira_ticket,
1168
+ };
1169
+ })
1170
+ .filter((ex) => ex.expires_at && ex.expires_at > nowIso && ex.expires_at <= threshold);
1171
+ }
1172
+ // ── Drift Overlay CRUD ──────────────────────────────────────────
1173
+ /**
1174
+ * Insert or update an entity in the drift overlay.
1175
+ */
1176
+ async upsertDriftEntity(entity) {
1177
+ await this.write(`?[key, name, kind, signature, body, file_path, line_start, line_end, content_hash, drift_status, intent_id, modified_at, origin, previous_body, previous_signature] <-
1178
+ [[$key, $name, $kind, $sig, $body, $fp, $ls, $le, $ch, $ds, $iid, $ma, $origin, $pb, $ps]]
1179
+ :put drift_overlay { key => name, kind, signature, body, file_path, line_start, line_end, content_hash, drift_status, intent_id, modified_at, origin, previous_body, previous_signature }`, {
1180
+ key: entity.key,
1181
+ name: entity.name,
1182
+ kind: entity.kind,
1183
+ sig: entity.signature,
1184
+ body: entity.body,
1185
+ fp: entity.file_path,
1186
+ ls: entity.line_start,
1187
+ le: entity.line_end,
1188
+ ch: entity.content_hash,
1189
+ ds: entity.drift_status,
1190
+ iid: entity.intent_id,
1191
+ ma: entity.modified_at,
1192
+ origin: entity.origin,
1193
+ pb: entity.previous_body,
1194
+ ps: entity.previous_signature,
1195
+ });
1196
+ }
1197
+ /**
1198
+ * Remove an entity from the drift overlay.
1199
+ */
1200
+ async removeDriftEntity(key) {
1201
+ await this.write("?[key] <- [[$key]] :rm drift_overlay { key }", { key });
1202
+ }
1203
+ /**
1204
+ * Get all drift overlay entities for a given file path.
1205
+ */
1206
+ async getDriftEntitiesForFile(filePath) {
1207
+ const result = await this.query(`?[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps] :=
1208
+ *drift_overlay[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps],
1209
+ fp = $fp`, { fp: filePath });
1210
+ return result.rows.map((row) => {
1211
+ const [key, name, kind, signature, body, file_path, line_start, line_end, content_hash, drift_status, intent_id, modified_at, origin, previous_body, previous_signature,] = row;
1212
+ return {
1213
+ key,
1214
+ name,
1215
+ kind,
1216
+ signature,
1217
+ body,
1218
+ file_path,
1219
+ line_start,
1220
+ line_end,
1221
+ content_hash,
1222
+ drift_status: drift_status,
1223
+ intent_id,
1224
+ modified_at,
1225
+ origin: origin,
1226
+ previous_body,
1227
+ previous_signature,
1228
+ };
1229
+ });
1230
+ }
1231
+ /**
1232
+ * Get a single drift entity by key.
1233
+ */
1234
+ async getDriftEntity(key) {
1235
+ const result = await this.query(`?[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps] :=
1236
+ *drift_overlay[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps],
1237
+ key = $key`, { key });
1238
+ if (result.rows.length === 0)
1239
+ return null;
1240
+ const [k, name, kind, signature, body, file_path, line_start, line_end, content_hash, drift_status, intent_id, modified_at, origin, previous_body, previous_signature,] = result.rows[0];
1241
+ return {
1242
+ key: k,
1243
+ name,
1244
+ kind,
1245
+ signature,
1246
+ body,
1247
+ file_path,
1248
+ line_start,
1249
+ line_end,
1250
+ content_hash,
1251
+ drift_status: drift_status,
1252
+ intent_id,
1253
+ modified_at,
1254
+ origin: origin,
1255
+ previous_body,
1256
+ previous_signature,
1257
+ };
1258
+ }
1259
+ /**
1260
+ * Get all drift overlay entities (for stash snapshot serialization).
1261
+ */
1262
+ async getAllDriftEntities() {
1263
+ const result = await this.query(`?[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps] :=
1264
+ *drift_overlay[key, name, kind, sig, body, fp, ls, le, ch, ds, iid, ma, origin, pb, ps]`);
1265
+ return result.rows.map((row) => {
1266
+ const [key, name, kind, signature, body, file_path, line_start, line_end, content_hash, drift_status, intent_id, modified_at, origin, previous_body, previous_signature,] = row;
1267
+ return {
1268
+ key,
1269
+ name,
1270
+ kind,
1271
+ signature,
1272
+ body,
1273
+ file_path,
1274
+ line_start,
1275
+ line_end,
1276
+ content_hash,
1277
+ drift_status: drift_status,
1278
+ intent_id,
1279
+ modified_at,
1280
+ origin: origin,
1281
+ previous_body,
1282
+ previous_signature,
1283
+ };
1284
+ });
1285
+ }
1286
+ /**
1287
+ * Clear all drift overlay entries (branch switch, graph re-pull).
1288
+ */
1289
+ async clearDriftOverlay() {
1290
+ // Get all keys, then remove them
1291
+ const result = await this.write("?[key] := *drift_overlay[key, _, _, _, _, _, _, _, _, _, _, _, _, _, _]");
1292
+ for (const row of result.rows) {
1293
+ const [key] = row;
1294
+ await this.write("?[key] <- [[$key]] :rm drift_overlay { key }", {
1295
+ key,
1296
+ });
1297
+ }
1298
+ // Also clear drift edges (same lifecycle — Task 6.4)
1299
+ await this.clearDriftEdges();
1300
+ }
1301
+ // ── Sprint 6.4: Drift Edges CRUD ───────────────────────────────────
1302
+ /**
1303
+ * Insert or update an edge in the drift_edges overlay.
1304
+ */
1305
+ async upsertDriftEdge(edge) {
1306
+ await this.write(`?[from_key, to_key, type, drift_status, modified_at] <-
1307
+ [[$fk, $tk, $type, $ds, $ma]]
1308
+ :put drift_edges { from_key, to_key, type => drift_status, modified_at }`, {
1309
+ fk: edge.from_key,
1310
+ tk: edge.to_key,
1311
+ type: edge.type,
1312
+ ds: edge.drift_status,
1313
+ ma: edge.modified_at,
1314
+ });
1315
+ }
1316
+ /**
1317
+ * Remove an edge from the drift_edges overlay.
1318
+ */
1319
+ async removeDriftEdge(fromKey, toKey, type) {
1320
+ await this.write("?[from_key, to_key, type] <- [[$fk, $tk, $type]] :rm drift_edges { from_key, to_key, type }", { fk: fromKey, tk: toKey, type });
1321
+ }
1322
+ /**
1323
+ * Get all drift edges (for snapshot serialization).
1324
+ */
1325
+ async getAllDriftEdges() {
1326
+ const result = await this.query("?[fk, tk, type, ds, ma] := *drift_edges[fk, tk, type, ds, ma]");
1327
+ return result.rows.map((row) => {
1328
+ const [from_key, to_key, type, drift_status, modified_at] = row;
1329
+ return {
1330
+ from_key,
1331
+ to_key,
1332
+ type,
1333
+ drift_status: drift_status,
1334
+ modified_at,
1335
+ };
1336
+ });
1337
+ }
1338
+ /**
1339
+ * Clear all drift edges (branch switch, graph re-pull).
1340
+ */
1341
+ async clearDriftEdges() {
1342
+ const result = await this.write("?[fk, tk, type] := *drift_edges[fk, tk, type, _, _]");
1343
+ for (const row of result.rows) {
1344
+ const [fk, tk, type] = row;
1345
+ await this.write("?[from_key, to_key, type] <- [[$fk, $tk, $type]] :rm drift_edges { from_key, to_key, type }", { fk, tk, type });
1346
+ }
1347
+ }
1348
+ // ── Sprint E-8: Delta Application (P5-EVO-19) ─────────────────────
1349
+ /**
1350
+ * Apply an incremental delta from the server to the local CozoDB graph.
1351
+ * Called when a delta message is received via Redis pub/sub.
1352
+ *
1353
+ * Operations:
1354
+ * 1. Upsert added/updated entities into :entities + :file_index
1355
+ * 2. Remove deleted entity keys from :entities + :file_index
1356
+ * 3. Upsert added edges into :edges, remove old edges for changed entities
1357
+ * 4. Upsert updated justifications into :justifications
1358
+ * 5. Rebuild search tokens for affected entities (incremental)
1359
+ */
1360
+ async applyDelta(delta) {
1361
+ let applied = 0;
1362
+ let deleted = 0;
1363
+ let edgeCount = 0;
1364
+ let justCount = 0;
1365
+ // 1. Upsert added + updated entities
1366
+ const allEntities = [...delta.entities.added, ...delta.entities.updated];
1367
+ for (const entity of allEntities) {
1368
+ await this.write(`?[key, kind, name, file_path, start_line, end_line, signature, body, fan_in, fan_out, risk_level] <- [[$key, $kind, $name, $fp, $sl, $el, $sig, $body, $fi, $fo, $rl]]
1369
+ :put entities { key => kind, name, file_path, start_line, end_line, signature, body, fan_in, fan_out, risk_level }`, {
1370
+ key: entity.key,
1371
+ kind: entity.kind,
1372
+ name: entity.name,
1373
+ fp: entity.file_path,
1374
+ sl: entity.start_line ?? 0,
1375
+ el: entity.end_line ?? 0,
1376
+ sig: entity.signature ?? "",
1377
+ body: entity.body ?? "",
1378
+ fi: entity.fan_in ?? 0,
1379
+ fo: entity.fan_out ?? 0,
1380
+ rl: entity.risk_level ?? "normal",
1381
+ });
1382
+ // Update file index
1383
+ await this.write("?[file_path, entity_key] <- [[$fp, $key]] :put file_index { file_path, entity_key }", { fp: entity.file_path, key: entity.key });
1384
+ applied++;
1385
+ }
1386
+ // 2. Delete removed entities
1387
+ for (const key of delta.entities.deletedKeys) {
1388
+ try {
1389
+ await this.write("?[key] <- [[$key]] :rm entities { key }", { key });
1390
+ // Clean up file_index entries for this entity
1391
+ await this.write("?[fp, ek] := *file_index[fp, ek], ek = $key :rm file_index { file_path: fp, entity_key: ek }", { key });
1392
+ deleted++;
1393
+ }
1394
+ catch {
1395
+ // Entity may not exist locally — that's fine
1396
+ }
1397
+ }
1398
+ // 3. Remove old edges for changed entities, then insert new edges
1399
+ for (const edge of delta.edges.removed) {
1400
+ try {
1401
+ await this.write("?[from_key, to_key, type] <- [[$from, $to, $type]] :rm edges { from_key, to_key, type }", { from: edge.from_key, to: edge.to_key, type: edge.type });
1402
+ }
1403
+ catch {
1404
+ // Edge may not exist
1405
+ }
1406
+ }
1407
+ for (const edge of delta.edges.added) {
1408
+ await this.write("?[from_key, to_key, type] <- [[$from, $to, $type]] :put edges { from_key, to_key, type }", { from: edge.from_key, to: edge.to_key, type: edge.type });
1409
+ edgeCount++;
1410
+ }
1411
+ // 4. Upsert justifications
1412
+ for (const j of delta.justifications.updated) {
1413
+ await this.write(`?[entity_key, purpose, taxonomy, feature_area, confidence] <-
1414
+ [[$ek, $purpose, $taxonomy, $fa, $conf]]
1415
+ :put justifications { entity_key => purpose, taxonomy, feature_area, confidence }`, {
1416
+ ek: j.entity_key,
1417
+ purpose: j.purpose,
1418
+ taxonomy: j.taxonomy,
1419
+ fa: j.feature_area,
1420
+ conf: j.confidence,
1421
+ });
1422
+ justCount++;
1423
+ }
1424
+ // 5. Rebuild search tokens for affected entities (incremental)
1425
+ const affectedKeys = new Set([
1426
+ ...allEntities.map((e) => e.key),
1427
+ ...delta.entities.deletedKeys,
1428
+ ]);
1429
+ // Remove old search tokens for affected entities
1430
+ for (const key of affectedKeys) {
1431
+ try {
1432
+ await this.write("?[token, ek] := *search_tokens[token, ek], ek = $key :rm search_tokens { token, entity_key: ek }", { key });
1433
+ }
1434
+ catch {
1435
+ // May not exist
1436
+ }
1437
+ }
1438
+ // Insert new search tokens for added/updated entities
1439
+ for (const entity of allEntities) {
1440
+ const tokens = tokenize(entity.name);
1441
+ for (const token of tokens) {
1442
+ await this.write("?[token, entity_key] <- [[$token, $key]] :put search_tokens { token, entity_key }", { token, key: entity.key });
1443
+ }
1444
+ }
1445
+ // 6. Task 6.9: Prune stale overlay entries — base now matches local after delta
1446
+ let expired = 0;
1447
+ const overlayEntities = await this.getAllDriftEntities();
1448
+ for (const overlay of overlayEntities) {
1449
+ // Only prune modified entries (added/deleted have different semantics)
1450
+ if (overlay.drift_status !== "modified")
1451
+ continue;
1452
+ const base = await this.getEntity(overlay.key);
1453
+ if (base &&
1454
+ base.body === overlay.body &&
1455
+ base.signature === overlay.signature) {
1456
+ await this.removeDriftEntity(overlay.key);
1457
+ expired++;
1458
+ }
1459
+ }
1460
+ return {
1461
+ applied,
1462
+ deleted,
1463
+ edges: edgeCount,
1464
+ justifications: justCount,
1465
+ overlayExpired: expired,
1466
+ };
1467
+ }
1468
+ /**
1469
+ * Get aggregate counts of drift overlay entries by status.
1470
+ */
1471
+ async getDriftSummary() {
1472
+ const result = await this.query("?[ds, count(key)] := *drift_overlay[key, _, _, _, _, _, _, _, _, ds, _, _, _, _, _]");
1473
+ const summary = {
1474
+ added: 0,
1475
+ modified: 0,
1476
+ deleted: 0,
1477
+ dependency_changed: 0,
1478
+ total: 0,
1479
+ };
1480
+ for (const row of result.rows) {
1481
+ const [status, count] = row;
1482
+ if (status === "added")
1483
+ summary.added = count;
1484
+ else if (status === "modified")
1485
+ summary.modified = count;
1486
+ else if (status === "deleted")
1487
+ summary.deleted = count;
1488
+ else if (status === "dependency_changed")
1489
+ summary.dependency_changed = count;
1490
+ }
1491
+ summary.total =
1492
+ summary.added +
1493
+ summary.modified +
1494
+ summary.deleted +
1495
+ summary.dependency_changed;
1496
+ return summary;
1497
+ }
1498
+ // ── Sprint 11: Phase 22 Blueprint Deep Dive Methods ──────────────
1499
+ /**
1500
+ * Load blueprint project data into CozoDB (from Butter-Sync or snapshot).
1501
+ */
1502
+ async loadDeepDiveProject(project) {
1503
+ await this.write(`?[key, name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at] <-
1504
+ [[$key, $name, $description, $status, $domain, $stage, $stack_recommendation, $design_system, $health_baseline, $org_id, $updated_at]]
1505
+ :put deep_dive_projects { key => name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at }`, {
1506
+ key: project.key,
1507
+ name: project.name,
1508
+ description: project.description,
1509
+ status: project.status,
1510
+ domain: JSON.stringify(project.domain ?? {}),
1511
+ stage: JSON.stringify(project.stage ?? {}),
1512
+ stack_recommendation: JSON.stringify(project.stackRecommendation ?? {}),
1513
+ design_system: JSON.stringify(project.designSystem ?? ""),
1514
+ health_baseline: JSON.stringify(project.healthBaseline ?? {}),
1515
+ org_id: project.orgId ?? "",
1516
+ updated_at: project.updatedAt ?? new Date().toISOString(),
1517
+ });
1518
+ }
1519
+ /**
1520
+ * Load blueprint slices for a project.
1521
+ */
1522
+ async loadDeepDiveSlices(slices) {
1523
+ for (const s of slices) {
1524
+ await this.write(`?[key, project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design] <-
1525
+ [[$key, $project_key, $name, $description, $repo_target_id, $parent_slice_key, $dependencies, $status, $order, $slice_type, $data_model, $api_surface, $conventions, $boundary_rules, $user_flows, $ui_design]]
1526
+ :put deep_dive_slices { key => project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design }`, {
1527
+ key: s.key,
1528
+ project_key: s.projectKey,
1529
+ name: s.name,
1530
+ description: s.description ?? "",
1531
+ repo_target_id: s.repoTargetId ?? "",
1532
+ parent_slice_key: s.parentSliceKey ?? "",
1533
+ dependencies: JSON.stringify(s.dependencies ?? []),
1534
+ status: s.status ?? "planned",
1535
+ order: s.order ?? 0,
1536
+ slice_type: s.sliceType ?? "",
1537
+ data_model: JSON.stringify(s.dataModel ?? ""),
1538
+ api_surface: JSON.stringify(s.apiSurface ?? ""),
1539
+ conventions: JSON.stringify(s.conventions ?? []),
1540
+ boundary_rules: JSON.stringify(s.boundaryRules ?? []),
1541
+ user_flows: JSON.stringify(s.userFlows ?? ""),
1542
+ ui_design: JSON.stringify(s.uiDesign ?? ""),
1543
+ });
1544
+ }
1545
+ }
1546
+ /**
1547
+ * Load blueprint tasks for a project.
1548
+ */
1549
+ async loadDeepDiveTasks(tasks) {
1550
+ for (const t of tasks) {
1551
+ await this.write(`?[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint] <-
1552
+ [[$key, $project_key, $sprint_number, $slice_name, $description, $status, $estimated_effort, $dependencies, $boundary_rules, $conventions, $acceptance_criteria, $completed_at, $completed_files, $checkpoint]]
1553
+ :put deep_dive_tasks { key => project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint }`, {
1554
+ key: t.key,
1555
+ project_key: t.projectKey,
1556
+ sprint_number: t.sprintNumber,
1557
+ slice_name: t.sliceName ?? "",
1558
+ description: t.description ?? "",
1559
+ status: t.status ?? "pending",
1560
+ estimated_effort: t.estimatedEffort ?? "",
1561
+ dependencies: JSON.stringify(t.dependencies ?? []),
1562
+ boundary_rules: JSON.stringify(t.boundaryRules ?? []),
1563
+ conventions: JSON.stringify(t.conventions ?? []),
1564
+ acceptance_criteria: JSON.stringify(t.acceptanceCriteria ?? []),
1565
+ completed_at: t.completedAt ?? "",
1566
+ completed_files: JSON.stringify(t.completedFiles ?? []),
1567
+ checkpoint: JSON.stringify(t.checkpoint ?? {}),
1568
+ });
1569
+ }
1570
+ }
1571
+ /**
1572
+ * Load design system tokens for a project.
1573
+ */
1574
+ async loadDeepDiveDesignSystem(projectKey, tokens) {
1575
+ await this.write(`?[project_key, tokens, updated_at] <-
1576
+ [[$project_key, $tokens, $updated_at]]
1577
+ :put deep_dive_design_system { project_key => tokens, updated_at }`, {
1578
+ project_key: projectKey,
1579
+ tokens: JSON.stringify(tokens ?? {}),
1580
+ updated_at: new Date().toISOString(),
1581
+ });
1582
+ }
1583
+ /**
1584
+ * Get a blueprint project by key.
1585
+ */
1586
+ async getDeepDiveProject(key) {
1587
+ const result = await this.query(`?[key, name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at] :=
1588
+ *deep_dive_projects[key, name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at],
1589
+ key = $key`, { key });
1590
+ if (result.rows.length === 0)
1591
+ return null;
1592
+ const row = result.rows[0];
1593
+ return {
1594
+ key: row[0],
1595
+ name: row[1],
1596
+ description: row[2],
1597
+ status: row[3],
1598
+ domain: JSON.parse(row[4] || "{}"),
1599
+ stage: JSON.parse(row[5] || "{}"),
1600
+ stackRecommendation: JSON.parse(row[6] || "{}"),
1601
+ designSystem: row[7] ? JSON.parse(row[7]) : null,
1602
+ healthBaseline: JSON.parse(row[8] || "{}"),
1603
+ orgId: row[9],
1604
+ updatedAt: row[10],
1605
+ };
1606
+ }
1607
+ /**
1608
+ * Get the first active project (most recently updated).
1609
+ */
1610
+ async getActiveDeepDiveProject() {
1611
+ const result = await this.query(`?[key, name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at] :=
1612
+ *deep_dive_projects[key, name, description, status, domain, stage, stack_recommendation, design_system, health_baseline, org_id, updated_at],
1613
+ status != "draft"
1614
+ :sort -updated_at
1615
+ :limit 1`);
1616
+ if (result.rows.length === 0)
1617
+ return null;
1618
+ const row = result.rows[0];
1619
+ return {
1620
+ key: row[0],
1621
+ name: row[1],
1622
+ description: row[2],
1623
+ status: row[3],
1624
+ domain: JSON.parse(row[4] || "{}"),
1625
+ stage: JSON.parse(row[5] || "{}"),
1626
+ stackRecommendation: JSON.parse(row[6] || "{}"),
1627
+ designSystem: row[7] ? JSON.parse(row[7]) : null,
1628
+ healthBaseline: JSON.parse(row[8] || "{}"),
1629
+ orgId: row[9],
1630
+ updatedAt: row[10],
1631
+ };
1632
+ }
1633
+ /**
1634
+ * Get all slices for a project, ordered by their order field.
1635
+ */
1636
+ async getDeepDiveSlices(projectKey) {
1637
+ const result = await this.query(`?[key, project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design] :=
1638
+ *deep_dive_slices[key, project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design],
1639
+ project_key = $project_key
1640
+ :sort order`, { project_key: projectKey });
1641
+ return result.rows.map((row) => {
1642
+ const [key, , name, description, repoTargetId, parentSliceKey, deps, status, order, sliceType, dataModel, apiSurface, conventions, boundaryRules, userFlows, uiDesign,] = row;
1643
+ return {
1644
+ key,
1645
+ name,
1646
+ description,
1647
+ repoTargetId,
1648
+ parentSliceKey,
1649
+ dependencies: JSON.parse(deps || "[]"),
1650
+ status,
1651
+ order,
1652
+ sliceType,
1653
+ dataModel: dataModel ? JSON.parse(dataModel) : null,
1654
+ apiSurface: apiSurface ? JSON.parse(apiSurface) : null,
1655
+ conventions: JSON.parse(conventions || "[]"),
1656
+ boundaryRules: JSON.parse(boundaryRules || "[]"),
1657
+ userFlows: userFlows ? JSON.parse(userFlows) : null,
1658
+ uiDesign: uiDesign ? JSON.parse(uiDesign) : null,
1659
+ };
1660
+ });
1661
+ }
1662
+ /**
1663
+ * Get a single slice by key.
1664
+ */
1665
+ async getDeepDiveSlice(key) {
1666
+ const result = await this.query(`?[key, project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design] :=
1667
+ *deep_dive_slices[key, project_key, name, description, repo_target_id, parent_slice_key, dependencies, status, order, slice_type, data_model, api_surface, conventions, boundary_rules, user_flows, ui_design],
1668
+ key = $key`, { key });
1669
+ if (result.rows.length === 0)
1670
+ return null;
1671
+ const [k, , name, description, repoTargetId, parentSliceKey, deps, status, order, sliceType, dataModel, apiSurface, conventions, boundaryRules, userFlows, uiDesign,] = result.rows[0];
1672
+ return {
1673
+ key: k,
1674
+ name,
1675
+ description,
1676
+ repoTargetId,
1677
+ parentSliceKey,
1678
+ dependencies: JSON.parse(deps || "[]"),
1679
+ status,
1680
+ order,
1681
+ sliceType,
1682
+ dataModel: dataModel ? JSON.parse(dataModel) : null,
1683
+ apiSurface: apiSurface ? JSON.parse(apiSurface) : null,
1684
+ conventions: JSON.parse(conventions || "[]"),
1685
+ boundaryRules: JSON.parse(boundaryRules || "[]"),
1686
+ userFlows: userFlows ? JSON.parse(userFlows) : null,
1687
+ uiDesign: uiDesign ? JSON.parse(uiDesign) : null,
1688
+ };
1689
+ }
1690
+ /**
1691
+ * Get all tasks for a project, optionally filtered by sprint number.
1692
+ */
1693
+ async getDeepDiveTasks(projectKey, sprintNumber) {
1694
+ const query = sprintNumber != null
1695
+ ? `?[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint] :=
1696
+ *deep_dive_tasks[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint],
1697
+ project_key = $project_key, sprint_number = $sprint_number
1698
+ :sort key`
1699
+ : `?[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint] :=
1700
+ *deep_dive_tasks[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint],
1701
+ project_key = $project_key
1702
+ :sort sprint_number, key`;
1703
+ const params = { project_key: projectKey };
1704
+ if (sprintNumber != null)
1705
+ params.sprint_number = sprintNumber;
1706
+ const result = await this.query(query, params);
1707
+ return result.rows.map((row) => {
1708
+ const [key, , sprintNum, sliceName, description, status, estimatedEffort, deps, bRules, convs, criteria, completedAt, completedFiles, checkpoint,] = row;
1709
+ return {
1710
+ key,
1711
+ sprintNumber: sprintNum,
1712
+ sliceName,
1713
+ description,
1714
+ status,
1715
+ estimatedEffort,
1716
+ dependencies: JSON.parse(deps || "[]"),
1717
+ boundaryRules: JSON.parse(bRules || "[]"),
1718
+ conventions: JSON.parse(convs || "[]"),
1719
+ acceptanceCriteria: JSON.parse(criteria || "[]"),
1720
+ completedAt,
1721
+ completedFiles: JSON.parse(completedFiles || "[]"),
1722
+ checkpoint: JSON.parse(checkpoint || "{}"),
1723
+ };
1724
+ });
1725
+ }
1726
+ /**
1727
+ * Mark a task as complete in the local graph.
1728
+ */
1729
+ async completeDeepDiveTask(taskKey, completedFiles) {
1730
+ const result = await this.query(`?[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint] :=
1731
+ *deep_dive_tasks[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint],
1732
+ key = $key`, { key: taskKey });
1733
+ if (result.rows.length === 0)
1734
+ return false;
1735
+ const row = result.rows[0];
1736
+ await this.write(`?[key, project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint] <-
1737
+ [[$key, $project_key, $sprint_number, $slice_name, $description, "complete", $estimated_effort, $dependencies, $boundary_rules, $conventions, $acceptance_criteria, $completed_at, $completed_files, $checkpoint]]
1738
+ :put deep_dive_tasks { key => project_key, sprint_number, slice_name, description, status, estimated_effort, dependencies, boundary_rules, conventions, acceptance_criteria, completed_at, completed_files, checkpoint }`, {
1739
+ key: taskKey,
1740
+ project_key: row[1],
1741
+ sprint_number: row[2],
1742
+ slice_name: row[3],
1743
+ description: row[4],
1744
+ estimated_effort: row[6],
1745
+ dependencies: row[7],
1746
+ boundary_rules: row[8],
1747
+ conventions: row[9],
1748
+ acceptance_criteria: row[10],
1749
+ completed_at: new Date().toISOString(),
1750
+ completed_files: JSON.stringify(completedFiles ?? []),
1751
+ checkpoint: row[13],
1752
+ });
1753
+ return true;
1754
+ }
1755
+ /**
1756
+ * Get design system tokens for a project.
1757
+ */
1758
+ async getDeepDiveDesignSystem(projectKey) {
1759
+ const result = await this.query(`?[project_key, tokens, updated_at] :=
1760
+ *deep_dive_design_system[project_key, tokens, updated_at],
1761
+ project_key = $project_key`, { project_key: projectKey });
1762
+ if (result.rows.length === 0)
1763
+ return null;
1764
+ const [, tokens] = result.rows[0];
1765
+ return JSON.parse(tokens || "{}");
1766
+ }
1767
+ /**
1768
+ * Check if any deep dive project exists (for dynamic tool loading).
1769
+ */
1770
+ async hasDeepDiveProject() {
1771
+ const result = await this.query("?[key] := *deep_dive_projects[key, _, _, _, _, _, _, _, _, _, _] :limit 1");
1772
+ return result.rows.length > 0;
1773
+ }
1774
+ /**
1775
+ * Get deep dive project state for dynamic tool loading.
1776
+ * Returns: "none" | "pre_approval" | "approved" | "building"
1777
+ */
1778
+ async getDeepDiveProjectState() {
1779
+ const project = await this.getActiveDeepDiveProject();
1780
+ if (!project)
1781
+ return "none";
1782
+ if (project.status === "building")
1783
+ return "building";
1784
+ if (project.status === "approved")
1785
+ return "approved";
1786
+ return "pre_approval";
1787
+ }
1788
+ // ── Leapfrog Sprint B: Correction Intelligence ─────────────────
1789
+ /**
1790
+ * Persist correction patterns from the detector into the CozoDB corrections relation.
1791
+ * Uses :put for upsert behavior — re-running the detector updates existing patterns
1792
+ * with higher occurrences and latest timestamps.
1793
+ */
1794
+ async persistCorrections(patterns) {
1795
+ for (const p of patterns) {
1796
+ // Check if existing pattern exists to merge occurrences
1797
+ const existing = await this.query(`?[entity_key, error_type, correction_summary, confidence, occurrences, last_seen] :=
1798
+ *corrections{entity_key: $ek, error_type: $et, correction_summary, confidence, occurrences, last_seen}`, { ek: p.entity_key, et: p.error_type });
1799
+ const prevOcc = existing.rows.length > 0 ? existing.rows[0]?.[4] : 0;
1800
+ const prevConf = existing.rows.length > 0 ? existing.rows[0]?.[3] : 0;
1801
+ await this.write(`?[entity_key, error_type, correction_summary, confidence, occurrences, last_seen] <-
1802
+ [[$ek, $et, $cs, $conf, $occ, $ls]]
1803
+ :put corrections {
1804
+ entity_key, error_type
1805
+ =>
1806
+ correction_summary, confidence, occurrences, last_seen
1807
+ }`, {
1808
+ ek: p.entity_key,
1809
+ et: p.error_type,
1810
+ cs: p.correction_summary,
1811
+ conf: Math.max(p.confidence, prevConf),
1812
+ occ: prevOcc + p.occurrences,
1813
+ ls: p.last_seen,
1814
+ });
1815
+ }
1816
+ }
1817
+ /**
1818
+ * Get corrections for a specific entity, filtered by minimum confidence.
1819
+ * Returns results sorted by confidence descending. <1ms (indexed query).
1820
+ */
1821
+ async getCorrections(entityKey, minConfidence = 0.7) {
1822
+ const result = await this.query(`?[entity_key, error_type, correction_summary, confidence, occurrences, last_seen] :=
1823
+ *corrections{entity_key, error_type, correction_summary, confidence, occurrences, last_seen},
1824
+ entity_key = $ek,
1825
+ confidence >= $min_conf
1826
+ :order -confidence`, { ek: entityKey, min_conf: minConfidence });
1827
+ return result.rows.map((row) => ({
1828
+ entity_key: row[0],
1829
+ error_type: row[1],
1830
+ correction_summary: row[2],
1831
+ confidence: row[3],
1832
+ occurrences: row[4],
1833
+ last_seen: row[5],
1834
+ }));
1835
+ }
1836
+ /**
1837
+ * Get all corrections above a confidence threshold.
1838
+ */
1839
+ async getAllCorrections(minConfidence = 0.7) {
1840
+ const result = await this.query(`?[entity_key, error_type, correction_summary, confidence, occurrences, last_seen] :=
1841
+ *corrections{entity_key, error_type, correction_summary, confidence, occurrences, last_seen},
1842
+ confidence >= $min_conf
1843
+ :order -confidence`, { min_conf: minConfidence });
1844
+ return result.rows.map((row) => ({
1845
+ entity_key: row[0],
1846
+ error_type: row[1],
1847
+ correction_summary: row[2],
1848
+ confidence: row[3],
1849
+ occurrences: row[4],
1850
+ last_seen: row[5],
1851
+ }));
1852
+ }
1853
+ /**
1854
+ * Local get_project_stats — pure CozoDB Datalog aggregation.
1855
+ * Returns entity/edge/file/rule/drift counts and breakdown by kind.
1856
+ * Target: <10ms.
1857
+ */
1858
+ async getLocalProjectStats() {
1859
+ const entityCount = (await this.query("?[count(key)] := *entities{key}"))
1860
+ .rows[0]?.[0] ?? 0;
1861
+ const edgeCount = (await this.query("?[count(from_key)] := *edges{from_key}"))
1862
+ .rows[0]?.[0] ?? 0;
1863
+ const fileCount = (await this.query("?[count_unique(file_path)] := *entities{key, file_path}")).rows[0]?.[0] ?? 0;
1864
+ const ruleCount = (await this.query("?[count(key)] := *rules{key, enabled}, enabled = true")).rows[0]?.[0] ?? 0;
1865
+ const driftCount = (await this.query("?[count(key)] := *drift_overlay{key}"))
1866
+ .rows[0]?.[0] ?? 0;
1867
+ const communityCount = (await this.query("?[count(id)] := *communities{id}"))
1868
+ .rows[0]?.[0] ?? 0;
1869
+ const correctionCount = (await this.query("?[count(entity_key)] := *corrections{entity_key}"))
1870
+ .rows[0]?.[0] ?? 0;
1871
+ // Entity breakdown by kind
1872
+ const entityByKind = {};
1873
+ const kindRows = (await this.query("?[kind, count(key)] := *entities{key, kind}")).rows;
1874
+ for (const row of kindRows) {
1875
+ entityByKind[row[0]] = row[1];
1876
+ }
1877
+ // Edge breakdown by type
1878
+ const edgeByType = {};
1879
+ const typeRows = (await this.query("?[type, count(from_key)] := *edges{from_key, type}")).rows;
1880
+ for (const row of typeRows) {
1881
+ edgeByType[row[0]] = row[1];
1882
+ }
1883
+ // Top 10 files by entity count
1884
+ const topFileRows = (await this.query("?[file_path, count(entity_key)] := *file_index{file_path, entity_key} :order -count(entity_key) :limit 10")).rows;
1885
+ const topFiles = topFileRows.map((row) => ({
1886
+ filePath: row[0],
1887
+ entityCount: row[1],
1888
+ }));
1889
+ // Language breakdown — aggregate unique file paths by extension.
1890
+ // CozoDB Datalog has no string-split, so we post-process in JS.
1891
+ const languageBreakdown = {};
1892
+ const filePathRows = (await this.query("?[file_path] := *file_index{file_path, entity_key}")).rows;
1893
+ const seenPaths = new Set();
1894
+ for (const row of filePathRows) {
1895
+ const filePath = row[0];
1896
+ if (seenPaths.has(filePath))
1897
+ continue;
1898
+ seenPaths.add(filePath);
1899
+ const lastDot = filePath.lastIndexOf(".");
1900
+ const lastSlash = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
1901
+ const ext = lastDot > lastSlash && lastDot >= 0
1902
+ ? filePath.slice(lastDot + 1).toLowerCase()
1903
+ : "other";
1904
+ languageBreakdown[ext] = (languageBreakdown[ext] ?? 0) + 1;
1905
+ }
1906
+ return {
1907
+ entityCount,
1908
+ edgeCount,
1909
+ fileCount,
1910
+ ruleCount,
1911
+ driftCount,
1912
+ communityCount,
1913
+ correctionCount,
1914
+ entityByKind,
1915
+ edgeByType,
1916
+ languageBreakdown,
1917
+ topFiles,
1918
+ };
1919
+ }
1920
+ /**
1921
+ * Remove corrections not seen in the last N days.
1922
+ * Returns the number of pruned entries.
1923
+ */
1924
+ async pruneCorrections(staleDays = 30) {
1925
+ const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
1926
+ const stale = await this.write(`?[entity_key, error_type] :=
1927
+ *corrections{entity_key, error_type, last_seen},
1928
+ last_seen < $cutoff`, { cutoff });
1929
+ let pruned = 0;
1930
+ for (const row of stale.rows) {
1931
+ await this.write(`?[entity_key, error_type] <- [[$ek, $et]]
1932
+ :rm corrections {entity_key, error_type}`, { ek: row[0], et: row[1] });
1933
+ pruned++;
1934
+ }
1935
+ return pruned;
1936
+ }
1937
+ }
1938
+ /** Simple glob matching — supports * and ** patterns. */
1939
+ function matchGlob(filePath, glob) {
1940
+ const regex = glob
1941
+ .replace(/\./g, "\\.")
1942
+ .replace(/\*\*/g, "{{GLOBSTAR}}")
1943
+ .replace(/\*/g, "[^/]*")
1944
+ .replace(/\{\{GLOBSTAR\}\}/g, ".*");
1945
+ return new RegExp(`^${regex}$`).test(filePath);
1946
+ }