devcouncil 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,98 @@
1
+ """Planned-file compliance checks extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Callable, List
7
+
8
+ from devcouncil.domain.gap import Gap
9
+ from devcouncil.domain.task import Task
10
+
11
+ _DEP_FILES = {
12
+ "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
13
+ "requirements.txt", "pyproject.toml", "uv.lock", "Pipfile.lock",
14
+ "go.mod", "go.sum", "Cargo.toml", "Cargo.lock",
15
+ }
16
+
17
+
18
+ def dependency_files_changed(changed_files: List[str]) -> List[str]:
19
+ """Return changed dependency manifest paths."""
20
+ return [path for path in changed_files if Path(path).name in _DEP_FILES]
21
+
22
+
23
+ def detect_no_work_gap(
24
+ *,
25
+ task: Task,
26
+ work_present: bool,
27
+ next_gap_id: Callable[[str, str], str],
28
+ ) -> Gap | None:
29
+ """Block when a task expects file changes but produced none."""
30
+ expects_change = any(pf.allowed_change != "read_only" for pf in task.planned_files)
31
+ if work_present or not expects_change:
32
+ return None
33
+ return Gap(
34
+ id=next_gap_id(task.id, "NODIFF"),
35
+ severity="high",
36
+ gap_type="task_not_implemented",
37
+ task_id=task.id,
38
+ description=(
39
+ f"Task {task.id} declares files to create or modify, but produced no "
40
+ "changes. Verification cannot prove work that does not exist."
41
+ ),
42
+ evidence=[f"planned files expecting change: {sorted(p.path for p in task.planned_files if p.allowed_change != 'read_only')}"],
43
+ recommended_fix=(
44
+ "Implement the planned changes so the diff is non-empty, then re-verify. "
45
+ "If you did make changes, ensure they are saved and visible to git "
46
+ "(not reverted, stashed, or written outside the project root)."
47
+ ),
48
+ blocking=True,
49
+ )
50
+
51
+
52
+ def detect_planned_file_gaps(
53
+ *,
54
+ task: Task,
55
+ changed_files: List[str],
56
+ next_gap_id: Callable[[str, str], str],
57
+ ) -> List[Gap]:
58
+ """Advisory gaps for planned files that were not modified."""
59
+ gaps: List[Gap] = []
60
+ changed_set = set(changed_files)
61
+ for pf in task.planned_files:
62
+ if pf.path not in changed_set and pf.allowed_change != "read_only":
63
+ gaps.append(Gap(
64
+ id=next_gap_id(task.id, f"FILE-{pf.path}"),
65
+ severity="medium",
66
+ gap_type="planned_file_not_changed",
67
+ task_id=task.id,
68
+ description=f"Planned file {pf.path} was not modified.",
69
+ recommended_fix=f"Modify {pf.path} as planned or update the task.",
70
+ blocking=False,
71
+ file=pf.path,
72
+ ))
73
+ return gaps
74
+
75
+
76
+ def detect_dependency_risk_gaps(
77
+ *,
78
+ task: Task,
79
+ changed_files: List[str],
80
+ planned_paths: set[str],
81
+ next_gap_id: Callable[[str, str], str],
82
+ ) -> List[Gap]:
83
+ """Blocking gaps for dependency manifest edits outside the plan."""
84
+ gaps: List[Gap] = []
85
+ for dep_file in dependency_files_changed(changed_files):
86
+ if dep_file not in planned_paths:
87
+ gaps.append(Gap(
88
+ id=next_gap_id(task.id, f"DEP-{dep_file}"),
89
+ severity="high",
90
+ gap_type="dependency_risk",
91
+ task_id=task.id,
92
+ description=f"Dependency file {dep_file} was modified without being in planned files.",
93
+ evidence=[dep_file],
94
+ recommended_fix=f"Justify the dependency change or revert {dep_file}.",
95
+ blocking=True,
96
+ file=dep_file,
97
+ ))
98
+ return gaps
@@ -0,0 +1,241 @@
1
+ """Semantic-index drift checks extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Callable, List, Optional
10
+
11
+ from devcouncil.domain.gap import Gap
12
+ from devcouncil.domain.requirement import Requirement
13
+ from devcouncil.domain.task import Task
14
+ from devcouncil.utils.json_persist import read_json
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def task_intent_text(task: Task, requirements: Optional[List[Requirement]]) -> str:
20
+ """Lowercased text describing what the task is meant to do."""
21
+ parts = [task.title or "", task.description or ""]
22
+ if requirements:
23
+ ac_ids = set(task.acceptance_criterion_ids)
24
+ for req in requirements:
25
+ for ac in req.acceptance_criteria:
26
+ if ac.id in ac_ids:
27
+ parts.append(ac.description or "")
28
+ return " ".join(parts).lower()
29
+
30
+
31
+ def import_top_level(statement: str) -> Optional[str]:
32
+ """Top-level package of an import statement, or None for relative/local/unparseable."""
33
+ s = (statement or "").strip()
34
+ if s.startswith("import "):
35
+ first = s[len("import "):].split(",")[0].strip()
36
+ top = first.split(" as ")[0].strip().split(".")[0].strip()
37
+ return top or None
38
+ if s.startswith("from "):
39
+ rest = s[len("from "):].lstrip()
40
+ if rest.startswith("."):
41
+ return None
42
+ mod = rest.split(" import ")[0].strip()
43
+ return (mod.split(".")[0].strip() or None) if mod else None
44
+ return None
45
+
46
+
47
+ def stdlib_modules() -> frozenset[str]:
48
+ names = getattr(sys, "stdlib_module_names", None)
49
+ return frozenset(names) if names else frozenset()
50
+
51
+
52
+ def load_project_dependencies(project_root: Path) -> set[str]:
53
+ """Lower-cased distribution names declared by the project."""
54
+ deps: set[str] = set()
55
+ split_re = r"[><=!~;\[\] ]"
56
+ pyproject = project_root / "pyproject.toml"
57
+ if pyproject.exists():
58
+ try:
59
+ import tomllib
60
+
61
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
62
+ project = data.get("project", {}) or {}
63
+ for dep in project.get("dependencies", []) or []:
64
+ pkg = re.split(split_re, dep.strip())[0].strip().lower()
65
+ if pkg:
66
+ deps.add(pkg)
67
+ for group in (project.get("optional-dependencies", {}) or {}).values():
68
+ for dep in group or []:
69
+ pkg = re.split(split_re, dep.strip())[0].strip().lower()
70
+ if pkg:
71
+ deps.add(pkg)
72
+ except Exception:
73
+ pass
74
+ requirements = project_root / "requirements.txt"
75
+ if requirements.exists():
76
+ try:
77
+ for line in requirements.read_text(encoding="utf-8").splitlines():
78
+ line = line.strip()
79
+ if line and not line.startswith("#"):
80
+ pkg = re.split(split_re, line)[0].strip().lower()
81
+ if pkg:
82
+ deps.add(pkg)
83
+ except Exception:
84
+ pass
85
+ package_json = project_root / "package.json"
86
+ if package_json.exists():
87
+ try:
88
+ data = read_json(package_json)
89
+ for key in ("dependencies", "devDependencies", "optionalDependencies"):
90
+ deps.update(k.lower() for k in (data.get(key) or {}).keys())
91
+ except Exception:
92
+ pass
93
+ return deps
94
+
95
+
96
+ def is_new_third_party_import(top: Optional[str], *, project_deps: set[str]) -> bool:
97
+ """True only when ``top`` is a genuinely new, undeclared third-party package."""
98
+ if not top:
99
+ return False
100
+ if top in stdlib_modules():
101
+ return False
102
+ if top.lower() in project_deps:
103
+ return False
104
+ try:
105
+ import importlib.util
106
+
107
+ if importlib.util.find_spec(top) is not None:
108
+ return False
109
+ except Exception:
110
+ return False
111
+ return True
112
+
113
+
114
+ def detect_semantic_diff_gaps(
115
+ *,
116
+ project_root: Path,
117
+ task: Task,
118
+ requirements: Optional[List[Requirement]],
119
+ next_gap_id: Callable[[str, str], str],
120
+ project_deps: set[str],
121
+ ) -> List[Gap]:
122
+ """Run semantic-index drift checks when an after snapshot exists."""
123
+ gaps: List[Gap] = []
124
+ after_path = project_root / ".devcouncil" / "semantic" / task.id / "after.json"
125
+ if not after_path.exists():
126
+ return gaps
127
+ try:
128
+ from devcouncil.indexing.semantic_index import SemanticIndex
129
+
130
+ result = SemanticIndex(project_root).diff(task.id)
131
+ except Exception as exc:
132
+ logger.warning("Semantic diff check failed for %s; skipping semantic gaps: %s", task.id, exc)
133
+ return gaps
134
+
135
+ planned_paths = {pf.path for pf in task.planned_files}
136
+ classifications = result.get("classifications", [])
137
+ readded_public = {
138
+ item.get("name") for item in classifications
139
+ if item.get("type") == "exported_symbol_added" and item.get("name")
140
+ }
141
+ intent_text = task_intent_text(task, requirements)
142
+ for item in classifications:
143
+ change_type = item.get("type", "")
144
+ path = item.get("path", "")
145
+ if change_type == "exported_symbol_removed":
146
+ name = item.get("name", "")
147
+ moved = name in readded_public
148
+ intended = bool(name) and name.lower() in intent_text
149
+ gaps.append(Gap(
150
+ id=next_gap_id(task.id, f"DRIFT-{path}:{name}"),
151
+ severity="high",
152
+ gap_type="architecture_drift",
153
+ task_id=task.id,
154
+ description=(
155
+ f"Public symbol '{name}' was removed from {path} — possible scope "
156
+ "drift: the executor changed a public API the task did not call for."
157
+ ),
158
+ evidence=[f"{path}:{name}"],
159
+ recommended_fix=(
160
+ "Restore the removed public symbol. If its removal IS part of this "
161
+ "task, state that in the task description / acceptance criteria so the "
162
+ "change is an intended, reviewed decision rather than silent drift."
163
+ ),
164
+ blocking=(not moved and not intended),
165
+ file=path,
166
+ ))
167
+ elif change_type == "public_api_change" and path not in planned_paths:
168
+ gaps.append(Gap(
169
+ id=next_gap_id(task.id, f"SEM-{path}"),
170
+ severity="high",
171
+ gap_type="architecture_drift",
172
+ task_id=task.id,
173
+ description=f"Unplanned public API change detected in {path}.",
174
+ evidence=[path],
175
+ recommended_fix="Add file to planned_files and document acceptance criteria.",
176
+ blocking=not bool(task.acceptance_criterion_ids),
177
+ ))
178
+ elif change_type == "public_api_change" and path in planned_paths:
179
+ gaps.append(Gap(
180
+ id=next_gap_id(task.id, f"SIGDRIFT-{path}:{item.get('name', '')}"),
181
+ severity="medium",
182
+ gap_type="architecture_drift",
183
+ task_id=task.id,
184
+ description=(
185
+ f"Public API signature change in planned file {path}"
186
+ + (f" ({item.get('name')})" if item.get("name") else "")
187
+ + ". Confirm callers are updated and the change is intended."
188
+ ),
189
+ evidence=[f"{path}:{item.get('name', '')}"],
190
+ recommended_fix=(
191
+ "If the signature change is part of this task, note it in the task "
192
+ "description / acceptance criteria; otherwise revert it."
193
+ ),
194
+ blocking=False,
195
+ ))
196
+ elif change_type == "import_dependency_change":
197
+ statement = item.get("statement", "")
198
+ top = import_top_level(statement)
199
+ new_third_party = is_new_third_party_import(top, project_deps=project_deps)
200
+ if new_third_party:
201
+ gaps.append(Gap(
202
+ id=next_gap_id(task.id, f"DEPADD-{path}:{top}"),
203
+ severity="high",
204
+ gap_type="dependency_risk",
205
+ task_id=task.id,
206
+ description=(
207
+ f"New undeclared third-party dependency '{top}' imported in {path} "
208
+ f"({statement.strip()}). Adding a dependency the task did not plan is "
209
+ "supply-chain drift."
210
+ ),
211
+ evidence=[path, statement.strip()],
212
+ recommended_fix=(
213
+ f"Declare '{top}' in the project's dependencies and plan the change, "
214
+ "or use an existing/standard-library alternative."
215
+ ),
216
+ blocking=True,
217
+ file=path,
218
+ ))
219
+ elif path not in planned_paths:
220
+ gaps.append(Gap(
221
+ id=next_gap_id(task.id, f"IMP-{path}"),
222
+ severity="medium",
223
+ gap_type="dependency_risk",
224
+ task_id=task.id,
225
+ description=f"Import dependency change in {path}.",
226
+ evidence=[path],
227
+ recommended_fix="Confirm dependency change is intentional.",
228
+ blocking=False,
229
+ ))
230
+ elif change_type == "config_schema_dependency_change" and path not in planned_paths:
231
+ gaps.append(Gap(
232
+ id=next_gap_id(task.id, f"CFG-{path}"),
233
+ severity="high",
234
+ gap_type="dependency_risk",
235
+ task_id=task.id,
236
+ description=f"Config/schema change detected in {path}.",
237
+ evidence=[path],
238
+ recommended_fix="Plan the config change or revert it.",
239
+ blocking=True,
240
+ ))
241
+ return gaps
@@ -0,0 +1,80 @@
1
+ """Stale-map gate: flag when ``repo_map.json`` is missing or lags git HEAD.
2
+
3
+ Missing map is always stale (fail-closed on hard rigor). Advisory on easy/normal;
4
+ blocking on hard. Never raises.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Callable, List, Mapping, Optional
12
+
13
+ from devcouncil.domain.gap import Gap
14
+ from devcouncil.domain.task import Task
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def detect_stale_map_gaps(
20
+ *,
21
+ task: Task,
22
+ project_root: Path,
23
+ next_gap_id: Callable[[str, str], str],
24
+ stale_map_enabled: bool = True,
25
+ stale_map_blocking: bool = False,
26
+ repo_map: Optional[Mapping] = None,
27
+ ) -> List[Gap]:
28
+ """Emit a ``stale_map`` gap when the on-disk map no longer matches the repo."""
29
+ if not stale_map_enabled:
30
+ return []
31
+ try:
32
+ data = repo_map
33
+ if data is None:
34
+ map_path = project_root / ".devcouncil" / "repo_map.json"
35
+ if not map_path.is_file():
36
+ return [Gap(
37
+ id=next_gap_id(task.id, "STALEMAP-MISSING"),
38
+ severity="high" if stale_map_blocking else "medium",
39
+ gap_type="stale_map",
40
+ task_id=task.id,
41
+ description=(
42
+ "Repository map ``.devcouncil/repo_map.json`` is missing. "
43
+ "Subsystem neighbors, dependents, and liveness gates cannot run."
44
+ ),
45
+ evidence=[".devcouncil/repo_map.json"],
46
+ recommended_fix="Run `dev map` to generate the repository map, then re-verify.",
47
+ blocking=stale_map_blocking,
48
+ suggested_command="dev map",
49
+ )]
50
+ from devcouncil.utils.json_persist import read_json
51
+
52
+ loaded = read_json(map_path)
53
+ if not isinstance(loaded, dict):
54
+ return []
55
+ data = loaded
56
+
57
+ from devcouncil.indexing.repo_mapper import RepoMapper
58
+
59
+ if not RepoMapper(project_root).map_is_stale(dict(data)):
60
+ return []
61
+
62
+ stored_head = str(data.get("generated_head") or "") or "(unknown)"
63
+ return [Gap(
64
+ id=next_gap_id(task.id, "STALEMAP"),
65
+ severity="high" if stale_map_blocking else "medium",
66
+ gap_type="stale_map",
67
+ task_id=task.id,
68
+ description=(
69
+ "The repository map (``.devcouncil/repo_map.json``) is behind the "
70
+ f"current code (stored HEAD {stored_head}). Subsystem neighbors, "
71
+ "dependents, and liveness lists may be wrong until it is refreshed."
72
+ ),
73
+ evidence=[".devcouncil/repo_map.json", f"generated_head:{stored_head}"],
74
+ recommended_fix="Run `dev map` to regenerate the repository map, then re-verify.",
75
+ blocking=stale_map_blocking,
76
+ suggested_command="dev map",
77
+ )]
78
+ except Exception:
79
+ logger.debug("detect_stale_map_gaps failed; degrading to zero gaps", exc_info=True)
80
+ return []
@@ -0,0 +1,71 @@
1
+ """Stub detection helpers extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Callable, List
7
+
8
+ from devcouncil.domain.gap import Gap
9
+ from devcouncil.domain.task import Task
10
+ from devcouncil.verification.stub_detector import (
11
+ detect_stub_declarations,
12
+ detect_stubs,
13
+ task_allows_scaffolding,
14
+ )
15
+
16
+
17
+ def detect_stub_gaps(
18
+ *,
19
+ task: Task,
20
+ diff_content: str,
21
+ project_root: Path,
22
+ stub_enabled: bool,
23
+ stub_blocking: bool,
24
+ next_gap_id: Callable[[str, str], str],
25
+ ) -> List[Gap]:
26
+ """Scan diff-added lines for stubs/TODOs and declared scaffolding markers."""
27
+ gaps: List[Gap] = []
28
+ if not stub_enabled or not diff_content:
29
+ return gaps
30
+ scaffolding_ok = task_allows_scaffolding(task)
31
+ for finding in detect_stubs(project_root, diff_content, honor_allow_stub=scaffolding_ok):
32
+ gaps.append(Gap(
33
+ id=next_gap_id(task.id, f"STUB-{finding.file}:{finding.line}"),
34
+ severity="high" if stub_blocking else "medium",
35
+ gap_type="stub_detected",
36
+ task_id=task.id,
37
+ description=(
38
+ f"Placeholder/incomplete code added at {finding.file}:{finding.line}: "
39
+ f"{finding.reason}."
40
+ ),
41
+ evidence=[f"{finding.file}:{finding.line}", finding.snippet],
42
+ recommended_fix=(
43
+ f"Replace the stub/placeholder at {finding.file}:{finding.line} with a "
44
+ "real implementation, then re-verify. Intentional scaffolding requires "
45
+ "the task description to mention 'scaffolding' and the line to carry "
46
+ "'devcouncil: allow-stub'."
47
+ ),
48
+ blocking=stub_blocking,
49
+ file=finding.file,
50
+ line=finding.line,
51
+ ))
52
+ for decl in detect_stub_declarations(diff_content):
53
+ gaps.append(Gap(
54
+ id=next_gap_id(task.id, f"STUBDECL-{decl.file}:{decl.line}"),
55
+ severity="medium",
56
+ gap_type="stub_declared",
57
+ task_id=task.id,
58
+ description=(
59
+ f"Intentional stub declared at {decl.file}:{decl.line} "
60
+ f"({decl.snippet[:80]})."
61
+ ),
62
+ evidence=[f"{decl.file}:{decl.line}", decl.snippet],
63
+ recommended_fix=(
64
+ "Review declared stubs before marking the task done; replace each "
65
+ "placeholder with a real implementation when scaffolding is complete."
66
+ ),
67
+ blocking=False,
68
+ file=decl.file,
69
+ line=decl.line,
70
+ ))
71
+ return gaps
@@ -0,0 +1,103 @@
1
+ """Advisory subsystem-boundary gate — architecture drift over the mapped area graph.
2
+
3
+ The repo map already knows which subsystems are *neighbors* (they hand off to each
4
+ other). A change that edits files in two subsystems the map does NOT consider neighbors,
5
+ and that the task plan never declared it would span, is a candidate architecture-drift
6
+ crossing: business logic leaking into a UI layer, a storage change reaching into the
7
+ council prompts, etc.
8
+
9
+ This gate flags those crossings. It is **advisory (non-blocking) by default** — an
10
+ undeclared crossing is often legitimate, so it should inform review rather than halt the
11
+ loop — but is ``blocking``-configurable for teams that want a hard architectural
12
+ boundary. It reads only ``repo_map.json`` (no model calls, no subprocess) and degrades to
13
+ a no-op when the map lacks ``subsystems``/``neighbors``.
14
+
15
+ "Plan coverage": a crossing between areas A and B is covered when the task's *planned
16
+ files* already live in both A and B — i.e. the plan declared the change would touch both
17
+ subsystems. Only crossings that reach an area the plan did not declare are flagged.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Callable, List, Mapping, Sequence
24
+
25
+ from devcouncil.domain.gap import Gap
26
+ from devcouncil.domain.task import Task
27
+ from devcouncil.indexing.subsystem_map import (
28
+ area_for_path,
29
+ areas_touched,
30
+ cross_boundary_pairs,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Cap emitted crossings so a sprawling change can't flood the gap list.
36
+ _MAX_CROSSINGS = 6
37
+
38
+
39
+ def detect_subsystem_boundary_gaps(
40
+ *,
41
+ task: Task,
42
+ changed_files: Sequence[str],
43
+ repo_map: Mapping | None,
44
+ next_gap_id: Callable[[str, str], str],
45
+ blocking: bool = False,
46
+ ) -> List[Gap]:
47
+ """Flag edits that cross non-neighbor subsystem boundaries without plan coverage.
48
+
49
+ Returns advisory (or blocking, per ``blocking``) architecture-drift gaps — one per
50
+ undeclared crossing pair. Empty when the map is missing/degenerate, the change stays
51
+ within one area, or every crossing was already declared by the plan.
52
+ """
53
+ if not repo_map or not (repo_map.get("subsystems") if hasattr(repo_map, "get") else None):
54
+ return []
55
+
56
+ changed = [p.replace("\\", "/") for p in changed_files if p and p.strip()]
57
+ if len(areas_touched(changed, repo_map)) < 2:
58
+ return []
59
+
60
+ planned_paths = [pf.path.replace("\\", "/") for pf in task.planned_files]
61
+ planned_areas = set(areas_touched(planned_paths, repo_map))
62
+
63
+ crossings = cross_boundary_pairs(changed, repo_map)
64
+ if not crossings:
65
+ return []
66
+
67
+ gaps: List[Gap] = []
68
+ for area_a, area_b in crossings[:_MAX_CROSSINGS]:
69
+ # Covered when the plan declared files in BOTH areas of the crossing.
70
+ if area_a in planned_areas and area_b in planned_areas:
71
+ continue
72
+ undeclared = sorted({area_a, area_b} - planned_areas)
73
+ files_a = [f for f in changed if area_for_path(f, repo_map) == area_a]
74
+ files_b = [f for f in changed if area_for_path(f, repo_map) == area_b]
75
+ evidence = [f"{area_a}: " + ", ".join(files_a[:5])] if files_a else []
76
+ if files_b:
77
+ evidence.append(f"{area_b}: " + ", ".join(files_b[:5]))
78
+ gaps.append(Gap(
79
+ id=next_gap_id(task.id, f"BOUNDARY-{area_a}-{area_b}"),
80
+ severity="medium",
81
+ gap_type="architecture_drift",
82
+ task_id=task.id,
83
+ description=(
84
+ f"Change spans non-neighbor subsystems `{area_a}` and `{area_b}` "
85
+ f"but the plan did not declare it would touch {', '.join(f'`{a}`' for a in undeclared)}. "
86
+ "Cross-boundary edits between areas the repo map does not consider "
87
+ "adjacent are a common source of architecture drift."
88
+ ),
89
+ evidence=evidence or [f"{area_a} ⇄ {area_b}"],
90
+ recommended_fix=(
91
+ "If this crossing is intended, declare planned files in both subsystems "
92
+ "(or split the work into per-subsystem tasks); otherwise keep the edit "
93
+ "within its subsystem and route through the neighboring interface. "
94
+ "Run `dev map` if the subsystem neighbors are out of date."
95
+ ),
96
+ blocking=blocking,
97
+ ))
98
+ if gaps:
99
+ logger.info(
100
+ "subsystem-boundary gate: %d undeclared crossing(s) for task %s (blocking=%s)",
101
+ len(gaps), task.id, blocking,
102
+ )
103
+ return gaps