devcouncil 0.3.1 → 0.4.1

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 (366) hide show
  1. package/README.md +92 -33
  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 +56 -40
  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 +964 -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 +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -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 +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -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 +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -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 +53 -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 +325 -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/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,147 @@
1
+ """Detect when a verification command failed to run vs. a real test failure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Optional, Tuple
8
+
9
+ from devcouncil.domain.evidence import CommandResult
10
+
11
+ # Signatures that mean the verification command itself could not run (or had
12
+ # nothing to run), so its non-zero exit says nothing about whether the
13
+ # implementation is correct — a tooling/plan defect, not a code defect.
14
+ MALFORMED_COMMAND_SIGNATURES = (
15
+ "syntaxerror",
16
+ "invalid syntax",
17
+ "indentationerror",
18
+ "no module named",
19
+ "can't open file",
20
+ "no such file or directory",
21
+ "file or directory not found",
22
+ "no tests ran",
23
+ "no tests collected",
24
+ "error: not found",
25
+ "is not recognized as an internal or external command",
26
+ "command not found",
27
+ "executable file not found",
28
+ "failed to run command",
29
+ "importerror",
30
+ "modulenotfounderror",
31
+ )
32
+
33
+ UNCONDITIONAL_UNRUNNABLE_SIGNATURES = (
34
+ "syntaxerror",
35
+ "invalid syntax",
36
+ "indentationerror",
37
+ "can't open file",
38
+ "is not recognized as an internal or external command",
39
+ "command not found",
40
+ "executable file not found",
41
+ "failed to run command",
42
+ "no tests ran",
43
+ "no tests collected",
44
+ "error: not found",
45
+ )
46
+
47
+ PYTEST_NONRUN_EXIT_CODES = {4, 5}
48
+
49
+ TRACEBACK_FRAME_RE = re.compile(r'File "(?P<file>[^"]+)", line (?P<line>\d+)')
50
+
51
+
52
+ def is_traceback_frame(line: str) -> bool:
53
+ """True for a Python traceback frame line: `` File "...", line N``."""
54
+ stripped = line.strip()
55
+ return stripped.startswith('File "') and ", line " in stripped
56
+
57
+
58
+ def malformed_signature_precedes_traceback(text: str) -> bool:
59
+ """Decide whether an unrunnable-launcher signature is authoritative."""
60
+ if not text:
61
+ return False
62
+ low_all = text.lower()
63
+ if any(sig in low_all for sig in UNCONDITIONAL_UNRUNNABLE_SIGNATURES):
64
+ return True
65
+ lines = text.splitlines()
66
+ lowered_lines = [ln.lower() for ln in lines]
67
+ first_frame_idx: Optional[int] = None
68
+ for idx, line in enumerate(lines):
69
+ if is_traceback_frame(line):
70
+ first_frame_idx = idx
71
+ break
72
+ for idx, low in enumerate(lowered_lines):
73
+ if any(sig in low for sig in MALFORMED_COMMAND_SIGNATURES):
74
+ if first_frame_idx is None or idx < first_frame_idx:
75
+ return True
76
+ return False
77
+ return False
78
+
79
+
80
+ def launcher_text(result: CommandResult) -> str:
81
+ """Captured output for launcher-vs-test analysis, ordered stderr then stdout."""
82
+ parts: list[str] = []
83
+ for path in (result.stderr_path, result.stdout_path):
84
+ if not path:
85
+ continue
86
+ try:
87
+ content = Path(path).read_text(encoding="utf-8", errors="replace")
88
+ if content.strip():
89
+ parts.append(content)
90
+ except Exception:
91
+ pass
92
+ if parts:
93
+ return "\n".join(parts)
94
+ return result.summary or ""
95
+
96
+
97
+ def relativize(project_root: Path, raw_path: str) -> str:
98
+ """Normalize a traceback file path to a repo-relative posix path when possible."""
99
+ normalized = raw_path.replace("\\", "/")
100
+ try:
101
+ candidate = Path(raw_path)
102
+ if candidate.is_absolute():
103
+ rel = candidate.resolve().relative_to(project_root.resolve())
104
+ return rel.as_posix()
105
+ except Exception:
106
+ pass
107
+ return normalized
108
+
109
+
110
+ def failure_location(project_root: Path, result: CommandResult) -> Tuple[Optional[str], Optional[int]]:
111
+ """Best-effort (file, line) of a failing command's deepest traceback frame."""
112
+ sources: list[str] = []
113
+ for path in (result.stdout_path, result.stderr_path):
114
+ if path:
115
+ try:
116
+ content = Path(path).read_text(encoding="utf-8", errors="replace")
117
+ if content.strip():
118
+ sources.append(content)
119
+ except Exception:
120
+ pass
121
+ sources.append(result.summary or "")
122
+ best_file: Optional[str] = None
123
+ best_line: Optional[int] = None
124
+ for text in sources:
125
+ for match in TRACEBACK_FRAME_RE.finditer(text):
126
+ raw_file = match.group("file")
127
+ if not raw_file or raw_file.startswith("<"):
128
+ continue
129
+ best_file = relativize(project_root, raw_file)
130
+ try:
131
+ best_line = int(match.group("line"))
132
+ except ValueError:
133
+ best_line = None
134
+ if best_file is not None:
135
+ return best_file, best_line
136
+ return best_file, best_line
137
+
138
+
139
+ def command_is_malformed(result: CommandResult) -> bool:
140
+ """True when a non-zero exit reflects a broken/unrunnable command."""
141
+ if result.timed_out:
142
+ return False
143
+ is_pytest = "pytest" in (result.command or "")
144
+ if is_pytest and result.exit_code in PYTEST_NONRUN_EXIT_CODES:
145
+ return True
146
+ text = launcher_text(result)
147
+ return malformed_signature_precedes_traceback(text)
@@ -0,0 +1,164 @@
1
+ """Run verification evidence commands with sanitized env and logging.
2
+
3
+ Extracted from ``verifier.py`` so orchestration stays focused on gate logic.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import logging
10
+ import shlex
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import List
16
+
17
+ from devcouncil.domain.evidence import CommandResult
18
+ from devcouncil.utils.redaction import redact_string
19
+ from devcouncil.utils.subprocess_env import clean_subprocess_env
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _timeout_stream(content: str | bytes | None) -> str:
25
+ """Normalize partial output attached to ``TimeoutExpired``."""
26
+ if isinstance(content, bytes):
27
+ return content.decode("utf-8", errors="replace")
28
+ return content or ""
29
+
30
+
31
+ def split_command(command: str) -> List[str]:
32
+ # Use POSIX splitting so quotes are interpreted, not preserved. With
33
+ # posix=False, `python -c "assert x"` keeps the surrounding quotes, so the
34
+ # interpreter receives the literal string `"assert x"` and treats it as a
35
+ # no-op string expression that exits 0 — every quoted-argument evidence
36
+ # command would then silently "pass" without running, producing false
37
+ # verification. posix=True strips the quotes correctly; planner-generated
38
+ # commands use forward-slash paths, which the interpreter accepts on Windows.
39
+ return shlex.split(command, posix=True)
40
+
41
+
42
+ def summarize_stream(content: str, budget: int = 360) -> str:
43
+ """Condense a command's stdout/stderr for the evidence summary."""
44
+ if not content or not content.strip():
45
+ return "(empty)"
46
+ lines = [ln.rstrip() for ln in content.splitlines() if ln.strip()]
47
+ markers = ("error", "exception", "assert", "traceback", "failed", "not found", "no module named")
48
+ salient = ""
49
+ for ln in reversed(lines):
50
+ low = ln.lower()
51
+ if any(m in low for m in markers):
52
+ salient = ln.strip()
53
+ break
54
+ if not salient:
55
+ salient = lines[-1].strip()
56
+ salient = salient[:240]
57
+ tail = content.strip()[-budget:]
58
+ summary = f"{salient} | {tail}" if salient not in tail[: len(salient) + 5] else tail
59
+ return summary[: budget + len(salient) + 8]
60
+
61
+
62
+ def save_command_log(
63
+ project_root: Path,
64
+ label: str,
65
+ command: str,
66
+ stream: str,
67
+ content: str,
68
+ ) -> str:
69
+ """Save command output to a log file and return the path."""
70
+ log_dir = project_root / ".devcouncil" / "logs"
71
+ log_dir.mkdir(parents=True, exist_ok=True)
72
+ cmd_hash = hashlib.sha256(command.encode()).hexdigest()[:8]
73
+ filename = f"{label}-{cmd_hash}-{stream}.log"
74
+ log_path = log_dir / filename
75
+ log_path.write_text(redact_string(content), encoding="utf-8")
76
+ return str(log_path)
77
+
78
+
79
+ def run_verification_command(
80
+ project_root: Path,
81
+ command: str,
82
+ *,
83
+ task_id: str = "verify",
84
+ timeout: int = 900,
85
+ ) -> CommandResult:
86
+ """Execute one evidence command in the target repo with a cleaned environment."""
87
+ env = clean_subprocess_env()
88
+ try:
89
+ argv = split_command(command)
90
+ except ValueError as e:
91
+ return CommandResult(
92
+ command=command,
93
+ exit_code=-1,
94
+ stdout_path="",
95
+ stderr_path="",
96
+ summary=f"Failed to run command: unparseable shell syntax ({e})",
97
+ )
98
+ if argv and argv[0] in {"python", "python3"}:
99
+ # Planner-generated checks use portable launcher names, but macOS may have
100
+ # no ``python`` and its system ``python3`` does not contain project deps.
101
+ # Verification must run under the interpreter hosting DevCouncil.
102
+ argv = [sys.executable, *argv[1:]]
103
+ elif argv:
104
+ resolved = shutil.which(argv[0], path=env.get("PATH"))
105
+ if resolved:
106
+ argv = [resolved, *argv[1:]]
107
+
108
+ try:
109
+ result = subprocess.run(
110
+ argv,
111
+ shell=False,
112
+ capture_output=True,
113
+ text=True,
114
+ encoding="utf-8",
115
+ errors="replace",
116
+ cwd=project_root,
117
+ timeout=timeout,
118
+ env=env,
119
+ )
120
+ stdout = result.stdout or ""
121
+ stderr = result.stderr or ""
122
+ stdout_path = save_command_log(project_root, task_id, command, "stdout", stdout)
123
+ stderr_path = save_command_log(project_root, task_id, command, "stderr", stderr)
124
+ stdout_summary = redact_string(summarize_stream(stdout))
125
+ stderr_summary = redact_string(summarize_stream(stderr))
126
+ return CommandResult(
127
+ command=command,
128
+ exit_code=result.returncode,
129
+ stdout_path=stdout_path,
130
+ stderr_path=stderr_path,
131
+ summary=(
132
+ f"Exit code {result.returncode}. "
133
+ f"stderr: {stderr_summary}. "
134
+ f"stdout: {stdout_summary}"
135
+ ),
136
+ )
137
+ except subprocess.TimeoutExpired as exc:
138
+ stdout = _timeout_stream(exc.stdout)
139
+ stderr = _timeout_stream(exc.stderr)
140
+ stdout_path = save_command_log(project_root, task_id, command, "stdout", stdout)
141
+ stderr_path = save_command_log(project_root, task_id, command, "stderr", stderr)
142
+ stdout_summary = redact_string(summarize_stream(stdout))
143
+ stderr_summary = redact_string(summarize_stream(stderr))
144
+ return CommandResult(
145
+ command=command,
146
+ exit_code=124,
147
+ stdout_path=stdout_path,
148
+ stderr_path=stderr_path,
149
+ summary=(
150
+ f"Verification command timed out after {timeout} seconds. "
151
+ f"stderr: {stderr_summary}. "
152
+ f"stdout: {stdout_summary}"
153
+ ),
154
+ timed_out=True,
155
+ )
156
+ except Exception as e:
157
+ logger.debug("Command run failed for %r: %s", command, e)
158
+ return CommandResult(
159
+ command=command,
160
+ exit_code=-1,
161
+ stdout_path="",
162
+ stderr_path="",
163
+ summary=f"Failed to run command: {e}",
164
+ )
@@ -0,0 +1,292 @@
1
+ """Diff-coverage instrumentation extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Callable, Dict, List, Optional
12
+
13
+ from devcouncil.app.config import load_config
14
+ from devcouncil.domain.task import Task
15
+ from devcouncil.utils.subprocess_env import clean_subprocess_env
16
+ from devcouncil.verification import command_runner as cmd_runner
17
+ from devcouncil.verification import diff_coverage as dc
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ SplitCommand = Callable[[str], List[str]]
22
+ VerificationEnv = Callable[[], Dict[str, str]]
23
+ LoadCommands = Callable[[], Dict[str, List[str]]]
24
+ CommandCanProve = Callable[[str, str], bool]
25
+
26
+
27
+ class DiffCoverageMeasurer:
28
+ """Run task test commands under coverage and intersect with a diff."""
29
+
30
+ def __init__(
31
+ self,
32
+ project_root: Path,
33
+ *,
34
+ get_coverage_python: Callable[[], Optional[str]],
35
+ split_command: SplitCommand,
36
+ verification_env: VerificationEnv,
37
+ load_commands: LoadCommands,
38
+ command_can_prove_acceptance: CommandCanProve,
39
+ ) -> None:
40
+ self.project_root = project_root
41
+ self._get_coverage_python = get_coverage_python
42
+ self._split_command = split_command
43
+ self._verification_env = verification_env
44
+ self._load_commands = load_commands
45
+ self._command_can_prove_acceptance = command_can_prove_acceptance
46
+
47
+ @classmethod
48
+ def from_verifier(cls, verifier: object) -> DiffCoverageMeasurer:
49
+ """Build a measurer bound to a Verifier instance."""
50
+ return cls(
51
+ verifier.project_root, # type: ignore[attr-defined]
52
+ get_coverage_python=lambda: verifier._coverage_python, # type: ignore[attr-defined]
53
+ split_command=cmd_runner.split_command,
54
+ verification_env=clean_subprocess_env,
55
+ load_commands=verifier._load_commands, # type: ignore[attr-defined]
56
+ command_can_prove_acceptance=verifier._command_can_prove_acceptance, # type: ignore[attr-defined]
57
+ )
58
+
59
+ def coverage_target_commands(self, task: Task) -> List[str]:
60
+ """The test command(s) to instrument — the ones that purport to prove the ACs."""
61
+ if task.expected_tests:
62
+ return list(task.expected_tests)
63
+ test_like = [
64
+ c for c in task.allowed_commands
65
+ if self._command_can_prove_acceptance("allowed", c)
66
+ ]
67
+ if test_like:
68
+ return test_like
69
+ return list(self._load_commands().get("test", []))
70
+
71
+ def measure(self, task: Task, diff_content: str) -> dc.DiffCoverageResult:
72
+ """Run instrumentation for Python, JS/TS, and Go changed paths."""
73
+ parsed = dc.parse_changed_lines(diff_content)
74
+ py_changed = dc.measurable_python_changes(parsed)
75
+ js_changed = dc.measurable_js_changes(parsed)
76
+ go_changed = dc.measurable_go_changes(parsed)
77
+ if not py_changed and not js_changed and not go_changed:
78
+ return dc.DiffCoverageResult(measured=False, reason="no measurable source changes in diff")
79
+
80
+ commands = self.coverage_target_commands(task)
81
+ if not commands:
82
+ return dc.DiffCoverageResult(measured=False, reason="no test command to instrument")
83
+
84
+ try:
85
+ timeout = load_config(self.project_root).execution.command_timeout
86
+ except Exception:
87
+ timeout = 300
88
+
89
+ env = self._verification_env()
90
+ tmp_dir = self.project_root / ".devcouncil" / "tmp"
91
+ tmp_dir.mkdir(parents=True, exist_ok=True)
92
+ results: List[dc.DiffCoverageResult] = []
93
+
94
+ if py_changed:
95
+ results.append(self._measure_python(task, commands, py_changed, tmp_dir, env, timeout))
96
+ if js_changed:
97
+ results.append(self._measure_js(task, commands, js_changed, tmp_dir, env, timeout))
98
+ if go_changed:
99
+ results.append(self._measure_go(task, commands, go_changed, tmp_dir, env, timeout))
100
+
101
+ return dc.merge_diff_coverage_results(results)
102
+
103
+ def _resolve_coverage_python(self, env: Dict[str, str]) -> str:
104
+ override = self._get_coverage_python()
105
+ if override:
106
+ return override
107
+ for name in ("python", "python3", "py"):
108
+ found = shutil.which(name, path=env.get("PATH"))
109
+ if found:
110
+ return found
111
+ return sys.executable
112
+
113
+ def _coverage_available(self, python: str, env: Dict[str, str]) -> bool:
114
+ try:
115
+ result = subprocess.run(
116
+ [python, "-m", "coverage", "--version"],
117
+ cwd=self.project_root,
118
+ capture_output=True,
119
+ text=True,
120
+ encoding="utf-8",
121
+ errors="replace",
122
+ timeout=30,
123
+ env=env,
124
+ )
125
+ return result.returncode == 0
126
+ except Exception:
127
+ return False
128
+
129
+ def _measure_python(
130
+ self,
131
+ task: Task,
132
+ commands: List[str],
133
+ changed: Dict[str, Dict[int, str]],
134
+ tmp_dir: Path,
135
+ env: dict,
136
+ timeout: int,
137
+ ) -> dc.DiffCoverageResult:
138
+ python = self._resolve_coverage_python(env)
139
+ if not self._coverage_available(python, env):
140
+ return dc.DiffCoverageResult(measured=False, reason="coverage.py not available")
141
+ data_file = tmp_dir / f"diffcov-{task.id}.coverage"
142
+ json_file = tmp_dir / f"diffcov-{task.id}.json"
143
+ for stale in (data_file, json_file):
144
+ try:
145
+ stale.unlink()
146
+ except FileNotFoundError:
147
+ pass
148
+ ran_any = False
149
+ append = False
150
+ inline_scripts: List[Path] = []
151
+ try:
152
+ for idx, cmd in enumerate(commands):
153
+ try:
154
+ argv = self._split_command(cmd)
155
+ except ValueError as exc:
156
+ logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
157
+ continue
158
+ inline = dc.inline_python_code(argv)
159
+ if inline is not None:
160
+ script = tmp_dir / f"diffcov-inline-{task.id}-{idx}.py"
161
+ try:
162
+ script.write_text(dc.inline_script_content(inline, self.project_root), encoding="utf-8")
163
+ except Exception as exc:
164
+ logger.warning("Diff-coverage inline script write failed for %s: %s", task.id, exc)
165
+ continue
166
+ inline_scripts.append(script)
167
+ cov_argv: Optional[List[str]] = dc.coverage_run_script_argv(
168
+ str(script), python, append=append, data_file=str(data_file)
169
+ )
170
+ else:
171
+ cov_argv = dc.coverage_run_argv(argv, python, append=append, data_file=str(data_file))
172
+ if cov_argv is None:
173
+ continue
174
+ try:
175
+ subprocess.run(
176
+ cov_argv, cwd=self.project_root, capture_output=True,
177
+ text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
178
+ )
179
+ except Exception as exc:
180
+ logger.warning("Diff-coverage run failed for %s: %s", task.id, exc)
181
+ continue
182
+ ran_any = True
183
+ append = True
184
+ if not ran_any or not data_file.exists():
185
+ return dc.DiffCoverageResult(measured=False, reason="no instrumentable Python test command")
186
+ subprocess.run(
187
+ [python, "-m", "coverage", "json", f"--data-file={data_file}", "-o", str(json_file)],
188
+ cwd=self.project_root, capture_output=True, text=True,
189
+ encoding="utf-8", errors="replace", timeout=120, env=env,
190
+ )
191
+ data = json.loads(json_file.read_text(encoding="utf-8"))
192
+ coverage = dc.parse_coverage_json(data, self.project_root)
193
+ return dc.intersect(changed, coverage, tool="coverage.py")
194
+ except Exception as exc:
195
+ return dc.DiffCoverageResult(measured=False, reason=f"coverage.py failed: {exc}")
196
+ finally:
197
+ for path in [data_file, json_file, *inline_scripts]:
198
+ try:
199
+ path.unlink()
200
+ except OSError:
201
+ pass
202
+
203
+ def _measure_js(
204
+ self,
205
+ task: Task,
206
+ commands: List[str],
207
+ changed: Dict[str, Dict[int, str]],
208
+ tmp_dir: Path,
209
+ env: dict,
210
+ timeout: int,
211
+ ) -> dc.DiffCoverageResult:
212
+ reports_dir = tmp_dir / f"c8-{task.id}"
213
+ reports_dir.mkdir(parents=True, exist_ok=True)
214
+ try:
215
+ for stale in reports_dir.glob("coverage-final.json"):
216
+ stale.unlink()
217
+ except OSError:
218
+ pass
219
+ ran_any = False
220
+ for cmd in commands:
221
+ try:
222
+ argv = self._split_command(cmd)
223
+ except ValueError as exc:
224
+ logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
225
+ continue
226
+ cov_argv = dc.c8_run_argv(argv, reports_dir=str(reports_dir))
227
+ if cov_argv is None:
228
+ continue
229
+ try:
230
+ subprocess.run(
231
+ cov_argv, cwd=self.project_root, capture_output=True,
232
+ text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
233
+ )
234
+ except Exception as exc:
235
+ logger.warning("c8 diff-coverage run failed for %s: %s", task.id, exc)
236
+ continue
237
+ ran_any = True
238
+ candidates = list(reports_dir.glob("**/coverage-final.json"))
239
+ if not ran_any or not candidates:
240
+ return dc.DiffCoverageResult(measured=False, reason="c8 not available or no JS test command")
241
+ try:
242
+ data = json.loads(candidates[0].read_text(encoding="utf-8"))
243
+ coverage = dc.parse_istanbul_json(data, self.project_root)
244
+ return dc.intersect(changed, coverage, tool="c8")
245
+ except Exception as exc:
246
+ return dc.DiffCoverageResult(measured=False, reason=f"c8 report unreadable: {exc}")
247
+
248
+ def _measure_go(
249
+ self,
250
+ task: Task,
251
+ commands: List[str],
252
+ changed: Dict[str, Dict[int, str]],
253
+ tmp_dir: Path,
254
+ env: dict,
255
+ timeout: int,
256
+ ) -> dc.DiffCoverageResult:
257
+ profile = tmp_dir / f"diffcov-{task.id}.out"
258
+ try:
259
+ profile.unlink()
260
+ except FileNotFoundError:
261
+ pass
262
+ ran_any = False
263
+ for cmd in commands:
264
+ try:
265
+ argv = self._split_command(cmd)
266
+ except ValueError as exc:
267
+ logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
268
+ continue
269
+ cov_argv = dc.go_cover_run_argv(argv, str(profile))
270
+ if cov_argv is None:
271
+ continue
272
+ try:
273
+ subprocess.run(
274
+ cov_argv, cwd=self.project_root, capture_output=True,
275
+ text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
276
+ )
277
+ except Exception as exc:
278
+ logger.warning("go cover run failed for %s: %s", task.id, exc)
279
+ continue
280
+ ran_any = True
281
+ if not ran_any or not profile.exists():
282
+ return dc.DiffCoverageResult(measured=False, reason="go test -coverprofile not available")
283
+ try:
284
+ coverage = dc.parse_go_coverprofile(profile.read_text(encoding="utf-8"), self.project_root)
285
+ return dc.intersect(changed, coverage, tool="go-cover")
286
+ except Exception as exc:
287
+ return dc.DiffCoverageResult(measured=False, reason=f"go cover profile unreadable: {exc}")
288
+ finally:
289
+ try:
290
+ profile.unlink()
291
+ except OSError:
292
+ pass