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,668 @@
1
+ """Graph intelligence: communities, god nodes, cycles, processes, diff impact.
2
+
3
+ LLM-free analysis over a :class:`CodeGraph` (NetworkX Louvain + BFS).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from collections import Counter, defaultdict, deque
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
12
+
13
+ from devcouncil.indexing.graph.schema import CodeGraph, GraphNode, NodeKind
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _STRUCTURAL_KINDS = frozenset({"imports", "calls"})
18
+ _FILE_EDGE_KINDS = frozenset({"imports"})
19
+ _CALL_KINDS = frozenset({"calls"})
20
+ # Ambiguous call/import edges fan out to every candidate and inflate hubs,
21
+ # PageRank, process walks, and blast radius. Keep them in the store for
22
+ # explanation UIs, but exclude them from those ranked/BFS surfaces.
23
+ _METRIC_CONFIDENCE = frozenset({"extracted", "inferred"})
24
+
25
+
26
+ def _edge_confidence(edge: object) -> str:
27
+ conf = getattr(edge, "confidence", None)
28
+ if conf is None:
29
+ return "extracted"
30
+ return conf.value if hasattr(conf, "value") else str(conf)
31
+
32
+
33
+ def _metric_edge(edge: object) -> bool:
34
+ """True when an edge should influence god-node / PageRank / process / impact."""
35
+ kind = getattr(edge, "kind", "")
36
+ if kind not in _STRUCTURAL_KINDS:
37
+ return False
38
+ return _edge_confidence(edge) in _METRIC_CONFIDENCE
39
+
40
+
41
+ def _is_file_node(n: GraphNode) -> bool:
42
+ kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
43
+ return kind == NodeKind.FILE.value or ("::" not in n.id and not n.name)
44
+
45
+
46
+ def _file_id(path_or_id: str) -> str:
47
+ return path_or_id.replace("\\", "/").split("::", 1)[0]
48
+
49
+
50
+ def _path_prefix_label(paths: Sequence[str], *, depth: int = 2) -> str:
51
+ """Dominant directory prefix among ``paths`` (LLM-free community label)."""
52
+ counts: Counter[str] = Counter()
53
+ for p in paths:
54
+ norm = p.replace("\\", "/").strip("/")
55
+ if not norm:
56
+ continue
57
+ parts = norm.split("/")
58
+ if len(parts) >= depth:
59
+ counts["/".join(parts[:depth])] += 1
60
+ elif parts:
61
+ counts[parts[0]] += 1
62
+ if not counts:
63
+ return "community"
64
+ return counts.most_common(1)[0][0]
65
+
66
+
67
+ def compute_communities(
68
+ graph: CodeGraph,
69
+ *,
70
+ seed: int = 0,
71
+ ) -> Dict[str, Any]:
72
+ """Louvain communities over import+call edges; label from dominant path prefixes.
73
+
74
+ Mutates ``graph.nodes`` in place, setting ``community``. Returns a summary dict
75
+ suitable for ``graph.meta["communities"]``.
76
+ """
77
+ try:
78
+ from networkx.algorithms.community import louvain_communities
79
+ except ImportError:
80
+ logger.warning("networkx missing; skipping communities")
81
+ return {"communities": [], "count": 0}
82
+
83
+ # Prefer file-level import/call collapse for stable, path-labelable clusters.
84
+ file_edges: List[Tuple[str, str]] = []
85
+ seen: Set[Tuple[str, str]] = set()
86
+ for e in graph.edges:
87
+ if e.kind not in _STRUCTURAL_KINDS:
88
+ continue
89
+ a, b = _file_id(e.source), _file_id(e.target)
90
+ if a == b:
91
+ continue
92
+ key = (a, b) if a < b else (b, a)
93
+ if key in seen:
94
+ continue
95
+ seen.add(key)
96
+ file_edges.append(key)
97
+
98
+ import networkx as nx
99
+
100
+ G = nx.Graph()
101
+ G.add_edges_from(sorted(file_edges))
102
+ # Include isolated file nodes so every file gets a community id.
103
+ isolated_file_ids = sorted(
104
+ {_file_id(n.id) for n in graph.nodes if _is_file_node(n)}
105
+ )
106
+ for node_id in isolated_file_ids:
107
+ if node_id not in G:
108
+ G.add_node(node_id)
109
+
110
+ if G.number_of_nodes() == 0:
111
+ return {"communities": [], "count": 0}
112
+
113
+ # Bound Louvain so pathological graphs cannot stall a full map/graph build.
114
+ timeout_seconds = 15.0
115
+
116
+ def _run_louvain() -> list:
117
+ return list(louvain_communities(G, seed=seed, weight=None))
118
+
119
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
120
+
121
+ try:
122
+ with ThreadPoolExecutor(max_workers=1) as pool:
123
+ raw = pool.submit(_run_louvain).result(timeout=timeout_seconds)
124
+ except FuturesTimeout:
125
+ logger.warning(
126
+ "community detection timed out after %.1fs (%d nodes); skipping",
127
+ timeout_seconds,
128
+ G.number_of_nodes(),
129
+ )
130
+ return {
131
+ "communities": [],
132
+ "count": 0,
133
+ "skipped": True,
134
+ "reason": f"louvain_timeout_{timeout_seconds:.0f}s",
135
+ }
136
+ except Exception:
137
+ logger.warning("community detection failed; skipping", exc_info=True)
138
+ return {"communities": [], "count": 0, "skipped": True, "reason": "louvain_error"}
139
+ # Deterministic order: sort communities by sorted member list.
140
+ ordered = sorted(
141
+ (sorted(c) for c in raw),
142
+ key=lambda members: (members[0] if members else ""),
143
+ )
144
+
145
+ id_to_path = {_file_id(n.id): (n.path or _file_id(n.id)) for n in graph.nodes}
146
+ communities: List[Dict[str, Any]] = []
147
+ file_to_label: Dict[str, str] = {}
148
+ used_labels: Counter[str] = Counter()
149
+
150
+ for idx, members in enumerate(ordered):
151
+ paths = [id_to_path.get(m, m) for m in members]
152
+ base = _path_prefix_label(paths)
153
+ used_labels[base] += 1
154
+ label = base if used_labels[base] == 1 else f"{base}#{used_labels[base]}"
155
+ for m in members:
156
+ file_to_label[m] = label
157
+ communities.append(
158
+ {
159
+ "id": idx,
160
+ "label": label,
161
+ "size": len(members),
162
+ "files": members[:40],
163
+ }
164
+ )
165
+
166
+ for n in graph.nodes:
167
+ fid = _file_id(n.path or n.id)
168
+ n.community = file_to_label.get(fid, "")
169
+
170
+ return {"communities": communities, "count": len(communities)}
171
+
172
+
173
+ def _pagerank_power_iteration(
174
+ edges: List[tuple],
175
+ *,
176
+ alpha: float = 0.85,
177
+ max_iter: int = 100,
178
+ tol: float = 1.0e-8,
179
+ ) -> Dict[str, float]:
180
+ """Deterministic pure-Python PageRank (networkx semantics, no numpy/scipy).
181
+
182
+ networkx's pagerank imports scipy→numpy at call time; installs without them
183
+ raise and previously zeroed every score. Fine for code-graph sizes.
184
+ """
185
+ nodes = sorted({n for pair in edges for n in pair})
186
+ if not nodes:
187
+ return {}
188
+ out_links: Dict[str, List[str]] = {n: [] for n in nodes}
189
+ for src, dst in edges:
190
+ out_links[src].append(dst)
191
+ count = len(nodes)
192
+ rank = dict.fromkeys(nodes, 1.0 / count)
193
+ for _ in range(max_iter):
194
+ prev = rank
195
+ dangling = sum(prev[n] for n in nodes if not out_links[n])
196
+ base = (1.0 - alpha) / count + alpha * dangling / count
197
+ rank = dict.fromkeys(nodes, base)
198
+ for src in nodes:
199
+ targets = out_links[src]
200
+ if not targets:
201
+ continue
202
+ share = alpha * prev[src] / len(targets)
203
+ for dst in targets:
204
+ rank[dst] += share
205
+ if sum(abs(rank[n] - prev[n]) for n in nodes) < count * tol:
206
+ break
207
+ return rank
208
+
209
+
210
+ def pagerank_scores(graph: CodeGraph) -> Dict[str, float]:
211
+ """PageRank over the directed import+call graph.
212
+
213
+ Prefers networkx; falls back to a pure-Python power iteration when
214
+ networkx (or its numpy/scipy solver deps) is unavailable. Ambiguous
215
+ edges are excluded so name-collision fan-out cannot dominate ranks.
216
+ """
217
+ structural = sorted(
218
+ {
219
+ (e.source, e.target)
220
+ for e in graph.edges
221
+ if _metric_edge(e) and e.source != e.target
222
+ }
223
+ )
224
+ if not structural:
225
+ return {}
226
+ raw: Dict[str, float] = {}
227
+ try:
228
+ import networkx as nx
229
+
230
+ G = nx.DiGraph()
231
+ G.add_edges_from(structural)
232
+ raw = dict(nx.pagerank(G, alpha=0.85, max_iter=100, tol=1.0e-8))
233
+ except Exception:
234
+ logger.debug(
235
+ "networkx pagerank unavailable; using pure-Python fallback", exc_info=True
236
+ )
237
+ raw = _pagerank_power_iteration(structural)
238
+ # Fixed precision so tiny solver/order noise cannot flip JSON bytes.
239
+ return {nid: round(float(score), 4) for nid, score in raw.items()}
240
+
241
+
242
+ def god_nodes(graph: CodeGraph, *, top_n: int = 15) -> List[Dict[str, Any]]:
243
+ """Top-connected production symbols/files: degree, fan-in/fan-out, PageRank.
244
+
245
+ Test-path nodes are excluded from the ranking — shared mocks and fixture
246
+ helpers accumulate huge fan-in and would otherwise crowd out real hubs.
247
+ Ambiguous edges are excluded for the same reason (collision fan-out).
248
+ """
249
+ from devcouncil.indexing.wiring import is_test_path
250
+
251
+ degree: Counter[str] = Counter()
252
+ fan_in: Counter[str] = Counter()
253
+ fan_out: Counter[str] = Counter()
254
+ for e in graph.edges:
255
+ if not _metric_edge(e):
256
+ continue
257
+ degree[e.source] += 1
258
+ degree[e.target] += 1
259
+ fan_out[e.source] += 1
260
+ fan_in[e.target] += 1
261
+ pr = pagerank_scores(graph)
262
+ by_id = graph.node_by_id()
263
+
264
+ def _is_test_node(nid: str) -> bool:
265
+ n = by_id.get(nid)
266
+ return is_test_path(n.path if n and n.path else _file_id(nid))
267
+
268
+ ranked = sorted(
269
+ ((nid, deg) for nid, deg in degree.items() if not _is_test_node(nid)),
270
+ key=lambda item: (-item[1], item[0]),
271
+ )[:top_n]
272
+ out: List[Dict[str, Any]] = []
273
+ for nid, deg in ranked:
274
+ n = by_id.get(nid)
275
+ out.append(
276
+ {
277
+ "id": nid,
278
+ "degree": deg,
279
+ "fan_in": fan_in.get(nid, 0),
280
+ "fan_out": fan_out.get(nid, 0),
281
+ "pagerank": pr.get(nid, 0.0),
282
+ "kind": (
283
+ n.kind.value if n and hasattr(n.kind, "value") else (n.kind if n else "")
284
+ ),
285
+ "path": n.path if n else _file_id(nid),
286
+ "name": n.name if n else "",
287
+ "community": n.community if n else "",
288
+ }
289
+ )
290
+ return out
291
+
292
+
293
+ def hotspots(
294
+ root: Path,
295
+ graph: CodeGraph,
296
+ *,
297
+ since: str = "90.days",
298
+ top_n: int = 20,
299
+ ) -> List[Dict[str, Any]]:
300
+ """Churn × coupling hotspots: files changed often AND heavily depended on.
301
+
302
+ Score = commits touching the file (last ``since``) × (1 + fan-in).
303
+ High scores are refactor-risk files where a change ripples widest.
304
+ """
305
+ import math
306
+
307
+ try:
308
+ from devcouncil.utils.proc import git_output
309
+
310
+ raw = git_output(
311
+ ["log", f"--since={since}", "--name-only", "--pretty=format:"],
312
+ cwd=root,
313
+ default="",
314
+ )
315
+ except Exception:
316
+ logger.debug("hotspot churn scan failed", exc_info=True)
317
+ return []
318
+ churn: Counter[str] = Counter()
319
+ for line in raw.splitlines():
320
+ p = line.strip().replace("\\", "/")
321
+ if p:
322
+ churn[p] += 1
323
+ if not churn:
324
+ return []
325
+
326
+ fan_in: Counter[str] = Counter()
327
+ for e in graph.edges:
328
+ if e.kind in _FILE_EDGE_KINDS and "::" not in e.source and "::" not in e.target:
329
+ fan_in[e.target] += 1
330
+
331
+ file_paths = {n.path or n.id for n in graph.nodes if _is_file_node(n)}
332
+ scored: List[Dict[str, Any]] = []
333
+ for path, count in churn.items():
334
+ if path not in file_paths:
335
+ continue
336
+ fi = fan_in.get(path, 0)
337
+ scored.append(
338
+ {
339
+ "path": path,
340
+ "churn": count,
341
+ "fan_in": fi,
342
+ "score": round(count * (1 + math.log1p(fi)), 2),
343
+ }
344
+ )
345
+ scored.sort(key=lambda h: (-h["score"], h["path"]))
346
+ return scored[:top_n]
347
+
348
+
349
+ def _is_package_init(path: str) -> bool:
350
+ return Path(path.replace("\\", "/")).name == "__init__.py"
351
+
352
+
353
+ def _import_components(
354
+ graph: CodeGraph,
355
+ *,
356
+ include_package_inits: bool,
357
+ ) -> List[Dict[str, Any]]:
358
+ """Deterministic strongly connected components in the file import graph."""
359
+ try:
360
+ import networkx as nx
361
+ except ImportError:
362
+ return []
363
+
364
+ G = nx.DiGraph()
365
+ for e in graph.edges:
366
+ if e.kind not in _FILE_EDGE_KINDS:
367
+ continue
368
+ src, tgt = e.source, e.target
369
+ if "::" in src or "::" in tgt:
370
+ continue
371
+ if not include_package_inits and (
372
+ _is_package_init(src) or _is_package_init(tgt)
373
+ ):
374
+ continue
375
+ if src != tgt:
376
+ G.add_edge(src, tgt)
377
+
378
+ try:
379
+ components = [
380
+ sorted(component)
381
+ for component in nx.strongly_connected_components(G)
382
+ if len(component) >= 2
383
+ ]
384
+ except Exception:
385
+ logger.debug("cycle detection failed", exc_info=True)
386
+ return []
387
+
388
+ components.sort(key=lambda nodes: (len(nodes), nodes))
389
+ return [{"nodes": nodes, "length": len(nodes)} for nodes in components]
390
+
391
+
392
+ def circular_imports(graph: CodeGraph, *, max_cycles: int = 50) -> List[Dict[str, Any]]:
393
+ """Report actionable import SCCs, excluding Python package-init barrel noise."""
394
+ return _import_components(graph, include_package_inits=False)[:max_cycles]
395
+
396
+
397
+ def graph_check(graph: CodeGraph, *, top_n: int = 15) -> Dict[str, Any]:
398
+ """God nodes + circular imports report (``dev graph check``)."""
399
+ gods = god_nodes(graph, top_n=top_n)
400
+ cycles = circular_imports(graph)
401
+ package_init_components = [
402
+ component
403
+ for component in _import_components(graph, include_package_inits=True)
404
+ if any(_is_package_init(node) for node in component["nodes"])
405
+ ]
406
+ return {
407
+ "god_nodes": gods,
408
+ "circular_imports": cycles,
409
+ "package_init_imports": package_init_components,
410
+ "god_count": len(gods),
411
+ "cycle_count": len(cycles),
412
+ "package_init_count": len(package_init_components),
413
+ }
414
+
415
+
416
+ def _call_adjacency(graph: CodeGraph) -> Dict[str, List[str]]:
417
+ adj: Dict[str, List[str]] = defaultdict(list)
418
+ for e in graph.edges:
419
+ if e.kind in _CALL_KINDS and _edge_confidence(e) in _METRIC_CONFIDENCE:
420
+ adj[e.source].append(e.target)
421
+ return {k: sorted(set(v)) for k, v in adj.items()}
422
+
423
+
424
+ def _entry_symbols(graph: CodeGraph, entry_root: str) -> List[str]:
425
+ """Symbol ids defined in an entry-root file (prefer functions named main/run/cli)."""
426
+ root = entry_root.replace("\\", "/")
427
+ symbols: List[GraphNode] = []
428
+ for n in graph.nodes:
429
+ if _is_file_node(n):
430
+ continue
431
+ if (n.path or _file_id(n.id)) == root:
432
+ symbols.append(n)
433
+ if not symbols:
434
+ return [root] if any(n.id == root for n in graph.nodes) else []
435
+
436
+ preferred = {"main", "run", "cli", "app", "handler", "execute"}
437
+ ranked = sorted(
438
+ symbols,
439
+ key=lambda n: (
440
+ 0 if n.name in preferred else 1,
441
+ 0
442
+ if (n.kind.value if hasattr(n.kind, "value") else str(n.kind)) == "function"
443
+ else 1,
444
+ n.line,
445
+ n.id,
446
+ ),
447
+ )
448
+ return [n.id for n in ranked[:5]]
449
+
450
+
451
+ def extract_processes(
452
+ graph: CodeGraph,
453
+ *,
454
+ entry: Optional[str] = None,
455
+ max_depth: int = 6,
456
+ max_processes: int = 20,
457
+ max_steps: int = 24,
458
+ ) -> List[Dict[str, Any]]:
459
+ """BFS call-flows from entry roots (named, step-ordered, depth-capped)."""
460
+ adj = _call_adjacency(graph)
461
+ roots = list(graph.entry_roots)
462
+ if entry:
463
+ q = entry.replace("\\", "/")
464
+ roots = [r for r in roots if r == q or r.endswith(q) or q in r]
465
+ if not roots:
466
+ roots = [q]
467
+
468
+ processes: List[Dict[str, Any]] = []
469
+ for root in roots:
470
+ starts = _entry_symbols(graph, root)
471
+ if not starts and root:
472
+ starts = [root]
473
+ for start in starts:
474
+ steps: List[str] = []
475
+ seen: Set[str] = set()
476
+ queue: deque[Tuple[str, int]] = deque([(start, 0)])
477
+ while queue and len(steps) < max_steps:
478
+ node, depth = queue.popleft()
479
+ if node in seen:
480
+ continue
481
+ seen.add(node)
482
+ steps.append(node)
483
+ if depth >= max_depth:
484
+ continue
485
+ for nxt in adj.get(node, []):
486
+ if nxt not in seen:
487
+ queue.append((nxt, depth + 1))
488
+ name = f"{Path(root).name}"
489
+ if start != root and "::" in start:
490
+ name = f"{Path(root).name}::{start.split('::', 1)[-1]}"
491
+ processes.append(
492
+ {
493
+ "name": name,
494
+ "entry": start,
495
+ "entry_root": root,
496
+ "steps": steps,
497
+ "depth": max(0, len(steps) - 1),
498
+ }
499
+ )
500
+ if len(processes) >= max_processes:
501
+ return processes
502
+ processes.sort(key=lambda p: (-p["depth"], -len(p["steps"]), p["name"]))
503
+ return processes[:max_processes]
504
+
505
+
506
+ def _enclosing_symbols(graph: CodeGraph, path: str) -> List[GraphNode]:
507
+ norm = path.replace("\\", "/")
508
+ hits = [
509
+ n
510
+ for n in graph.nodes
511
+ if not _is_file_node(n) and (n.path or _file_id(n.id)) == norm
512
+ ]
513
+ return sorted(hits, key=lambda n: (n.line, n.id))
514
+
515
+
516
+ def _inbound_adj(graph: CodeGraph) -> Dict[str, List[str]]:
517
+ """Callers/importers → reverse edges for blast radius.
518
+
519
+ Ambiguous edges are omitted so collision fan-out does not inflate impact.
520
+ """
521
+ rev: Dict[str, List[str]] = defaultdict(list)
522
+ for e in graph.edges:
523
+ if _metric_edge(e):
524
+ rev[e.target].append(e.source)
525
+ return {k: sorted(set(v)) for k, v in rev.items()}
526
+
527
+
528
+ def blast_radius(
529
+ graph: CodeGraph,
530
+ seed_ids: Sequence[str],
531
+ *,
532
+ max_depth: int = 3,
533
+ ) -> Dict[str, Any]:
534
+ """Inbound blast radius at depths 1/2/3 with confidence tiers.
535
+
536
+ Traversal follows extracted/inferred import+call edges only. Layer labels
537
+ remain depth-based (depth 1 extracted … depth 3 ambiguous) as a distance
538
+ heuristic, not a claim about edge confidence.
539
+ """
540
+ rev = _inbound_adj(graph)
541
+ by_depth: Dict[int, List[str]] = {1: [], 2: [], 3: []}
542
+ confidence_for_depth = {1: "extracted", 2: "inferred", 3: "ambiguous"}
543
+ seen: Set[str] = set(seed_ids)
544
+ frontier = set(seed_ids)
545
+ for depth in range(1, max_depth + 1):
546
+ nxt: Set[str] = set()
547
+ for nid in frontier:
548
+ for caller in rev.get(nid, []):
549
+ if caller in seen:
550
+ continue
551
+ seen.add(caller)
552
+ nxt.add(caller)
553
+ by_depth[depth].append(caller)
554
+ by_depth[depth] = sorted(by_depth[depth])
555
+ frontier = nxt
556
+ if not frontier:
557
+ break
558
+
559
+ layers = []
560
+ for d in range(1, max_depth + 1):
561
+ layers.append(
562
+ {
563
+ "depth": d,
564
+ "confidence": confidence_for_depth[d],
565
+ "nodes": by_depth[d],
566
+ "count": len(by_depth[d]),
567
+ }
568
+ )
569
+ return {
570
+ "seeds": list(seed_ids),
571
+ "layers": layers,
572
+ "total_impacted": sum(len(by_depth[d]) for d in by_depth),
573
+ }
574
+
575
+
576
+ def working_tree_changed_paths(root: Path) -> List[str]:
577
+ """Repository-relative paths changed in the working tree (unstaged+staged)."""
578
+ try:
579
+ from devcouncil.verification.git_diff_fallback import GitDiffFallback
580
+
581
+ return [p.replace("\\", "/") for p in GitDiffFallback(root).get_changed_files()]
582
+ except Exception:
583
+ logger.debug("working tree paths failed", exc_info=True)
584
+ return []
585
+
586
+
587
+ def diff_impact(
588
+ root: Path,
589
+ graph: CodeGraph,
590
+ *,
591
+ paths: Optional[Sequence[str]] = None,
592
+ use_diff: bool = False,
593
+ max_depth: int = 3,
594
+ ) -> Dict[str, Any]:
595
+ """Map paths (or working-tree diff) → enclosing symbols → inbound blast radius."""
596
+ if use_diff or not paths:
597
+ changed = working_tree_changed_paths(root)
598
+ if paths:
599
+ want = {p.replace("\\", "/") for p in paths}
600
+ changed = [p for p in changed if p in want]
601
+ target_paths = changed
602
+ else:
603
+ target_paths = [p.replace("\\", "/") for p in paths]
604
+
605
+ items: List[Dict[str, Any]] = []
606
+ all_seeds: List[str] = []
607
+ for path in target_paths:
608
+ symbols = _enclosing_symbols(graph, path)
609
+ seed_ids = [s.id for s in symbols] or [path]
610
+ all_seeds.extend(seed_ids)
611
+ radius = blast_radius(graph, seed_ids, max_depth=max_depth)
612
+ items.append(
613
+ {
614
+ "path": path,
615
+ "symbols": [
616
+ {
617
+ "id": s.id,
618
+ "name": s.name,
619
+ "line": s.line,
620
+ "kind": s.kind.value if hasattr(s.kind, "value") else str(s.kind),
621
+ }
622
+ for s in symbols[:40]
623
+ ],
624
+ "blast": radius,
625
+ }
626
+ )
627
+
628
+ aggregate = (
629
+ blast_radius(graph, all_seeds, max_depth=max_depth)
630
+ if all_seeds
631
+ else {"seeds": [], "layers": [], "total_impacted": 0}
632
+ )
633
+ return {
634
+ "paths": items,
635
+ "aggregate": aggregate,
636
+ "path_count": len(items),
637
+ "source": "diff" if (use_diff or not paths) else "paths",
638
+ }
639
+
640
+
641
+ def enrich_graph_intel(
642
+ graph: CodeGraph, *, root: Optional[Path] = None, seed: int = 0
643
+ ) -> CodeGraph:
644
+ """Run communities + processes + centrality + hotspots; persist to ``graph.meta``."""
645
+ community_summary = compute_communities(graph, seed=seed)
646
+ processes = extract_processes(graph)
647
+ meta = dict(graph.meta or {})
648
+ meta["communities"] = community_summary
649
+ # Per-node community map so downstream consumers (viz) never have to
650
+ # re-derive it from mutated node attributes.
651
+ meta["node_communities"] = {
652
+ n.id: n.community for n in graph.nodes if n.community
653
+ }
654
+ meta["processes"] = processes[:12]
655
+ meta["god_nodes"] = god_nodes(graph)[:15]
656
+ meta["circular_imports"] = circular_imports(graph)[:30]
657
+ meta["package_init_import_count"] = sum(
658
+ any(_is_package_init(node) for node in component["nodes"])
659
+ for component in _import_components(graph, include_package_inits=True)
660
+ )
661
+ if root is not None:
662
+ meta["hotspots"] = hotspots(root, graph)
663
+ graph.meta = meta
664
+ return graph
665
+
666
+
667
+ # Re-export leaf helper for callers that still import from intel.
668
+ from devcouncil.indexing.graph.communities import community_label_for_area # noqa: E402,F401