devcouncil 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -12,15 +12,19 @@ from pathlib import Path
12
12
  from devcouncil.app.config import DevCouncilConfig, load_config
13
13
 
14
14
  WORKFLOW_RELPATH = Path(".github") / "workflows" / "devcouncil.yml"
15
+ EVIDENCE_WORKFLOW_RELPATH = Path(".github") / "workflows" / "devcouncil-evidence.yml"
15
16
 
16
17
  _PYTHON_TOOLS = {
17
18
  "pytest", "flake8", "ruff", "mypy", "tox", "python", "python3", "uv",
18
- "poetry", "black", "isort", "pyright",
19
+ "poetry", "black", "isort", "pyright", "pip-audit",
19
20
  }
20
21
  _NODE_TOOLS = {
21
22
  "npm", "npx", "pnpm", "yarn", "bun", "eslint", "tsc", "jest", "vitest", "node",
22
23
  }
24
+ _GO_TOOLS = {"go", "govulncheck"}
25
+ _RUST_TOOLS = {"cargo", "rustc", "rustfmt", "clippy-driver"}
23
26
  _PYTHON_MARKERS = ("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg", "Pipfile")
27
+ _UV_LOCK = "uv.lock"
24
28
 
25
29
 
26
30
  def detect_stacks(project_root: Path) -> set[str]:
@@ -28,8 +32,14 @@ def detect_stacks(project_root: Path) -> set[str]:
28
32
  stacks: set[str] = set()
29
33
  if any((project_root / marker).exists() for marker in _PYTHON_MARKERS):
30
34
  stacks.add("python")
35
+ if (project_root / _UV_LOCK).exists():
36
+ stacks.add("python")
31
37
  if (project_root / "package.json").exists():
32
38
  stacks.add("node")
39
+ if (project_root / "go.mod").exists():
40
+ stacks.add("go")
41
+ if (project_root / "Cargo.toml").exists():
42
+ stacks.add("rust")
33
43
  return stacks
34
44
 
35
45
 
@@ -39,6 +49,10 @@ def _command_stack(command: str) -> str | None:
39
49
  return "python"
40
50
  if tool in _NODE_TOOLS:
41
51
  return "node"
52
+ if tool in _GO_TOOLS:
53
+ return "go"
54
+ if tool in _RUST_TOOLS:
55
+ return "rust"
42
56
  return None
43
57
 
44
58
 
@@ -68,6 +82,16 @@ _AUDIT_STEPS: dict[str, list[str]] = {
68
82
  " continue-on-error: true",
69
83
  " run: npm audit --audit-level=high",
70
84
  ],
85
+ "go": [
86
+ " - name: Dependency audit (govulncheck)",
87
+ " continue-on-error: true",
88
+ " run: govulncheck ./...",
89
+ ],
90
+ "rust": [
91
+ " - name: Dependency audit (cargo audit)",
92
+ " continue-on-error: true",
93
+ " run: cargo audit",
94
+ ],
71
95
  }
72
96
 
73
97
 
@@ -86,6 +110,45 @@ def _python_version(project_root: Path) -> str:
86
110
  return "3.12"
87
111
 
88
112
 
