devcouncil 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,179 @@
1
+ """Compiled per-criterion acceptance check orchestration extracted from Verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Awaitable, Callable, Dict, List, Tuple
8
+
9
+ from devcouncil.domain.evidence import CommandResult
10
+ from devcouncil.domain.gap import Gap
11
+ from devcouncil.domain.requirement import Requirement
12
+ from devcouncil.domain.task import Task
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class CompiledAcceptanceResult:
19
+ gaps: List[Gap] = field(default_factory=list)
20
+ compiled_pass: Dict[str, bool] = field(default_factory=dict)
21
+ compiled_cmds_by_ac: Dict[str, List[str]] = field(default_factory=dict)
22
+ failing_results_by_ac: Dict[str, List[CommandResult]] = field(default_factory=dict)
23
+ compiled_vote: Dict[str, Tuple[int, int, bool]] = field(default_factory=dict)
24
+ inconclusive_acs: set[str] = field(default_factory=set)
25
+ command_results: List[CommandResult] = field(default_factory=list)
26
+ evidence: List[Any] = field(default_factory=list)
27
+ genuine_failure: bool = False
28
+ had_unrunnable: bool = False
29
+
30
+
31
+ async def run_compiled_acceptance_checks(
32
+ *,
33
+ task: Task,
34
+ requirements: List[Requirement],
35
+ compile_future: Any | None,
36
+ diff_content: str,
37
+ ac_repair_attempts: int,
38
+ acceptance_compiler: Any | None,
39
+ command_applicable: Callable[[str], Tuple[bool, str]],
40
+ run_command: Callable[[str], CommandResult],
41
+ command_is_malformed: Callable[[CommandResult], bool],
42
+ failure_location: Callable[[CommandResult], Tuple[str | None, int | None]],
43
+ next_gap_id: Callable[[str, str], str],
44
+ repair_command: Callable[
45
+ [str, str, str, str, str],
46
+ Awaitable[str | None],
47
+ ] | None = None,
48
+ had_unrunnable: bool = False,
49
+ genuine_failure: bool = False,
50
+ ) -> CompiledAcceptanceResult:
51
+ """Run DevCouncil-owned per-criterion checks from the acceptance compiler."""
52
+ result = CompiledAcceptanceResult(
53
+ had_unrunnable=had_unrunnable,
54
+ genuine_failure=genuine_failure,
55
+ )
56
+ if compile_future is None:
57
+ return result
58
+
59
+ try:
60
+ compiled = await compile_future
61
+ except Exception as exc: # pragma: no cover - best effort
62
+ logger.warning("Acceptance compiler failed for %s: %s", task.id, exc)
63
+ compiled = {}
64
+
65
+ ac_meta = {ac.id: ac for req in requirements for ac in req.acceptance_criteria}
66
+ for ac_id, raw_cmds in compiled.items():
67
+ candidates = [c for c in raw_cmds if command_applicable(c)[0]]
68
+ result.compiled_cmds_by_ac[ac_id] = list(candidates)
69
+ if not candidates:
70
+ result.had_unrunnable = True
71
+ continue
72
+
73
+ passes = 0
74
+ genuine_fails = 0
75
+ repaired = False
76
+ fail_results: List[Tuple[str, CommandResult]] = []
77
+ for cmd in candidates:
78
+ cmd_result = run_command(cmd)
79
+ result.command_results.append(cmd_result)
80
+ result.evidence.append(cmd_result)
81
+ attempts = 0
82
+ while (
83
+ cmd_result.exit_code != 0
84
+ and command_is_malformed(cmd_result)
85
+ and attempts < ac_repair_attempts
86
+ and repair_command is not None
87
+ ):
88
+ attempts += 1
89
+ ac_desc = ac_meta[ac_id].description if ac_id in ac_meta else ac_id
90
+ try:
91
+ fixed = await repair_command(
92
+ ac_id, ac_desc, cmd, cmd_result.summary[:800], diff_content
93
+ )
94
+ except Exception:
95
+ fixed = None
96
+ if not fixed or not command_applicable(fixed)[0]:
97
+ break
98
+ cmd = fixed
99
+ result.compiled_cmds_by_ac[ac_id].append(cmd)
100
+ cmd_result = run_command(cmd)
101
+ result.command_results.append(cmd_result)
102
+ result.evidence.append(cmd_result)
103
+ if cmd_result.exit_code == 0:
104
+ passes += 1
105
+ if attempts > 0:
106
+ repaired = True
107
+ elif command_is_malformed(cmd_result):
108
+ result.had_unrunnable = True
109
+ result.failing_results_by_ac.setdefault(ac_id, []).append(cmd_result)
110
+ else:
111
+ genuine_fails += 1
112
+ fail_results.append((cmd, cmd_result))
113
+ result.failing_results_by_ac.setdefault(ac_id, []).append(cmd_result)
114
+
115
+ decisive = passes + genuine_fails
116
+ if decisive == 0:
117
+ continue
118
+
119
+ ac_proven = passes > genuine_fails
120
+ result.compiled_pass[ac_id] = ac_proven
121
+ if ac_proven:
122
+ result.compiled_vote[ac_id] = (passes, decisive, repaired)
123
+ continue
124
+ if passes == 0 and genuine_fails > 0:
125
+ result.genuine_failure = True
126
+ cmd, cmd_result = fail_results[0]
127
+ fail_file, fail_line = failure_location(cmd_result)
128
+ agree = (
129
+ f" {genuine_fails}/{decisive} independent checks agreed it fails."
130
+ if decisive > 1 else ""
131
+ )
132
+ result.gaps.append(Gap(
133
+ id=next_gap_id(task.id, f"ACCHK-{ac_id}"),
134
+ severity="high",
135
+ gap_type="test_failed",
136
+ task_id=task.id,
137
+ description=(
138
+ f"Acceptance check for {ac_id} failed: '{cmd}' "
139
+ f"(exit {cmd_result.exit_code}).{agree}"
140
+ ),
141
+ evidence=[cmd_result.summary[:500]],
142
+ recommended_fix=f"Fix the implementation so acceptance criterion {ac_id} holds.",
143
+ blocking=True,
144
+ acceptance_criterion_id=ac_id,
145
+ suggested_command=cmd,
146
+ file=fail_file,
147
+ line=fail_line,
148
+ stdout_path=cmd_result.stdout_path or None,
149
+ stderr_path=cmd_result.stderr_path or None,
150
+ ))
151
+ elif passes > 0 and genuine_fails > 0:
152
+ result.inconclusive_acs.add(ac_id)
153
+
154
+ return result
155
+
156
+
157
+ def promote_demoted_failures_when_compiler_incomplete(
158
+ *,
159
+ task: Task,
160
+ compiler_active: bool,
161
+ compiled_cmds_by_ac: Dict[str, List[str]],
162
+ demoted_failures: List[Gap],
163
+ genuine_failure: bool,
164
+ ) -> bool:
165
+ """Re-promote demoted planner test failures when compile coverage is partial."""
166
+ compiler_covered_all = bool(task.acceptance_criterion_ids) and all(
167
+ compiled_cmds_by_ac.get(ac_id) for ac_id in task.acceptance_criterion_ids
168
+ )
169
+ if compiler_active and not compiler_covered_all and demoted_failures:
170
+ for gap in demoted_failures:
171
+ gap.blocking = True
172
+ gap.severity = "high"
173
+ genuine_failure = True
174
+ logger.info(
175
+ "Re-promoted demoted test failure %s to blocking: acceptance compiler "
176
+ "did not produce a check for every criterion of task %s.",
177
+ gap.id, task.id,
178
+ )
179
+ return genuine_failure
@@ -0,0 +1,124 @@
1
+ """Corpus freshness gate for tasks touching docs/corpus paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Callable, List, Optional
8
+
9
+ from devcouncil.domain.gap import Gap
10
+ from devcouncil.domain.task import Task
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _norm(path: str) -> str:
16
+ return path.replace("\\", "/").lstrip("./")
17
+
18
+
19
+ def _task_touches_corpus(
20
+ *,
21
+ task: Task,
22
+ changed_files: List[str],
23
+ corpus_paths: List[str],
24
+ ) -> bool:
25
+ corpus_norm = {_norm(p) for p in corpus_paths}
26
+ for pf in task.planned_files:
27
+ rel = _norm(pf.path)
28
+ if rel in corpus_norm or any(rel.startswith(c + "/") for c in corpus_norm):
29
+ return True
30
+ if rel.endswith(".md") or rel.startswith("docs/"):
31
+ return True
32
+ for path in changed_files:
33
+ rel = _norm(path)
34
+ if rel in corpus_norm or any(rel.startswith(c + "/") for c in corpus_norm):
35
+ return True
36
+ if rel.endswith(".md") or rel.startswith("docs/"):
37
+ return True
38
+ return False
39
+
40
+
41
+ def _corpus_is_stale(project_root: Path) -> tuple[bool, str]:
42
+ try:
43
+ from devcouncil.indexing.wiring import corpus_graph_path, load_corpus_settings
44
+ from devcouncil.indexing.repo_mapper import RepoMapper
45
+
46
+ settings = load_corpus_settings(project_root)
47
+ if not settings.enabled:
48
+ return False, "corpus disabled"
49
+ graph_path = corpus_graph_path(project_root)
50
+ if not graph_path.is_file():
51
+ return True, "corpus graph missing"
52
+ mapper = RepoMapper(project_root)
53
+ map_path = project_root / ".devcouncil" / "repo_map.json"
54
+ if map_path.is_file():
55
+ from devcouncil.utils.json_persist import read_json
56
+
57
+ loaded = read_json(map_path)
58
+ if isinstance(loaded, dict) and mapper.map_is_stale(loaded):
59
+ return True, "repo map stale vs corpus fingerprint"
60
+ return False, "fresh"
61
+ except Exception as exc:
62
+ logger.debug("corpus stale check failed: %s", exc)
63
+ return True, "corpus check error"
64
+
65
+
66
+ def detect_corpus_stale_gaps(
67
+ *,
68
+ task: Task,
69
+ project_root: Path,
70
+ changed_files: List[str],
71
+ next_gap_id: Callable[[str, str], str],
72
+ corpus_stale_enabled: bool = True,
73
+ corpus_stale_blocking: bool = False,
74
+ corpus_paths: Optional[List[str]] = None,
75
+ ) -> List[Gap]:
76
+ if not corpus_stale_enabled:
77
+ return []
78
+ try:
79
+ from devcouncil.indexing.wiring import load_corpus_settings
80
+
81
+ settings = load_corpus_settings(project_root)
82
+ paths = corpus_paths if corpus_paths is not None else list(settings.paths)
83
+ if not _task_touches_corpus(task=task, changed_files=changed_files, corpus_paths=paths):
84
+ return []
85
+ stale, reason = _corpus_is_stale(project_root)
86
+ if not stale:
87
+ return []
88
+ return [Gap(
89
+ id=next_gap_id(task.id, "CORPUSSTALE"),
90
+ severity="high" if corpus_stale_blocking else "medium",
91
+ gap_type="corpus_stale",
92
+ task_id=task.id,
93
+ description=(
94
+ f"Corpus index is stale or missing ({reason}). "
95
+ "Doc/corpus navigation and doc-code-ref gates may be wrong."
96
+ ),
97
+ evidence=[".devcouncil/corpus/graph.json", reason],
98
+ recommended_fix="Run `dev map` (corpus rebuild) or enable auto_refresh_on_verify.",
99
+ blocking=corpus_stale_blocking,
100
+ suggested_command="dev map",
101
+ )]
102
+ except Exception:
103
+ logger.debug("detect_corpus_stale_gaps failed", exc_info=True)
104
+ return []
105
+
106
+
107
+ def refresh_corpus_on_verify_if_needed(project_root: Path) -> bool:
108
+ """Best-effort corpus rebuild when enabled and stale."""
109
+ try:
110
+ from devcouncil.indexing.wiring import load_corpus_settings
111
+
112
+ settings = load_corpus_settings(project_root)
113
+ if not settings.enabled or not settings.auto_refresh_on_verify:
114
+ return False
115
+ stale, _ = _corpus_is_stale(project_root)
116
+ if not stale:
117
+ return False
118
+ from devcouncil.indexing.wiring import build_corpus
119
+
120
+ build_corpus(project_root)
121
+ return True
122
+ except Exception:
123
+ logger.debug("corpus verify refresh failed", exc_info=True)
124
+ return False
@@ -0,0 +1,9 @@
1
+ """Corpus verification gate facade (re-exports split gate modules)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devcouncil.verification.checks.acceptance_corpus import detect_acceptance_corpus_gaps
6
+ from devcouncil.verification.checks.corpus_stale import detect_corpus_stale_gaps
7
+ from devcouncil.verification.checks.doc_code_ref import detect_doc_code_ref_gaps
8
+
9
+ __all__ = ["detect_acceptance_corpus_gaps", "detect_corpus_stale_gaps", "detect_doc_code_ref_gaps"]
@@ -0,0 +1,360 @@
1
+ """Dead-symbol gate: flag newly added public top-level symbols nothing references.
2
+
3
+ Diff-scoped via ``added_lines_by_file`` + AST/span intersection. Test references
4
+ and intent-text naming clear a symbol; files already flagged ``unwired_file`` are
5
+ skipped. Never raises.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Callable, List, Optional, Set, Tuple
15
+
16
+ from devcouncil.domain.gap import Gap
17
+ from devcouncil.domain.requirement import Requirement
18
+ from devcouncil.domain.task import Task
19
+ from devcouncil.indexing.wiring import (
20
+ ALLOW_UNWIRED,
21
+ decorator_names,
22
+ is_liveness_code_file,
23
+ is_private_symbol,
24
+ is_test_path,
25
+ is_vendored_path,
26
+ is_wiring_decorated,
27
+ iter_js_export_symbols,
28
+ parse_python_all_exports,
29
+ parse_python_reexport_names,
30
+ strip_js_comments,
31
+ strip_py_comments,
32
+ strip_string_literals,
33
+ )
34
+ from devcouncil.verification.checks.semantic_diff import task_intent_text
35
+ from devcouncil.verification.stub_detector import added_lines_by_file, task_allows_scaffolding
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ _IDENT_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
40
+ _CODE_SUFFIXES = {".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
41
+
42
+ # (path, start_line, end_line, name)
43
+ _SymbolCand = Tuple[str, int, int, str]
44
+
45
+
46
+ def _norm(path: str) -> str:
47
+ s = path.replace("\\", "/")
48
+ while s.startswith("./"):
49
+ s = s[2:]
50
+ return s
51
+
52
+
53
+ def _python_candidates(
54
+ project_root: Path,
55
+ path: str,
56
+ added_lines: Set[int],
57
+ ) -> List[_SymbolCand]:
58
+ """Return (path, start, end, name) for public top-level defs intersecting added lines."""
59
+ try:
60
+ source = (project_root / path).read_text(encoding="utf-8", errors="replace")
61
+ tree = ast.parse(source)
62
+ except Exception:
63
+ return []
64
+ all_exports = parse_python_all_exports(source)
65
+ reexports = parse_python_reexport_names(path, source)
66
+ protected = all_exports | reexports
67
+ out: List[_SymbolCand] = []
68
+ for node in tree.body:
69
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
70
+ continue
71
+ name = node.name
72
+ if is_private_symbol(name):
73
+ continue
74
+ # ``__all__`` / barrel re-exports = public API surface (parity with graph).
75
+ if name in protected:
76
+ continue
77
+ start = getattr(node, "lineno", None)
78
+ end = getattr(node, "end_lineno", start) or start
79
+ if start is None:
80
+ continue
81
+ span = set(range(start, (end or start) + 1))
82
+ if not span & added_lines:
83
+ continue
84
+ if is_wiring_decorated(decorator_names(node)):
85
+ continue
86
+ out.append((path, start, end or start, name))
87
+ return out
88
+
89
+
90
+ def _js_definition_span(source: str, name: str) -> Optional[Tuple[int, int]]:
91
+ """Best-effort start/end lines for a JS/TS binding named ``name``."""
92
+ patterns = (
93
+ rf"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+{re.escape(name)}\b",
94
+ rf"(?m)^\s*(?:export\s+)?class\s+{re.escape(name)}\b",
95
+ rf"(?m)^\s*(?:export\s+)?(?:const|let|var)\s+{re.escape(name)}\b",
96
+ )
97
+ for pat in patterns:
98
+ m = re.search(pat, source)
99
+ if m:
100
+ line = source[: m.start()].count("\n") + 1
101
+ # Single-line approx is enough for token outside-span checks.
102
+ return line, line
103
+ return None
104
+
105
+
106
+ def _js_candidates(
107
+ project_root: Path,
108
+ path: str,
109
+ added_lines: Set[int],
110
+ ) -> List[_SymbolCand]:
111
+ try:
112
+ source = (project_root / path).read_text(encoding="utf-8", errors="replace")
113
+ except OSError:
114
+ return []
115
+ out: List[_SymbolCand] = []
116
+ seen: Set[str] = set()
117
+ export_hits = iter_js_export_symbols(source)
118
+ for line, name in export_hits:
119
+ if name in seen:
120
+ continue
121
+ span = _js_definition_span(source, name)
122
+ if span is not None:
123
+ start, end = span
124
+ else:
125
+ start = end = line
126
+ # Fold export-list lines into the defining span so `export { name }` is
127
+ # not treated as an external reference that clears the symbol.
128
+ for eline, ename in export_hits:
129
+ if ename == name:
130
+ start = min(start, eline)
131
+ end = max(end, eline)
132
+ if line not in added_lines and not (set(range(start, end + 1)) & added_lines):
133
+ continue
134
+ lines = source.splitlines()
135
+ if 0 < line <= len(lines):
136
+ prev_lines = lines[: line - 1]
137
+ if prev_lines:
138
+ prev = prev_lines[-1].strip()
139
+ if prev.startswith("@"):
140
+ continue
141
+ seen.add(name)
142
+ out.append((path, start, end, name))
143
+ return out
144
+
145
+
146
+ def _build_token_index(
147
+ project_root: Path,
148
+ files: List[str],
149
+ *,
150
+ exclude: Set[str],
151
+ ) -> tuple[dict[str, Set[str]], dict[str, dict[str, Set[int]]]]:
152
+ """Map identifier token -> files, and token -> file -> line numbers."""
153
+ index: dict[str, Set[str]] = {}
154
+ lines_index: dict[str, dict[str, Set[int]]] = {}
155
+ try:
156
+ for rel in files:
157
+ norm = _norm(rel)
158
+ if norm in exclude:
159
+ continue
160
+ if is_vendored_path(norm):
161
+ continue
162
+ if Path(norm).suffix.lower() not in _CODE_SUFFIXES:
163
+ continue
164
+ path = project_root / norm
165
+ if not path.is_file():
166
+ continue
167
+ try:
168
+ text = path.read_text(encoding="utf-8", errors="replace")
169
+ except OSError:
170
+ continue
171
+ if path.suffix.lower() == ".py":
172
+ cleaned = strip_string_literals(strip_py_comments(text))
173
+ else:
174
+ cleaned = strip_string_literals(strip_js_comments(text))
175
+ for lineno, line in enumerate(cleaned.splitlines(), 1):
176
+ for tok in _IDENT_RE.findall(line):
177
+ if len(tok) < 2:
178
+ continue
179
+ index.setdefault(tok, set()).add(norm)
180
+ lines_index.setdefault(tok, {}).setdefault(norm, set()).add(lineno)
181
+ except Exception:
182
+ logger.debug("token index build failed", exc_info=True)
183
+ return index, lines_index
184
+
185
+
186
+ def _symbol_is_referenced(
187
+ name: str,
188
+ path: str,
189
+ start: int,
190
+ end: int,
191
+ token_index: dict[str, Set[str]],
192
+ lines_index: dict[str, dict[str, Set[int]]],
193
+ ) -> bool:
194
+ """True when ``name`` is used outside its defining span (other file or same-file)."""
195
+ refs = token_index.get(name, set())
196
+ if refs - {path}:
197
+ return True
198
+ same_lines = lines_index.get(name, {}).get(path, set())
199
+ return any(ln < start or ln > end for ln in same_lines)
200
+
201
+
202
+ def detect_dead_symbol_gaps(
203
+ *,
204
+ task: Task,
205
+ project_root: Path,
206
+ diff_content: str,
207
+ next_gap_id: Callable[[str, str], str],
208
+ dead_symbol_enabled: bool = True,
209
+ dead_symbol_blocking: bool = False,
210
+ requirements: Optional[List[Requirement]] = None,
211
+ unwired_files: Optional[Set[str]] = None,
212
+ git_files: Optional[List[str]] = None,
213
+ lsp_refs: Optional[bool] = None,
214
+ ) -> List[Gap]:
215
+ """Flag diff-added public top-level symbols with zero external references.
216
+
217
+ When ``lsp_refs`` is True (or config ``indexing.lsp_refs`` when ``None``),
218
+ token-scan dead candidates are confirmed via the optional live LSP client
219
+ before becoming gaps — external references clear false positives.
220
+ """
221
+ gaps: List[Gap] = []
222
+ if not dead_symbol_enabled or not diff_content:
223
+ return gaps
224
+ try:
225
+ by_file = added_lines_by_file(diff_content)
226
+ if not by_file:
227
+ return gaps
228
+
229
+ scaffolding_ok = task_allows_scaffolding(task)
230
+ intent = task_intent_text(task, requirements) if requirements is not None else (
231
+ f"{task.title} {task.description}"
232
+ )
233
+ unwired = {_norm(p) for p in (unwired_files or set())}
234
+
235
+ candidates: List[_SymbolCand] = []
236
+ for path, added in by_file.items():
237
+ norm = _norm(path)
238
+ if not is_liveness_code_file(norm) or is_test_path(norm):
239
+ continue
240
+ if norm in unwired:
241
+ continue
242
+ line_nums = {ln for ln, _ in added}
243
+ if Path(norm).suffix.lower() == ".py":
244
+ candidates.extend(_python_candidates(project_root, norm, line_nums))
245
+ else:
246
+ candidates.extend(_js_candidates(project_root, norm, line_nums))
247
+
248
+ if not candidates:
249
+ return gaps
250
+
251
+ candidates.sort(key=lambda item: (item[0], item[1], item[3]))
252
+
253
+ if git_files is None:
254
+ try:
255
+ from devcouncil.indexing.repo_mapper import RepoMapper
256
+
257
+ tracked = RepoMapper(project_root).get_git_files()
258
+ except Exception:
259
+ tracked = []
260
+ else:
261
+ tracked = list(git_files)
262
+ token_index, lines_index = _build_token_index(project_root, tracked, exclude=set())
263
+
264
+ use_lsp = lsp_refs
265
+ if use_lsp is None:
266
+ try:
267
+ from devcouncil.indexing.lsp_client import lsp_refs_enabled
268
+
269
+ use_lsp = lsp_refs_enabled(project_root)
270
+ except Exception:
271
+ use_lsp = False
272
+ lsp_pool = None
273
+ if use_lsp:
274
+ try:
275
+ from devcouncil.indexing.lsp_client import LspSessionPool
276
+
277
+ lsp_pool = LspSessionPool(project_root)
278
+ except Exception:
279
+ lsp_pool = None
280
+
281
+ try:
282
+ for path, start, end, name in candidates:
283
+ # Intent-text naming = deliberate API addition.
284
+ if re.search(rf"\b{re.escape(name)}\b", intent or ""):
285
+ continue
286
+
287
+ # allow-unwired on the defining line/file with scaffolding parity.
288
+ try:
289
+ source = (project_root / path).read_text(encoding="utf-8", errors="replace")
290
+ src_lines = source.splitlines()
291
+ line_text = src_lines[start - 1] if 0 < start <= len(src_lines) else ""
292
+ except OSError:
293
+ line_text = ""
294
+ source = ""
295
+ if ALLOW_UNWIRED in line_text or ALLOW_UNWIRED in source:
296
+ gaps.append(Gap(
297
+ id=next_gap_id(task.id, f"DEADDECL-{path}:{start}:{name}"),
298
+ severity="medium",
299
+ gap_type="dead_symbol",
300
+ task_id=task.id,
301
+ description=(
302
+ f"Intentional unused symbol `{name}` declared at {path}:{start}."
303
+ ),
304
+ evidence=[f"{path}:{start}", f"symbol:{name}", ALLOW_UNWIRED],
305
+ recommended_fix=(
306
+ f"Wire `{name}` into its caller when scaffolding is complete, "
307
+ "or remove it."
308
+ ),
309
+ blocking=False,
310
+ file=path,
311
+ line=start,
312
+ ))
313
+ if scaffolding_ok:
314
+ continue
315
+
316
+ if _symbol_is_referenced(name, path, start, end, token_index, lines_index):
317
+ continue
318
+
319
+ try:
320
+ from devcouncil.indexing.graph.query import symbol_has_non_test_inbound
321
+
322
+ if symbol_has_non_test_inbound(project_root, path, name):
323
+ continue
324
+ except Exception:
325
+ logger.debug("graph inbound check failed for %s", name, exc_info=True)
326
+
327
+ if lsp_pool is not None:
328
+ try:
329
+ confirmed = lsp_pool.confirm_unreferenced(path, start, name)
330
+ if confirmed is False:
331
+ continue
332
+ except Exception:
333
+ logger.debug("LSP dead-symbol confirm failed for %s", name, exc_info=True)
334
+
335
+ gaps.append(Gap(
336
+ id=next_gap_id(task.id, f"DEAD-{path}:{start}:{name}"),
337
+ severity="high" if dead_symbol_blocking else "medium",
338
+ gap_type="dead_symbol",
339
+ task_id=task.id,
340
+ description=(
341
+ f"New public symbol `{name}` at {path}:{start} is never referenced "
342
+ "outside its own definition."
343
+ ),
344
+ evidence=[f"{path}:{start}", f"symbol:{name}"],
345
+ recommended_fix=(
346
+ f"Call or register `{name}` from the code that needs it "
347
+ f"(use `dev scope update {task.id} --lease-token <token> "
348
+ f"--planned-file <caller>` if the caller is out of scope), or remove it."
349
+ ),
350
+ blocking=dead_symbol_blocking,
351
+ file=path,
352
+ line=start,
353
+ ))
354
+ finally:
355
+ if lsp_pool is not None:
356
+ lsp_pool.close()
357
+ except Exception:
358
+ logger.debug("detect_dead_symbol_gaps failed; degrading to zero gaps", exc_info=True)
359
+ return []
360
+ return gaps