devcouncil 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,329 @@
1
+ """Extraction cache v5 — symbols, calls, import_details, modules/specs keyed by sha256."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Set, cast
10
+
11
+ from devcouncil.indexing.graph.extract_python import (
12
+ ExtractedCall,
13
+ ExtractedImport,
14
+ ExtractedSymbol,
15
+ FileExtraction,
16
+ )
17
+ from devcouncil.indexing.graph.extract_ts import extract_file
18
+ from devcouncil.utils.fsio import atomic_write_json
19
+ from devcouncil.utils.json_persist import read_json
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # v1 = Python modules; v2 = + JS/TS specs; v3 = + symbols + calls;
24
+ # v4 = + import_details (named imports / alias maps) for warm/cold edge parity;
25
+ # v5 = + references (non-call name/attribute loads) for callback-aware liveness;
26
+ # v6 = generic-extractor callee-style qualname hints (owner hints poisoned
27
+ # same-file call resolution with self-loops).
28
+ # v8 = declarator-chain name recovery for C-family definitions (the language
29
+ # pack returns name=None, dropping every C/C++/ObjC symbol) + grammar
30
+ # availability folded into the cache identity.
31
+ PARSE_CACHE_VERSION = 8
32
+
33
+ # Keys preserved across partial updates (RepoMapper modules/specs vs graph symbols).
34
+ _PRESERVE_KEYS = (
35
+ "symbols",
36
+ "calls",
37
+ "all_exports",
38
+ "reexports",
39
+ "references",
40
+ "language",
41
+ "import_details",
42
+ "modules",
43
+ "specs",
44
+ )
45
+
46
+
47
+ def cache_path(root: Path) -> Path:
48
+ return root / ".devcouncil" / "cache" / "repo_map_parse.json"
49
+
50
+
51
+ def load_parse_cache(root: Path) -> Dict[str, Dict[str, Any]]:
52
+ """Load cache. Version mismatch → empty dict (caller rebuilds in place)."""
53
+ try:
54
+ data = read_json(cache_path(root))
55
+ if data.get("version") == PARSE_CACHE_VERSION and isinstance(data.get("files"), dict):
56
+ return cast(Dict[str, Dict[str, Any]], data["files"])
57
+ except Exception:
58
+ pass
59
+ return {}
60
+
61
+
62
+ def save_parse_cache(root: Path, files: Dict[str, Dict[str, Any]]) -> None:
63
+ try:
64
+ path = cache_path(root)
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ atomic_write_json(path, {"version": PARSE_CACHE_VERSION, "files": files})
67
+ except Exception:
68
+ logger.debug("Failed to write graph parse cache", exc_info=True)
69
+
70
+
71
+ def merge_parse_cache(
72
+ root: Path,
73
+ updates: Dict[str, Dict[str, Any]],
74
+ managed: Set[str],
75
+ ) -> None:
76
+ """Merge updates for ``managed`` paths; preserve sibling-language / sibling-field keys.
77
+
78
+ Paths in ``managed`` but absent from ``updates`` are pruned. When an update
79
+ omits graph or import fields, existing values from the prior entry are kept
80
+ so RepoMapper modules/specs passes and graph extract passes share one store.
81
+ """
82
+ cache = load_parse_cache(root)
83
+ merged = {k: v for k, v in cache.items() if k not in managed}
84
+ for path, entry in updates.items():
85
+ prev = cache.get(path)
86
+ if isinstance(prev, dict) and isinstance(entry, dict):
87
+ for key in _PRESERVE_KEYS:
88
+ if key not in entry and key in prev:
89
+ entry = {**entry, key: prev[key]}
90
+ merged[path] = entry
91
+ if merged != cache:
92
+ save_parse_cache(root, merged)
93
+
94
+
95
+ def _symbol_to_dict(s: ExtractedSymbol) -> Dict[str, Any]:
96
+ return {
97
+ "kind": s.kind,
98
+ "name": s.name,
99
+ "qualname": s.qualname,
100
+ "line": s.line,
101
+ "end_line": s.end_line,
102
+ "bases": list(s.bases),
103
+ "implements": list(getattr(s, "implements", []) or []),
104
+ "decorators": list(s.decorators),
105
+ "exported": s.exported,
106
+ }
107
+
108
+
109
+ def _call_to_dict(c: ExtractedCall) -> Dict[str, Any]:
110
+ return {
111
+ "name": c.name,
112
+ "line": c.line,
113
+ "receiver": c.receiver,
114
+ "qualname_hint": c.qualname_hint,
115
+ }
116
+
117
+
118
+ def _import_to_dict(imp: ExtractedImport) -> Dict[str, Any]:
119
+ return {
120
+ "module": imp.module,
121
+ "names": list(imp.names),
122
+ "alias_map": dict(imp.alias_map),
123
+ }
124
+
125
+
126
+ def _symbol_from_dict(d: Dict[str, Any]) -> ExtractedSymbol:
127
+ return ExtractedSymbol(
128
+ kind=str(d.get("kind") or "function"),
129
+ name=str(d.get("name") or ""),
130
+ qualname=str(d.get("qualname") or d.get("name") or ""),
131
+ line=int(d.get("line") or 0),
132
+ end_line=int(d.get("end_line") or d.get("line") or 0),
133
+ bases=list(d.get("bases") or []),
134
+ implements=list(d.get("implements") or []),
135
+ decorators=list(d.get("decorators") or []),
136
+ exported=bool(d.get("exported", False)),
137
+ )
138
+
139
+
140
+ def _call_from_dict(d: Dict[str, Any]) -> ExtractedCall:
141
+ return ExtractedCall(
142
+ name=str(d.get("name") or ""),
143
+ line=int(d.get("line") or 0),
144
+ receiver=str(d.get("receiver") or ""),
145
+ qualname_hint=str(d.get("qualname_hint") or ""),
146
+ )
147
+
148
+
149
+ def _import_from_dict(d: Dict[str, Any]) -> ExtractedImport:
150
+ alias_raw = d.get("alias_map") or {}
151
+ alias_map = {
152
+ str(k): str(v) for k, v in alias_raw.items()
153
+ } if isinstance(alias_raw, dict) else {}
154
+ return ExtractedImport(
155
+ module=str(d.get("module") or ""),
156
+ names=[n for n in (d.get("names") or []) if isinstance(n, str)],
157
+ alias_map=alias_map,
158
+ )
159
+
160
+
161
+ def extraction_to_cache_entry(ext: FileExtraction, digest: str) -> Dict[str, Any]:
162
+ entry: Dict[str, Any] = {
163
+ "sha256": digest,
164
+ "symbols": [_symbol_to_dict(s) for s in ext.symbols],
165
+ "calls": [_call_to_dict(c) for c in ext.calls],
166
+ "import_details": [_import_to_dict(i) for i in ext.import_details],
167
+ "all_exports": list(ext.all_exports),
168
+ "reexports": list(ext.reexports),
169
+ "references": list(ext.references),
170
+ "language": ext.language,
171
+ }
172
+ if ext.path.endswith(".py"):
173
+ entry["modules"] = list(ext.imports)
174
+ elif ext.path.endswith(".go"):
175
+ entry["specs"] = list(ext.imports)
176
+ else:
177
+ entry["specs"] = list(ext.imports)
178
+ return entry
179
+
180
+
181
+ def extraction_from_cache_entry(path: str, entry: Dict[str, Any]) -> FileExtraction:
182
+ imports: List[str] = []
183
+ if isinstance(entry.get("modules"), list):
184
+ imports = [m for m in entry["modules"] if isinstance(m, str)]
185
+ elif isinstance(entry.get("specs"), list):
186
+ imports = [m for m in entry["specs"] if isinstance(m, str)]
187
+ symbols = [
188
+ _symbol_from_dict(s)
189
+ for s in (entry.get("symbols") or [])
190
+ if isinstance(s, dict)
191
+ ]
192
+ calls = [
193
+ _call_from_dict(c)
194
+ for c in (entry.get("calls") or [])
195
+ if isinstance(c, dict)
196
+ ]
197
+ import_details = [
198
+ _import_from_dict(i)
199
+ for i in (entry.get("import_details") or [])
200
+ if isinstance(i, dict)
201
+ ]
202
+ return FileExtraction(
203
+ path=path,
204
+ language=str(entry.get("language") or ""),
205
+ imports=imports,
206
+ import_details=import_details,
207
+ symbols=symbols,
208
+ calls=calls,
209
+ all_exports=[x for x in (entry.get("all_exports") or []) if isinstance(x, str)],
210
+ reexports=[x for x in (entry.get("reexports") or []) if isinstance(x, str)],
211
+ references=[x for x in (entry.get("references") or []) if isinstance(x, str)],
212
+ )
213
+
214
+
215
+ def extract_cached(
216
+ root: Path,
217
+ path: str,
218
+ *,
219
+ cache: Optional[Dict[str, Dict[str, Any]]] = None,
220
+ force: bool = False,
221
+ ) -> tuple[FileExtraction, Dict[str, Any]]:
222
+ """Extract one file, using cache when sha256 matches.
223
+
224
+ Returns ``(extraction, cache_entry)``.
225
+ """
226
+ cache = cache if cache is not None else load_parse_cache(root)
227
+ try:
228
+ raw = (root / path).read_bytes()
229
+ except OSError:
230
+ empty = FileExtraction(path=path, language="")
231
+ return empty, {
232
+ "sha256": "",
233
+ "modules": [],
234
+ "symbols": [],
235
+ "calls": [],
236
+ "import_details": [],
237
+ }
238
+ digest = hashlib.sha256(raw).hexdigest()
239
+ grammar = _grammar_identity(path)
240
+ config_hash = f"parse-cache-v{PARSE_CACHE_VERSION}"
241
+ try:
242
+ from devcouncil.codeintel import get_codeintel_service
243
+
244
+ store = get_codeintel_service(root).store
245
+ payload = store.get_extraction(
246
+ content_hash=digest,
247
+ language=grammar[0],
248
+ grammar_version=grammar[1],
249
+ config_hash=config_hash,
250
+ )
251
+ if payload is not None and not force:
252
+ stored_entry = json.loads(payload.decode("utf-8"))
253
+ if isinstance(stored_entry, dict):
254
+ return extraction_from_cache_entry(path, stored_entry), stored_entry
255
+ except Exception:
256
+ logger.debug("SQLite extraction-cache read failed", exc_info=True)
257
+ entry = cache.get(path)
258
+ if (
259
+ not force
260
+ and isinstance(entry, dict)
261
+ and entry.get("sha256") == digest
262
+ and isinstance(entry.get("symbols"), list)
263
+ and isinstance(entry.get("calls"), list)
264
+ and isinstance(entry.get("import_details"), list)
265
+ ):
266
+ return extraction_from_cache_entry(path, entry), entry
267
+ source = raw.decode("utf-8", errors="replace")
268
+ try:
269
+ ext = extract_file(path, source)
270
+ except Exception:
271
+ # One pathological file (e.g. RecursionError on absurd nesting) must not
272
+ # fail every subsequent full build. Index it as opaque; sha256 stays ""
273
+ # so the failure is never treated as a valid warm cache entry.
274
+ logger.warning("extraction failed for %s; indexing as opaque file", path, exc_info=True)
275
+ empty = FileExtraction(path=path, language="")
276
+ return empty, {
277
+ "sha256": "",
278
+ "modules": [],
279
+ "symbols": [],
280
+ "calls": [],
281
+ "import_details": [],
282
+ }
283
+ fresh = extraction_to_cache_entry(ext, digest)
284
+ try:
285
+ from devcouncil.codeintel import get_codeintel_service
286
+
287
+ get_codeintel_service(root).store.put_extraction(
288
+ content_hash=digest,
289
+ language=grammar[0],
290
+ grammar_version=grammar[1],
291
+ config_hash=config_hash,
292
+ payload=json.dumps(fresh, sort_keys=True, separators=(",", ":")).encode("utf-8"),
293
+ )
294
+ except Exception:
295
+ logger.debug("SQLite extraction-cache write failed", exc_info=True)
296
+ return ext, fresh
297
+
298
+
299
+ _GRAMMAR_PROBE_CACHE: Dict[str, bool] = {}
300
+
301
+
302
+ def _grammar_identity(path: str) -> tuple[str, str]:
303
+ from devcouncil.codeintel.languages import detect_language
304
+
305
+ spec = detect_language(path)
306
+ language = spec.grammar if spec is not None else Path(path).suffix.lower().lstrip(".")
307
+ if language == "python":
308
+ return language, "stdlib-ast"
309
+ try:
310
+ import tree_sitter_language_pack as pack
311
+ except ImportError:
312
+ return language, "unavailable"
313
+ version = str(getattr(pack, "__version__", "unknown"))
314
+ # Per-grammar availability must be part of the identity: installing or
315
+ # removing the grammar companion changes extraction output without
316
+ # changing the pack version, and a version-only key would keep serving
317
+ # pre-grammar (empty/regex) extractions forever.
318
+ loadable = _GRAMMAR_PROBE_CACHE.get(language)
319
+ if loadable is None:
320
+ try:
321
+ from devcouncil.codeintel.languages.workers import _activate_companion_once
322
+
323
+ _activate_companion_once()
324
+ pack.get_language(language)
325
+ loadable = True
326
+ except Exception:
327
+ loadable = False
328
+ _GRAMMAR_PROBE_CACHE[language] = loadable
329
+ return language, f"{version}:{'native' if loadable else 'fallback'}"
@@ -0,0 +1,28 @@
1
+ """Community labeling helpers (leaf module — no intel/verifier imports)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+
7
+ from devcouncil.indexing.graph.schema import CodeGraph, GraphNode, NodeKind
8
+
9
+
10
+ def _is_file_node(n: GraphNode) -> bool:
11
+ kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
12
+ return kind == NodeKind.FILE.value or ("::" not in n.id and not n.name)
13
+
14
+
15
+ def community_label_for_area(graph: CodeGraph, area: str) -> str:
16
+ """Dominant community label among file nodes under ``area`` (generic subsystems)."""
17
+ counts: Counter[str] = Counter()
18
+ prefix = area.replace("\\", "/").rstrip("/") + "/"
19
+ for n in graph.nodes:
20
+ if not _is_file_node(n):
21
+ continue
22
+ path = (n.path or n.id).replace("\\", "/")
23
+ if path == area or path.startswith(prefix):
24
+ if n.community:
25
+ counts[n.community] += 1
26
+ if not counts:
27
+ return ""
28
+ return counts.most_common(1)[0][0]
@@ -0,0 +1,107 @@
1
+ """Minimal openCypher-style subset over the native in-memory code graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ _SUPPORTED_REL = frozenset({
10
+ "CALLS", "IMPORTS", "CONTAINS", "REFERENCES", "IMPLEMENTS", "EXTENDS", "DECORATES",
11
+ })
12
+
13
+ _MATCH_RE = re.compile(
14
+ r"^\s*MATCH\s+\(a(?::(?P<alabel>[\w]+))?\)"
15
+ r"(?:-\[r:(?P<rel>[\w|]+)\]->\(b(?::(?P<blabel>[\w]+))?\))?"
16
+ r"(?:\s+WHERE\s+(?P<where>.+?))?"
17
+ r"\s+RETURN\s+(?P<ret>.+?)(?:\s+LIMIT\s+(?P<limit>\d+))?\s*$",
18
+ re.IGNORECASE | re.DOTALL,
19
+ )
20
+
21
+
22
+ def _parse_where(clause: str) -> Tuple[Optional[str], Optional[str]]:
23
+ if not clause:
24
+ return None, None
25
+ lowered = clause.strip().lower()
26
+ if "a.name" in lowered and "contains" in lowered:
27
+ m = re.search(r"contains\s*\(\s*a\.name\s*,\s*['\"]([^'\"]+)['\"]\s*\)", clause, re.I)
28
+ if m:
29
+ return m.group(1), None
30
+ if "b.path" in lowered and "starts with" in lowered:
31
+ m = re.search(r"starts\s+with\s*\(\s*b\.path\s*,\s*['\"]([^'\"]+)['\"]\s*\)", clause, re.I)
32
+ if m:
33
+ return None, m.group(1)
34
+ return None, None
35
+
36
+
37
+ def run_cypher(project_root: Path, query: str, *, default_limit: int = 50) -> Dict[str, Any]:
38
+ """Execute a supported MATCH … RETURN subset; reject unsupported clauses."""
39
+ normalized = " ".join(query.split())
40
+ if re.search(r"\b(CREATE|DELETE|MERGE|SET|REMOVE|DETACH)\b", normalized, re.I):
41
+ return {"ok": False, "error": "Mutating Cypher clauses are not supported."}
42
+ m = _MATCH_RE.match(normalized)
43
+ if not m:
44
+ return {
45
+ "ok": False,
46
+ "error": (
47
+ "Unsupported Cypher. Supported: "
48
+ "MATCH (a)-[r:CALLS|IMPORTS|...]->(b) WHERE … RETURN a,b LIMIT n"
49
+ ),
50
+ }
51
+ rels = m.group("rel") or ""
52
+ rel_types = {r.strip().upper() for r in rels.split("|") if r.strip()}
53
+ for rel in rel_types:
54
+ if rel not in _SUPPORTED_REL:
55
+ return {"ok": False, "error": f"Unsupported relationship type: {rel}"}
56
+ limit = int(m.group("limit") or default_limit)
57
+ name_filter, path_prefix = _parse_where(m.group("where") or "")
58
+
59
+ from devcouncil.codeintel.query.engine import CodeIntelQueryEngine
60
+
61
+ try:
62
+ graph = CodeIntelQueryEngine(project_root)._graph()
63
+ except FileNotFoundError:
64
+ return {"ok": False, "error": "No committed graph generation."}
65
+
66
+ nodes_by_id = {n.id: n for n in graph.nodes}
67
+ rows: List[Dict[str, Any]] = []
68
+
69
+ if rel_types:
70
+ for edge in graph.edges:
71
+ if edge.kind.upper() not in rel_types:
72
+ continue
73
+ a = nodes_by_id.get(edge.source)
74
+ b = nodes_by_id.get(edge.target)
75
+ if a is None or b is None:
76
+ continue
77
+ if name_filter and name_filter.lower() not in a.name.lower():
78
+ continue
79
+ if path_prefix and not b.path.startswith(path_prefix):
80
+ continue
81
+ rows.append({
82
+ "a_id": a.id,
83
+ "a_name": a.name,
84
+ "a_path": a.path,
85
+ "a_kind": a.kind,
86
+ "rel": edge.kind,
87
+ "b_id": b.id,
88
+ "b_name": b.name,
89
+ "b_path": b.path,
90
+ "b_kind": b.kind,
91
+ })
92
+ if len(rows) >= limit:
93
+ break
94
+ else:
95
+ for node in graph.nodes:
96
+ if name_filter and name_filter.lower() not in node.name.lower():
97
+ continue
98
+ rows.append({
99
+ "a_id": node.id,
100
+ "a_name": node.name,
101
+ "a_path": node.path,
102
+ "a_kind": node.kind,
103
+ })
104
+ if len(rows) >= limit:
105
+ break
106
+
107
+ return {"ok": True, "rows": rows, "count": len(rows)}
@@ -0,0 +1,194 @@
1
+ """Opt-in local embeddings for semantic graph search (hash fallback)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import logging
8
+ import math
9
+ import re
10
+ import sqlite3
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _EMBEDDINGS_TABLE = "symbol_embeddings"
17
+
18
+
19
+ def _index_db(project_root: Path) -> Path:
20
+ return project_root / ".devcouncil" / "codeintel" / "index.sqlite"
21
+
22
+
23
+ def _tokenize(text: str) -> List[str]:
24
+ return [t for t in re.split(r"[^a-zA-Z0-9_]+", text.lower()) if len(t) > 1]
25
+
26
+
27
+ def _hash_vector(tokens: List[str], dims: int = 64) -> List[float]:
28
+ vec = [0.0] * dims
29
+ for token in tokens:
30
+ digest = hashlib.sha256(token.encode("utf-8")).digest()
31
+ for i in range(dims):
32
+ vec[i] += (digest[i % len(digest)] - 128) / 128.0
33
+ norm = math.sqrt(sum(v * v for v in vec)) or 1.0
34
+ return [v / norm for v in vec]
35
+
36
+
37
+ def _cosine(a: List[float], b: List[float]) -> float:
38
+ return sum(x * y for x, y in zip(a, b))
39
+
40
+
41
+ def _current_generation(project_root: Path) -> int | None:
42
+ try:
43
+ from devcouncil.codeintel import get_codeintel_service
44
+
45
+ return get_codeintel_service(project_root).store.current_generation()
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ def ensure_embeddings_schema(project_root: Path) -> None:
51
+ db = _index_db(project_root)
52
+ db.parent.mkdir(parents=True, exist_ok=True)
53
+ with sqlite3.connect(db) as conn:
54
+ conn.execute(
55
+ f"""
56
+ CREATE TABLE IF NOT EXISTS {_EMBEDDINGS_TABLE} (
57
+ node_id TEXT NOT NULL,
58
+ path TEXT NOT NULL,
59
+ label TEXT NOT NULL,
60
+ model TEXT NOT NULL,
61
+ vector_json TEXT NOT NULL,
62
+ generation_id INTEGER NOT NULL DEFAULT 0,
63
+ PRIMARY KEY (generation_id, node_id)
64
+ )
65
+ """
66
+ )
67
+ cols = {
68
+ str(row[1])
69
+ for row in conn.execute(f"PRAGMA table_info({_EMBEDDINGS_TABLE})").fetchall()
70
+ }
71
+ if "generation_id" not in cols:
72
+ # Legacy single-generation table: rebuild on next build_embeddings.
73
+ conn.execute(f"ALTER TABLE {_EMBEDDINGS_TABLE} ADD COLUMN generation_id INTEGER NOT NULL DEFAULT 0")
74
+ conn.execute(
75
+ f"CREATE INDEX IF NOT EXISTS idx_{_EMBEDDINGS_TABLE}_gen "
76
+ f"ON {_EMBEDDINGS_TABLE}(generation_id)"
77
+ )
78
+ conn.commit()
79
+
80
+
81
+ def embeddings_enabled(project_root: Path) -> bool:
82
+ try:
83
+ from devcouncil.app.config import load_config
84
+
85
+ return bool(load_config(project_root).indexing.embeddings.enabled)
86
+ except Exception:
87
+ return False
88
+
89
+
90
+ def build_embeddings(project_root: Path) -> int:
91
+ """Rebuild symbol embeddings for the committed codeintel generation."""
92
+ if not embeddings_enabled(project_root):
93
+ return 0
94
+ ensure_embeddings_schema(project_root)
95
+ generation = _current_generation(project_root)
96
+ if generation is None:
97
+ return 0
98
+ from devcouncil.codeintel.query.engine import CodeIntelQueryEngine
99
+
100
+ try:
101
+ graph = CodeIntelQueryEngine(project_root)._graph()
102
+ except FileNotFoundError:
103
+ return 0
104
+ model = "hash-v1"
105
+ try:
106
+ from devcouncil.app.config import load_config
107
+
108
+ model = load_config(project_root).indexing.embeddings.model_name or model
109
+ except Exception:
110
+ pass
111
+ count = 0
112
+ try:
113
+ with sqlite3.connect(_index_db(project_root)) as conn:
114
+ conn.execute(
115
+ f"DELETE FROM {_EMBEDDINGS_TABLE} WHERE generation_id=?",
116
+ (generation,),
117
+ )
118
+ # Drop older generations; keep only current + previous.
119
+ conn.execute(
120
+ f"DELETE FROM {_EMBEDDINGS_TABLE} WHERE generation_id NOT IN (?, ?)",
121
+ (generation, max(0, generation - 1)),
122
+ )
123
+ for node in graph.nodes:
124
+ label = f"{node.path} {node.name} {node.kind}"
125
+ vec = _hash_vector(_tokenize(label))
126
+ conn.execute(
127
+ f"INSERT OR REPLACE INTO {_EMBEDDINGS_TABLE} "
128
+ f"(node_id, path, label, model, vector_json, generation_id) "
129
+ f"VALUES (?, ?, ?, ?, ?, ?)",
130
+ (node.id, node.path, node.name, model, json.dumps(vec), generation),
131
+ )
132
+ count += 1
133
+ conn.commit()
134
+ except sqlite3.Error:
135
+ logger.warning("embedding rebuild failed; continuing without embeddings", exc_info=True)
136
+ return 0
137
+ return count
138
+
139
+
140
+ def semantic_search(
141
+ project_root: Path,
142
+ query: str,
143
+ *,
144
+ limit: int = 50,
145
+ ) -> Dict[str, Any]:
146
+ """Semantic search over stored embeddings; empty when disabled."""
147
+ if not embeddings_enabled(project_root):
148
+ return {"ok": False, "reason": "embeddings disabled", "matches": []}
149
+ ensure_embeddings_schema(project_root)
150
+ generation = _current_generation(project_root)
151
+ qvec = _hash_vector(_tokenize(query))
152
+ matches: List[Dict[str, Any]] = []
153
+ scanned = 0
154
+ max_scan = max(500, limit * 200)
155
+ try:
156
+ with sqlite3.connect(_index_db(project_root)) as conn:
157
+ if generation is None:
158
+ rows = conn.execute(
159
+ f"SELECT node_id, path, label, model, vector_json FROM {_EMBEDDINGS_TABLE}"
160
+ )
161
+ else:
162
+ rows = conn.execute(
163
+ f"SELECT node_id, path, label, model, vector_json FROM {_EMBEDDINGS_TABLE} "
164
+ f"WHERE generation_id=?",
165
+ (generation,),
166
+ )
167
+ for node_id, path, label, model, vector_json in rows:
168
+ scanned += 1
169
+ if scanned > max_scan:
170
+ break
171
+ try:
172
+ vec = json.loads(vector_json)
173
+ except json.JSONDecodeError:
174
+ continue
175
+ score = _cosine(qvec, vec)
176
+ matches.append({
177
+ "id": node_id,
178
+ "path": path,
179
+ "label": label,
180
+ "score": round(score, 4),
181
+ "model": model,
182
+ })
183
+ except sqlite3.Error as exc:
184
+ logger.warning("semantic search failed: %s", exc)
185
+ return {"ok": False, "reason": f"embeddings store error: {exc}", "matches": []}
186
+ matches.sort(key=lambda item: item["score"], reverse=True)
187
+ return {
188
+ "ok": True,
189
+ "matches": matches[: max(1, limit)],
190
+ "scanned": scanned,
191
+ "truncated": scanned > max_scan,
192
+ "generation": generation,
193
+ "backend": "hash-v1",
194
+ }