devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,522 @@
1
+ """API route mapping over the native code graph.
2
+
3
+ Deterministic, LLM-free: ROUTE nodes, ``routes_to`` / ``registers`` edges, plus
4
+ token scans for client fetch sites and handler/consumer response shapes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
13
+
14
+ from devcouncil.indexing.graph.build import load_code_graph
15
+ from devcouncil.indexing.graph.schema import CodeGraph, GraphNode, NodeKind
16
+
17
+ _PARAM_RE = re.compile(
18
+ r":\w+" # FastAPI / Flask
19
+ r"|\{[^}]+\}" # Express / Spring
20
+ r"|\[[^\]]+\]" # Next.js dynamic segments
21
+ r"|\$\{[^}]+\}" # JS template-literal fetch URLs
22
+ )
23
+
24
+ _FETCH_LITERAL = re.compile(
25
+ r"""fetch\s*\(\s*(['"`])(?P<url>[^'"`]+)\1""",
26
+ re.IGNORECASE,
27
+ )
28
+ _FETCH_TEMPLATE = re.compile(
29
+ r"""fetch\s*\(\s*`(?P<url>[^`]+)`""",
30
+ re.IGNORECASE,
31
+ )
32
+ _AXIOS_LITERAL = re.compile(
33
+ r"""(?:axios|axios\.default)\.(?P<verb>get|post|put|patch|delete)\s*\(\s*(['"`])(?P<url>[^'"`]+)\2""",
34
+ re.IGNORECASE,
35
+ )
36
+ _REQUESTS_LITERAL = re.compile(
37
+ r"""requests\.(?P<verb>get|post|put|patch|delete)\s*\(\s*(['"`])(?P<url>[^'"`]+)\2""",
38
+ re.IGNORECASE,
39
+ )
40
+ _HTTPX_LITERAL = re.compile(
41
+ r"""httpx\.(?P<verb>get|post|put|patch|delete)\s*\(\s*(['"`])(?P<url>[^'"`]+)\2""",
42
+ re.IGNORECASE,
43
+ )
44
+ _RESP_VAR = re.compile(
45
+ r"""(?:const|let|var)\s+(?P<var>\w+)\s*=\s*(?:await\s+)?(?:fetch|axios|requests|httpx)""",
46
+ re.IGNORECASE,
47
+ )
48
+ _JSON_VAR = re.compile(
49
+ r"""(?:const|let|var)\s+(?P<var>\w+)\s*=\s*(?:await\s+)?(?P<src>\w+)\.json\s*\(""",
50
+ re.IGNORECASE,
51
+ )
52
+ _DESTRUCTURE = re.compile(
53
+ r"""const\s*\{\s*(?P<keys>[^}]+)\s*\}\s*=\s*(?:await\s+)?""",
54
+ re.IGNORECASE,
55
+ )
56
+ _TS_RETURN_DICT = re.compile(r"""return\s*\{([^}]+)\}""", re.MULTILINE | re.DOTALL)
57
+ _TS_KEY = re.compile(r"""['"]?(\w+)['"]?\s*:""")
58
+
59
+ _SCAN_WINDOW = 40
60
+
61
+ _ROUTE_HANDLER_KINDS = frozenset(
62
+ {
63
+ NodeKind.FUNCTION,
64
+ NodeKind.METHOD,
65
+ NodeKind.CLASS,
66
+ }
67
+ )
68
+
69
+
70
+ def _load(root: Path, graph: Optional[CodeGraph] = None) -> Optional[CodeGraph]:
71
+ return graph if graph is not None else load_code_graph(root)
72
+
73
+
74
+ def normalize_route_path(path: str) -> str:
75
+ """Normalize ``:param``, ``{id}``, ``[id]``, ``${id}`` segments to ``*``."""
76
+ norm = path.replace("\\", "/").strip()
77
+ if not norm.startswith("/"):
78
+ norm = "/" + norm.lstrip("/")
79
+ return _PARAM_RE.sub("*", norm)
80
+
81
+
82
+ def _segments(path: str) -> List[str]:
83
+ return [s for s in normalize_route_path(path).split("/") if s]
84
+
85
+
86
+ def paths_match(route_path: str, fetch_path: str) -> bool:
87
+ """Segment-wise match after param normalization."""
88
+ a, b = _segments(route_path), _segments(fetch_path)
89
+ if len(a) != len(b):
90
+ return False
91
+ for left, right in zip(a, b):
92
+ if left == "*" or right == "*":
93
+ continue
94
+ if left != right:
95
+ return False
96
+ return True
97
+
98
+
99
+ def _verbs_compatible(route_verb: str, fetch_verb: str) -> bool:
100
+ rv = (route_verb or "ANY").upper()
101
+ fv = (fetch_verb or "GET").upper()
102
+ if rv in {"ANY", "FILE", "ROUTE"}:
103
+ return True
104
+ if fv == "GET" and rv in {"GET", "HEAD"}:
105
+ return True
106
+ return rv == fv
107
+
108
+
109
+ def _route_nodes(graph: CodeGraph) -> List[GraphNode]:
110
+ return [n for n in graph.nodes if n.kind == NodeKind.ROUTE]
111
+
112
+
113
+ def _index_edges(graph: CodeGraph) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
114
+ """``routes_to``: route -> handlers; ``registers``: route -> owners."""
115
+ routes_to: Dict[str, List[str]] = {}
116
+ registers: Dict[str, List[str]] = {}
117
+ for e in graph.edges:
118
+ if e.kind == "routes_to":
119
+ routes_to.setdefault(e.source, []).append(e.target)
120
+ elif e.kind == "registers":
121
+ registers.setdefault(e.target, []).append(e.source)
122
+ return routes_to, registers
123
+
124
+
125
+ def _node_map(graph: CodeGraph) -> Dict[str, GraphNode]:
126
+ return graph.node_by_id()
127
+
128
+
129
+ def _read_lines(root: Path, rel: str) -> List[str]:
130
+ try:
131
+ return (root / rel).read_text(encoding="utf-8", errors="replace").splitlines()
132
+ except OSError:
133
+ return []
134
+
135
+
136
+ def _scan_fetch_sites(root: Path, graph: CodeGraph) -> List[Dict[str, Any]]:
137
+ seen_paths: Set[str] = set()
138
+ for n in graph.nodes:
139
+ if n.path:
140
+ seen_paths.add(n.path.replace("\\", "/"))
141
+ sites: List[Dict[str, Any]] = []
142
+ for rel in sorted(seen_paths):
143
+ lines = _read_lines(root, rel)
144
+ if not lines:
145
+ continue
146
+ for line_no, line in enumerate(lines, start=1):
147
+ for match in _FETCH_LITERAL.finditer(line):
148
+ sites.append(_site(rel, line_no, match.group("url"), "GET", line, lines))
149
+ for match in _FETCH_TEMPLATE.finditer(line):
150
+ sites.append(_site(rel, line_no, match.group("url"), "GET", line, lines))
151
+ for pat in (_AXIOS_LITERAL, _REQUESTS_LITERAL, _HTTPX_LITERAL):
152
+ for match in pat.finditer(line):
153
+ sites.append(
154
+ _site(
155
+ rel,
156
+ line_no,
157
+ match.group("url"),
158
+ match.group("verb").upper(),
159
+ line,
160
+ lines,
161
+ )
162
+ )
163
+ return sites
164
+
165
+
166
+ def _site(
167
+ path: str,
168
+ line: int,
169
+ url: str,
170
+ verb: str,
171
+ line_text: str,
172
+ lines: List[str],
173
+ ) -> Dict[str, Any]:
174
+ window = lines[line - 1 : line - 1 + _SCAN_WINDOW]
175
+ response_var = ""
176
+ for wline in window:
177
+ m = _RESP_VAR.search(wline)
178
+ if m:
179
+ response_var = m.group("var")
180
+ break
181
+ data_var = response_var
182
+ for wline in window:
183
+ m = _JSON_VAR.search(wline)
184
+ if m and (not response_var or m.group("src") == response_var):
185
+ data_var = m.group("var")
186
+ break
187
+ accessed = _consumer_keys(window, data_var or response_var)
188
+ return {
189
+ "path": path,
190
+ "line": line,
191
+ "url": url.strip(),
192
+ "verb": verb,
193
+ "response_var": data_var or response_var,
194
+ "accessed_keys": sorted(accessed),
195
+ }
196
+
197
+
198
+ def _consumer_keys(window: Iterable[str], response_var: str) -> Set[str]:
199
+ keys: Set[str] = set()
200
+ text = "\n".join(window)
201
+ if response_var:
202
+ dot = re.compile(rf"\b{re.escape(response_var)}\.(\w+)")
203
+ bracket = re.compile(
204
+ rf"\b{re.escape(response_var)}\s*\[\s*['\"](\w+)['\"]\s*\]"
205
+ )
206
+ for m in dot.finditer(text):
207
+ keys.add(m.group(1))
208
+ for m in bracket.finditer(text):
209
+ keys.add(m.group(1))
210
+ for m in _DESTRUCTURE.finditer(text):
211
+ for part in m.group("keys").split(","):
212
+ name = part.strip().split(":")[0].strip()
213
+ if name and re.match(r"^\w+$", name):
214
+ keys.add(name)
215
+ return keys
216
+
217
+
218
+ def _handler_node(handler_id: str, nodes: Dict[str, GraphNode]) -> Optional[GraphNode]:
219
+ node = nodes.get(handler_id)
220
+ if node is not None:
221
+ return node
222
+ if "::" not in handler_id:
223
+ return nodes.get(f"{handler_id}::{handler_id.split('/')[-1]}")
224
+ return None
225
+
226
+
227
+ def _following_handler(route: GraphNode, graph: CodeGraph) -> Optional[str]:
228
+ """Resolve decorator-style handler: next callable after the route line."""
229
+ line = route.line or 0
230
+ path = route.path
231
+ candidates = [
232
+ node
233
+ for node in graph.nodes
234
+ if node.path == path
235
+ and node.kind in _ROUTE_HANDLER_KINDS
236
+ and line < node.line <= line + 8
237
+ ]
238
+ if not candidates:
239
+ return None
240
+ return min(candidates, key=lambda node: node.line).id
241
+
242
+
243
+ def _resolve_route_handlers(
244
+ route: GraphNode,
245
+ handler_ids: List[str],
246
+ nodes: Dict[str, GraphNode],
247
+ graph: CodeGraph,
248
+ ) -> List[str]:
249
+ """Fix routes_to edges that point at the ROUTE node instead of the handler."""
250
+ resolved: List[str] = []
251
+ seen: Set[str] = set()
252
+ for handler_id in handler_ids:
253
+ node = _handler_node(handler_id, nodes)
254
+ needs_fallback = (
255
+ node is None
256
+ or node.kind == NodeKind.ROUTE
257
+ or node.id == route.id
258
+ or node.kind not in _ROUTE_HANDLER_KINDS
259
+ or (route.line and node.line <= route.line)
260
+ )
261
+ if needs_fallback:
262
+ fallback = _following_handler(route, graph)
263
+ if fallback and fallback not in seen:
264
+ resolved.append(fallback)
265
+ seen.add(fallback)
266
+ continue
267
+ if handler_id not in seen:
268
+ resolved.append(handler_id)
269
+ seen.add(handler_id)
270
+ if not resolved:
271
+ fallback = _following_handler(route, graph)
272
+ if fallback:
273
+ resolved.append(fallback)
274
+ return resolved
275
+
276
+
277
+ def _python_return_keys(root: Path, node: GraphNode) -> Set[str]:
278
+ lines = _read_lines(root, node.path)
279
+ if not lines:
280
+ return set()
281
+ try:
282
+ tree = ast.parse("\n".join(lines))
283
+ except SyntaxError:
284
+ return set()
285
+ keys: Set[str] = set()
286
+ for fn in ast.walk(tree):
287
+ if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
288
+ continue
289
+ if fn.name != node.name and node.name not in {fn.name}:
290
+ if node.line and not (fn.lineno <= node.line <= (fn.end_lineno or fn.lineno)):
291
+ continue
292
+ for sub in ast.walk(fn):
293
+ if isinstance(sub, ast.Return) and isinstance(sub.value, ast.Dict):
294
+ for k in sub.value.keys:
295
+ if isinstance(k, ast.Constant) and isinstance(k.value, str):
296
+ keys.add(k.value)
297
+ return keys
298
+
299
+
300
+ def _ts_return_keys(root: Path, node: GraphNode) -> Set[str]:
301
+ lines = _read_lines(root, node.path)
302
+ if not lines:
303
+ return set()
304
+ start = max(0, (node.line or 1) - 1)
305
+ end = min(len(lines), start + 120)
306
+ chunk = "\n".join(lines[start:end])
307
+ keys: Set[str] = set()
308
+ for m in _TS_RETURN_DICT.finditer(chunk):
309
+ for km in _TS_KEY.finditer(m.group(1)):
310
+ keys.add(km.group(1))
311
+ return keys
312
+
313
+
314
+ def handler_return_keys(root: Path, handler: GraphNode) -> Set[str]:
315
+ suffix = Path(handler.path).suffix.lower()
316
+ if suffix == ".py":
317
+ return _python_return_keys(root, handler)
318
+ return _ts_return_keys(root, handler)
319
+
320
+
321
+ def _summarize_handler(handler_id: str, nodes: Dict[str, GraphNode]) -> Optional[Dict[str, Any]]:
322
+ node = _handler_node(handler_id, nodes)
323
+ if node is None:
324
+ return {"id": handler_id}
325
+ kind = node.kind.value if hasattr(node.kind, "value") else str(node.kind)
326
+ return {
327
+ "id": node.id,
328
+ "path": node.path,
329
+ "name": node.name,
330
+ "line": node.line,
331
+ "kind": kind,
332
+ }
333
+
334
+
335
+ def route_map(root: Path, graph: Optional[CodeGraph] = None) -> Dict[str, Any]:
336
+ """Map HTTP routes to handlers, registrations, and client fetch consumers."""
337
+ g = _load(root, graph)
338
+ if g is None:
339
+ return {"error": "no code graph; run `dev map` first", "routes": []}
340
+
341
+ nodes = _node_map(g)
342
+ routes_to, registers = _index_edges(g)
343
+ fetch_sites = _scan_fetch_sites(root, g)
344
+ routes_out: List[Dict[str, Any]] = []
345
+
346
+ for route in sorted(_route_nodes(g), key=lambda n: (n.extras.get("route", ""), n.extras.get("verb", ""), n.id)):
347
+ route_path = str(route.extras.get("route") or "")
348
+ verb = str(route.extras.get("verb") or "ANY")
349
+ handler_ids = _resolve_route_handlers(route, routes_to.get(route.id, []), nodes, g)
350
+ handlers = [_summarize_handler(hid, nodes) for hid in handler_ids]
351
+ handler_keys: Set[str] = set()
352
+ for hid in handler_ids:
353
+ hnode = _handler_node(hid, nodes)
354
+ if hnode is not None:
355
+ handler_keys.update(handler_return_keys(root, hnode))
356
+
357
+ consumers: List[Dict[str, Any]] = []
358
+ for site in fetch_sites:
359
+ if not paths_match(route_path, site["url"]):
360
+ continue
361
+ if not _verbs_compatible(verb, site["verb"]):
362
+ continue
363
+ consumers.append(dict(site))
364
+
365
+ reg_sources = []
366
+ for owner_id in registers.get(route.id, []):
367
+ owner = nodes.get(owner_id)
368
+ reg_sources.append(
369
+ {
370
+ "id": owner_id,
371
+ "path": owner.path if owner else "",
372
+ "name": owner.name if owner else owner_id,
373
+ }
374
+ )
375
+
376
+ routes_out.append(
377
+ {
378
+ "id": route.id,
379
+ "path": route_path,
380
+ "normalized_path": normalize_route_path(route_path),
381
+ "verb": verb,
382
+ "framework": route.extras.get("framework", ""),
383
+ "file": route.path,
384
+ "line": route.line,
385
+ "handlers": [h for h in handlers if h],
386
+ "registrations": reg_sources,
387
+ "handler_keys": sorted(handler_keys),
388
+ "consumers": consumers,
389
+ }
390
+ )
391
+
392
+ return {"routes": routes_out, "count": len(routes_out)}
393
+
394
+
395
+ def _route_matches_filter(route: Dict[str, Any], route_filter: str) -> bool:
396
+ q = route_filter.strip()
397
+ if not q:
398
+ return True
399
+ norm_q = normalize_route_path(q)
400
+ if q in (route.get("path") or "") or q in (route.get("id") or ""):
401
+ return True
402
+ if norm_q == route.get("normalized_path"):
403
+ return True
404
+ return paths_match(route.get("path") or "", q)
405
+
406
+
407
+ def shape_check(
408
+ root: Path,
409
+ graph: Optional[CodeGraph] = None,
410
+ route_filter: Optional[str] = None,
411
+ ) -> Dict[str, Any]:
412
+ """Compare handler return dict keys vs consumer accessed keys."""
413
+ mapped = route_map(root, graph)
414
+ if mapped.get("error"):
415
+ return mapped
416
+
417
+ checks: List[Dict[str, Any]] = []
418
+ for route in mapped.get("routes") or []:
419
+ if route_filter and not _route_matches_filter(route, route_filter):
420
+ continue
421
+ handler_keys = set(route.get("handler_keys") or [])
422
+ consumer_keys: Set[str] = set()
423
+ for consumer in route.get("consumers") or []:
424
+ consumer_keys.update(consumer.get("accessed_keys") or [])
425
+
426
+ missing_in_handler = sorted(consumer_keys - handler_keys)
427
+ unused_by_consumers = sorted(handler_keys - consumer_keys)
428
+ mismatch = bool(missing_in_handler) if consumer_keys else False
429
+
430
+ checks.append(
431
+ {
432
+ "route": route.get("path"),
433
+ "verb": route.get("verb"),
434
+ "route_id": route.get("id"),
435
+ "handler_keys": sorted(handler_keys),
436
+ "consumer_keys": sorted(consumer_keys),
437
+ "missing_in_handler": missing_in_handler,
438
+ "unused_by_consumers": unused_by_consumers,
439
+ "mismatch": mismatch,
440
+ "consumer_count": len(route.get("consumers") or []),
441
+ }
442
+ )
443
+
444
+ mismatches = [c for c in checks if c["mismatch"]]
445
+ return {
446
+ "checks": checks,
447
+ "mismatch_count": len(mismatches),
448
+ "mismatches": mismatches,
449
+ }
450
+
451
+
452
+ def _risk_level(
453
+ *,
454
+ consumers: List[Dict[str, Any]],
455
+ mismatches: List[Dict[str, Any]],
456
+ ) -> str:
457
+ if not consumers:
458
+ return "none"
459
+ if mismatches and len(consumers) >= 2:
460
+ return "high"
461
+ if mismatches:
462
+ return "medium"
463
+ if len(consumers) >= 2:
464
+ return "low"
465
+ return "low"
466
+
467
+
468
+ def api_impact(
469
+ root: Path,
470
+ route_or_path: str,
471
+ graph: Optional[CodeGraph] = None,
472
+ ) -> Dict[str, Any]:
473
+ """Consumers, middleware registrations, shape mismatches, and risk for one route."""
474
+ mapped = route_map(root, graph)
475
+ if mapped.get("error"):
476
+ return {"error": mapped["error"], "route": route_or_path}
477
+
478
+ matched = [
479
+ r for r in (mapped.get("routes") or []) if _route_matches_filter(r, route_or_path)
480
+ ]
481
+ if not matched:
482
+ return {
483
+ "route": route_or_path,
484
+ "found": False,
485
+ "consumers": [],
486
+ "middleware": [],
487
+ "shape_mismatches": [],
488
+ "risk": "none",
489
+ }
490
+
491
+ # Prefer exact path match; otherwise first segment match.
492
+ route = matched[0]
493
+ if len(matched) > 1:
494
+ exact = [r for r in matched if r.get("path") == route_or_path or r.get("id") == route_or_path]
495
+ if exact:
496
+ route = exact[0]
497
+
498
+ shape = shape_check(root, graph, route_filter=route.get("path") or route_or_path)
499
+ route_shape = next(
500
+ (c for c in shape.get("checks") or [] if c.get("route_id") == route.get("id")),
501
+ None,
502
+ )
503
+ mismatches = shape.get("mismatches") or []
504
+ if route_shape and route_shape.get("mismatch"):
505
+ mismatches = [route_shape]
506
+
507
+ consumers = route.get("consumers") or []
508
+ middleware = route.get("registrations") or []
509
+ risk = _risk_level(consumers=consumers, mismatches=mismatches)
510
+
511
+ return {
512
+ "route": route.get("path"),
513
+ "route_id": route.get("id"),
514
+ "verb": route.get("verb"),
515
+ "found": True,
516
+ "handlers": route.get("handlers") or [],
517
+ "consumers": consumers,
518
+ "middleware": middleware,
519
+ "handler_keys": route.get("handler_keys") or [],
520
+ "shape_mismatches": mismatches,
521
+ "risk": risk,
522
+ }