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,960 @@
1
+ """Self-contained interactive HTML visualizer for the code knowledge graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Set
9
+
10
+ from devcouncil.indexing.graph.build import graph_path, load_code_graph
11
+ from devcouncil.indexing.graph.schema import (
12
+ CodeGraph,
13
+ Confidence,
14
+ DeadCodeEntry,
15
+ GraphEdge,
16
+ GraphNode,
17
+ NodeKind,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _VENDOR_REL = Path("assets") / "vendor" / "force-graph.min.js"
23
+ _FILE_EDGE_KINDS = frozenset({"imports"})
24
+ _SYMBOL_EDGE_KINDS = frozenset({"calls", "inherits", "implements", "overrides", "named_import"})
25
+
26
+
27
+ def _vendor_js() -> str:
28
+ """Load vendored force-graph (or a tiny fallback shim)."""
29
+ try:
30
+ import importlib.resources as resources
31
+
32
+ pkg = resources.files("devcouncil")
33
+ path = pkg.joinpath("assets/vendor/force-graph.min.js")
34
+ if path.is_file():
35
+ return path.read_text(encoding="utf-8")
36
+ except Exception:
37
+ pass
38
+ # Repo checkout path
39
+ here = Path(__file__).resolve().parents[1] / "assets" / "vendor" / "force-graph.min.js"
40
+ if here.is_file():
41
+ return here.read_text(encoding="utf-8")
42
+ # Minimal fallback so the page still loads with a message
43
+ return (
44
+ "window.ForceGraph=function(){return{"
45
+ "graphData:function(){return this},nodeId:function(){return this},"
46
+ "nodeLabel:function(){return this},nodeAutoColorBy:function(){return this},"
47
+ "nodeVal:function(){return this},linkColor:function(){return this},"
48
+ "linkDirectionalParticles:function(){return this},"
49
+ "linkDirectionalParticleWidth:function(){return this},"
50
+ "onNodeClick:function(){return this},onNodeDblClick:function(){return this},"
51
+ "width:function(){return this},height:function(){return this},_missing:true};};"
52
+ )
53
+
54
+
55
+ def _conf_val(obj: Any) -> str:
56
+ if hasattr(obj, "value"):
57
+ return str(obj.value)
58
+ return str(obj or "")
59
+
60
+
61
+ def _node_community(n: Any, graph: CodeGraph) -> str:
62
+ """Prefer intel community; fall back to area when communities are absent."""
63
+ extras = getattr(n, "extras", None) or {}
64
+ if isinstance(extras, dict):
65
+ c = extras.get("community") or extras.get("community_id")
66
+ if c is not None and str(c).strip():
67
+ return str(c)
68
+ # compute_communities writes the Louvain label to the node attribute —
69
+ # the old code never read it, so community coloring silently degraded
70
+ # to area grouping.
71
+ direct = getattr(n, "community", "") or ""
72
+ if str(direct).strip():
73
+ return str(direct)
74
+ meta = graph.meta or {}
75
+ by_id = meta.get("node_communities") or meta.get("communities_by_node") or {}
76
+ if isinstance(by_id, dict) and n.id in by_id:
77
+ return str(by_id[n.id])
78
+ area = getattr(n, "area", "") or ""
79
+ return area or "unknown"
80
+
81
+
82
+ def _processes(graph: CodeGraph) -> List[Dict[str, Any]]:
83
+ meta = graph.meta or {}
84
+ raw = meta.get("processes") or meta.get("process_flows") or []
85
+ if not isinstance(raw, list):
86
+ return []
87
+ out: List[Dict[str, Any]] = []
88
+ for p in raw[:100]:
89
+ if isinstance(p, dict):
90
+ out.append(p)
91
+ else:
92
+ out.append({"name": str(p), "steps": []})
93
+ return out
94
+
95
+
96
+ def _communities_summary(nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
97
+ counts: Dict[str, int] = {}
98
+ for n in nodes:
99
+ key = n.get("community") or n.get("area") or "unknown"
100
+ counts[key] = counts.get(key, 0) + 1
101
+ return [
102
+ {"id": k, "label": k, "size": counts[k]}
103
+ for k in sorted(counts.keys(), key=lambda x: (-counts[x], x))
104
+ ]
105
+
106
+
107
+ def _level_payload(
108
+ graph: CodeGraph,
109
+ *,
110
+ file_level: bool,
111
+ dead_by_id: Dict[str, Any],
112
+ dead_paths: Set[str],
113
+ unwired: Set[str],
114
+ unreachable: Set[str],
115
+ entry_set: Set[str],
116
+ ) -> Dict[str, Any]:
117
+ degree: Dict[str, int] = {}
118
+ for e in graph.edges:
119
+ if file_level:
120
+ if e.kind not in _FILE_EDGE_KINDS:
121
+ continue
122
+ if "::" in e.source or "::" in e.target:
123
+ continue
124
+ else:
125
+ if e.kind not in _SYMBOL_EDGE_KINDS and e.kind not in {"calls", "inherits", "implements", "overrides"}:
126
+ # Allow calls/inherits family; skip pure file imports in symbol mode
127
+ if e.kind == "imports" and ("::" not in e.source and "::" not in e.target):
128
+ continue
129
+ degree[e.source] = degree.get(e.source, 0) + 1
130
+ degree[e.target] = degree.get(e.target, 0) + 1
131
+
132
+ nodes: List[Dict[str, Any]] = []
133
+ for n in graph.nodes:
134
+ kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
135
+ if file_level:
136
+ if kind != "file":
137
+ continue
138
+ else:
139
+ if kind == "file":
140
+ continue
141
+ nid = n.id
142
+ flags: List[str] = []
143
+ dead_conf = ""
144
+ if nid in dead_by_id:
145
+ flags.append("dead")
146
+ dead_conf = _conf_val(dead_by_id[nid].confidence)
147
+ elif n.path in dead_paths and kind == "file":
148
+ flags.append("dead")
149
+ if n.path in unwired or nid in unwired:
150
+ flags.append("unwired")
151
+ if n.path in unreachable or nid in unreachable:
152
+ flags.append("unreachable")
153
+ community = _node_community(n, graph)
154
+ is_entry = nid in entry_set or n.path in entry_set or any(
155
+ nid.startswith(f"{er}::") or n.path == er for er in entry_set
156
+ )
157
+ nodes.append(
158
+ {
159
+ "id": nid,
160
+ "name": n.name or n.path,
161
+ "path": n.path,
162
+ "kind": kind,
163
+ "area": n.area or "",
164
+ "community": community,
165
+ "line": n.line,
166
+ "val": max(1, degree.get(nid, 1)),
167
+ "flags": flags,
168
+ "dead_confidence": dead_conf,
169
+ "entry": is_entry,
170
+ }
171
+ )
172
+ node_ids = {n["id"] for n in nodes}
173
+ links: List[Dict[str, Any]] = []
174
+ for e in graph.edges:
175
+ if e.source not in node_ids or e.target not in node_ids:
176
+ continue
177
+ if file_level and e.kind not in _FILE_EDGE_KINDS:
178
+ continue
179
+ if not file_level and e.kind == "imports" and "::" not in e.source and "::" not in e.target:
180
+ continue
181
+ if not file_level and e.kind in {"contains", "defines", "documents"}:
182
+ continue
183
+ links.append(
184
+ {
185
+ "source": e.source,
186
+ "target": e.target,
187
+ "kind": e.kind,
188
+ "confidence": _conf_val(e.confidence),
189
+ }
190
+ )
191
+ return {
192
+ "nodes": nodes,
193
+ "links": links,
194
+ "areas": sorted({n["area"] for n in nodes if n.get("area")}),
195
+ "communities": _communities_summary(nodes),
196
+ }
197
+
198
+
199
+ def _adjacency(links: List[Dict[str, Any]]) -> Dict[str, List[str]]:
200
+ adj: Dict[str, List[str]] = {}
201
+ for link in links:
202
+ s, t = link["source"], link["target"]
203
+ adj.setdefault(s, []).append(t)
204
+ adj.setdefault(t, []).append(s)
205
+ return adj
206
+
207
+
208
+ def _payload_from_graph(graph: CodeGraph, *, file_level: bool = True) -> Dict[str, Any]:
209
+ """Build a compact viz payload with file and symbol modes."""
210
+ dead_by_id = {d.id: d for d in graph.dead_code}
211
+ dead_paths = {d.path for d in graph.dead_code}
212
+ unwired = set(graph.unwired_candidates)
213
+ unreachable = set(graph.unreachable_files)
214
+ entry_set = set(graph.entry_roots)
215
+
216
+ file_payload = _level_payload(
217
+ graph,
218
+ file_level=True,
219
+ dead_by_id=dead_by_id,
220
+ dead_paths=dead_paths,
221
+ unwired=unwired,
222
+ unreachable=unreachable,
223
+ entry_set=entry_set,
224
+ )
225
+ symbol_payload = _level_payload(
226
+ graph,
227
+ file_level=False,
228
+ dead_by_id=dead_by_id,
229
+ dead_paths=dead_paths,
230
+ unwired=unwired,
231
+ unreachable=unreachable,
232
+ entry_set=entry_set,
233
+ )
234
+ # Build neighbor indexes for detail panel (callers/callees/importers)
235
+ callers: Dict[str, List[str]] = {}
236
+ callees: Dict[str, List[str]] = {}
237
+ importers: Dict[str, List[str]] = {}
238
+ for e in graph.edges:
239
+ if e.kind == "calls":
240
+ callers.setdefault(e.target, []).append(e.source)
241
+ callees.setdefault(e.source, []).append(e.target)
242
+ elif e.kind == "imports":
243
+ importers.setdefault(e.target, []).append(e.source)
244
+
245
+ dead_dump = []
246
+ for d in graph.dead_code[:2000]:
247
+ dead_dump.append(
248
+ {
249
+ "id": d.id,
250
+ "path": d.path,
251
+ "line": d.line,
252
+ "kind": d.kind,
253
+ "confidence": _conf_val(d.confidence),
254
+ "reason": d.reason,
255
+ }
256
+ )
257
+
258
+ meta = graph.meta or {}
259
+ active = "file" if file_level else "symbol"
260
+ return {
261
+ "mode": active,
262
+ "file": file_payload,
263
+ "symbol": symbol_payload,
264
+ "dead_code": dead_dump,
265
+ "processes": _processes(graph),
266
+ "god_nodes": list(meta.get("god_nodes") or [])[:30],
267
+ "hotspots": list(meta.get("hotspots") or [])[:30],
268
+ "circular_imports": list(meta.get("circular_imports") or [])[:30],
269
+ "neighbors": {"callers": callers, "callees": callees, "importers": importers},
270
+ "meta": {
271
+ "entry_roots": list(graph.entry_roots)[:100],
272
+ "schema_version": graph.schema_version,
273
+ "has_communities": bool(
274
+ (graph.meta or {}).get("communities")
275
+ or (graph.meta or {}).get("node_communities")
276
+ or any(
277
+ (getattr(n, "extras", None) or {}).get("community")
278
+ for n in graph.nodes[:200]
279
+ )
280
+ ),
281
+ },
282
+ # Back-compat flat fields used by older tests / consumers
283
+ "nodes": file_payload["nodes"] if file_level else symbol_payload["nodes"],
284
+ "links": file_payload["links"] if file_level else symbol_payload["links"],
285
+ "areas": file_payload["areas"] if file_level else symbol_payload["areas"],
286
+ }
287
+
288
+
289
+ def render_graph_html(
290
+ graph: CodeGraph,
291
+ *,
292
+ file_level: bool = True,
293
+ ) -> str:
294
+ """Return a self-contained HTML document (no network)."""
295
+ payload = _payload_from_graph(graph, file_level=file_level)
296
+ # Escape </script> in JSON so embed cannot break out
297
+ raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
298
+ raw = raw.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
299
+ vendor = _vendor_js()
300
+ return f"""<!DOCTYPE html>
301
+ <html lang="en">
302
+ <head>
303
+ <meta charset="utf-8"/>
304
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
305
+ <title>DevCouncil Code Graph</title>
306
+ <style>
307
+ :root {{ --bg:#0f1419; --panel:#1a2332; --fg:#e7ecf3; --muted:#8b9bb4; --accent:#3d8bfd; --dead:#e35d6a; --entry:#34d399; }}
308
+ * {{ box-sizing:border-box; }}
309
+ body {{ margin:0; font:14px/1.4 ui-sans-serif,system-ui,sans-serif; background:var(--bg); color:var(--fg); display:flex; height:100vh; }}
310
+ #sidebar {{ width:360px; background:var(--panel); padding:0; overflow:hidden; border-right:1px solid #243044; display:flex; flex-direction:column; }}
311
+ #graph {{ flex:1; position:relative; }}
312
+ h1 {{ font-size:16px; margin:0 0 8px; }}
313
+ .tabs {{ display:flex; border-bottom:1px solid #243044; }}
314
+ .tab {{ flex:1; padding:8px 4px; text-align:center; cursor:pointer; color:var(--muted); font-size:12px; border:none; background:transparent; }}
315
+ .tab.active {{ color:var(--fg); border-bottom:2px solid var(--accent); }}
316
+ .panel {{ display:none; padding:12px; overflow:auto; flex:1; }}
317
+ .panel.active {{ display:block; }}
318
+ input,select,button {{ width:100%; margin:4px 0 8px; padding:6px 8px; background:#0f1419; color:var(--fg); border:1px solid #334155; border-radius:4px; }}
319
+ button.primary {{ cursor:pointer; background:var(--accent); border:none; font-weight:600; }}
320
+ label {{ color:var(--muted); font-size:12px; display:block; }}
321
+ label.inline {{ display:flex; align-items:center; gap:6px; margin:4px 0; }}
322
+ label.inline input {{ width:auto; margin:0; }}
323
+ #detail {{ margin-top:8px; font-size:12px; color:var(--muted); white-space:pre-wrap; }}
324
+ .flag-dead {{ color:var(--dead); }}
325
+ .badge-entry {{ color:var(--entry); font-weight:600; }}
326
+ .list {{ list-style:none; padding:0; margin:0; font-size:12px; }}
327
+ .list li {{ padding:4px 0; border-bottom:1px solid #243044; cursor:pointer; }}
328
+ .list li:hover {{ color:var(--accent); }}
329
+ .muted {{ color:var(--muted); }}
330
+ .row {{ display:flex; gap:6px; }}
331
+ .row > * {{ flex:1; }}
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <aside id="sidebar">
336
+ <div class="tabs">
337
+ <button class="tab active" data-tab="graph">Graph</button>
338
+ <button class="tab" data-tab="dead">Dead code</button>
339
+ <button class="tab" data-tab="communities">Communities</button>
340
+ <button class="tab" data-tab="processes">Processes</button>
341
+ <button class="tab" data-tab="intel">Intel</button>
342
+ </div>
343
+ <div id="panel-graph" class="panel active">
344
+ <h1>DevCouncil Code Graph</h1>
345
+ <label>Mode</label>
346
+ <select id="mode"><option value="file">File-level</option><option value="symbol">Symbol-level</option></select>
347
+ <label>Search</label>
348
+ <input id="search" placeholder="name or path" autocomplete="off"/>
349
+ <label>Color by</label>
350
+ <select id="colorBy"><option value="community">Community / area</option><option value="area">Area only</option><option value="kind">Kind</option></select>
351
+ <label>Area / community</label>
352
+ <select id="area"><option value="">(all)</option></select>
353
+ <label>Edge kind</label>
354
+ <select id="ekind"><option value="">(all)</option><option>imports</option><option>calls</option><option>inherits</option><option>implements</option><option>overrides</option><option>decorates</option></select>
355
+ <label>Edge confidence</label>
356
+ <select id="econf"><option value="">(all)</option><option>extracted</option><option>inferred</option><option>ambiguous</option></select>
357
+ <label class="inline"><input type="checkbox" id="lensDead"/> Lens: dead</label>
358
+ <label class="inline"><input type="checkbox" id="lensUnwired"/> Lens: unwired</label>
359
+ <label class="inline"><input type="checkbox" id="lensUnreach"/> Lens: unreachable</label>
360
+ <label>Dead confidence</label>
361
+ <select id="deadConf"><option value="">(any)</option><option>extracted</option><option>inferred</option><option>ambiguous</option></select>
362
+ <div class="row"><button class="primary" id="reset">Reset view</button><button class="primary" id="clearPath">Clear path</button></div>
363
+ <div id="detail">Click a node for details. Select two nodes to highlight shortest path. Double-click to expand neighborhood.</div>
364
+ </div>
365
+ <div id="panel-dead" class="panel">
366
+ <h1>Dead code</h1>
367
+ <label>Sort by</label>
368
+ <select id="deadSort"><option value="path">path</option><option value="confidence">confidence</option><option value="kind">kind</option><option value="id">id</option></select>
369
+ <ul class="list" id="deadList"></ul>
370
+ </div>
371
+ <div id="panel-communities" class="panel">
372
+ <h1>Communities</h1>
373
+ <p class="muted" id="commHint">Colored by community when present; falls back to area.</p>
374
+ <ul class="list" id="commList"></ul>
375
+ </div>
376
+ <div id="panel-processes" class="panel">
377
+ <h1>Processes</h1>
378
+ <p class="muted" id="procHint">Entry-root call flows (when intel is available).</p>
379
+ <ul class="list" id="procList"></ul>
380
+ </div>
381
+ <div id="panel-intel" class="panel">
382
+ <h1>Hotspots</h1>
383
+ <p class="muted">Churn × fan-in over the last 90 days — highest refactor risk first.</p>
384
+ <ul class="list" id="hotList"></ul>
385
+ <h1 style="margin-top:12px">God nodes</h1>
386
+ <p class="muted">Highest coupling (degree, fan-in/out, PageRank).</p>
387
+ <ul class="list" id="godList"></ul>
388
+ <h1 style="margin-top:12px">Circular imports</h1>
389
+ <ul class="list" id="cycleList"></ul>
390
+ </div>
391
+ </aside>
392
+ <div id="graph">
393
+ <div id="vendorWarn" style="display:none;position:absolute;inset:0;z-index:10;padding:40px;color:var(--dead);font-size:14px;background:var(--bg)">
394
+ force-graph vendor bundle is missing — the canvas cannot render.<br/>
395
+ Expected at <code>src/devcouncil/assets/vendor/force-graph.min.js</code>.
396
+ Sidebar data (dead code, intel, processes) still works.
397
+ </div>
398
+ </div>
399
+ <script>{vendor}</script>
400
+ <script>
401
+ const DATA = {raw};
402
+ let mode = DATA.mode || 'file';
403
+ let selected = [];
404
+ let pathHighlight = new Set();
405
+ let expandIds = null; // null = show all filtered; Set = neighborhood focus
406
+
407
+ document.getElementById('mode').value = mode;
408
+
409
+ function activePayload() {{
410
+ return (mode === 'symbol' ? DATA.symbol : DATA.file) || {{nodes:[], links:[], areas:[], communities:[]}};
411
+ }}
412
+
413
+ function refillArea() {{
414
+ const areaSel = document.getElementById('area');
415
+ const cur = areaSel.value;
416
+ areaSel.innerHTML = '<option value="">(all)</option>';
417
+ const p = activePayload();
418
+ const vals = new Set([...(p.areas||[]), ...((p.communities||[]).map(c => c.id))]);
419
+ [...vals].filter(Boolean).sort().forEach(a => {{
420
+ const o=document.createElement('option'); o.value=a; o.textContent=a; areaSel.appendChild(o);
421
+ }});
422
+ if ([...vals].includes(cur)) areaSel.value = cur;
423
+ }}
424
+
425
+ function lid(l, key) {{
426
+ const v = l[key];
427
+ return typeof v === 'object' && v ? v.id : v;
428
+ }}
429
+
430
+ function filtered() {{
431
+ const p = activePayload();
432
+ const q = (document.getElementById('search').value||'').toLowerCase();
433
+ const area = document.getElementById('area').value;
434
+ const lensDead = document.getElementById('lensDead').checked;
435
+ const lensUnwired = document.getElementById('lensUnwired').checked;
436
+ const lensUnreach = document.getElementById('lensUnreach').checked;
437
+ const deadConf = document.getElementById('deadConf').value;
438
+ const anyLens = lensDead || lensUnwired || lensUnreach;
439
+ let nodes = p.nodes.filter(n => {{
440
+ if (expandIds && !expandIds.has(n.id)) return false;
441
+ if (area && n.area !== area && n.community !== area) return false;
442
+ if (anyLens) {{
443
+ const flags = n.flags || [];
444
+ let ok = false;
445
+ if (lensDead && flags.includes('dead')) ok = true;
446
+ if (lensUnwired && flags.includes('unwired')) ok = true;
447
+ if (lensUnreach && flags.includes('unreachable')) ok = true;
448
+ if (!ok) return false;
449
+ if (lensDead && deadConf && n.dead_confidence && n.dead_confidence !== deadConf) return false;
450
+ }}
451
+ if (q && !(n.id.toLowerCase().includes(q) || (n.path||'').toLowerCase().includes(q) || (n.name||'').toLowerCase().includes(q))) return false;
452
+ return true;
453
+ }});
454
+ const ids = new Set(nodes.map(n => n.id));
455
+ const ek = document.getElementById('ekind').value;
456
+ const econf = document.getElementById('econf').value;
457
+ const links = p.links.filter(l => {{
458
+ const s = lid(l,'source'), t = lid(l,'target');
459
+ if (!ids.has(s)||!ids.has(t)) return false;
460
+ if (ek && l.kind !== ek) return false;
461
+ if (econf && l.confidence !== econf) return false;
462
+ return true;
463
+ }});
464
+ return {{nodes, links}};
465
+ }}
466
+
467
+ function bfsPath(start, end, links) {{
468
+ const adj = {{}};
469
+ links.forEach(l => {{
470
+ const s = lid(l,'source'), t = lid(l,'target');
471
+ (adj[s]=adj[s]||[]).push(t);
472
+ (adj[t]=adj[t]||[]).push(s);
473
+ }});
474
+ if (start === end) return [start];
475
+ const q = [start], prev = {{[start]: null}};
476
+ while (q.length) {{
477
+ const cur = q.shift();
478
+ for (const nxt of (adj[cur]||[])) {{
479
+ if (nxt in prev) continue;
480
+ prev[nxt] = cur;
481
+ if (nxt === end) {{
482
+ const path = [end];
483
+ let x = end;
484
+ while (prev[x] !== null) {{ path.push(prev[x]); x = prev[x]; }}
485
+ return path.reverse();
486
+ }}
487
+ q.push(nxt);
488
+ }}
489
+ }}
490
+ return null;
491
+ }}
492
+
493
+ function neighborExpand(id, links, depth) {{
494
+ const adj = {{}};
495
+ links.forEach(l => {{
496
+ const s = lid(l,'source'), t = lid(l,'target');
497
+ (adj[s]=adj[s]||[]).push(t);
498
+ (adj[t]=adj[t]||[]).push(s);
499
+ }});
500
+ const seen = new Set([id]);
501
+ let frontier = [id];
502
+ for (let d=0; d<depth; d++) {{
503
+ const next = [];
504
+ frontier.forEach(n => (adj[n]||[]).forEach(x => {{
505
+ if (!seen.has(x)) {{ seen.add(x); next.push(x); }}
506
+ }}));
507
+ frontier = next;
508
+ }}
509
+ return seen;
510
+ }}
511
+
512
+ function escapeHtml(s) {{
513
+ return String(s||'').replace(/[&<>"']/g, c => ({{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}}[c]));
514
+ }}
515
+
516
+ function showDetail(n) {{
517
+ const nb = DATA.neighbors || {{}};
518
+ const callers = (nb.callers||{{}})[n.id] || [];
519
+ const callees = (nb.callees||{{}})[n.id] || [];
520
+ const importers = (nb.importers||{{}})[n.id] || [];
521
+ const d = document.getElementById('detail');
522
+ let html = '<strong>'+escapeHtml(n.id)+'</strong>\\n';
523
+ html += 'kind: '+escapeHtml(n.kind)+'\\n';
524
+ html += 'community: '+escapeHtml(n.community||'')+'\\n';
525
+ html += 'area: '+escapeHtml(n.area||'')+'\\n';
526
+ html += 'path: '+escapeHtml(n.path||'');
527
+ if (n.line) html += '\\nline: '+n.line;
528
+ if (n.entry) html += '\\n<span class="badge-entry">entry root</span>';
529
+ if ((n.flags||[]).length) html += '\\n<span class="flag-dead">'+escapeHtml(n.flags.join(', '))+(n.dead_confidence?' ['+escapeHtml(n.dead_confidence)+']':'')+'</span>';
530
+ html += '\\n\\ncallers: '+(callers.slice(0,12).map(escapeHtml).join(', ')||'(none)');
531
+ html += '\\ncallees: '+(callees.slice(0,12).map(escapeHtml).join(', ')||'(none)');
532
+ html += '\\nimporters: '+(importers.slice(0,12).map(escapeHtml).join(', ')||'(none)');
533
+ if (selected.length === 2) {{
534
+ html += '\\n\\nselected path: '+escapeHtml(selected.join(' → '));
535
+ }}
536
+ d.innerHTML = html;
537
+ }}
538
+
539
+ function colorKey(n) {{
540
+ const by = document.getElementById('colorBy').value;
541
+ if (by === 'kind') return n.kind || 'unknown';
542
+ if (by === 'area') return n.area || 'unknown';
543
+ return n.community || n.area || 'unknown';
544
+ }}
545
+
546
+ const elem = document.getElementById('graph');
547
+ const Graph = ForceGraph();
548
+ const g = Graph(elem)
549
+ .nodeId('id')
550
+ .nodeLabel(n => (n.entry ? '★ ' : '') + (n.path || n.id))
551
+ .nodeAutoColorBy(n => colorKey(n))
552
+ .nodeVal(n => n.val || 1)
553
+ .linkColor(l => {{
554
+ const s = lid(l,'source'), t = lid(l,'target');
555
+ if (pathHighlight.size && pathHighlight.has(s) && pathHighlight.has(t)) return '#f472b6';
556
+ return l.kind==='calls' ? '#f59e0b' : (l.kind==='inherits'||l.kind==='implements'||l.kind==='overrides') ? '#a78bfa' : l.kind==='decorates' ? '#2dd4bf' : '#64748b';
557
+ }})
558
+ .linkDirectionalParticles(l => {{
559
+ const s = lid(l,'source'), t = lid(l,'target');
560
+ if (pathHighlight.size && pathHighlight.has(s) && pathHighlight.has(t)) return 4;
561
+ return l.kind==='calls' ? 2 : 1;
562
+ }})
563
+ .linkDirectionalParticleWidth(l => {{
564
+ const s = lid(l,'source'), t = lid(l,'target');
565
+ if (pathHighlight.size && pathHighlight.has(s) && pathHighlight.has(t)) return 3;
566
+ return 1.5;
567
+ }})
568
+ .onNodeClick(n => {{
569
+ showDetail(n);
570
+ if (selected.length === 1 && selected[0] === n.id) {{ selected = []; pathHighlight = new Set(); redraw(); return; }}
571
+ if (selected.length >= 2) selected = [];
572
+ selected.push(n.id);
573
+ if (selected.length === 2) {{
574
+ const fd = filtered();
575
+ const path = bfsPath(selected[0], selected[1], fd.links.length ? fd.links : activePayload().links);
576
+ pathHighlight = path ? new Set(path) : new Set();
577
+ showDetail(n);
578
+ }}
579
+ redraw();
580
+ }})
581
+ .onNodeDblClick(n => {{
582
+ const links = activePayload().links;
583
+ expandIds = neighborExpand(n.id, links, 1);
584
+ redraw();
585
+ }});
586
+
587
+ function redraw() {{
588
+ const fd = filtered();
589
+ g.graphData(fd);
590
+ g.nodeAutoColorBy(n => colorKey(n));
591
+ g.width(elem.clientWidth).height(elem.clientHeight);
592
+ }}
593
+
594
+ function renderDeadList() {{
595
+ const ul = document.getElementById('deadList');
596
+ const sort = document.getElementById('deadSort').value;
597
+ const items = [...(DATA.dead_code||[])];
598
+ items.sort((a,b) => String(a[sort]||'').localeCompare(String(b[sort]||'')));
599
+ ul.innerHTML = items.slice(0,500).map(d =>
600
+ '<li data-id="'+escapeHtml(d.id)+'"><span class="flag-dead">['+escapeHtml(d.confidence)+']</span> '+escapeHtml(d.id)+'<br/><span class="muted">'+escapeHtml(d.path)+':'+(d.line||0)+' — '+escapeHtml(d.reason||'')+'</span></li>'
601
+ ).join('') || '<li class="muted">(none)</li>';
602
+ ul.querySelectorAll('li[data-id]').forEach(li => li.addEventListener('click', () => {{
603
+ const id = li.getAttribute('data-id');
604
+ mode = id.includes('::') ? 'symbol' : 'file';
605
+ document.getElementById('mode').value = mode;
606
+ expandIds = new Set([id]);
607
+ document.querySelector('.tab[data-tab="graph"]').click();
608
+ refillArea();
609
+ redraw();
610
+ const node = (activePayload().nodes||[]).find(n => n.id === id);
611
+ if (node) showDetail(node);
612
+ }}));
613
+ }}
614
+
615
+ function renderCommunities() {{
616
+ const ul = document.getElementById('commList');
617
+ const p = activePayload();
618
+ const items = p.communities || [];
619
+ document.getElementById('commHint').textContent = DATA.meta && DATA.meta.has_communities
620
+ ? 'Louvain communities (or area fallback).'
621
+ : 'Communities not computed yet — showing area groupings.';
622
+ ul.innerHTML = items.map(c =>
623
+ '<li data-c="'+escapeHtml(c.id)+'">'+escapeHtml(c.label)+' <span class="muted">('+c.size+')</span></li>'
624
+ ).join('') || '<li class="muted">(none)</li>';
625
+ ul.querySelectorAll('li[data-c]').forEach(li => li.addEventListener('click', () => {{
626
+ document.getElementById('area').value = li.getAttribute('data-c');
627
+ document.querySelector('.tab[data-tab="graph"]').click();
628
+ redraw();
629
+ }}));
630
+ }}
631
+
632
+ function renderProcesses() {{
633
+ const ul = document.getElementById('procList');
634
+ const procs = DATA.processes || [];
635
+ document.getElementById('procHint').textContent = procs.length
636
+ ? 'Click a process to focus its steps.'
637
+ : 'No processes in graph meta yet (intel phase). Entry roots: '+((((DATA.meta&&DATA.meta.entry_roots)||[]).slice(0,8).join(', '))||'(none)');
638
+ ul.innerHTML = procs.map((p,i) => {{
639
+ const name = p.name || p.entry || ('process '+i);
640
+ const steps = (p.steps || p.nodes || []).slice(0,20);
641
+ return '<li data-i="'+i+'"><strong>'+escapeHtml(name)+'</strong><br/><span class="muted">'+escapeHtml(steps.join(' → '))+'</span></li>';
642
+ }}).join('') || '<li class="muted">(none — entry roots listed above)</li>';
643
+ ul.querySelectorAll('li[data-i]').forEach(li => li.addEventListener('click', () => {{
644
+ const p = procs[Number(li.getAttribute('data-i'))];
645
+ const steps = p.steps || p.nodes || [];
646
+ if (steps.length) {{
647
+ expandIds = new Set(steps);
648
+ mode = String(steps[0]).includes('::') ? 'symbol' : 'file';
649
+ document.getElementById('mode').value = mode;
650
+ document.querySelector('.tab[data-tab="graph"]').click();
651
+ refillArea();
652
+ redraw();
653
+ }}
654
+ }}));
655
+ }}
656
+
657
+ function focusNode(id) {{
658
+ mode = String(id).includes('::') ? 'symbol' : 'file';
659
+ document.getElementById('mode').value = mode;
660
+ expandIds = new Set([id]);
661
+ document.querySelector('.tab[data-tab="graph"]').click();
662
+ refillArea();
663
+ redraw();
664
+ const node = (activePayload().nodes||[]).find(n => n.id === id);
665
+ if (node) showDetail(node);
666
+ }}
667
+
668
+ function renderIntel() {{
669
+ const hot = document.getElementById('hotList');
670
+ hot.innerHTML = (DATA.hotspots||[]).map(h =>
671
+ '<li data-id="'+escapeHtml(h.path)+'"><strong>'+escapeHtml(h.path)+'</strong><br/><span class="muted">score '+h.score+' — churn '+h.churn+', fan-in '+h.fan_in+'</span></li>'
672
+ ).join('') || '<li class="muted">(no churn data — needs git history)</li>';
673
+ const god = document.getElementById('godList');
674
+ god.innerHTML = (DATA.god_nodes||[]).map(gn =>
675
+ '<li data-id="'+escapeHtml(gn.id)+'"><strong>'+escapeHtml(gn.name||gn.path||gn.id)+'</strong><br/><span class="muted">deg '+gn.degree+' (in '+(gn.fan_in||0)+' / out '+(gn.fan_out||0)+')'+(gn.pagerank?' — pr '+gn.pagerank:'')+'</span></li>'
676
+ ).join('') || '<li class="muted">(none)</li>';
677
+ const cyc = document.getElementById('cycleList');
678
+ cyc.innerHTML = (DATA.circular_imports||[]).map(c =>
679
+ '<li><span class="flag-dead">len '+c.length+'</span> <span class="muted">'+escapeHtml((c.nodes||[]).join(' → '))+'</span></li>'
680
+ ).join('') || '<li class="muted">(none)</li>';
681
+ [...hot.querySelectorAll('li[data-id]'), ...god.querySelectorAll('li[data-id]')].forEach(li =>
682
+ li.addEventListener('click', () => focusNode(li.getAttribute('data-id'))));
683
+ }}
684
+
685
+ document.querySelectorAll('.tab').forEach(tab => tab.addEventListener('click', () => {{
686
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
687
+ document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
688
+ tab.classList.add('active');
689
+ document.getElementById('panel-'+tab.dataset.tab).classList.add('active');
690
+ if (tab.dataset.tab === 'dead') renderDeadList();
691
+ if (tab.dataset.tab === 'communities') renderCommunities();
692
+ if (tab.dataset.tab === 'processes') renderProcesses();
693
+ if (tab.dataset.tab === 'intel') renderIntel();
694
+ }}));
695
+
696
+ document.getElementById('mode').addEventListener('change', () => {{
697
+ mode = document.getElementById('mode').value;
698
+ expandIds = null;
699
+ selected = [];
700
+ pathHighlight = new Set();
701
+ refillArea();
702
+ redraw();
703
+ renderCommunities();
704
+ }});
705
+ ['search','area','ekind','econf','lensDead','lensUnwired','lensUnreach','deadConf','colorBy','deadSort'].forEach(id => {{
706
+ const el = document.getElementById(id);
707
+ el.addEventListener('input', redraw);
708
+ el.addEventListener('change', () => {{ redraw(); if (id==='deadSort') renderDeadList(); }});
709
+ }});
710
+ document.getElementById('reset').addEventListener('click', () => {{
711
+ document.getElementById('search').value='';
712
+ document.getElementById('area').value='';
713
+ document.getElementById('ekind').value='';
714
+ document.getElementById('econf').value='';
715
+ document.getElementById('lensDead').checked=false;
716
+ document.getElementById('lensUnwired').checked=false;
717
+ document.getElementById('lensUnreach').checked=false;
718
+ document.getElementById('deadConf').value='';
719
+ expandIds = null;
720
+ selected = [];
721
+ pathHighlight = new Set();
722
+ redraw();
723
+ }});
724
+ document.getElementById('clearPath').addEventListener('click', () => {{
725
+ selected = [];
726
+ pathHighlight = new Set();
727
+ redraw();
728
+ }});
729
+ window.addEventListener('resize', () => g.width(elem.clientWidth).height(elem.clientHeight));
730
+ if (!g || typeof g.zoomToFit !== 'function') document.getElementById('vendorWarn').style.display = 'block';
731
+ refillArea();
732
+ redraw();
733
+ renderDeadList();
734
+ renderCommunities();
735
+ renderProcesses();
736
+ renderIntel();
737
+ </script>
738
+ </body>
739
+ </html>
740
+ """
741
+
742
+
743
+ def write_graph_html(
744
+ root: Path,
745
+ *,
746
+ open_browser: bool = False,
747
+ symbols: bool = False,
748
+ ) -> Path:
749
+ """Write ``.devcouncil/graph/graph.html`` from the on-disk code graph."""
750
+ graph = load_code_graph(root)
751
+ if graph is None:
752
+ raise FileNotFoundError("No code graph found; run `dev map` first.")
753
+ out = graph_path(root).with_name("graph.html")
754
+ out.parent.mkdir(parents=True, exist_ok=True)
755
+ out.write_text(render_graph_html(graph, file_level=not symbols), encoding="utf-8")
756
+ if open_browser:
757
+ import webbrowser
758
+
759
+ webbrowser.open(out.resolve().as_uri())
760
+ return out
761
+
762
+
763
+ def sample_demo_graph() -> CodeGraph:
764
+ """Return a deterministic graph that exercises every visualizer surface."""
765
+ nodes = [
766
+ GraphNode(
767
+ id="src/devcouncil/cli/main.py",
768
+ kind=NodeKind.FILE,
769
+ path="src/devcouncil/cli/main.py",
770
+ name="main.py",
771
+ area="cli",
772
+ language="python",
773
+ community="demo-community",
774
+ ),
775
+ GraphNode(
776
+ id="src/devcouncil/execution/task_runner.py",
777
+ kind=NodeKind.FILE,
778
+ path="src/devcouncil/execution/task_runner.py",
779
+ name="task_runner.py",
780
+ area="execution",
781
+ language="python",
782
+ community="demo-community",
783
+ ),
784
+ GraphNode(
785
+ id="src/devcouncil/verification/verifier.py",
786
+ kind=NodeKind.FILE,
787
+ path="src/devcouncil/verification/verifier.py",
788
+ name="verifier.py",
789
+ area="verification",
790
+ language="python",
791
+ community="verification",
792
+ ),
793
+ GraphNode(
794
+ id="src/devcouncil/verification/unused_check.py",
795
+ kind=NodeKind.FILE,
796
+ path="src/devcouncil/verification/unused_check.py",
797
+ name="unused_check.py",
798
+ area="verification",
799
+ language="python",
800
+ community="verification",
801
+ ),
802
+ GraphNode(
803
+ id="src/devcouncil/cli/main.py::main",
804
+ kind=NodeKind.FUNCTION,
805
+ path="src/devcouncil/cli/main.py",
806
+ name="main",
807
+ line=20,
808
+ end_line=34,
809
+ area="cli",
810
+ language="python",
811
+ community="demo-community",
812
+ ),
813
+ GraphNode(
814
+ id="src/devcouncil/execution/task_runner.py::run_task",
815
+ kind=NodeKind.FUNCTION,
816
+ path="src/devcouncil/execution/task_runner.py",
817
+ name="run_task",
818
+ line=42,
819
+ end_line=78,
820
+ area="execution",
821
+ language="python",
822
+ community="demo-community",
823
+ ),
824
+ GraphNode(
825
+ id="src/devcouncil/verification/verifier.py::verify",
826
+ kind=NodeKind.FUNCTION,
827
+ path="src/devcouncil/verification/verifier.py",
828
+ name="verify",
829
+ line=55,
830
+ end_line=92,
831
+ area="verification",
832
+ language="python",
833
+ community="verification",
834
+ ),
835
+ GraphNode(
836
+ id="src/devcouncil/verification/unused_check.py::unused_check",
837
+ kind=NodeKind.FUNCTION,
838
+ path="src/devcouncil/verification/unused_check.py",
839
+ name="unused_check",
840
+ line=8,
841
+ end_line=12,
842
+ area="verification",
843
+ language="python",
844
+ community="verification",
845
+ ),
846
+ ]
847
+ edges = [
848
+ GraphEdge(
849
+ source="src/devcouncil/cli/main.py",
850
+ target="src/devcouncil/execution/task_runner.py",
851
+ kind="imports",
852
+ ),
853
+ GraphEdge(
854
+ source="src/devcouncil/execution/task_runner.py",
855
+ target="src/devcouncil/verification/verifier.py",
856
+ kind="imports",
857
+ ),
858
+ GraphEdge(
859
+ source="src/devcouncil/cli/main.py::main",
860
+ target="src/devcouncil/execution/task_runner.py::run_task",
861
+ kind="calls",
862
+ ),
863
+ GraphEdge(
864
+ source="src/devcouncil/execution/task_runner.py::run_task",
865
+ target="src/devcouncil/verification/verifier.py::verify",
866
+ kind="calls",
867
+ ),
868
+ ]
869
+ return CodeGraph(
870
+ nodes=nodes,
871
+ edges=edges,
872
+ dead_code=[
873
+ DeadCodeEntry(
874
+ id="src/devcouncil/verification/unused_check.py::unused_check",
875
+ path="src/devcouncil/verification/unused_check.py",
876
+ line=8,
877
+ kind="function",
878
+ confidence=Confidence.EXTRACTED,
879
+ reason="no inbound calls and token-scan agrees",
880
+ )
881
+ ],
882
+ entry_roots=["src/devcouncil/cli/main.py"],
883
+ unwired_candidates=["src/devcouncil/verification/unused_check.py"],
884
+ meta={
885
+ "processes": [
886
+ {
887
+ "name": "entry_flow",
888
+ "entry": "src/devcouncil/cli/main.py",
889
+ "steps": [
890
+ "src/devcouncil/cli/main.py::main",
891
+ "src/devcouncil/execution/task_runner.py::run_task",
892
+ "src/devcouncil/verification/verifier.py::verify",
893
+ ],
894
+ }
895
+ ]
896
+ },
897
+ )
898
+
899
+
900
+ def render_graph_preview_svg(graph: CodeGraph | None = None) -> str:
901
+ """Render a small dependency-flow preview without browser or JS support."""
902
+ from html import escape
903
+
904
+ demo = graph or sample_demo_graph()
905
+ files = [node for node in demo.nodes if node.kind == NodeKind.FILE][:4]
906
+ positions = {
907
+ node.id: (90 + index * 210, 150 + (index % 2) * 95)
908
+ for index, node in enumerate(files)
909
+ }
910
+ edge_lines: list[str] = []
911
+ for edge in demo.edges:
912
+ if edge.kind != "imports" or edge.source not in positions or edge.target not in positions:
913
+ continue
914
+ x1, y1 = positions[edge.source]
915
+ x2, y2 = positions[edge.target]
916
+ edge_lines.append(
917
+ f'<path d="M{x1 + 66},{y1} L{x2 - 66},{y2}" stroke="#64748b" '
918
+ 'stroke-width="3" fill="none" marker-end="url(#arrow)"/>'
919
+ )
920
+ node_groups: list[str] = []
921
+ dead_paths = {entry.path for entry in demo.dead_code}
922
+ for node in files:
923
+ x, y = positions[node.id]
924
+ color = "#e35d6a" if node.path in dead_paths else "#3d8bfd"
925
+ label = escape(node.name or node.path)
926
+ node_groups.append(
927
+ f'<g><rect x="{x - 68}" y="{y - 28}" width="136" height="56" rx="10" '
928
+ f'fill="{color}"/><text x="{x}" y="{y + 5}" text-anchor="middle" '
929
+ f'font-family="ui-sans-serif,system-ui" font-size="13" fill="#ffffff">{label}</text></g>'
930
+ )
931
+ return "\n".join(
932
+ [
933
+ '<svg xmlns="http://www.w3.org/2000/svg" width="860" height="360" viewBox="0 0 860 360" role="img" aria-labelledby="title desc">',
934
+ '<title id="title">DevCouncil Code Graph</title>',
935
+ '<desc id="desc">Sample entry, execution, verification, and dead-code dependency flow.</desc>',
936
+ '<rect width="860" height="360" fill="#0f1419"/>',
937
+ '<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L7,3 z" fill="#64748b"/></marker></defs>',
938
+ '<text x="36" y="48" font-family="ui-sans-serif,system-ui" font-size="24" font-weight="700" fill="#e7ecf3">DevCouncil Code Graph</text>',
939
+ '<text x="36" y="75" font-family="ui-sans-serif,system-ui" font-size="13" fill="#8b9bb4">Native dependency and liveness preview</text>',
940
+ *edge_lines,
941
+ *node_groups,
942
+ '</svg>',
943
+ ]
944
+ )
945
+
946
+
947
+ def write_graph_demo(root: Path, *, open_browser: bool = False) -> dict[str, Path]:
948
+ """Write deterministic HTML and SVG demo artifacts without requiring a map."""
949
+ out_dir = root / ".devcouncil" / "graph"
950
+ out_dir.mkdir(parents=True, exist_ok=True)
951
+ graph = sample_demo_graph()
952
+ html_path = out_dir / "demo.html"
953
+ svg_path = out_dir / "demo.svg"
954
+ html_path.write_text(render_graph_html(graph), encoding="utf-8")
955
+ svg_path.write_text(render_graph_preview_svg(graph), encoding="utf-8")
956
+ if open_browser:
957
+ import webbrowser
958
+
959
+ webbrowser.open(html_path.resolve().as_uri())
960
+ return {"html": html_path, "svg": svg_path}