devcouncil 0.3.0 → 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,676 @@
1
+ """Read-only MCP tools for querying ``.devcouncil/repo_map.json``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mcp.types import TextContent
10
+
11
+ from devcouncil.indexing.repo_mapper import RepoMapper
12
+ from devcouncil.indexing.subsystem_map import (
13
+ area_for_path,
14
+ cross_boundary_pairs,
15
+ dead_symbol_candidates_of,
16
+ dependents_total_of,
17
+ impact_targets,
18
+ is_entry_root,
19
+ unreachable_of,
20
+ unwired_candidates_of,
21
+ )
22
+ from devcouncil.integrations.mcp.util import (
23
+ error_text,
24
+ json_text,
25
+ optional_bool_argument,
26
+ optional_string_argument,
27
+ optional_string_list_argument,
28
+ with_codeintel_freshness,
29
+ )
30
+ from devcouncil.utils.json_persist import read_json
31
+
32
+
33
+ def _load_repo_map(root: Path) -> dict[str, Any] | None:
34
+ map_path = root / ".devcouncil" / "repo_map.json"
35
+ if not map_path.exists():
36
+ return None
37
+ try:
38
+ data = read_json(map_path)
39
+ return data if isinstance(data, dict) else None
40
+ except Exception:
41
+ return None
42
+
43
+
44
+ def _map_stale(root: Path, data: dict[str, Any] | None) -> bool:
45
+ if not data:
46
+ return False
47
+ try:
48
+ return RepoMapper(root).map_is_stale(data)
49
+ except Exception:
50
+ # Fail closed: unverifiable map must not be treated as fresh.
51
+ return True
52
+
53
+
54
+ def _graph_degraded_fields(root: Path) -> dict[str, Any]:
55
+ """Surface lean-map handshake on legacy graph tool responses."""
56
+ data = _load_repo_map(root) or {}
57
+ degraded = bool(data.get("graph_degraded"))
58
+ fields: dict[str, Any] = {"graph_degraded": degraded}
59
+ if degraded:
60
+ fields["graph_degraded_reason"] = str(data.get("graph_degraded_reason") or "")
61
+ return fields
62
+
63
+
64
+ def _subsystem_summary(sub: dict[str, Any]) -> dict[str, Any]:
65
+ return {
66
+ "area": sub.get("area"),
67
+ "summary": sub.get("summary") or "",
68
+ }
69
+
70
+
71
+ def _subsystem_detail(sub: dict[str, Any]) -> dict[str, Any]:
72
+ role_files = sub.get("role_files") or {}
73
+ if not isinstance(role_files, dict):
74
+ role_files = {}
75
+ return {
76
+ "area": sub.get("area"),
77
+ "summary": sub.get("summary") or "",
78
+ "entry_points": list(sub.get("entry_points") or []),
79
+ "critical_files": list(sub.get("critical_files") or []),
80
+ "neighbors": list(sub.get("neighbors") or []),
81
+ "handoff_paths": list(sub.get("handoff_paths") or []),
82
+ "role_files": {str(k): list(v or []) for k, v in role_files.items()},
83
+ }
84
+
85
+
86
+ def _find_subsystem(data: dict[str, Any], area: str) -> dict[str, Any] | None:
87
+ for sub in data.get("subsystems") or []:
88
+ if isinstance(sub, dict) and str(sub.get("area") or "") == area:
89
+ return sub
90
+ return None
91
+
92
+
93
+ def _path_from_dead_symbol(entry: str) -> str:
94
+ """Extract the file path from a ``path:line name`` dead-symbol entry."""
95
+ loc = entry.split(" ", 1)[0]
96
+ path, _, _ = loc.rpartition(":")
97
+ return path.replace("\\", "/")
98
+
99
+
100
+ def _matches_filters(
101
+ path: str,
102
+ data: dict[str, Any],
103
+ *,
104
+ area: str | None,
105
+ path_prefix: str | None,
106
+ ) -> bool:
107
+ norm = path.replace("\\", "/")
108
+ if path_prefix:
109
+ prefix = path_prefix.replace("\\", "/").rstrip("/")
110
+ if not (norm == prefix or norm.startswith(prefix + "/")):
111
+ return False
112
+ if area:
113
+ return area_for_path(norm, data) == area
114
+ return True
115
+
116
+
117
+ def _filter_paths(
118
+ paths: list[str],
119
+ data: dict[str, Any],
120
+ *,
121
+ area: str | None,
122
+ path_prefix: str | None,
123
+ ) -> list[str]:
124
+ if not area and not path_prefix:
125
+ return paths
126
+ return [p for p in paths if _matches_filters(p, data, area=area, path_prefix=path_prefix)]
127
+
128
+
129
+ def _filter_dead_symbols(
130
+ entries: list[str],
131
+ data: dict[str, Any],
132
+ *,
133
+ area: str | None,
134
+ path_prefix: str | None,
135
+ ) -> list[str]:
136
+ if not area and not path_prefix:
137
+ return entries
138
+ return [
139
+ e
140
+ for e in entries
141
+ if _matches_filters(_path_from_dead_symbol(e), data, area=area, path_prefix=path_prefix)
142
+ ]
143
+
144
+
145
+ async def handle_repo_map(root: Path, arguments: dict) -> list[TextContent]:
146
+ async def _run() -> list[TextContent]:
147
+ subsystem = optional_string_argument(arguments, "subsystem")
148
+ path = optional_string_argument(arguments, "path")
149
+ for arg_name, value in [("subsystem", subsystem), ("path", path)]:
150
+ if value == "":
151
+ return error_text(
152
+ f"{arg_name} must be a string",
153
+ code="invalid_arguments",
154
+ argument=arg_name,
155
+ )
156
+
157
+ data = _load_repo_map(root)
158
+ if data is None:
159
+ return error_text(
160
+ "No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
161
+ code="map_missing",
162
+ )
163
+
164
+ stale = _map_stale(root, data)
165
+ resolved_area: str | None = None
166
+ if path:
167
+ resolved_area = area_for_path(path, data)
168
+ if not subsystem and resolved_area:
169
+ subsystem = resolved_area
170
+
171
+ if subsystem:
172
+ sub = _find_subsystem(data, subsystem)
173
+ if sub is None:
174
+ return json_text({
175
+ "ok": True,
176
+ "stale": stale,
177
+ "subsystem": None,
178
+ "path": path,
179
+ "area": resolved_area,
180
+ "error": f"Unknown subsystem area: {subsystem}",
181
+ "code": "unknown_subsystem",
182
+ })
183
+ return json_text({
184
+ "ok": True,
185
+ "stale": stale,
186
+ "path": path,
187
+ "area": resolved_area or subsystem,
188
+ "subsystem": _subsystem_detail(sub),
189
+ })
190
+
191
+ subsystems = [
192
+ _subsystem_summary(s)
193
+ for s in (data.get("subsystems") or [])
194
+ if isinstance(s, dict) and s.get("area")
195
+ ]
196
+ return json_text({
197
+ "ok": True,
198
+ "stale": stale,
199
+ "languages": list(data.get("languages") or []),
200
+ "frameworks": list(data.get("frameworks") or []),
201
+ "package_managers": list(data.get("package_managers") or []),
202
+ "subsystems": subsystems,
203
+ "path": path,
204
+ "area": resolved_area,
205
+ "symbols": _symbols_for_path(root, path) if path else [],
206
+ })
207
+
208
+ return await with_codeintel_freshness(root, _run)
209
+
210
+
211
+ def _symbols_for_path(root: Path, path: str) -> list[dict[str, Any]]:
212
+ """Per-path symbol listings from the code graph (empty when unavailable)."""
213
+ try:
214
+ from devcouncil.indexing.graph.build import load_code_graph
215
+
216
+ graph = load_code_graph(root)
217
+ if graph is None:
218
+ return []
219
+ norm = path.replace("\\", "/")
220
+ out = []
221
+ for n in graph.nodes:
222
+ if n.path != norm:
223
+ continue
224
+ kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
225
+ if kind == "file":
226
+ continue
227
+ out.append(
228
+ {
229
+ "id": n.id,
230
+ "kind": kind,
231
+ "name": n.name,
232
+ "line": n.line,
233
+ }
234
+ )
235
+ return out[:200]
236
+ except Exception:
237
+ return []
238
+
239
+
240
+ async def handle_impact(root: Path, arguments: dict) -> list[TextContent]:
241
+ async def _run() -> list[TextContent]:
242
+ paths, list_error = optional_string_list_argument(arguments, "paths")
243
+ if list_error:
244
+ return list_error
245
+ if not paths:
246
+ return error_text("Missing paths", code="missing_argument", argument="paths")
247
+ precise, precise_error = optional_bool_argument(arguments, "precise")
248
+ if precise_error:
249
+ return precise_error
250
+ precise = bool(precise)
251
+
252
+ data = _load_repo_map(root)
253
+ if data is None:
254
+ return error_text(
255
+ "No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
256
+ code="map_missing",
257
+ )
258
+
259
+ stale = _map_stale(root, data)
260
+ lsp_pool = None
261
+ if precise:
262
+ try:
263
+ from devcouncil.indexing.lsp_client import LspSessionPool
264
+
265
+ lsp_pool = LspSessionPool(root)
266
+ except Exception:
267
+ lsp_pool = None
268
+
269
+ try:
270
+ items: list[dict[str, Any]] = []
271
+ all_neighbor_areas: set[str] = set()
272
+ for raw in paths:
273
+ path = raw.replace("\\", "/")
274
+ dependents, neighbors = impact_targets(path, data)
275
+ resolution = "import"
276
+ if precise and lsp_pool is not None:
277
+ try:
278
+ lsp_deps = lsp_pool.dependents_of_file(path)
279
+ if lsp_deps is not None:
280
+ dependents = lsp_deps
281
+ resolution = "lsp"
282
+ except Exception:
283
+ pass
284
+ area = area_for_path(path, data)
285
+ all_neighbor_areas.update(neighbors)
286
+ item: dict[str, Any] = {
287
+ "path": path,
288
+ "area": area,
289
+ "is_entry_root": is_entry_root(path, data),
290
+ "dependents": dependents,
291
+ "neighbors": neighbors,
292
+ }
293
+ dep_total = dependents_total_of(path, data)
294
+ if dep_total is not None and dep_total > len(dependents):
295
+ item["dependents_total"] = dep_total
296
+ if precise:
297
+ item["resolution"] = resolution
298
+ items.append(item)
299
+
300
+ crossings = [
301
+ {"areas": [a, b]}
302
+ for a, b in cross_boundary_pairs(paths, data)
303
+ ]
304
+ payload: dict[str, Any] = {
305
+ "ok": True,
306
+ "stale": stale,
307
+ "paths": items,
308
+ "neighbor_areas": sorted(all_neighbor_areas),
309
+ "cross_boundary_pairs": crossings,
310
+ }
311
+ if precise:
312
+ payload["precise"] = True
313
+ return json_text(payload)
314
+ finally:
315
+ if lsp_pool is not None:
316
+ lsp_pool.close()
317
+
318
+ return await with_codeintel_freshness(root, _run)
319
+
320
+
321
+ async def handle_liveness(root: Path, arguments: dict) -> list[TextContent]:
322
+ async def _run() -> list[TextContent]:
323
+ area = optional_string_argument(arguments, "area")
324
+ path_prefix = optional_string_argument(arguments, "path_prefix")
325
+ min_confidence = optional_string_argument(arguments, "min_confidence") or "inferred"
326
+ for arg_name, value in [
327
+ ("area", area),
328
+ ("path_prefix", path_prefix),
329
+ ("min_confidence", min_confidence),
330
+ ]:
331
+ if value == "":
332
+ return error_text(
333
+ f"{arg_name} must be a string",
334
+ code="invalid_arguments",
335
+ argument=arg_name,
336
+ )
337
+ if min_confidence not in {"extracted", "inferred", "ambiguous"}:
338
+ return error_text(
339
+ "min_confidence must be extracted|inferred|ambiguous",
340
+ code="invalid_arguments",
341
+ argument="min_confidence",
342
+ )
343
+
344
+ data = _load_repo_map(root)
345
+ if data is None:
346
+ return error_text(
347
+ "No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
348
+ code="map_missing",
349
+ )
350
+
351
+ stale = _map_stale(root, data)
352
+ raw_roots = data.get("entry_roots") or []
353
+ if not isinstance(raw_roots, list):
354
+ raw_roots = []
355
+ global_entry_roots = [str(p) for p in raw_roots]
356
+
357
+ unwired = _filter_paths(
358
+ unwired_candidates_of(data), data, area=area, path_prefix=path_prefix
359
+ )
360
+ unreachable = _filter_paths(
361
+ unreachable_of(data), data, area=area, path_prefix=path_prefix
362
+ )
363
+ dead_symbols = _filter_dead_symbols(
364
+ dead_symbol_candidates_of(data), data, area=area, path_prefix=path_prefix
365
+ )
366
+ entry_roots = global_entry_roots
367
+ if area or path_prefix:
368
+ entry_roots = _filter_paths(entry_roots, data, area=area, path_prefix=path_prefix)
369
+
370
+ dead_code, hidden = _structured_dead_code(
371
+ root,
372
+ area=area,
373
+ path_prefix=path_prefix,
374
+ min_confidence=min_confidence,
375
+ )
376
+ # Reliability is a GLOBAL property of the map: an area/path_prefix scope that
377
+ # merely contains no entry root (normal for library subsystems) must not
378
+ # flip the response to unreliable and drop its unreachable list.
379
+ unreachable_unreliable = (
380
+ data.get("liveness_unreachable_unreliable") is True
381
+ or not global_entry_roots
382
+ )
383
+ warn = None
384
+ if unreachable_unreliable:
385
+ unreachable = []
386
+ warn = (
387
+ "entry_roots empty or liveness_unreachable_unreliable: "
388
+ "unreachable_files omitted; ignore mass inferred dead"
389
+ )
390
+ if data.get("graph_degraded"):
391
+ warn = (
392
+ (warn + "; " if warn else "")
393
+ + "graph_degraded: lean map — treat liveness/dead tiers as unreliable"
394
+ )
395
+ payload: dict[str, object] = {
396
+ "ok": True,
397
+ "stale": stale,
398
+ "graph_degraded": bool(data.get("graph_degraded")),
399
+ "area": area,
400
+ "path_prefix": path_prefix,
401
+ "min_confidence": min_confidence,
402
+ "entry_roots": entry_roots,
403
+ "unwired_candidates": unwired,
404
+ "unreachable_files": unreachable,
405
+ "dead_symbol_candidates": dead_symbols,
406
+ "dead_code": dead_code,
407
+ "dead_code_hidden": hidden,
408
+ "unreachable_unreliable": unreachable_unreliable
409
+ or bool(data.get("graph_degraded")),
410
+ }
411
+ if warn:
412
+ payload["warning"] = warn
413
+ return json_text(payload)
414
+
415
+ return await with_codeintel_freshness(root, _run)
416
+
417
+
418
+ def _structured_dead_code(
419
+ root: Path,
420
+ *,
421
+ area: str | None,
422
+ path_prefix: str | None,
423
+ min_confidence: str = "inferred",
424
+ ) -> tuple[list[dict[str, Any]], int]:
425
+ try:
426
+ from devcouncil.indexing.graph.build import load_code_graph
427
+ from devcouncil.indexing.graph.liveness import confidence_at_least
428
+
429
+ graph = load_code_graph(root)
430
+ if graph is None:
431
+ return [], 0
432
+ data = _load_repo_map(root) or {}
433
+ matched: list[dict[str, Any]] = []
434
+ hidden = 0
435
+ for d in graph.dead_code:
436
+ if not _matches_filters(d.path, data, area=area, path_prefix=path_prefix):
437
+ continue
438
+ if not confidence_at_least(d.confidence, min_confidence):
439
+ hidden += 1
440
+ continue
441
+ matched.append(d.model_dump())
442
+ return matched[:200], hidden
443
+ except Exception:
444
+ return [], 0
445
+
446
+
447
+ async def handle_graph_ingest(root: Path, arguments: dict) -> list[TextContent]:
448
+ paths, _ = optional_string_list_argument(arguments, "paths")
449
+ from devcouncil.indexing.map_artifacts import refresh_map_artifacts
450
+ from devcouncil.codeintel import get_codeintel_service
451
+ from devcouncil.codeintel.build_control import GraphBuildBusy
452
+ from devcouncil.codeintel.sync import get_sync_coordinator
453
+ from devcouncil.indexing.graph.embeddings import build_embeddings
454
+
455
+ coordinator = get_sync_coordinator(root)
456
+ changed = list(paths or [])
457
+ map_path = root / ".devcouncil" / "repo_map.json"
458
+ if paths is None:
459
+ try:
460
+ refresh = await asyncio.to_thread(
461
+ refresh_map_artifacts,
462
+ root,
463
+ map_path,
464
+ quiet=True,
465
+ )
466
+ except GraphBuildBusy as exc:
467
+ return error_text(str(exc), code="graph_writer_busy")
468
+ else:
469
+ try:
470
+ ok = await asyncio.to_thread(coordinator.sync_now, changed)
471
+ if not ok:
472
+ state = coordinator.status().as_dict()
473
+ return error_text(
474
+ str(state.get("last_error") or state.get("degraded_reason") or "sync failed"),
475
+ code="codeintel_sync_failed",
476
+ **state,
477
+ )
478
+ refresh = await asyncio.to_thread(
479
+ refresh_map_artifacts,
480
+ root,
481
+ map_path,
482
+ quiet=True,
483
+ graph=get_codeintel_service(root).load(),
484
+ paths=changed,
485
+ )
486
+ except GraphBuildBusy as exc:
487
+ # Same structured error as the full-refresh branch — a held writer
488
+ # lease must not surface as an unhandled MCP exception.
489
+ return error_text(str(exc), code="graph_writer_busy")
490
+ embedded = await asyncio.to_thread(build_embeddings, root)
491
+ return json_text({
492
+ "ok": not refresh.degraded,
493
+ "paths": changed,
494
+ "embeddings_built": embedded,
495
+ "generation": refresh.generation,
496
+ "mode": refresh.mode,
497
+ "degraded": refresh.degraded,
498
+ "reason": refresh.reason,
499
+ })
500
+
501
+
502
+ async def handle_graph_cypher(root: Path, arguments: dict) -> list[TextContent]:
503
+ async def _run() -> list[TextContent]:
504
+ query = optional_string_argument(arguments, "query")
505
+ if not query:
506
+ return error_text("Missing query", code="missing_argument", argument="query")
507
+ from devcouncil.indexing.graph.cypher import run_cypher
508
+
509
+ return json_text(run_cypher(root, query))
510
+
511
+ return await with_codeintel_freshness(root, _run)
512
+
513
+
514
+ async def handle_pdg_query(root: Path, arguments: dict) -> list[TextContent]:
515
+ async def _run() -> list[TextContent]:
516
+ mode = optional_string_argument(arguments, "mode")
517
+ target = optional_string_argument(arguments, "target")
518
+ if not mode:
519
+ return error_text("Missing mode", code="missing_argument", argument="mode")
520
+ if not target:
521
+ return error_text("Missing target", code="missing_argument", argument="target")
522
+ variable = optional_string_argument(arguments, "variable")
523
+ from devcouncil.indexing.graph.query import query_pdg_controls, query_pdg_flows
524
+
525
+ if mode == "controls":
526
+ return json_text(query_pdg_controls(root, target))
527
+ if mode == "flows":
528
+ return json_text(query_pdg_flows(root, target, variable=variable or None))
529
+ return error_text(
530
+ "mode must be controls or flows",
531
+ code="invalid_arguments",
532
+ argument="mode",
533
+ )
534
+
535
+ return await with_codeintel_freshness(root, _run)
536
+
537
+
538
+ async def handle_explain(root: Path, arguments: dict) -> list[TextContent]:
539
+ async def _run() -> list[TextContent]:
540
+ path = optional_string_argument(arguments, "path")
541
+ category = optional_string_argument(arguments, "category")
542
+ from devcouncil.indexing.graph.query import explain_pdg_taint
543
+
544
+ return json_text(explain_pdg_taint(root, path=path or None, category=category or None))
545
+
546
+ return await with_codeintel_freshness(root, _run)
547
+
548
+
549
+ async def handle_graph_query(root: Path, arguments: dict) -> list[TextContent]:
550
+ async def _run() -> list[TextContent]:
551
+ name = optional_string_argument(arguments, "name_or_path")
552
+ if not name:
553
+ return error_text(
554
+ "Missing name_or_path", code="missing_argument", argument="name_or_path"
555
+ )
556
+ from devcouncil.indexing.graph import query_symbol
557
+
558
+ return json_text({"ok": True, **query_symbol(root, name), **_graph_degraded_fields(root)})
559
+
560
+ return await with_codeintel_freshness(root, _run)
561
+
562
+
563
+ async def handle_graph_trace(root: Path, arguments: dict) -> list[TextContent]:
564
+ async def _run() -> list[TextContent]:
565
+ start = optional_string_argument(arguments, "from")
566
+ end = optional_string_argument(arguments, "to")
567
+ if not start:
568
+ return error_text("Missing from", code="missing_argument", argument="from")
569
+ if not end:
570
+ return error_text("Missing to", code="missing_argument", argument="to")
571
+ from devcouncil.indexing.graph import trace_path
572
+
573
+ return json_text({"ok": True, **trace_path(root, start, end), **_graph_degraded_fields(root)})
574
+
575
+ return await with_codeintel_freshness(root, _run)
576
+
577
+
578
+ async def handle_graph_impact(root: Path, arguments: dict) -> list[TextContent]:
579
+ """Symbol-level blast radius from paths or working-tree diff (code graph)."""
580
+
581
+ async def _run() -> list[TextContent]:
582
+ paths, list_error = optional_string_list_argument(arguments, "paths")
583
+ if list_error:
584
+ return list_error
585
+ use_diff, diff_error = optional_bool_argument(arguments, "diff")
586
+ if diff_error:
587
+ return diff_error
588
+ use_diff = bool(use_diff)
589
+ if not use_diff and not paths:
590
+ return error_text(
591
+ "Provide paths or set diff=true",
592
+ code="missing_argument",
593
+ argument="paths",
594
+ )
595
+
596
+ from devcouncil.indexing.graph.build import load_code_graph
597
+ from devcouncil.indexing.graph.intel import diff_impact
598
+
599
+ graph = load_code_graph(root)
600
+ if graph is None:
601
+ return error_text(
602
+ "No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
603
+ code="graph_missing",
604
+ )
605
+ result = diff_impact(
606
+ root,
607
+ graph,
608
+ paths=paths,
609
+ use_diff=use_diff,
610
+ max_depth=3,
611
+ )
612
+ return json_text({"ok": True, **result, **_graph_degraded_fields(root)})
613
+
614
+ return await with_codeintel_freshness(root, _run)
615
+
616
+
617
+ async def handle_route_map(root: Path, arguments: dict) -> list[TextContent]:
618
+ async def _run() -> list[TextContent]:
619
+ from devcouncil.indexing.graph.api_routes import route_map
620
+ from devcouncil.indexing.graph.build import load_code_graph
621
+
622
+ graph = load_code_graph(root)
623
+ if graph is None:
624
+ return error_text(
625
+ "No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
626
+ code="graph_missing",
627
+ )
628
+ return json_text({"ok": True, **route_map(root, graph), **_graph_degraded_fields(root)})
629
+
630
+ return await with_codeintel_freshness(root, _run)
631
+
632
+
633
+ async def handle_shape_check(root: Path, arguments: dict) -> list[TextContent]:
634
+ async def _run() -> list[TextContent]:
635
+ route = optional_string_argument(arguments, "route")
636
+ if route == "":
637
+ return error_text("route must be a string", code="invalid_arguments", argument="route")
638
+ from devcouncil.indexing.graph.api_routes import shape_check
639
+ from devcouncil.indexing.graph.build import load_code_graph
640
+
641
+ graph = load_code_graph(root)
642
+ if graph is None:
643
+ return error_text(
644
+ "No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
645
+ code="graph_missing",
646
+ )
647
+ return json_text({
648
+ "ok": True,
649
+ **shape_check(root, graph, route_filter=route),
650
+ **_graph_degraded_fields(root),
651
+ })
652
+
653
+ return await with_codeintel_freshness(root, _run)
654
+
655
+
656
+ async def handle_api_impact(root: Path, arguments: dict) -> list[TextContent]:
657
+ async def _run() -> list[TextContent]:
658
+ route_or_path = optional_string_argument(arguments, "route_or_path")
659
+ if not route_or_path:
660
+ return error_text(
661
+ "Missing route_or_path",
662
+ code="missing_argument",
663
+ argument="route_or_path",
664
+ )
665
+ from devcouncil.indexing.graph.api_routes import api_impact
666
+ from devcouncil.indexing.graph.build import load_code_graph
667
+
668
+ graph = load_code_graph(root)
669
+ if graph is None:
670
+ return error_text(
671
+ "No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
672
+ code="graph_missing",
673
+ )
674
+ return json_text({"ok": True, **api_impact(root, route_or_path, graph), **_graph_degraded_fields(root)})
675
+
676
+ return await with_codeintel_freshness(root, _run)