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,289 @@
1
+ """Generation-consistent search, exploration, path, and impact queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict, deque
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
9
+ from devcouncil.codeintel.service import CodeIntelService, get_codeintel_service
10
+ from devcouncil.indexing.graph.schema import CodeGraph, Confidence, GraphEdge, GraphNode
11
+
12
+
13
+ class CodeIntelQueryEngine:
14
+ def __init__(self, root: Path | CodeIntelService):
15
+ self.service = root if isinstance(root, CodeIntelService) else get_codeintel_service(root)
16
+
17
+ def _graph(self) -> CodeGraph:
18
+ graph = self.service.load()
19
+ if graph is None:
20
+ raise FileNotFoundError("no code-intelligence index; run `dev graph init`")
21
+ # Fingerprinting shells out to git (diff + untracked hashing) — only pay
22
+ # that per-query cost when runtime evidence actually exists to match.
23
+ if not self.service.store.has_runtime_observations():
24
+ return graph
25
+ from devcouncil.codeintel.debug.fingerprint import source_fingerprint
26
+
27
+ fingerprint = source_fingerprint(self.service.project_root)
28
+ runtime = self.service.store.runtime_observations(source_fingerprint=fingerprint)
29
+ existing = {(edge.source, edge.target, edge.kind) for edge in graph.edges}
30
+ for observation in runtime:
31
+ kind = str(observation["kind"])
32
+ key = (str(observation["source"]), str(observation["target"]), kind)
33
+ if key in existing:
34
+ continue
35
+ sampled = kind in {"sampled_calls", "sampled_stack"}
36
+ graph.edges.append(GraphEdge(
37
+ source=key[0],
38
+ target=key[1],
39
+ kind=kind,
40
+ confidence=Confidence.INFERRED if sampled else Confidence.EXTRACTED,
41
+ reason="fingerprint-matched runtime observation",
42
+ extras={
43
+ "provenance": "runtime",
44
+ "confidence_score": 0.75 if sampled else 1.0,
45
+ "source_fingerprint": fingerprint,
46
+ "runtime_session": observation["session_id"],
47
+ "count": observation["count"],
48
+ "evidence": observation["evidence"],
49
+ },
50
+ ))
51
+ existing.add(key)
52
+ return graph
53
+
54
+ def _envelope(self, payload: dict[str, Any]) -> dict[str, Any]:
55
+ from devcouncil.codeintel.sync import get_sync_coordinator
56
+
57
+ status = self.service.status()
58
+ sync = get_sync_coordinator(self.service.project_root).status().as_dict()
59
+ graph_degraded = False
60
+ graph_degraded_reason = ""
61
+ try:
62
+ from devcouncil.utils.json_persist import read_json
63
+
64
+ map_path = self.service.project_root / ".devcouncil" / "repo_map.json"
65
+ if map_path.is_file():
66
+ data = read_json(map_path)
67
+ if isinstance(data, dict) and data.get("graph_degraded"):
68
+ graph_degraded = True
69
+ graph_degraded_reason = str(data.get("graph_degraded_reason") or "")
70
+ except Exception:
71
+ pass
72
+ return {
73
+ "ok": True,
74
+ "project_root": str(self.service.project_root),
75
+ "generation": status.get("generation"),
76
+ "schema_version": status.get("schema_version"),
77
+ "analyzer_version": status.get("analyzer_version"),
78
+ "sync": sync,
79
+ "graph_degraded": graph_degraded,
80
+ "graph_degraded_reason": graph_degraded_reason,
81
+ **payload,
82
+ }
83
+
84
+ def search(self, query: str, *, limit: int = 50) -> dict[str, Any]:
85
+ rows = self.service.cached_query(
86
+ "search", f"{query}\0{limit}", lambda: self.service.store.search(query, limit=limit)
87
+ )
88
+ return self._envelope({"query": query, "matches": rows})
89
+
90
+ def explore(self, query: str, *, limit: int = 20) -> dict[str, Any]:
91
+ graph = self._graph()
92
+ matches = self._match(graph, query)[:limit]
93
+ inbound, outbound = self._relations(graph.edges)
94
+ definitions: list[dict[str, Any]] = []
95
+ for node in matches:
96
+ content = self.service.store.content_for_path(node.path)
97
+ snippet = self._snippet(content, node.line, node.end_line)
98
+ callers = [self._edge_dict(edge) for edge in inbound.get(node.id, [])[:50]]
99
+ callees = [self._edge_dict(edge) for edge in outbound.get(node.id, [])[:50]]
100
+ definitions.append({
101
+ "id": node.id,
102
+ "kind": node.kind.value if hasattr(node.kind, "value") else str(node.kind),
103
+ "path": node.path,
104
+ "name": node.name,
105
+ "line": node.line,
106
+ "end_line": node.end_line,
107
+ "language": node.language,
108
+ "source": snippet,
109
+ "callers": callers,
110
+ "callees": callees,
111
+ })
112
+ impacted = self._impact_for_ids(graph, [node.id for node in matches], max_depth=3)
113
+ return self._envelope({
114
+ "query": query,
115
+ "match_count": len(matches),
116
+ "definitions": definitions,
117
+ "blast_radius": impacted,
118
+ })
119
+
120
+ def path(self, start: str, end: str, *, max_depth: int = 32) -> dict[str, Any]:
121
+ graph = self._graph()
122
+ starts = {node.id for node in self._match(graph, start)}
123
+ ends = {node.id for node in self._match(graph, end)}
124
+ if not starts or not ends:
125
+ return self._envelope({"from": start, "to": end, "found": False, "path": [], "reason": "endpoint not found"})
126
+ adjacency: dict[str, list[tuple[str, GraphEdge]]] = defaultdict(list)
127
+ for edge in graph.edges:
128
+ adjacency[edge.source].append((edge.target, edge))
129
+ if edge.kind in {"imports", "contains", "defines", "inherits", "implements", "overrides"}:
130
+ adjacency[edge.target].append((edge.source, edge))
131
+ queue = deque((node_id, 0) for node_id in starts)
132
+ parent: dict[str, tuple[str | None, GraphEdge | None]] = {node_id: (None, None) for node_id in starts}
133
+ found: str | None = None
134
+ while queue:
135
+ current, depth = queue.popleft()
136
+ if current in ends:
137
+ found = current
138
+ break
139
+ if depth >= max_depth:
140
+ continue
141
+ for target, edge in adjacency.get(current, []):
142
+ if target not in parent:
143
+ parent[target] = (current, edge)
144
+ queue.append((target, depth + 1))
145
+ if found is None:
146
+ return self._envelope({"from": start, "to": end, "found": False, "path": []})
147
+ steps: list[dict[str, Any]] = []
148
+ walk = found
149
+ while True:
150
+ previous, path_edge = parent[walk]
151
+ steps.append({"node": walk, "via": self._edge_dict(path_edge) if path_edge else None})
152
+ if previous is None:
153
+ break
154
+ walk = previous
155
+ steps.reverse()
156
+ return self._envelope({"from": start, "to": end, "found": True, "length": len(steps) - 1, "path": steps})
157
+
158
+ def impact(self, targets: Iterable[str], *, max_depth: int = 3) -> dict[str, Any]:
159
+ graph = self._graph()
160
+ ids: list[str] = []
161
+ for target in targets:
162
+ ids.extend(node.id for node in self._match(graph, target))
163
+ return self._envelope({"targets": list(targets), "blast_radius": self._impact_for_ids(graph, ids, max_depth=max_depth)})
164
+
165
+ def dead(self, *, minimum_confidence: str = "inferred") -> dict[str, Any]:
166
+ graph = self._graph()
167
+ ranks = {"ambiguous": 0, "inferred": 1, "extracted": 2}
168
+ floor = ranks.get(minimum_confidence, 1)
169
+ runtime_live = {
170
+ edge.target
171
+ for edge in graph.edges
172
+ if edge.extras.get("provenance") == "runtime"
173
+ }
174
+ rows = []
175
+ for entry in graph.dead_code:
176
+ confidence = entry.confidence.value if hasattr(entry.confidence, "value") else str(entry.confidence)
177
+ if ranks.get(confidence, 0) < floor:
178
+ continue
179
+ if entry.id in runtime_live:
180
+ continue
181
+ tier = "high-confidence dead candidate" if confidence == "extracted" else "unconfirmed/unwired"
182
+ rows.append({**entry.model_dump(mode="json"), "tier": tier})
183
+ return self._envelope({
184
+ "minimum_confidence": minimum_confidence,
185
+ "dead_code": rows,
186
+ "runtime_proven_live": sorted(runtime_live),
187
+ })
188
+
189
+ def affected_tests(self, targets: Iterable[str], *, max_depth: int = 3) -> dict[str, Any]:
190
+ graph = self._graph()
191
+ target_list = list(targets)
192
+ ids = [node.id for target in target_list for node in self._match(graph, target)]
193
+ impact = self._impact_for_ids(graph, ids, max_depth=max_depth)
194
+ impacted_ids = {node_id for layer in impact["layers"] for node_id in layer["nodes"]} | set(ids)
195
+ nodes = graph.node_by_id()
196
+ tests = sorted({
197
+ node.path
198
+ for node_id in impacted_ids
199
+ if (node := nodes.get(node_id)) is not None and self._is_test_path(node.path)
200
+ })
201
+ return self._envelope({"targets": target_list, "tests": tests, "blast_radius": impact})
202
+
203
+ @staticmethod
204
+ def _match(graph: CodeGraph, query: str) -> list[GraphNode]:
205
+ normalized = query.replace("\\", "/").lower()
206
+ exact: list[GraphNode] = []
207
+ partial: list[GraphNode] = []
208
+ for node in graph.nodes:
209
+ values = (node.id.lower(), node.path.lower(), node.name.lower())
210
+ if normalized in values or node.id.lower().endswith(f"::{normalized}"):
211
+ exact.append(node)
212
+ elif any(normalized in value for value in values):
213
+ partial.append(node)
214
+ return exact + partial
215
+
216
+ @staticmethod
217
+ def _relations(edges: Iterable[GraphEdge]) -> tuple[dict[str, list[GraphEdge]], dict[str, list[GraphEdge]]]:
218
+ inbound: dict[str, list[GraphEdge]] = defaultdict(list)
219
+ outbound: dict[str, list[GraphEdge]] = defaultdict(list)
220
+ for edge in edges:
221
+ inbound[edge.target].append(edge)
222
+ outbound[edge.source].append(edge)
223
+ return inbound, outbound
224
+
225
+ @staticmethod
226
+ def _edge_dict(edge: GraphEdge | None) -> dict[str, Any]:
227
+ if edge is None:
228
+ return {}
229
+ confidence = edge.confidence.value if hasattr(edge.confidence, "value") else str(edge.confidence)
230
+ return {
231
+ "source": edge.source,
232
+ "target": edge.target,
233
+ "kind": edge.kind,
234
+ "confidence": confidence,
235
+ "confidence_score": edge.extras.get("confidence_score", {"extracted": 1.0, "inferred": 0.7, "ambiguous": 0.4}.get(confidence, 0.4)),
236
+ "provenance": edge.extras.get("provenance", "extracted" if confidence == "extracted" else "inferred"),
237
+ "reason": edge.reason,
238
+ "evidence": edge.extras.get("evidence", []),
239
+ }
240
+
241
+ @staticmethod
242
+ def _snippet(content: bytes | None, start: int, end: int) -> str:
243
+ if content is None:
244
+ return ""
245
+ lines = content.decode("utf-8", errors="replace").splitlines()
246
+ if start <= 0:
247
+ return ""
248
+ final = end if end >= start else start
249
+ return "\n".join(f"{line_no}: {lines[line_no - 1]}" for line_no in range(start, min(final, len(lines)) + 1))
250
+
251
+ def _impact_for_ids(self, graph: CodeGraph, ids: Iterable[str], *, max_depth: int) -> dict[str, Any]:
252
+ seeds = list(dict.fromkeys(ids))
253
+ inbound, _ = self._relations(graph.edges)
254
+ visited = set(seeds)
255
+ frontier = set(seeds)
256
+ layers: list[dict[str, Any]] = []
257
+ for depth in range(1, max(1, min(8, max_depth)) + 1):
258
+ next_frontier: set[str] = set()
259
+ confidences: list[str] = []
260
+ for node_id in frontier:
261
+ for edge in inbound.get(node_id, []):
262
+ if edge.source in visited:
263
+ continue
264
+ next_frontier.add(edge.source)
265
+ confidence = edge.confidence.value if hasattr(edge.confidence, "value") else str(edge.confidence)
266
+ confidences.append(confidence)
267
+ visited.update(next_frontier)
268
+ layers.append({
269
+ "depth": depth,
270
+ "nodes": sorted(next_frontier),
271
+ "confidence": self._lowest_confidence(confidences),
272
+ "count": len(next_frontier),
273
+ })
274
+ frontier = next_frontier
275
+ if not frontier:
276
+ break
277
+ return {"seeds": seeds, "layers": layers, "total_impacted": len(visited - set(seeds))}
278
+
279
+ @staticmethod
280
+ def _lowest_confidence(values: Iterable[str]) -> str:
281
+ ranks = {"extracted": 2, "inferred": 1, "ambiguous": 0}
282
+ values = list(values)
283
+ return min(values, key=lambda value: ranks.get(value, 0)) if values else "extracted"
284
+
285
+ @staticmethod
286
+ def _is_test_path(path: str) -> bool:
287
+ normalized = f"/{path.lower().replace('\\', '/')}"
288
+ name = Path(path).name.lower()
289
+ return "/test" in normalized or "/tests/" in normalized or name.startswith("test_") or ".test." in name or ".spec." in name
@@ -0,0 +1,6 @@
1
+ """Semantic synthesis beyond direct syntax edges."""
2
+
3
+ from devcouncil.codeintel.resolution.abstract_state import AbstractState, AbstractValue
4
+ from devcouncil.codeintel.resolution.semantic import enrich_semantic_edges
5
+
6
+ __all__ = ["AbstractState", "AbstractValue", "enrich_semantic_edges"]
@@ -0,0 +1,301 @@
1
+ """Bounded abstract values for computed names, imports, and paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from typing import Iterable, Mapping
10
+
11
+ MAX_STRINGS = 32
12
+ MAX_CALLABLES = 32
13
+ MAX_TYPES = 16
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class AbstractValue:
18
+ scalars: frozenset[str | int | float | bool | None] = frozenset()
19
+ callables: frozenset[str] = frozenset()
20
+ types: frozenset[str] = frozenset()
21
+ unknown: bool = False
22
+
23
+ @classmethod
24
+ def unknown_value(cls) -> "AbstractValue":
25
+ return cls(unknown=True)
26
+
27
+ @classmethod
28
+ def scalar(cls, value: str | int | float | bool | None) -> "AbstractValue":
29
+ return cls(scalars=frozenset({value}))
30
+
31
+ @classmethod
32
+ def callable(cls, name: str) -> "AbstractValue":
33
+ return cls(callables=frozenset({name}))
34
+
35
+ @classmethod
36
+ def type_name(cls, name: str) -> "AbstractValue":
37
+ return cls(types=frozenset({name}))
38
+
39
+ def merge(self, other: "AbstractValue") -> "AbstractValue":
40
+ scalars = self.scalars | other.scalars
41
+ callables = self.callables | other.callables
42
+ types = self.types | other.types
43
+ overflow = len(scalars) > MAX_STRINGS or len(callables) > MAX_CALLABLES or len(types) > MAX_TYPES
44
+ if overflow:
45
+ return AbstractValue.unknown_value()
46
+ return AbstractValue(scalars, callables, types, self.unknown or other.unknown)
47
+
48
+ def strings(self) -> frozenset[str]:
49
+ return frozenset(value for value in self.scalars if isinstance(value, str))
50
+
51
+
52
+ @dataclass
53
+ class AbstractState:
54
+ values: dict[str, AbstractValue] = field(default_factory=dict)
55
+ functions: dict[str, tuple[tuple[str, ...], str]] = field(default_factory=dict)
56
+
57
+ def assign(self, name: str, value: AbstractValue) -> None:
58
+ self.values[name] = value
59
+
60
+ def get(self, name: str) -> AbstractValue:
61
+ return self.values.get(name, AbstractValue.unknown_value())
62
+
63
+ def analyze(
64
+ self,
65
+ source: str,
66
+ *,
67
+ callable_names: Iterable[str] = (),
68
+ type_names: Iterable[str] = (),
69
+ callable_aliases: Mapping[str, str] | None = None,
70
+ type_aliases: Mapping[str, str] | None = None,
71
+ ) -> "AbstractState":
72
+ """Collect bounded constants, aliases, object properties, and wrappers."""
73
+ for name in callable_names:
74
+ self.assign(name, AbstractValue.callable(name))
75
+ for name in type_names:
76
+ self.assign(name, AbstractValue.type_name(name))
77
+ for local, remote in (callable_aliases or {}).items():
78
+ self.assign(local, AbstractValue.callable(remote))
79
+ for local, remote in (type_aliases or {}).items():
80
+ self.assign(local, AbstractValue.type_name(remote))
81
+
82
+ for match in re.finditer(
83
+ r"^\s*function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*"
84
+ r"\{\s*return\s+(.+)\s*;\s*\}\s*$",
85
+ source,
86
+ re.MULTILINE,
87
+ ):
88
+ params = tuple(
89
+ value.strip()
90
+ for value in match.group(2).split(",")
91
+ if value.strip()
92
+ )
93
+ self.functions[match.group(1)] = (params, match.group(3).rstrip("; "))
94
+
95
+ for line in source.splitlines():
96
+ assignment_match = re.match(
97
+ r"^\s*(?:(?:const|let|var)\s+)?"
98
+ r"([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*=\s*(.+?)\s*;?\s*$",
99
+ line,
100
+ )
101
+ if assignment_match is None or "==" in line or "=>" in line:
102
+ continue
103
+ name, expression = assignment_match.groups()
104
+ expression = expression.rstrip("; ")
105
+ if expression.startswith("{") and expression.endswith("}"):
106
+ for key, value in re.findall(
107
+ r"([A-Za-z_$][\w$]*)\s*:\s*([^,}]+)", expression
108
+ ):
109
+ self.assign(f"{name}.{key}", self.evaluate(value.strip()))
110
+ continue
111
+ self.assign(name, self.evaluate(expression))
112
+ return self
113
+
114
+ def evaluate(self, expression: str) -> AbstractValue:
115
+ """Evaluate Python or JS-like scalar/callable/type expressions."""
116
+ expression = expression.strip().rstrip(";")
117
+ python_value = self.evaluate_python(expression)
118
+ if not python_value.unknown:
119
+ return python_value
120
+ return self._eval_text(expression)
121
+
122
+ def evaluate_python(self, expression: str | ast.AST) -> AbstractValue:
123
+ try:
124
+ node = ast.parse(expression, mode="eval").body if isinstance(expression, str) else expression
125
+ except SyntaxError:
126
+ return AbstractValue.unknown_value()
127
+ return self._eval(node)
128
+
129
+ def _eval(self, node: ast.AST) -> AbstractValue:
130
+ if isinstance(node, ast.Constant) and isinstance(node.value, (str, int, float, bool, type(None))):
131
+ return AbstractValue.scalar(node.value)
132
+ if isinstance(node, ast.Name):
133
+ return self.get(node.id)
134
+ if isinstance(node, ast.IfExp):
135
+ return self._eval(node.body).merge(self._eval(node.orelse))
136
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
137
+ return self._combine_strings(self._eval(node.left).strings(), self._eval(node.right).strings(), "")
138
+ if isinstance(node, ast.JoinedStr):
139
+ choices: set[str] = {""}
140
+ for part in node.values:
141
+ if isinstance(part, ast.Constant) and isinstance(part.value, str):
142
+ values = {part.value}
143
+ elif isinstance(part, ast.FormattedValue):
144
+ values = set(self._eval(part.value).strings())
145
+ else:
146
+ return AbstractValue.unknown_value()
147
+ choices = {left + right for left in choices for right in values}
148
+ if len(choices) > MAX_STRINGS:
149
+ return AbstractValue.unknown_value()
150
+ return AbstractValue(scalars=frozenset(choices))
151
+ if isinstance(node, (ast.Tuple, ast.List, ast.Set)):
152
+ value = AbstractValue()
153
+ for item in node.elts:
154
+ value = value.merge(self._eval(item))
155
+ return value
156
+ if isinstance(node, ast.Call):
157
+ name = self._call_name(node.func)
158
+ if name in {"str", "os.fspath"} and node.args:
159
+ return self._eval(node.args[0])
160
+ if name in {"os.path.join", "pathlib.Path", "Path"}:
161
+ parts = [self._eval(arg).strings() for arg in node.args]
162
+ if not parts or any(not part for part in parts):
163
+ return AbstractValue.unknown_value()
164
+ path_values: set[str] = {""}
165
+ for path_part in parts:
166
+ path_values = {
167
+ os.path.join(left, right) if left else right
168
+ for left in path_values
169
+ for right in path_part
170
+ }
171
+ if len(path_values) > MAX_STRINGS:
172
+ return AbstractValue.unknown_value()
173
+ return AbstractValue(scalars=frozenset(value.replace("\\", "/") for value in path_values))
174
+ return AbstractValue.unknown_value()
175
+
176
+ def _eval_text(self, expression: str) -> AbstractValue:
177
+ expression = expression.strip().rstrip(";")
178
+ while expression.startswith("(") and expression.endswith(")"):
179
+ expression = expression[1:-1].strip()
180
+ if not expression:
181
+ return AbstractValue.unknown_value()
182
+ if re.fullmatch(r"[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*", expression):
183
+ return self.get(expression)
184
+ if (
185
+ len(expression) >= 2
186
+ and expression[0] == expression[-1]
187
+ and expression[0] in {"'", '"'}
188
+ ):
189
+ try:
190
+ return AbstractValue.scalar(ast.literal_eval(expression))
191
+ except (SyntaxError, ValueError):
192
+ return AbstractValue.unknown_value()
193
+ if expression.startswith("`") and expression.endswith("`"):
194
+ values = {expression[1:-1]}
195
+ for nested in re.findall(r"\$\{([^}]+)\}", expression):
196
+ replacements = self.evaluate(nested).strings()
197
+ if not replacements:
198
+ return AbstractValue.unknown_value()
199
+ values = {
200
+ value.replace(f"${{{nested}}}", replacement)
201
+ for value in values
202
+ for replacement in replacements
203
+ }
204
+ if len(values) > MAX_STRINGS:
205
+ return AbstractValue.unknown_value()
206
+ return AbstractValue(scalars=frozenset(values))
207
+ conditional = re.match(r"^.+?\?\s*(.+?)\s*:\s*(.+)$", expression)
208
+ if conditional is not None:
209
+ return self.evaluate(conditional.group(1)).merge(
210
+ self.evaluate(conditional.group(2))
211
+ )
212
+ parts = self._split_top_level(expression, "+")
213
+ if len(parts) > 1:
214
+ value = self.evaluate(parts[0])
215
+ for part in parts[1:]:
216
+ value = self._combine_strings(
217
+ value.strings(), self.evaluate(part).strings(), ""
218
+ )
219
+ return value
220
+ if expression.startswith("[") and expression.endswith("]"):
221
+ value = AbstractValue()
222
+ for part in self._split_top_level(expression[1:-1], ","):
223
+ value = value.merge(self.evaluate(part))
224
+ return value
225
+ call = re.match(r"^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\((.*)\)$", expression)
226
+ if call is not None:
227
+ name, raw_args = call.groups()
228
+ args = [self.evaluate(part) for part in self._split_top_level(raw_args, ",")]
229
+ if name in {"path.join", "path.resolve", "os.path.join", "Path", "pathlib.Path"}:
230
+ choices = [arg.strings() for arg in args]
231
+ if not choices or any(not choice for choice in choices):
232
+ return AbstractValue.unknown_value()
233
+ values = {""}
234
+ for choice in choices:
235
+ values = {
236
+ os.path.join(left, right) if left else right
237
+ for left in values
238
+ for right in choice
239
+ }
240
+ if len(values) > MAX_STRINGS:
241
+ return AbstractValue.unknown_value()
242
+ return AbstractValue(
243
+ scalars=frozenset(value.replace("\\", "/") for value in values)
244
+ )
245
+ bound = self.get(name)
246
+ if bound.types or bound.callables:
247
+ return bound
248
+ wrapper = self.functions.get(name)
249
+ if wrapper is not None:
250
+ params, body = wrapper
251
+ previous = {param: self.values.get(param) for param in params}
252
+ try:
253
+ for param, value in zip(params, args):
254
+ self.assign(param, value)
255
+ return self.evaluate(body)
256
+ finally:
257
+ for param, previous_value in previous.items():
258
+ if previous_value is None:
259
+ self.values.pop(param, None)
260
+ else:
261
+ self.values[param] = previous_value
262
+ return AbstractValue.unknown_value()
263
+
264
+ @staticmethod
265
+ def _split_top_level(expression: str, separator: str) -> list[str]:
266
+ parts: list[str] = []
267
+ start = 0
268
+ depth = 0
269
+ quote = ""
270
+ for index, char in enumerate(expression):
271
+ if quote:
272
+ if char == quote and (index == 0 or expression[index - 1] != "\\"):
273
+ quote = ""
274
+ continue
275
+ if char in {"'", '"', "`"}:
276
+ quote = char
277
+ elif char in "([{":
278
+ depth += 1
279
+ elif char in ")]}":
280
+ depth = max(0, depth - 1)
281
+ elif char == separator and depth == 0:
282
+ parts.append(expression[start:index].strip())
283
+ start = index + 1
284
+ parts.append(expression[start:].strip())
285
+ return [part for part in parts if part]
286
+
287
+ @staticmethod
288
+ def _call_name(node: ast.AST) -> str:
289
+ if isinstance(node, ast.Name):
290
+ return node.id
291
+ if isinstance(node, ast.Attribute):
292
+ prefix = AbstractState._call_name(node.value)
293
+ return f"{prefix}.{node.attr}" if prefix else node.attr
294
+ return ""
295
+
296
+ @staticmethod
297
+ def _combine_strings(left: Iterable[str], right: Iterable[str], separator: str) -> AbstractValue:
298
+ values = {f"{a}{separator}{b}" for a in left for b in right}
299
+ if not values or len(values) > MAX_STRINGS:
300
+ return AbstractValue.unknown_value()
301
+ return AbstractValue(scalars=frozenset(values))
@@ -0,0 +1,33 @@
1
+ """Manifest for independently testable framework semantic augmenters."""
2
+
3
+ from devcouncil.codeintel.resolution.frameworks.base import FrameworkSpec
4
+ from devcouncil.codeintel.resolution.frameworks.di import (
5
+ DI_SPECS,
6
+ iter_provider_matches,
7
+ )
8
+ from devcouncil.codeintel.resolution.frameworks.events import (
9
+ EVENT_SPECS,
10
+ iter_event_matches,
11
+ )
12
+ from devcouncil.codeintel.resolution.frameworks.routes import (
13
+ COMPUTED_ROUTE_PATTERN,
14
+ ROUTE_SPECS,
15
+ iter_route_matches,
16
+ )
17
+
18
+ FRAMEWORK_MANIFEST: tuple[FrameworkSpec, ...] = (
19
+ *ROUTE_SPECS,
20
+ *DI_SPECS,
21
+ *EVENT_SPECS,
22
+ )
23
+
24
+ __all__ = [
25
+ "COMPUTED_ROUTE_PATTERN",
26
+ "DI_SPECS",
27
+ "EVENT_SPECS",
28
+ "FRAMEWORK_MANIFEST",
29
+ "ROUTE_SPECS",
30
+ "iter_event_matches",
31
+ "iter_provider_matches",
32
+ "iter_route_matches",
33
+ ]
@@ -0,0 +1,46 @@
1
+ """Shared records for isolated framework semantic augmenters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Pattern
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class FrameworkSpec:
12
+ """A framework matcher advertised by the semantic resolver."""
13
+
14
+ name: str
15
+ family: str
16
+ pattern: Pattern[str]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RouteMatch:
21
+ framework: str
22
+ verb: str
23
+ path: str
24
+ handler_expression: str
25
+ start: int
26
+ end: int
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class EventMatch:
31
+ operation: str
32
+ event: str
33
+ callback_expression: str
34
+ start: int
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ProviderMatch:
39
+ framework: str
40
+ target_expression: str
41
+ start: int
42
+
43
+
44
+ def compile_pattern(pattern: str, *, multiline: bool = False) -> Pattern[str]:
45
+ flags = re.IGNORECASE | (re.MULTILINE if multiline else 0)
46
+ return re.compile(pattern, flags)