113
+ def _uses_uv(project_root: Path) -> bool:
114
+ return (project_root / _UV_LOCK).exists()
115
+
116
+
117
+ def _default_install_command(project_root: Path) -> str:
118
+ if _uses_uv(project_root):
119
+ return f"uv sync --all-groups --python {_python_version(project_root)}"
120
+ return "pip install -e ."
121
+
122
+
123
+ def _default_dev_command(project_root: Path) -> str:
124
+ if _uses_uv(project_root):
125
+ return f"uv run --python {_python_version(project_root)} dev"
126
+ return "dev"
127
+
128
+
129
+ def _evidence_python_setup_steps(project_root: Path, *, install_command: str) -> list[str]:
130
+ py_version = _python_version(project_root)
131
+ if _uses_uv(project_root):
132
+ return [
133
+ " - name: Install uv",
134
+ " uses: astral-sh/setup-uv@v7",
135
+ " - name: Set up Python",
136
+ " uses: actions/setup-python@v6",
137
+ " with:",
138
+ f' python-version: "{py_version}"',
139
+ " - name: Install DevCouncil",
140
+ f" run: {install_command}",
141
+ ]
142
+ return [
143
+ " - name: Set up Python",
144
+ " uses: actions/setup-python@v5",
145
+ " with:",
146
+ f' python-version: "{py_version}"',
147
+ " - name: Install DevCouncil",
148
+ f" run: {install_command}",
149
+ ]
150
+
151
+
89
152
  def render_workflow(
90
153
  project_root: Path,
91
154
  default_branch: str = "main",
@@ -115,6 +178,20 @@ def render_workflow(
115
178
  " with:",
116
179
  ' node-version: "20"',
117
180
  ]
181
+ if "go" in stacks:
182
+ steps += [
183
+ " - name: Set up Go",
184
+ " uses: actions/setup-go@v5",
185
+ " with:",
186
+ ' go-version: "stable"',
187
+ ]
188
+ if "rust" in stacks:
189
+ steps += [
190
+ " - name: Set up Rust",
191
+ " uses: dtolnay/rust-toolchain@stable",
192
+ " with:",
193
+ " components: clippy",
194
+ ]
118
195
 
119
196
  def add_command_steps(label: str, raw_commands: list[str]) -> None:
120
197
  for command in _applicable_commands(raw_commands, stacks):
@@ -146,6 +223,104 @@ def render_workflow(
146
223
  )
147
224
 
148
225
 
226
+ def render_evidence_workflow(
227
+ project_root: Path,
228
+ default_branch: str = "main",
229
+ *,
230
+ install_command: str | None = None,
231
+ dev_command: str | None = None,
232
+ ) -> str:
233
+ """Render a PR evidence workflow: verify → JSON/HTML artifacts → optional GitHub integrations.
234
+
235
+ Expects ``GITHUB_TOKEN`` (for Checks/comments), ``GITHUB_REPOSITORY``, ``GITHUB_SHA``,
236
+ and ``GITHUB_PR_NUMBER`` (or ``PR_NUMBER``) when posting PR comments. The Check
237
+ conclusion is blocking-only (``--fail-on-blocking``); advisory gaps stay in artifacts.
238
+ """
239
+ if install_command is None:
240
+ install_command = _default_install_command(project_root)
241
+ if dev_command is None:
242
+ dev_command = _default_dev_command(project_root)
243
+
244
+ stacks = detect_stacks(project_root)
245
+ steps: list[str] = [
246
+ " - name: Checkout",
247
+ " uses: actions/checkout@v4",
248
+ " with:",
249
+ " fetch-depth: 0",
250
+ ]
251
+ if "python" in stacks or not stacks:
252
+ steps += _evidence_python_setup_steps(project_root, install_command=install_command)
253
+ steps += [
254
+ " - name: DevCouncil verify (scoped diff)",
255
+ " env:",
256
+ " # pull_request → base branch SHA; push → previous commit (github.event.before).",
257
+ " # New branch / first push: before is all zeros — skip verify (no diff base).",
258
+ " VERIFY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}",
259
+ " run: |",
260
+ ' ZERO_SHA="0000000000000000000000000000000000000000"',
261
+ ' if [ "$VERIFY_BASE" = "$ZERO_SHA" ]; then',
262
+ ' VERIFY_BASE=""',
263
+ " fi",
264
+ ' if [ -n "$VERIFY_BASE" ]; then',
265
+ f" {dev_command} check --verify --base \"$VERIFY_BASE\" --persist --project-root .",
266
+ " else",
267
+ ' echo "::notice::Skipping DevCouncil verify — no diff base (first push / new branch). Push again or open a PR for scoped verify."',
268
+ " exit 0",
269
+ " fi",
270
+ " - name: Write evidence JSON artifact",
271
+ f" run: {dev_command} report --evidence-json .devcouncil/evidence.json --fail-on-blocking --project-root .",
272
+ " - name: Write evidence HTML artifact",
273
+ f" run: {dev_command} report --evidence-html .devcouncil/evidence.html --fail-on-blocking --project-root .",
274
+ " - name: Upload evidence artifacts",
275
+ " uses: actions/upload-artifact@v4",
276
+ " with:",
277
+ ' name: devcouncil-evidence',
278
+ " path: |",
279
+ " .devcouncil/evidence.json",
280
+ " .devcouncil/evidence.html",
281
+ " if-no-files-found: error",
282
+ " - name: Post GitHub Check (blocking gaps only)",
283
+ " if: ${{ secrets.GITHUB_TOKEN != '' }}",
284
+ " env:",
285
+ " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
286
+ " GITHUB_REPOSITORY: ${{ github.repository }}",
287
+ " GITHUB_SHA: ${{ github.sha }}",
288
+ f" run: {dev_command} report --github --fail-on-blocking --project-root .",
289
+ " - name: Post PR comment (includes advisory gaps)",
290
+ " if: github.event_name == 'pull_request'",
291
+ " env:",
292
+ " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
293
+ " GITHUB_REPOSITORY: ${{ github.repository }}",
294
+ " GITHUB_SHA: ${{ github.sha }}",
295
+ " GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}",
296
+ f" run: {dev_command} report --github-pr-comment --fail-on-blocking --project-root .",
297
+ ]
298
+ body = "\n".join(steps)
299
+ return (
300
+ "# DevCouncil evidence workflow — verify, export artifacts, optional GitHub Check/comment.\n"
301
+ "# Generated by DevCouncil; adjust install/dev commands for your stack.\n"
302
+ "# Check conclusion fails only on blocking gaps; advisory gaps appear in artifacts/comments.\n"
303
+ "name: DevCouncil Evidence\n"
304
+ "\n"
305
+ "permissions:\n"
306
+ " contents: read\n"
307
+ " checks: write\n"
308
+ " pull-requests: write\n"
309
+ "\n"
310
+ "on:\n"
311
+ " push:\n"
312
+ f' branches: ["{default_branch}"]\n'
313
+ " pull_request:\n"
314
+ f' branches: ["{default_branch}"]\n'
315
+ "\n"
316
+ "jobs:\n"
317
+ " evidence:\n"
318
+ " runs-on: ubuntu-latest\n"
319
+ " steps:\n"
320
+ f"{body}\n"
321
+ )
322
+
323
+
149
324
  def scaffold_ci(project_root: Path, force: bool = False) -> Path | None:
150
325
  """Write the starter workflow. Returns the path, or None if one already exists.
151
326
 
@@ -163,3 +338,24 @@ def scaffold_ci(project_root: Path, force: bool = False) -> Path | None:
163
338
  render_workflow(project_root, default_branch, config), encoding="utf-8"
164
339
  )
165
340
  return target
341
+
342
+
343
+ def scaffold_evidence_ci(project_root: Path, force: bool = False) -> Path | None:
344
+ """Write the evidence export workflow. Returns the path, or None if one already exists."""
345
+ project_root = project_root.resolve()
346
+ target = project_root / EVIDENCE_WORKFLOW_RELPATH
347
+ if target.exists() and not force:
348
+ return None
349
+ config = load_config(project_root)
350
+ default_branch = config.project.default_branch or "main"
351
+ target.parent.mkdir(parents=True, exist_ok=True)
352
+ target.write_text(
353
+ render_evidence_workflow(
354
+ project_root,
355
+ default_branch,
356
+ install_command=_default_install_command(project_root),
357
+ dev_command=_default_dev_command(project_root),
358
+ ),
359
+ encoding="utf-8",
360
+ )
361
+ return target
@@ -10,7 +10,6 @@ GITIGNORE_SECTIONS: tuple[tuple[str, tuple[str, ...]], ...] = (
10
10
  ".devcouncil/*",
11
11
  "!.devcouncil/",
12
12
  "!.devcouncil/config.yaml",
13
- "!.devcouncil/graphify.yaml",
14
13
  ),
15
14
  ),
16
15
  (
@@ -28,7 +27,7 @@ GITIGNORE_SECTIONS: tuple[tuple[str, tuple[str, ...]], ...] = (
28
27
  ".conducor/",
29
28
  ".antigravity/",
30
29
  ".warp/",
31
- ".gitnexus",
30
+ ".grok/",
32
31
  ),
33
32
  ),
34
33
  (
@@ -0,0 +1,124 @@
1
+ """Reviewer-facing export of the requirement→task→diff→evidence graph.
2
+
3
+ This is a pure serializer over an already-loaded :class:`ArtifactGraph` (no new
4
+ analysis): it flattens what the standard report loads into a single JSON document a
5
+ PR reviewer can read — or attach as a CI artifact — without installing DevCouncil.
6
+ Written by ``dev report --evidence-json PATH``.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List
12
+
13
+ from devcouncil.artifacts.graph import ArtifactGraph
14
+ from devcouncil.reporting.verdict import classify_verdict
15
+
16
+ _SEVERITY_RANK = {"low": 0, "medium": 1, "high": 2, "critical": 3}
17
+
18
+
19
+ class EvidenceExportGenerator:
20
+ """Serializes the artifact graph as a single reviewer-readable JSON file."""
21
+
22
+ FORMAT = "devcouncil-evidence-export"
23
+ VERSION = 1
24
+
25
+ @staticmethod
26
+ def generate(graph: ArtifactGraph, live_review: dict | None = None, wiki_refresh: dict | None = None) -> str:
27
+ summary = graph.coverage_summary()
28
+ live_blockers = len((live_review or {}).get("blocking_cards", []))
29
+
30
+ verdict, incomplete_kind = classify_verdict(graph, live_blockers=live_blockers)
31
+
32
+ # Index evidence and diffs once so requirements/tasks can nest their own slices.
33
+ evidence_by_ac: Dict[str, List[Dict[str, Any]]] = {}
34
+ for ev in graph.test_evidence:
35
+ evidence_by_ac.setdefault(ev.acceptance_criterion_id, []).append(
36
+ {
37
+ "requirement_id": ev.requirement_id,
38
+ "command": ev.command,
39
+ "status": ev.status, # passed | failed | not_run
40
+ "mode": getattr(ev, "mode", "") or "unspecified",
41
+ "summary": ev.evidence_summary,
42
+ }
43
+ )
44
+
45
+ diffs_by_task: Dict[str, List[Dict[str, Any]]] = {}
46
+ for de in graph.diff_evidence:
47
+ diffs_by_task.setdefault(de.task_id, []).append(
48
+ {
49
+ "changed_files": de.changed_files,
50
+ "added_files": de.added_files,
51
+ "deleted_files": de.deleted_files,
52
+ "diff_summary": de.diff_summary,
53
+ }
54
+ )
55
+
56
+ tasks_by_requirement: Dict[str, List[str]] = {}
57
+ for task in graph.tasks.values():
58
+ for req_id in task.requirement_ids:
59
+ tasks_by_requirement.setdefault(req_id, []).append(task.id)
60
+
61
+ unproven_ac_ids = {ac.id for _, ac in graph.acceptance_criteria_without_evidence()}
62
+
63
+ requirements = [
64
+ {
65
+ "id": req.id,
66
+ "title": req.title,
67
+ "description": req.description,
68
+ "priority": req.priority,
69
+ "task_ids": sorted(tasks_by_requirement.get(req.id, [])),
70
+ "acceptance_criteria": [
71
+ {
72
+ "id": ac.id,
73
+ "description": ac.description,
74
+ "verification_method": ac.verification_method,
75
+ "required": ac.required,
76
+ "proven": ac.id not in unproven_ac_ids,
77
+ "evidence": evidence_by_ac.get(ac.id, []),
78
+ }
79
+ for ac in req.acceptance_criteria
80
+ ],
81
+ }
82
+ for req in sorted(graph.requirements.values(), key=lambda r: r.id)
83
+ ]
84
+
85
+ tasks = [
86
+ {
87
+ "id": task.id,
88
+ "title": task.title,
89
+ "status": task.status,
90
+ "requirement_ids": task.requirement_ids,
91
+ "acceptance_criterion_ids": task.acceptance_criterion_ids,
92
+ "diffs": diffs_by_task.get(task.id, []),
93
+ }
94
+ for task in sorted(graph.tasks.values(), key=lambda t: t.id)
95
+ ]
96
+
97
+ # All gaps (each carries its own ``blocking`` flag), blocking-first then by
98
+ # severity so a reviewer reads the show-stoppers before the advisories.
99
+ gaps = [
100
+ gap.model_dump()
101
+ for gap in sorted(
102
+ graph.gaps.values(),
103
+ key=lambda g: (not g.blocking, -_SEVERITY_RANK.get(g.severity, 0), g.id),
104
+ )
105
+ ]
106
+
107
+ report: Dict[str, Any] = {
108
+ "format": EvidenceExportGenerator.FORMAT,
109
+ "version": EvidenceExportGenerator.VERSION,
110
+ "generated_at": datetime.now(timezone.utc).isoformat(),
111
+ "verdict": verdict,
112
+ "coverage_summary": summary,
113
+ "requirements": requirements,
114
+ "tasks": tasks,
115
+ "gaps": gaps,
116
+ }
117
+ if incomplete_kind is not None:
118
+ report["incomplete_kind"] = incomplete_kind
119
+ if live_review is not None:
120
+ report["live_review"] = live_review
121
+ if wiki_refresh is not None:
122
+ report["wiki_refresh"] = wiki_refresh
123
+
124
+ return json.dumps(report, indent=2)
@@ -0,0 +1,210 @@
1
+ """Self-contained HTML renderer for the requirement→task→diff→evidence graph.
2
+
3
+ Companion to :mod:`devcouncil.reporting.evidence_export` — same reviewer-facing
4
+ content, browsable in CI artifact previews without installing DevCouncil.
5
+ Written by ``dev report --evidence-html PATH``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html
11
+ from datetime import datetime, timezone
12
+ from typing import Any
13
+
14
+ from devcouncil.artifacts.graph import ArtifactGraph
15
+ from devcouncil.reporting.evidence_export import EvidenceExportGenerator
16
+
17
+ _STYLE = """
18
+ body{font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
19
+ max-width:64rem;margin:2rem auto;padding:0 1rem;color:#1a1a1a;background:#fff}
20
+ h1,h2,h3{line-height:1.25}a{color:#0b66c3}table{border-collapse:collapse;width:100%;margin:1rem 0}
21
+ th,td{border:1px solid #ddd;padding:.45rem .6rem;text-align:left;vertical-align:top}
22
+ th{background:#f6f8fa}.meta{color:#555;font-size:.92em;margin:.5rem 0}
23
+ .verdict-passed{color:#116329}.verdict-incomplete{color:#9a6700}.verdict-blocked{color:#cf222e}
24
+ .badge{display:inline-block;border-radius:999px;padding:.1em .55em;font-size:.78em;font-weight:600}
25
+ .badge-pass{background:#dafbe1;color:#116329}.badge-fail{background:#ffebe9;color:#cf222e}
26
+ .badge-warn{background:#fff8c5;color:#9a6700}.badge-advisory{background:#eef3fb;color:#0b66c3}
27
+ .gap-blocking{font-weight:600}.section{margin:2rem 0 1rem}
28
+ ul.compact{margin:.3rem 0;padding-left:1.2rem}code{background:#f3f3f3;padding:.1em .3em;border-radius:3px}
29
+ """.strip()
30
+
31
+
32
+ def _esc(text: object) -> str:
33
+ return html.escape(str(text), quote=True)
34
+
35
+
36
+ def _verdict_class(verdict: str) -> str:
37
+ return {
38
+ "passed": "verdict-passed",
39
+ "incomplete": "verdict-incomplete",
40
+ "blocked": "verdict-blocked",
41
+ }.get(verdict, "")
42
+
43
+
44
+ def _status_badge(status: str) -> str:
45
+ cls = {
46
+ "passed": "badge-pass",
47
+ "failed": "badge-fail",
48
+ "not_run": "badge-warn",
49
+ }.get(status, "badge-warn")
50
+ return f'<span class="badge {cls}">{_esc(status)}</span>'
51
+
52
+
53
+ def _proven_badge(proven: bool) -> str:
54
+ if proven:
55
+ return '<span class="badge badge-pass">proven</span>'
56
+ return '<span class="badge badge-warn">unproven</span>'
57
+
58
+
59
+ def _render_gaps(gaps: list[dict[str, Any]]) -> str:
60
+ if not gaps:
61
+ return "<p>No gaps recorded.</p>"
62
+ rows: list[str] = []
63
+ for gap in gaps:
64
+ blocking = gap.get("blocking", False)
65
+ severity = gap.get("severity", "")
66
+ row_cls = "gap-blocking" if blocking else ""
67
+ kind = "blocking" if blocking else "advisory"
68
+ badge = "badge-fail" if blocking else "badge-advisory"
69
+ rows.append(
70
+ "<tr>"
71
+ f'<td class="{row_cls}"><span class="badge {badge}">{_esc(kind)}</span></td>'
72
+ f"<td>{_esc(gap.get('id', ''))}</td>"
73
+ f"<td>{_esc(severity)}</td>"
74
+ f"<td>{_esc(gap.get('description', ''))}</td>"
75
+ f"<td>{_esc(gap.get('task_id', '') or '—')}</td>"
76
+ "</tr>"
77
+ )
78
+ return (
79
+ "<table><thead><tr><th>Kind</th><th>ID</th><th>Severity</th>"
80
+ "<th>Description</th><th>Task</th></tr></thead><tbody>"
81
+ + "".join(rows)
82
+ + "</tbody></table>"
83
+ )
84
+
85
+
86
+ def _render_requirements(requirements: list[dict[str, Any]]) -> str:
87
+ parts: list[str] = []
88
+ for req in requirements:
89
+ parts.append(f"<h3 id=\"{_esc(req.get('id', ''))}\">{_esc(req.get('id', ''))}: {_esc(req.get('title', ''))}</h3>")
90
+ parts.append(f"<p class=\"meta\">{_esc(req.get('description', ''))}</p>")
91
+ task_ids = req.get("task_ids") or []
92
+ if task_ids:
93
+ links = ", ".join(f'<a href="#task-{_esc(tid)}">{_esc(tid)}</a>' for tid in task_ids)
94
+ parts.append(f"<p class=\"meta\">Tasks: {links}</p>")
95
+ parts.append(
96
+ "<table><thead><tr><th>AC</th><th>Description</th><th>Method</th>"
97
+ "<th>Status</th><th>Evidence</th></tr></thead><tbody>"
98
+ )
99
+ for ac in req.get("acceptance_criteria") or []:
100
+ evidence_items = ac.get("evidence") or []
101
+ if evidence_items:
102
+ ev_html = "<ul class=\"compact\">" + "".join(
103
+ "<li>"
104
+ f"{_status_badge(ev.get('status', ''))} "
105
+ f"<code>{_esc(ev.get('command', ''))}</code> — {_esc(ev.get('summary', ''))}"
106
+ "</li>"
107
+ for ev in evidence_items
108
+ ) + "</ul>"
109
+ else:
110
+ ev_html = "<em>none</em>"
111
+ parts.append(
112
+ "<tr>"
113
+ f"<td>{_esc(ac.get('id', ''))}</td>"
114
+ f"<td>{_esc(ac.get('description', ''))}</td>"
115
+ f"<td>{_esc(ac.get('verification_method', ''))}</td>"
116
+ f"<td>{_proven_badge(bool(ac.get('proven')))}</td>"
117
+ f"<td>{ev_html}</td>"
118
+ "</tr>"
119
+ )
120
+ parts.append("</tbody></table>")
121
+ return "\n".join(parts)
122
+
123
+
124
+ def _render_tasks(tasks: list[dict[str, Any]]) -> str:
125
+ parts: list[str] = []
126
+ for task in tasks:
127
+ tid = task.get("id", "")
128
+ parts.append(
129
+ f"<h3 id=\"task-{_esc(tid)}\">{_esc(tid)}: {_esc(task.get('title', ''))}</h3>"
130
+ )
131
+ parts.append(
132
+ f"<p class=\"meta\">Status: {_esc(task.get('status', ''))} · "
133
+ f"Requirements: {_esc(', '.join(task.get('requirement_ids') or []))}</p>"
134
+ )
135
+ diffs = task.get("diffs") or []
136
+ if not diffs:
137
+ parts.append("<p class=\"meta\">No diff evidence linked.</p>")
138
+ continue
139
+ parts.append("<ul class=\"compact\">")
140
+ for diff in diffs:
141
+ changed = diff.get("changed_files") or []
142
+ added = diff.get("added_files") or []
143
+ deleted = diff.get("deleted_files") or []
144
+ parts.append("<li>")
145
+ if changed:
146
+ parts.append(f"Changed: {_esc(', '.join(changed))}<br>")
147
+ if added:
148
+ parts.append(f"Added: {_esc(', '.join(added))}<br>")
149
+ if deleted:
150
+ parts.append(f"Deleted: {_esc(', '.join(deleted))}<br>")
151
+ parts.append(_esc(diff.get("diff_summary", "")))
152
+ parts.append("</li>")
153
+ parts.append("</ul>")
154
+ return "\n".join(parts)
155
+
156
+
157
+ class EvidenceHtmlGenerator:
158
+ """Renders the artifact graph as a single self-contained HTML document."""
159
+
160
+ FORMAT = "devcouncil-evidence-html"
161
+ VERSION = 1
162
+
163
+ @classmethod
164
+ def generate(
165
+ cls,
166
+ graph: ArtifactGraph,
167
+ live_review: dict | None = None,
168
+ wiki_refresh: dict | None = None,
169
+ ) -> str:
170
+ payload = EvidenceExportGenerator.generate(
171
+ graph, live_review=live_review, wiki_refresh=wiki_refresh
172
+ )
173
+ import json
174
+
175
+ data = json.loads(payload)
176
+ verdict = data.get("verdict", "incomplete")
177
+ summary = data.get("coverage_summary") or {}
178
+ generated_at = data.get("generated_at") or datetime.now(timezone.utc).isoformat()
179
+
180
+ body = [
181
+ "<!DOCTYPE html>",
182
+ '<html lang="en">',
183
+ "<head>",
184
+ '<meta charset="utf-8">',
185
+ "<title>DevCouncil Evidence Report</title>",
186
+ f"<style>{_STYLE}</style>",
187
+ "</head>",
188
+ "<body>",
189
+ "<h1>DevCouncil Evidence Report</h1>",
190
+ f'<p class="meta">Generated {_esc(generated_at)} · format {_esc(cls.FORMAT)} v{cls.VERSION}</p>',
191
+ f'<p class="{_verdict_class(verdict)}"><strong>Verdict:</strong> {_esc(verdict)}</p>',
192
+ "<p class=\"meta\">"
193
+ f"Requirements: {_esc(summary.get('total_requirements', 0))} · "
194
+ f"Tasks: {_esc(summary.get('total_tasks', 0))} · "
195
+ f"Blocking gaps: {_esc(summary.get('blocking_gaps', 0))} · "
196
+ f"AC without evidence: {_esc(summary.get('ac_without_evidence', 0))}"
197
+ "</p>",
198
+ '<div class="section"><h2>Gaps</h2>',
199
+ "<p class=\"meta\">GitHub Checks fail only on blocking gaps; advisory gaps appear here for reviewers.</p>",
200
+ _render_gaps(data.get("gaps") or []),
201
+ "</div>",
202
+ '<div class="section"><h2>Requirements &amp; Acceptance Criteria</h2>',
203
+ _render_requirements(data.get("requirements") or []),
204
+ "</div>",
205
+ '<div class="section"><h2>Tasks &amp; Diffs</h2>',
206
+ _render_tasks(data.get("tasks") or []),
207
+ "</div>",
208
+ "</body></html>",
209
+ ]
210
+ return "\n".join(body)
@@ -1,24 +1,22 @@
1
1
  import json
2
+ from typing import Any
3
+
4
+ from devcouncil.artifacts.coverage import (
5
+ acceptance_criteria_evidence_matrix,
6
+ requirement_task_matrix,
7
+ )
2
8
  from devcouncil.artifacts.graph import ArtifactGraph
9
+ from devcouncil.reporting.verdict import classify_verdict
3
10
 
4
11
  class JsonReportGenerator:
5
12
  """Generates a JSON evidence report."""
6
13
 
7
14
  @staticmethod
8
- def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
15
+ def generate(graph: ArtifactGraph, live_review: dict | None = None, wiki_refresh: dict | None = None) -> str:
9
16
  summary = graph.coverage_summary()
10
17
  live_blockers = len((live_review or {}).get("blocking_cards", []))
11
18
 
12
- # Three honest states (see markdown_report for rationale):
13
- # blocked - positive evidence of a problem.
14
- # incomplete - nothing failing, but not every AC has passing evidence.
15
- # passed - no blocking gaps and every AC proven.
16
- if summary["blocking_gaps"] > 0 or live_blockers > 0:
17
- verdict = "blocked"
18
- elif summary["ac_without_evidence"] > 0:
19
- verdict = "incomplete"
20
- else:
21
- verdict = "passed"
19
+ verdict, incomplete_kind = classify_verdict(graph, live_blockers=live_blockers)
22
20
  # Proof-rigor breakdown: of the criteria that ARE proven, how were they proven?
23
21
  # ``compiled``/``vote`` are precise per-criterion checks (trustworthy); ``coarse``
24
22
  # means proven only by a passing acceptance-capable command (weak). Surfacing this
@@ -29,13 +27,19 @@ class JsonReportGenerator:
29
27
  if getattr(ev, "status", "") == "passed":
30
28
  key = getattr(ev, "mode", "") or "unspecified"
31
29
  proof_modes[key] = proof_modes.get(key, 0) + 1
32
- report = {
30
+ report: dict[str, Any] = {
33
31
  "verdict": verdict,
34
32
  "coverage_summary": summary,
35
33
  "proof_modes": proof_modes,
34
+ "requirement_task_matrix": requirement_task_matrix(graph),
35
+ "acceptance_criteria_evidence_matrix": acceptance_criteria_evidence_matrix(graph),
36
36
  "blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
37
37
  }
38
+ if incomplete_kind is not None:
39
+ report["incomplete_kind"] = incomplete_kind
38
40
  if live_review is not None:
39
41
  report["live_review"] = live_review
42
+ if wiki_refresh is not None:
43
+ report["wiki_refresh"] = wiki_refresh
40
44
 
41
45
  return json.dumps(report, indent=2)