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,253 @@
1
+ """Serializable PDG layer types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, cast, Dict, List, Literal, Optional
7
+
8
+ PDG_VERSION = 1
9
+
10
+ TaintCategory = Literal[
11
+ "command-injection",
12
+ "path-traversal",
13
+ "sql-injection",
14
+ "code-injection",
15
+ "ssrf",
16
+ "deserialization",
17
+ "other",
18
+ ]
19
+
20
+
21
+ @dataclass
22
+ class BasicBlock:
23
+ id: str
24
+ start_line: int
25
+ end_line: int
26
+ text: str = ""
27
+ lines: List[int] = field(default_factory=list)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "id": self.id,
32
+ "start_line": self.start_line,
33
+ "end_line": self.end_line,
34
+ "text": self.text,
35
+ "lines": self.lines,
36
+ }
37
+
38
+ @classmethod
39
+ def from_dict(cls, raw: dict[str, Any]) -> "BasicBlock":
40
+ return cls(
41
+ id=str(raw["id"]),
42
+ start_line=int(raw["start_line"]),
43
+ end_line=int(raw["end_line"]),
44
+ text=str(raw.get("text") or ""),
45
+ lines=[int(x) for x in raw.get("lines") or []],
46
+ )
47
+
48
+
49
+ @dataclass
50
+ class CFGEdge:
51
+ source: str
52
+ target: str
53
+ kind: str # fallthrough | true | false | loop | exception | return
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ return {"source": self.source, "target": self.target, "kind": self.kind}
57
+
58
+ @classmethod
59
+ def from_dict(cls, raw: dict[str, Any]) -> "CFGEdge":
60
+ return cls(
61
+ source=str(raw["source"]),
62
+ target=str(raw["target"]),
63
+ kind=str(raw.get("kind") or "fallthrough"),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class DataDepEdge:
69
+ variable: str
70
+ def_line: int
71
+ use_line: int
72
+ def_block: str
73
+ use_block: str
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ return {
77
+ "variable": self.variable,
78
+ "def_line": self.def_line,
79
+ "use_line": self.use_line,
80
+ "def_block": self.def_block,
81
+ "use_block": self.use_block,
82
+ }
83
+
84
+ @classmethod
85
+ def from_dict(cls, raw: dict[str, Any]) -> "DataDepEdge":
86
+ return cls(
87
+ variable=str(raw["variable"]),
88
+ def_line=int(raw["def_line"]),
89
+ use_line=int(raw["use_line"]),
90
+ def_block=str(raw["def_block"]),
91
+ use_block=str(raw["use_block"]),
92
+ )
93
+
94
+
95
+ @dataclass
96
+ class CDGEdge:
97
+ controller: str
98
+ dependent: str
99
+ branch: str # T | F | *
100
+ guard: bool = False
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ return {
104
+ "controller": self.controller,
105
+ "dependent": self.dependent,
106
+ "branch": self.branch,
107
+ "guard": self.guard,
108
+ }
109
+
110
+ @classmethod
111
+ def from_dict(cls, raw: dict[str, Any]) -> "CDGEdge":
112
+ return cls(
113
+ controller=str(raw["controller"]),
114
+ dependent=str(raw["dependent"]),
115
+ branch=str(raw.get("branch") or "*"),
116
+ guard=bool(raw.get("guard")),
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class TaintFinding:
122
+ path: str
123
+ function: str
124
+ category: TaintCategory
125
+ source_line: int
126
+ sink_line: int
127
+ variable: str
128
+ source_expr: str
129
+ sink_expr: str
130
+ confidence: str = "extracted"
131
+
132
+ def to_dict(self) -> dict[str, Any]:
133
+ return {
134
+ "path": self.path,
135
+ "function": self.function,
136
+ "category": self.category,
137
+ "source_line": self.source_line,
138
+ "sink_line": self.sink_line,
139
+ "variable": self.variable,
140
+ "source_expr": self.source_expr,
141
+ "sink_expr": self.sink_expr,
142
+ "confidence": self.confidence,
143
+ }
144
+
145
+ @classmethod
146
+ def from_dict(cls, raw: dict[str, Any]) -> "TaintFinding":
147
+ return cls(
148
+ path=str(raw["path"]),
149
+ function=str(raw["function"]),
150
+ category=cast(TaintCategory, raw.get("category") or "other"),
151
+ source_line=int(raw["source_line"]),
152
+ sink_line=int(raw["sink_line"]),
153
+ variable=str(raw.get("variable") or ""),
154
+ source_expr=str(raw.get("source_expr") or ""),
155
+ sink_expr=str(raw.get("sink_expr") or ""),
156
+ confidence=str(raw.get("confidence") or "extracted"),
157
+ )
158
+
159
+
160
+ @dataclass
161
+ class FunctionPDG:
162
+ path: str
163
+ qualname: str
164
+ start_line: int
165
+ end_line: int
166
+ blocks: List[BasicBlock] = field(default_factory=list)
167
+ cfg_edges: List[CFGEdge] = field(default_factory=list)
168
+ reaching_def: List[DataDepEdge] = field(default_factory=list)
169
+ cdg: List[CDGEdge] = field(default_factory=list)
170
+ taint: List[TaintFinding] = field(default_factory=list)
171
+
172
+ def to_dict(self) -> dict[str, Any]:
173
+ return {
174
+ "path": self.path,
175
+ "qualname": self.qualname,
176
+ "start_line": self.start_line,
177
+ "end_line": self.end_line,
178
+ "blocks": [b.to_dict() for b in self.blocks],
179
+ "cfg_edges": [e.to_dict() for e in self.cfg_edges],
180
+ "reaching_def": [e.to_dict() for e in self.reaching_def],
181
+ "cdg": [e.to_dict() for e in self.cdg],
182
+ "taint": [t.to_dict() for t in self.taint],
183
+ }
184
+
185
+ @classmethod
186
+ def from_dict(cls, raw: dict[str, Any]) -> "FunctionPDG":
187
+ return cls(
188
+ path=str(raw["path"]),
189
+ qualname=str(raw["qualname"]),
190
+ start_line=int(raw["start_line"]),
191
+ end_line=int(raw["end_line"]),
192
+ blocks=[BasicBlock.from_dict(b) for b in raw.get("blocks") or []],
193
+ cfg_edges=[CFGEdge.from_dict(e) for e in raw.get("cfg_edges") or []],
194
+ reaching_def=[DataDepEdge.from_dict(e) for e in raw.get("reaching_def") or []],
195
+ cdg=[CDGEdge.from_dict(e) for e in raw.get("cdg") or []],
196
+ taint=[TaintFinding.from_dict(t) for t in raw.get("taint") or []],
197
+ )
198
+
199
+
200
+ @dataclass
201
+ class FilePDG:
202
+ path: str
203
+ language: str
204
+ functions: List[FunctionPDG] = field(default_factory=list)
205
+
206
+ def to_dict(self) -> dict[str, Any]:
207
+ return {
208
+ "path": self.path,
209
+ "language": self.language,
210
+ "functions": [f.to_dict() for f in self.functions],
211
+ }
212
+
213
+ @classmethod
214
+ def from_dict(cls, raw: dict[str, Any]) -> "FilePDG":
215
+ return cls(
216
+ path=str(raw["path"]),
217
+ language=str(raw.get("language") or ""),
218
+ functions=[FunctionPDG.from_dict(f) for f in raw.get("functions") or []],
219
+ )
220
+
221
+
222
+ @dataclass
223
+ class PDGLayer:
224
+ version: int = PDG_VERSION
225
+ files: Dict[str, FilePDG] = field(default_factory=dict)
226
+ taint_findings: List[TaintFinding] = field(default_factory=list)
227
+
228
+ def to_meta(self) -> dict[str, Any]:
229
+ findings = [f.to_dict() for f in self.taint_findings]
230
+ stats = {
231
+ "file_count": len(self.files),
232
+ "function_count": sum(len(f.functions) for f in self.files.values()),
233
+ "cfg_edge_count": sum(
234
+ len(fn.cfg_edges) for f in self.files.values() for fn in f.functions
235
+ ),
236
+ "reaching_def_count": sum(
237
+ len(fn.reaching_def) for f in self.files.values() for fn in f.functions
238
+ ),
239
+ "cdg_edge_count": sum(len(fn.cdg) for f in self.files.values() for fn in f.functions),
240
+ "taint_count": len(findings),
241
+ }
242
+ return {
243
+ "version": self.version,
244
+ "stats": stats,
245
+ "files": sorted(self.files.keys()),
246
+ "taint_findings": findings[:500],
247
+ }
248
+
249
+ def shard_payload(self, path: str) -> Optional[dict[str, Any]]:
250
+ file_pdg = self.files.get(path)
251
+ if file_pdg is None:
252
+ return None
253
+ return {"pdg": file_pdg.to_dict()}
@@ -0,0 +1,154 @@
1
+ """Heuristic intra-procedural taint analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from dataclasses import dataclass
7
+ from typing import Dict, Iterable, List, Set, Tuple
8
+
9
+ from devcouncil.indexing.graph.pdg.schema import DataDepEdge, TaintCategory, TaintFinding
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class _Site:
14
+ line: int
15
+ variable: str
16
+ expr: str
17
+ category: TaintCategory
18
+
19
+
20
+ _SOURCE_PATTERNS: Dict[TaintCategory, Tuple[str, ...]] = {
21
+ "command-injection": ("input", "sys.argv", "os.environ.get", "request.args", "request.form"),
22
+ "path-traversal": ("input", "sys.argv", "os.environ.get"),
23
+ "sql-injection": ("input", "sys.argv", "request.args"),
24
+ "code-injection": ("input", "sys.argv"),
25
+ "ssrf": ("input", "sys.argv", "request.url"),
26
+ "deserialization": ("json.loads", "yaml.load", "pickle.loads"),
27
+ "other": (),
28
+ }
29
+
30
+ _SINK_PATTERNS: Dict[TaintCategory, Tuple[str, ...]] = {
31
+ "command-injection": ("os.system", "subprocess.call", "subprocess.run", "subprocess.Popen", "os.popen"),
32
+ "path-traversal": ("open", "Path", "shutil.copy", "shutil.move"),
33
+ "sql-injection": (".execute", ".raw", "executemany"),
34
+ "code-injection": ("eval", "exec", "compile"),
35
+ "ssrf": ("urlopen", "requests.get", "requests.post", "httpx.get", "httpx.post"),
36
+ "deserialization": (),
37
+ "other": (),
38
+ }
39
+
40
+
41
+ def _call_name(node: ast.Call) -> str:
42
+ func = node.func
43
+ if isinstance(func, ast.Name):
44
+ return func.id
45
+ if isinstance(func, ast.Attribute):
46
+ parts: List[str] = []
47
+ cur: ast.AST = func
48
+ while isinstance(cur, ast.Attribute):
49
+ parts.append(cur.attr)
50
+ cur = cur.value
51
+ if isinstance(cur, ast.Name):
52
+ parts.append(cur.id)
53
+ return ".".join(reversed(parts))
54
+ return ""
55
+
56
+
57
+ class _SiteVisitor(ast.NodeVisitor):
58
+ def __init__(self) -> None:
59
+ self.sites: List[_Site] = []
60
+
61
+ def visit_Call(self, node: ast.Call) -> None:
62
+ name = _call_name(node)
63
+ line = int(node.lineno)
64
+ for category, patterns in _SINK_PATTERNS.items():
65
+ if any(p in name for p in patterns):
66
+ var = ""
67
+ if node.args and isinstance(node.args[0], ast.Name):
68
+ var = node.args[0].id
69
+ self.sites.append(_Site(line=line, variable=var, expr=name, category=category))
70
+ for category, patterns in _SOURCE_PATTERNS.items():
71
+ if any(name.endswith(p) or name == p for p in patterns):
72
+ var = ""
73
+ if node.args and isinstance(node.args[0], ast.Name):
74
+ var = node.args[0].id
75
+ self.sites.append(_Site(line=line, variable=var or name, expr=name, category=category))
76
+ self.generic_visit(node)
77
+
78
+ def visit_Name(self, node: ast.Name) -> None:
79
+ if isinstance(node.ctx, ast.Load) and node.id == "input":
80
+ self.sites.append(_Site(line=int(node.lineno), variable="input", expr="input", category="command-injection"))
81
+ self.generic_visit(node)
82
+
83
+
84
+ def _param_sources(func_node: ast.AST) -> List[_Site]:
85
+ sites: List[_Site] = []
86
+ if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
87
+ return sites
88
+ for arg in func_node.args.args:
89
+ sites.append(_Site(line=int(arg.lineno), variable=arg.arg, expr=arg.arg, category="other"))
90
+ return sites
91
+
92
+
93
+ def _reaching_map(edges: Iterable[DataDepEdge]) -> Dict[Tuple[int, str], Set[Tuple[int, str]]]:
94
+ m: Dict[Tuple[int, str], Set[Tuple[int, str]]] = {}
95
+ for edge in edges:
96
+ m.setdefault((edge.use_line, edge.variable), set()).add((edge.def_line, edge.variable))
97
+ return m
98
+
99
+
100
+ def analyze_taint(
101
+ path: str,
102
+ qualname: str,
103
+ func_node: ast.AST,
104
+ reaching: List[DataDepEdge],
105
+ ) -> List[TaintFinding]:
106
+ visitor = _SiteVisitor()
107
+ visitor.visit(func_node)
108
+ sites = _param_sources(func_node) + visitor.sites
109
+ sources = [s for s in sites if s.category != "other" or s.expr in {p for pats in _SOURCE_PATTERNS.values() for p in pats}]
110
+ sinks = [s for s in sites if any(s.expr.startswith(p) or p in s.expr for pats in _SINK_PATTERNS.values() for p in pats if pats)]
111
+ reach = _reaching_map(reaching)
112
+ findings: List[TaintFinding] = []
113
+ seen: Set[tuple[int, int, str]] = set()
114
+ for src in sources:
115
+ for sink in sinks:
116
+ if sink.line < src.line:
117
+ continue
118
+ if src.category != "other" and sink.category != "other" and src.category != sink.category:
119
+ continue
120
+ tainted = {src.variable}
121
+ if sink.variable and sink.variable not in tainted:
122
+ # walk reaching defs from sink arg
123
+ queue = [(sink.line, sink.variable)]
124
+ ok = False
125
+ visited: Set[Tuple[int, str]] = set()
126
+ while queue:
127
+ use_line, var = queue.pop()
128
+ if (use_line, var) in visited:
129
+ continue
130
+ visited.add((use_line, var))
131
+ if var == src.variable or (src.line, src.variable) in reach.get((use_line, var), set()):
132
+ ok = True
133
+ break
134
+ for def_line, def_var in reach.get((use_line, var), set()):
135
+ queue.append((def_line, def_var))
136
+ if not ok and sink.variable != src.variable:
137
+ continue
138
+ key = (src.line, sink.line, sink.category)
139
+ if key in seen:
140
+ continue
141
+ seen.add(key)
142
+ findings.append(
143
+ TaintFinding(
144
+ path=path,
145
+ function=qualname,
146
+ category=sink.category if sink.category != "other" else src.category,
147
+ source_line=src.line,
148
+ sink_line=sink.line,
149
+ variable=sink.variable or src.variable,
150
+ source_expr=src.expr,
151
+ sink_expr=sink.expr,
152
+ )
153
+ )
154
+ return findings
@@ -0,0 +1,302 @@
1
+ """Query helpers over a persisted CodeGraph.
2
+
3
+ devcouncil: allow-unwired — package-private; reached via package ``__init__`` / CLI.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections import defaultdict, deque
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Set
11
+
12
+ from devcouncil.indexing.graph.build import load_code_graph
13
+ from devcouncil.indexing.graph.schema import CodeGraph, GraphNode
14
+
15
+
16
+ def _load(root: Path, graph: Optional[CodeGraph] = None) -> Optional[CodeGraph]:
17
+ return graph if graph is not None else load_code_graph(root)
18
+
19
+
20
+ def _match_nodes(graph: CodeGraph, name_or_path: str) -> List[GraphNode]:
21
+ q = name_or_path.replace("\\", "/")
22
+ hits: List[GraphNode] = []
23
+ for n in graph.nodes:
24
+ if n.id == q or n.path == q or n.name == q:
25
+ hits.append(n)
26
+ continue
27
+ if q in n.id or n.path.endswith(q) or n.id.endswith(f"::{q}"):
28
+ hits.append(n)
29
+ return hits
30
+
31
+
32
+ def query_symbol(
33
+ root: Path,
34
+ name_or_path: str,
35
+ *,
36
+ graph: Optional[CodeGraph] = None,
37
+ ) -> Dict[str, Any]:
38
+ """360° view: definition, callers, callees, importers."""
39
+ g = _load(root, graph)
40
+ if g is None:
41
+ return {"error": "no code graph; run `dev map` first", "query": name_or_path}
42
+ nodes = _match_nodes(g, name_or_path)
43
+ if not nodes:
44
+ return {"query": name_or_path, "matches": [], "definitions": []}
45
+
46
+ callers: Dict[str, List[str]] = defaultdict(list)
47
+ callees: Dict[str, List[str]] = defaultdict(list)
48
+ importers: Dict[str, List[str]] = defaultdict(list)
49
+ imports: Dict[str, List[str]] = defaultdict(list)
50
+ for e in g.edges:
51
+ if e.kind == "calls":
52
+ callers[e.target].append(e.source)
53
+ callees[e.source].append(e.target)
54
+ elif e.kind == "imports":
55
+ importers[e.target].append(e.source)
56
+ imports[e.source].append(e.target)
57
+
58
+ results = []
59
+ for n in nodes[:20]:
60
+ results.append(
61
+ {
62
+ "id": n.id,
63
+ "kind": n.kind.value if hasattr(n.kind, "value") else n.kind,
64
+ "path": n.path,
65
+ "name": n.name,
66
+ "line": n.line,
67
+ "area": n.area,
68
+ "callers": sorted(set(callers.get(n.id, [])))[:50],
69
+ "callees": sorted(set(callees.get(n.id, [])))[:50],
70
+ "importers": sorted(set(importers.get(n.id, [])))[:50],
71
+ "imports": sorted(set(imports.get(n.id, [])))[:50],
72
+ }
73
+ )
74
+ return {"query": name_or_path, "matches": len(nodes), "definitions": results}
75
+
76
+
77
+ def symbol_has_non_test_inbound(
78
+ root: Path,
79
+ path: str,
80
+ name: str,
81
+ *,
82
+ graph: Optional[CodeGraph] = None,
83
+ ) -> bool:
84
+ """Return whether a symbol has a non-structural inbound production edge.
85
+
86
+ Structural ownership edges (``contains``/``defines``) exist for every
87
+ declaration and therefore do not prove wiring. Test-only callers also do not
88
+ clear a production dead-symbol finding.
89
+ """
90
+ from devcouncil.indexing.wiring import is_test_path
91
+
92
+ loaded = _load(root, graph)
93
+ if loaded is None:
94
+ return False
95
+ normalized = path.replace("\\", "/")
96
+ targets = {
97
+ node.id
98
+ for node in loaded.nodes
99
+ if node.path.replace("\\", "/") == normalized and node.name == name
100
+ }
101
+ if not targets:
102
+ return False
103
+ by_id = {node.id: node for node in loaded.nodes}
104
+ structural = {"contains", "defines", "documents"}
105
+ for edge in loaded.edges:
106
+ if edge.target not in targets or edge.kind in structural or edge.source in targets:
107
+ continue
108
+ source = by_id.get(edge.source)
109
+ source_path = source.path if source is not None else edge.source.split("::", 1)[0]
110
+ if source_path and not is_test_path(source_path):
111
+ return True
112
+ return False
113
+
114
+
115
+ def trace_path(
116
+ root: Path,
117
+ start: str,
118
+ end: str,
119
+ *,
120
+ graph: Optional[CodeGraph] = None,
121
+ max_depth: int = 32,
122
+ ) -> Dict[str, Any]:
123
+ """Shortest path between two nodes (by id/name/path) over imports+calls+contains."""
124
+ g = _load(root, graph)
125
+ if g is None:
126
+ return {"error": "no code graph; run `dev map` first", "from": start, "to": end}
127
+
128
+ starts = _match_nodes(g, start)
129
+ ends = _match_nodes(g, end)
130
+ if not starts or not ends:
131
+ return {
132
+ "from": start,
133
+ "to": end,
134
+ "path": [],
135
+ "found": False,
136
+ "reason": "endpoint not found",
137
+ }
138
+
139
+ start_ids = {n.id for n in starts}
140
+ end_ids = {n.id for n in ends}
141
+ adj: Dict[str, Set[str]] = defaultdict(set)
142
+ for e in g.edges:
143
+ if e.kind in {"imports", "calls", "contains", "defines", "inherits"}:
144
+ adj[e.source].add(e.target)
145
+ adj[e.target].add(e.source) # undirected for reachability UX
146
+
147
+ # BFS from all starts
148
+ queue: deque = deque()
149
+ parent: Dict[str, Optional[str]] = {}
150
+ for s in start_ids:
151
+ queue.append(s)
152
+ parent[s] = None
153
+ found: Optional[str] = None
154
+ depth = 0
155
+ while queue and depth < max_depth:
156
+ for _ in range(len(queue)):
157
+ cur = queue.popleft()
158
+ if cur in end_ids:
159
+ found = cur
160
+ break
161
+ for nxt in adj.get(cur, ()):
162
+ if nxt not in parent:
163
+ parent[nxt] = cur
164
+ queue.append(nxt)
165
+ if found:
166
+ break
167
+ depth += 1
168
+
169
+ if not found:
170
+ return {"from": start, "to": end, "path": [], "found": False}
171
+
172
+ path: List[str] = []
173
+ walk: Optional[str] = found
174
+ while walk is not None:
175
+ path.append(walk)
176
+ walk = parent.get(walk)
177
+ path.reverse()
178
+ # Prefer a path that starts at a start_id
179
+ return {"from": start, "to": end, "path": path, "found": True, "length": len(path) - 1}
180
+
181
+
182
+ # --- Opt-in PDG query helpers ---
183
+
184
+
185
+ def _load_file_pdg_from_store(root: Path, path: str):
186
+ from devcouncil.indexing.graph.pdg.schema import FilePDG
187
+
188
+ path = path.replace("\\", "/")
189
+ try:
190
+ from devcouncil.codeintel import get_codeintel_service
191
+
192
+ shards = get_codeintel_service(root).store.analysis_shards()
193
+ raw = (shards.get(path) or {}).get("pdg")
194
+ if isinstance(raw, dict):
195
+ return FilePDG.from_dict(raw)
196
+ except Exception:
197
+ pass
198
+ return None
199
+
200
+
201
+ def _match_pdg_functions(root: Path, graph: CodeGraph, target: str):
202
+ q = target.replace("\\", "/")
203
+ paths: set[str] = set()
204
+ if q.endswith(".py"):
205
+ paths.add(q)
206
+ else:
207
+ for n in graph.nodes:
208
+ if n.name == q or n.id.endswith(f"::{q}") or q in n.id:
209
+ if n.path:
210
+ paths.add(n.path.replace("\\", "/"))
211
+ hits = []
212
+ for path in paths:
213
+ file_pdg = _load_file_pdg_from_store(root, path)
214
+ if file_pdg is None:
215
+ continue
216
+ for fn in file_pdg.functions:
217
+ if q.endswith(".py") or fn.qualname == q or fn.qualname.endswith(f".{q}") or q in fn.qualname:
218
+ hits.append(fn)
219
+ return hits
220
+
221
+
222
+ def explain_pdg_taint(
223
+ root: Path,
224
+ *,
225
+ graph: Optional[CodeGraph] = None,
226
+ path: Optional[str] = None,
227
+ category: Optional[str] = None,
228
+ ) -> Dict[str, Any]:
229
+ from devcouncil.indexing.graph.build import load_pdg_layer
230
+ from devcouncil.indexing.graph.pdg.schema import TaintFinding
231
+
232
+ g = _load(root, graph)
233
+ if g is None:
234
+ return {"ok": False, "error": "no code graph; run `dev map --pdg` or `dev graph pdg build` first"}
235
+ layer = load_pdg_layer(g)
236
+ findings: List[TaintFinding] = list(layer.taint_findings) if layer else []
237
+ if not findings and isinstance(g.meta.get("pdg"), dict):
238
+ for item in g.meta["pdg"].get("taint_findings") or []:
239
+ if isinstance(item, dict):
240
+ findings.append(TaintFinding.from_dict(item))
241
+ if path:
242
+ path = path.replace("\\", "/")
243
+ findings = [f for f in findings if f.path == path]
244
+ if category:
245
+ findings = [f for f in findings if f.category == category]
246
+ return {"ok": True, "count": len(findings), "findings": [f.to_dict() for f in findings]}
247
+
248
+
249
+ def query_pdg_controls(
250
+ root: Path,
251
+ target: str,
252
+ *,
253
+ graph: Optional[CodeGraph] = None,
254
+ ) -> Dict[str, Any]:
255
+ g = _load(root, graph)
256
+ if g is None:
257
+ return {"ok": False, "error": "no code graph"}
258
+ functions = _match_pdg_functions(root, g, target)
259
+ if not functions:
260
+ return {"ok": False, "error": f"no PDG for target {target!r}; run `dev graph pdg build`"}
261
+ return {
262
+ "ok": True,
263
+ "target": target,
264
+ "functions": [
265
+ {"path": fn.path, "qualname": fn.qualname, "cdg": [e.to_dict() for e in fn.cdg]}
266
+ for fn in functions
267
+ ],
268
+ }
269
+
270
+
271
+ def query_pdg_flows(
272
+ root: Path,
273
+ target: str,
274
+ *,
275
+ variable: Optional[str] = None,
276
+ graph: Optional[CodeGraph] = None,
277
+ ) -> Dict[str, Any]:
278
+ g = _load(root, graph)
279
+ if g is None:
280
+ return {"ok": False, "error": "no code graph"}
281
+ functions = _match_pdg_functions(root, g, target)
282
+ if not functions:
283
+ return {"ok": False, "error": f"no PDG for target {target!r}; run `dev graph pdg build`"}
284
+ out: List[dict[str, Any]] = []
285
+ for fn in functions:
286
+ flows = fn.reaching_def
287
+ if variable:
288
+ flows = [e for e in flows if e.variable == variable]
289
+ out.append(
290
+ {
291
+ "path": fn.path,
292
+ "qualname": fn.qualname,
293
+ "reaching_def": [e.to_dict() for e in flows],
294
+ }
295
+ )
296
+ return {"ok": True, "target": target, "variable": variable, "functions": out}
297
+
298
+
299
+ # Aliases matching pdg.query public API
300
+ explain_taint = explain_pdg_taint
301
+ query_controls = query_pdg_controls
302
+ query_flows = query_pdg_flows