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,887 @@
1
+ """Framework, event, state, callback, and dynamic-reference synthesis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import time
8
+ from collections import defaultdict
9
+ from pathlib import Path
10
+ from typing import Any, Callable, Iterable, Mapping
11
+
12
+ from devcouncil.codeintel.resolution.abstract_state import AbstractState
13
+ from devcouncil.codeintel.resolution.frameworks import (
14
+ COMPUTED_ROUTE_PATTERN,
15
+ iter_event_matches,
16
+ iter_provider_matches,
17
+ iter_route_matches,
18
+ )
19
+ from devcouncil.indexing.graph.extract_python import FileExtraction
20
+ from devcouncil.indexing.graph.schema import CodeGraph, Confidence, GraphEdge, GraphNode, NodeKind
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _REACT_STATE = re.compile(r"\[\s*(?P<state>[A-Za-z_$][\w$]*)\s*,\s*(?P<setter>[A-Za-z_$][\w$]*)\s*\]\s*=\s*(?:React\.)?useState\s*\(")
25
+ _DYNAMIC = re.compile(r"\b(?P<kind>eval|exec|getattr|setattr|import_module|Class\.forName|Activator\.CreateInstance|NSClassFromString|require|import)\s*\(")
26
+ _WORKER_URL = re.compile(
27
+ r"""(?:new\s+(?:Worker|SharedWorker)\s*\(\s*)?"""
28
+ r"""new\s+URL\s*\(\s*['"](?P<spec>[^'"]+)['"]\s*,\s*import\.meta\.url"""
29
+ )
30
+ _CALLBACK = re.compile(r"\b(?:then|catch|finally|map|filter|reduce|forEach|subscribe|useEffect)\s*\(\s*(?P<callback>[A-Za-z_$][\w$]*)")
31
+ _ABSTRACT_ALIAS_CALL = re.compile(
32
+ r"\b(?:new\s+)?(?P<callee>[A-Za-z_$][\w$]*)\s*\("
33
+ )
34
+ _C_FUNCTION_POINTER = re.compile(r"(?P<pointer>[A-Za-z_]\w*)\s*=\s*&?(?P<target>[A-Za-z_]\w*)\s*;")
35
+ _LITERAL_EVAL = re.compile(r"\b(?:eval|exec)\(\s*(?P<quote>['\"])(?P<body>.*?)(?P=quote)\s*\)")
36
+ _BRIDGE_DECL = re.compile(
37
+ r"(?:RCT_EXPORT_METHOD\s*\(|@ReactMethod\s+(?:fun\s+)?|AsyncFunction\s*\(\s*['\"]|Function\s*\(\s*['\"]|@objc\s+(?:public\s+)?func\s+)(?P<name>[A-Za-z_$][\w$]*)"
38
+ )
39
+ _BRIDGE_CALL = re.compile(
40
+ r"(?:NativeModules\.[A-Za-z_$][\w$]*\.|requireNativeModule\([^)]*\)\.)(?P<name>[A-Za-z_$][\w$]*)\s*\("
41
+ )
42
+ _FILE_ROUTE_PREFIXES = (
43
+ "src/pages/",
44
+ "pages/",
45
+ "src/routes/",
46
+ "server/api/",
47
+ )
48
+
49
+
50
+ def _enclosing(nodes: list[GraphNode], path: str, line: int) -> GraphNode | None:
51
+ candidates = [
52
+ node
53
+ for node in nodes
54
+ if node.path == path
55
+ and node.kind != NodeKind.FILE
56
+ and node.line <= line
57
+ and (node.end_line <= 0 or line <= node.end_line)
58
+ ]
59
+ return max(candidates, key=lambda node: node.line, default=None)
60
+
61
+
62
+ def _edge(
63
+ source: str,
64
+ target: str,
65
+ kind: str,
66
+ *,
67
+ reason: str,
68
+ score: float,
69
+ synthesizer: str,
70
+ ) -> GraphEdge:
71
+ confidence = Confidence.INFERRED if score >= 0.6 else Confidence.AMBIGUOUS
72
+ return GraphEdge(
73
+ source=source,
74
+ target=target,
75
+ kind=kind,
76
+ confidence=confidence,
77
+ reason=reason,
78
+ extras={
79
+ "provenance": "framework"
80
+ if kind in {"registers", "routes_to", "listens", "injects", "provides"}
81
+ else "inferred",
82
+ "confidence_score": score,
83
+ "evidence": [{"synthesized_by": synthesizer}],
84
+ "synthesized_by": synthesizer,
85
+ },
86
+ )
87
+
88
+
89
+ def _symbol_index(nodes: Iterable[GraphNode]) -> dict[str, list[GraphNode]]:
90
+ result: dict[str, list[GraphNode]] = defaultdict(list)
91
+ for node in nodes:
92
+ # Synthesized pathless bridge/channel nodes may already exist when an
93
+ # incremental refresh re-enriches a persisted graph. They are graph
94
+ # endpoints, not source-declared symbols, and admitting them here makes
95
+ # a second enrichment resolve reflections that a clean build cannot.
96
+ if (
97
+ node.name
98
+ and node.path
99
+ and node.kind
100
+ not in {
101
+ NodeKind.FILE,
102
+ NodeKind.DYNAMIC,
103
+ NodeKind.EVENT,
104
+ NodeKind.PROVIDER,
105
+ NodeKind.ROUTE,
106
+ NodeKind.STATE,
107
+ }
108
+ ):
109
+ keys = {node.name, node.id.rsplit("::", 1)[-1]}
110
+ for key in keys:
111
+ result[key].append(node)
112
+ return result
113
+
114
+
115
+ def _resolved_symbol_targets(
116
+ symbols: dict[str, list[GraphNode]],
117
+ name: str,
118
+ path: str,
119
+ *,
120
+ imported_paths: set[str] | None = None,
121
+ aliases: Mapping[str, str] | None = None,
122
+ ) -> list[GraphNode]:
123
+ """Return one unambiguous local/imported symbol before global fallback."""
124
+ remote = (aliases or {}).get(name, name)
125
+ lookup_name = remote.rsplit(".", 1)[-1].rsplit(":", 1)[-1]
126
+ targets = symbols.get(lookup_name) or []
127
+ same_file = [target for target in targets if target.path == path]
128
+ if len(same_file) == 1:
129
+ return same_file
130
+ if same_file:
131
+ return []
132
+ imported = [
133
+ target for target in targets if target.path in (imported_paths or set())
134
+ ]
135
+ if len(imported) == 1:
136
+ return imported
137
+ if imported:
138
+ return []
139
+ return targets if len(targets) == 1 else []
140
+
141
+
142
+ def _imported_paths_by_file(
143
+ nodes: Iterable[GraphNode],
144
+ edges: Iterable[GraphEdge],
145
+ ) -> dict[str, set[str]]:
146
+ node_by_id = {node.id: node for node in nodes}
147
+ imported: dict[str, set[str]] = defaultdict(set)
148
+ for edge in edges:
149
+ if edge.kind != "imports":
150
+ continue
151
+ source = node_by_id.get(edge.source)
152
+ target = node_by_id.get(edge.target)
153
+ source_path = source.path if source is not None else edge.source.split("::", 1)[0]
154
+ target_path = target.path if target is not None else edge.target.split("::", 1)[0]
155
+ if source_path and target_path and source_path != target_path:
156
+ imported[source_path].add(target_path)
157
+ return imported
158
+
159
+
160
+ def enrich_semantic_edges(
161
+ graph: CodeGraph,
162
+ *,
163
+ root: Path,
164
+ paths: set[str] | None = None,
165
+ extractions: Mapping[str, FileExtraction] | None = None,
166
+ progress: Callable[[str, int, int], None] | None = None,
167
+ budget_seconds: float | None = None,
168
+ ) -> CodeGraph:
169
+ """Add isolated semantic nodes/edges without changing extracted identities.
170
+
171
+ ``budget_seconds`` bounds the per-file enrichment loop by wall clock: when
172
+ exceeded, remaining files are skipped, partial results are kept, and
173
+ ``meta["semantic_enrich_partial"]`` records how far it got — a visible
174
+ partial instead of an unbounded hang on very large repos. The check runs
175
+ between files, so one pathological file can still overshoot the budget.
176
+ """
177
+
178
+ root = root.expanduser().resolve()
179
+ deadline = time.monotonic() + budget_seconds if budget_seconds else None
180
+ nodes = list(graph.nodes)
181
+ edges = list(graph.edges)
182
+ initial_node_count = len(nodes)
183
+ initial_edge_count = len(edges)
184
+ symbols = _symbol_index(nodes)
185
+ imported_paths = _imported_paths_by_file(nodes, edges)
186
+ aliases_by_path = {
187
+ path: {
188
+ local: remote
189
+ for detail in extraction.import_details
190
+ for local, remote in detail.alias_map.items()
191
+ }
192
+ for path, extraction in (extractions or {}).items()
193
+ }
194
+ existing_nodes = {node.id for node in nodes}
195
+ existing_edges = {(edge.source, edge.target, edge.kind) for edge in edges}
196
+ nodes_by_path: dict[str, list[GraphNode]] = defaultdict(list)
197
+ for node in nodes:
198
+ if node.path:
199
+ nodes_by_path[node.path].append(node)
200
+ file_paths = sorted({node.path for node in nodes if node.path})
201
+ if paths is not None:
202
+ file_paths = [path for path in file_paths if path in paths]
203
+
204
+ def add_node(node: GraphNode) -> None:
205
+ if node.id not in existing_nodes:
206
+ existing_nodes.add(node.id)
207
+ nodes.append(node)
208
+
209
+ def add_edge(edge: GraphEdge) -> None:
210
+ key = (edge.source, edge.target, edge.kind)
211
+ if edge.source in existing_nodes and edge.target in existing_nodes and key not in existing_edges:
212
+ existing_edges.add(key)
213
+ edges.append(edge)
214
+
215
+ total_files = len(file_paths)
216
+ for file_index, rel in enumerate(file_paths, start=1):
217
+ if deadline is not None and time.monotonic() > deadline:
218
+ graph.meta["semantic_enrich_partial"] = {
219
+ "files_done": file_index - 1,
220
+ "files_total": total_files,
221
+ "budget_seconds": budget_seconds,
222
+ }
223
+ logger.warning(
224
+ "semantic enrichment budget (%ss) exhausted after %d/%d files; "
225
+ "keeping partial results",
226
+ budget_seconds,
227
+ file_index - 1,
228
+ total_files,
229
+ )
230
+ break
231
+ if progress is not None:
232
+ progress("semantic", file_index - 1, total_files)
233
+ try:
234
+ text = (root / rel).read_text(encoding="utf-8", errors="replace")
235
+ except OSError:
236
+ continue
237
+ lines = text.splitlines()
238
+ file_symbols = nodes_by_path.get(rel, [])
239
+ callable_aliases, type_aliases = _abstract_import_aliases(
240
+ aliases_by_path.get(rel, {}),
241
+ symbols,
242
+ )
243
+ abstract_state = AbstractState().analyze(
244
+ text,
245
+ callable_names={
246
+ node.name
247
+ for node in file_symbols
248
+ if node.kind in {NodeKind.FUNCTION, NodeKind.METHOD}
249
+ },
250
+ type_names={
251
+ node.name
252
+ for node in file_symbols
253
+ if node.kind
254
+ in {
255
+ NodeKind.CLASS,
256
+ NodeKind.INTERFACE,
257
+ NodeKind.TYPE,
258
+ NodeKind.STRUCT,
259
+ NodeKind.ENUM,
260
+ NodeKind.TRAIT,
261
+ }
262
+ },
263
+ callable_aliases=callable_aliases,
264
+ type_aliases=type_aliases,
265
+ )
266
+ language = next((node.language for node in file_symbols if node.language), "")
267
+ normalized_rel = rel.replace("\\", "/")
268
+ if any(normalized_rel.startswith(prefix) for prefix in _FILE_ROUTE_PREFIXES) and Path(rel).suffix.lower() in {
269
+ ".astro", ".vue", ".svelte", ".tsx", ".ts", ".jsx", ".js"
270
+ }:
271
+ route_path = _file_route_path(normalized_rel)
272
+ route_id = f"{rel}::route:FILE:{route_path}"
273
+ add_node(GraphNode(
274
+ id=route_id,
275
+ kind=NodeKind.ROUTE,
276
+ path=rel,
277
+ name=f"FILE {route_path}",
278
+ line=1,
279
+ end_line=1,
280
+ language=language,
281
+ extras={"verb": "FILE", "route": route_path, "provenance": "framework"},
282
+ ))
283
+ handler = next((node for node in file_symbols if node.kind != NodeKind.FILE), None)
284
+ add_edge(_edge(
285
+ rel,
286
+ route_id,
287
+ "registers",
288
+ reason="reachable file registers framework route",
289
+ score=0.95,
290
+ synthesizer="file-route-resolver",
291
+ ))
292
+ add_edge(_edge(
293
+ route_id,
294
+ handler.id if handler else rel,
295
+ "routes_to",
296
+ reason="file-based framework route",
297
+ score=0.9,
298
+ synthesizer="file-route-resolver",
299
+ ))
300
+ for line_no, line in enumerate(lines, start=1):
301
+ if progress is not None and line_no % 1000 == 0:
302
+ progress("semantic", file_index - 1, total_files)
303
+ owner = _enclosing(file_symbols, rel, line_no)
304
+ owner_id = owner.id if owner is not None else rel
305
+ match: Any
306
+
307
+ for match in iter_route_matches(line):
308
+ route_id = f"{rel}::route:{match.verb}:{match.path}"
309
+ add_node(GraphNode(
310
+ id=route_id,
311
+ kind=NodeKind.ROUTE,
312
+ path=rel,
313
+ name=f"{match.verb} {match.path}",
314
+ line=line_no,
315
+ end_line=line_no,
316
+ language=language,
317
+ extras={
318
+ "verb": match.verb,
319
+ "route": match.path,
320
+ "framework": match.framework,
321
+ "provenance": "framework",
322
+ },
323
+ ))
324
+ handler_id = _route_handler_id(
325
+ file_symbols,
326
+ symbols,
327
+ rel,
328
+ line_no,
329
+ match.handler_expression,
330
+ owner_id,
331
+ imported_paths.get(rel, set()),
332
+ aliases_by_path.get(rel, {}),
333
+ abstract_state,
334
+ )
335
+ registration_owner = _registration_owner(
336
+ file_symbols, rel, owner_id, handler_id
337
+ )
338
+ add_edge(_edge(
339
+ registration_owner,
340
+ route_id,
341
+ "registers",
342
+ reason=f"{match.framework} route registration",
343
+ score=0.95,
344
+ synthesizer=f"{match.framework}-route",
345
+ ))
346
+ if handler_id is not None:
347
+ add_edge(_edge(
348
+ route_id,
349
+ handler_id,
350
+ "routes_to",
351
+ reason=f"{match.framework} route binds handler",
352
+ score=0.9,
353
+ synthesizer=f"{match.framework}-route",
354
+ ))
355
+
356
+ for match in COMPUTED_ROUTE_PATTERN.finditer(line):
357
+ expression = match.group("expr").strip()
358
+ if expression[:1] in {"'", '"'}:
359
+ continue
360
+ for route_path in sorted(abstract_state.evaluate(expression).strings()):
361
+ verb = match.group("verb").upper()
362
+ route_id = f"{rel}::route:{verb}:{route_path}"
363
+ add_node(GraphNode(
364
+ id=route_id,
365
+ kind=NodeKind.ROUTE,
366
+ path=rel,
367
+ name=f"{verb} {route_path}",
368
+ line=line_no,
369
+ end_line=line_no,
370
+ language=language,
371
+ extras={
372
+ "verb": verb,
373
+ "route": route_path,
374
+ "framework": "computed",
375
+ "provenance": "inferred",
376
+ "expression": expression,
377
+ },
378
+ ))
379
+ handler_id = _route_handler_id(
380
+ file_symbols,
381
+ symbols,
382
+ rel,
383
+ line_no,
384
+ match.group("handler"),
385
+ owner_id,
386
+ imported_paths.get(rel, set()),
387
+ aliases_by_path.get(rel, {}),
388
+ abstract_state,
389
+ )
390
+ registration_owner = _registration_owner(
391
+ file_symbols, rel, owner_id, handler_id
392
+ )
393
+ add_edge(_edge(
394
+ registration_owner,
395
+ route_id,
396
+ "registers",
397
+ reason="computed route registration",
398
+ score=0.85,
399
+ synthesizer="abstract-state-route-resolver",
400
+ ))
401
+ if handler_id is not None:
402
+ add_edge(_edge(
403
+ route_id,
404
+ handler_id,
405
+ "routes_to",
406
+ reason="bounded computed route path",
407
+ score=0.75,
408
+ synthesizer="abstract-state-route-resolver",
409
+ ))
410
+
411
+ for match in iter_event_matches(line):
412
+ channel_id = f"event::{match.event}"
413
+ add_node(GraphNode(
414
+ id=channel_id,
415
+ kind=NodeKind.EVENT,
416
+ name=match.event,
417
+ extras={"channel": match.event},
418
+ ))
419
+ if match.operation in {"emit", "sendeventwithname", "sendevent"}:
420
+ add_edge(_edge(
421
+ owner_id,
422
+ channel_id,
423
+ "emits",
424
+ reason="literal event emission",
425
+ score=0.9,
426
+ synthesizer="event-channel",
427
+ ))
428
+ continue
429
+ listener_id = (
430
+ f"{rel}::event-listener:{match.event}:{line_no}:{match.start}"
431
+ )
432
+ add_node(GraphNode(
433
+ id=listener_id,
434
+ kind=NodeKind.EVENT,
435
+ path=rel,
436
+ name=match.event,
437
+ line=line_no,
438
+ end_line=line_no,
439
+ language=language,
440
+ extras={"channel": match.event, "registration": "listener"},
441
+ ))
442
+ add_edge(_edge(owner_id, listener_id, "registers", reason="event listener registration", score=0.95, synthesizer="event-channel"))
443
+ add_edge(_edge(listener_id, channel_id, "subscribes", reason="literal event channel", score=0.95, synthesizer="event-channel"))
444
+ targets = _expression_targets(
445
+ match.callback_expression,
446
+ symbols,
447
+ rel,
448
+ imported_paths.get(rel, set()),
449
+ aliases_by_path.get(rel, {}),
450
+ abstract_state,
451
+ )
452
+ if len(targets) == 1:
453
+ callback_targets = [targets[0]]
454
+ if match.operation == "schedule" and targets[0].kind == NodeKind.CLASS:
455
+ callback_targets.extend(
456
+ node
457
+ for node in nodes_by_path.get(targets[0].path, [])
458
+ if node.kind == NodeKind.METHOD
459
+ and str(node.extras.get("qualname") or "").startswith(
460
+ f"{targets[0].name}."
461
+ )
462
+ and node.name.startswith("on_")
463
+ )
464
+ for target in callback_targets:
465
+ add_edge(_edge(listener_id, target.id, "listens", reason="literal event listener callback", score=0.85, synthesizer="event-channel"))
466
+
467
+ for match in _REACT_STATE.finditer(line):
468
+ state_name = match.group("state")
469
+ setter = match.group("setter")
470
+ state_id = f"{rel}::state:{state_name}"
471
+ add_node(GraphNode(
472
+ id=state_id,
473
+ kind=NodeKind.STATE,
474
+ path=rel,
475
+ name=state_name,
476
+ line=line_no,
477
+ end_line=line_no,
478
+ language=language,
479
+ extras={"setter": setter, "framework": "react"},
480
+ ))
481
+ add_edge(_edge(owner_id, state_id, "owns_state", reason="React useState binding", score=0.95, synthesizer="react-state"))
482
+ for later_no, later in enumerate(lines[line_no:], start=line_no + 1):
483
+ if re.search(rf"\b{re.escape(setter)}\s*\(", later):
484
+ writer = _enclosing(file_symbols, rel, later_no)
485
+ add_edge(_edge((writer.id if writer else rel), state_id, "writes_state", reason=f"calls state setter {setter}", score=0.9, synthesizer="react-state"))
486
+
487
+ for match in iter_provider_matches(line):
488
+ target_name = (
489
+ match.target_expression.split(".")[-1].split(":")[-1]
490
+ )
491
+ targets = _expression_targets(
492
+ target_name,
493
+ symbols,
494
+ rel,
495
+ imported_paths.get(rel, set()),
496
+ aliases_by_path.get(rel, {}),
497
+ abstract_state,
498
+ )
499
+ if len(targets) != 1:
500
+ continue
501
+ provider_id = f"{rel}::provider:{target_name}:{line_no}"
502
+ add_node(GraphNode(
503
+ id=provider_id,
504
+ kind=NodeKind.PROVIDER,
505
+ path=rel,
506
+ name=target_name,
507
+ line=line_no,
508
+ end_line=line_no,
509
+ language=language,
510
+ extras={"framework": match.framework},
511
+ ))
512
+ add_edge(_edge(owner_id, provider_id, "registers", reason="DI provider registration", score=0.9, synthesizer=f"{match.framework}-resolver"))
513
+ add_edge(_edge(provider_id, targets[0].id, "provides", reason="DI registration", score=0.8, synthesizer=f"{match.framework}-resolver"))
514
+ add_edge(_edge(owner_id, provider_id, "injects", reason="DI consumer/registration", score=0.75, synthesizer=f"{match.framework}-resolver"))
515
+
516
+ for match in _CALLBACK.finditer(line):
517
+ callback = match.group("callback")
518
+ callback_names = set(abstract_state.get(callback).callables) or {callback}
519
+ for callback_name in sorted(callback_names):
520
+ for target in _resolved_symbol_targets(
521
+ symbols,
522
+ callback_name,
523
+ rel,
524
+ imported_paths=imported_paths.get(rel, set()),
525
+ aliases=aliases_by_path.get(rel, {}),
526
+ ):
527
+ add_edge(_edge(owner_id, target.id, "calls", reason="callback passed to continuation", score=0.75, synthesizer="callback-resolver"))
528
+
529
+ for match in _ABSTRACT_ALIAS_CALL.finditer(line):
530
+ callee = match.group("callee")
531
+ abstract_callee = abstract_state.get(callee)
532
+ target_names = set(abstract_callee.callables) | set(abstract_callee.types)
533
+ for target_name in sorted(target_names):
534
+ if target_name == callee:
535
+ continue
536
+ for target in _resolved_symbol_targets(
537
+ symbols,
538
+ target_name,
539
+ rel,
540
+ imported_paths=imported_paths.get(rel, set()),
541
+ aliases=aliases_by_path.get(rel, {}),
542
+ ):
543
+ add_edge(_edge(
544
+ owner_id,
545
+ target.id,
546
+ "calls",
547
+ reason="abstract callable/type dispatch",
548
+ score=0.8,
549
+ synthesizer="abstract-dispatch",
550
+ ))
551
+
552
+ if Path(rel).suffix.lower() in {".c", ".h", ".cc", ".cpp", ".cxx", ".hpp"}:
553
+ for match in _C_FUNCTION_POINTER.finditer(line):
554
+ target_name = match.group("target")
555
+ for target in (symbols.get(target_name) or [])[:8]:
556
+ add_edge(_edge(owner_id, target.id, "calls", reason="C/C++ function pointer assignment", score=0.7, synthesizer="c-function-pointer"))
557
+
558
+ for match in _DYNAMIC.finditer(line):
559
+ kind = match.group("kind")
560
+ dynamic_id = f"{rel}::dynamic:{kind}:{line_no}:{match.start()}"
561
+ arguments = _dynamic_arguments(line[match.end():])
562
+ argument_index = 1 if kind in {"getattr", "setattr"} else 0
563
+ abstract = (
564
+ abstract_state.evaluate(arguments[argument_index])
565
+ if len(arguments) > argument_index
566
+ else abstract_state.get(kind)
567
+ )
568
+ resolved_values = sorted(abstract.strings())
569
+ add_node(GraphNode(
570
+ id=dynamic_id,
571
+ kind=NodeKind.DYNAMIC,
572
+ path=rel,
573
+ name=kind,
574
+ line=line_no,
575
+ end_line=line_no,
576
+ language=language,
577
+ extras={
578
+ "sink": kind,
579
+ "resolved": bool(resolved_values),
580
+ "values": resolved_values,
581
+ },
582
+ ))
583
+ add_edge(_edge(
584
+ owner_id,
585
+ dynamic_id,
586
+ "dynamic_reference",
587
+ reason=(
588
+ f"resolved dynamic reference: {kind}"
589
+ if resolved_values
590
+ else f"unresolved dynamic sink: {kind}"
591
+ ),
592
+ score=0.7 if resolved_values else 0.4,
593
+ synthesizer="dynamic-reference",
594
+ ))
595
+ if kind in {"import", "require", "import_module"}:
596
+ for value in resolved_values:
597
+ target_path = _resolve_dynamic_file(rel, value, file_paths, root=root)
598
+ if target_path is not None:
599
+ add_edge(_edge(owner_id, target_path, "imports_dynamic", reason="abstract-state computed import", score=0.8, synthesizer="dynamic-import"))
600
+ else:
601
+ for value in resolved_values:
602
+ target_name = value.rsplit(".", 1)[-1].rsplit(":", 1)[-1]
603
+ for target in _resolved_symbol_targets(
604
+ symbols,
605
+ target_name,
606
+ rel,
607
+ imported_paths=imported_paths.get(rel, set()),
608
+ aliases=aliases_by_path.get(rel, {}),
609
+ ):
610
+ add_edge(_edge(owner_id, target.id, "reflects_to", reason=f"resolved reflection via {kind}", score=0.75, synthesizer="reflection-resolver"))
611
+
612
+ for match in _LITERAL_EVAL.finditer(line):
613
+ body = match.group("body")
614
+ dynamic_id = f"{rel}::dynamic:literal-eval:{line_no}:{match.start()}"
615
+ add_node(GraphNode(
616
+ id=dynamic_id,
617
+ kind=NodeKind.DYNAMIC,
618
+ path=rel,
619
+ name="literal-eval",
620
+ line=line_no,
621
+ end_line=line_no,
622
+ language=language,
623
+ extras={"sink": "eval", "resolved": True, "derived_source": body},
624
+ ))
625
+ for call in re.finditer(r"\b([A-Za-z_$][\w$]*)\s*\(", body):
626
+ for target in (symbols.get(call.group(1)) or [])[:8]:
627
+ add_edge(_edge(owner_id, target.id, "calls", reason="constant eval payload", score=0.65, synthesizer="literal-eval"))
628
+
629
+ for match in _BRIDGE_DECL.finditer(line):
630
+ name = match.group("name")
631
+ bridge_id = f"bridge::native-method:{name}"
632
+ add_node(GraphNode(
633
+ id=bridge_id,
634
+ kind=NodeKind.DYNAMIC,
635
+ name=name,
636
+ extras={"channel": "native-method", "provenance": "framework"},
637
+ ))
638
+ add_edge(_edge(bridge_id, owner_id, "bridges_to", reason="native bridge declaration", score=0.8, synthesizer="native-bridge"))
639
+
640
+ for match in _BRIDGE_CALL.finditer(line):
641
+ name = match.group("name")
642
+ bridge_id = f"bridge::native-method:{name}"
643
+ add_node(GraphNode(
644
+ id=bridge_id,
645
+ kind=NodeKind.DYNAMIC,
646
+ name=name,
647
+ extras={"channel": "native-method", "provenance": "framework"},
648
+ ))
649
+ add_edge(_edge(owner_id, bridge_id, "calls", reason="React Native/Expo bridge call", score=0.8, synthesizer="native-bridge"))
650
+
651
+ for match in _WORKER_URL.finditer(text):
652
+ spec = match.group("spec")
653
+ target_path = _resolve_dynamic_file(rel, spec, file_paths, root=root)
654
+ if target_path is None:
655
+ continue
656
+ owner = next(
657
+ (node for node in file_symbols if node.kind == NodeKind.FILE),
658
+ None,
659
+ )
660
+ owner_id = owner.id if owner is not None else rel
661
+ add_edge(
662
+ _edge(
663
+ owner_id,
664
+ target_path,
665
+ "imports_dynamic",
666
+ reason="Worker/import.meta.url module load",
667
+ score=0.9,
668
+ synthesizer="worker-url",
669
+ )
670
+ )
671
+
672
+ if progress is not None:
673
+ progress("semantic", file_index, total_files)
674
+
675
+ graph.nodes = nodes
676
+ graph.edges = edges
677
+ graph.meta["semantic_synthesized_nodes"] = len(nodes) - initial_node_count
678
+ graph.meta["semantic_synthesized_edges"] = len(edges) - initial_edge_count
679
+ graph.meta["semantic_resolver_version"] = 1
680
+ return graph
681
+
682
+
683
+ def _abstract_import_aliases(
684
+ aliases: Mapping[str, str],
685
+ symbols: Mapping[str, list[GraphNode]],
686
+ ) -> tuple[dict[str, str], dict[str, str]]:
687
+ callable_aliases: dict[str, str] = {}
688
+ type_aliases: dict[str, str] = {}
689
+ callable_kinds = {NodeKind.FUNCTION, NodeKind.METHOD}
690
+ type_kinds = {
691
+ NodeKind.CLASS,
692
+ NodeKind.INTERFACE,
693
+ NodeKind.TYPE,
694
+ NodeKind.STRUCT,
695
+ NodeKind.ENUM,
696
+ NodeKind.TRAIT,
697
+ }
698
+ for local, remote in aliases.items():
699
+ remote_name = remote.rsplit(".", 1)[-1].rsplit(":", 1)[-1]
700
+ candidates = symbols.get(remote_name) or []
701
+ kinds = {candidate.kind for candidate in candidates}
702
+ if kinds and kinds <= callable_kinds:
703
+ callable_aliases[local] = remote_name
704
+ elif kinds and kinds <= type_kinds:
705
+ type_aliases[local] = remote_name
706
+ return callable_aliases, type_aliases
707
+
708
+
709
+ def _expression_targets(
710
+ expression: str,
711
+ symbols: dict[str, list[GraphNode]],
712
+ path: str,
713
+ imported_paths: set[str],
714
+ aliases: Mapping[str, str],
715
+ abstract_state: AbstractState,
716
+ ) -> list[GraphNode]:
717
+ bare = expression.strip().split(".")[-1].split(":")[-1]
718
+ value = abstract_state.get(bare)
719
+ names = set(value.callables) | set(value.types)
720
+ if value.unknown:
721
+ names.add(bare)
722
+ targets = {
723
+ target.id: target
724
+ for name in names
725
+ for target in _resolved_symbol_targets(
726
+ symbols,
727
+ name,
728
+ path,
729
+ imported_paths=imported_paths,
730
+ aliases=aliases,
731
+ )
732
+ }
733
+ return list(targets.values()) if len(targets) == 1 else []
734
+
735
+
736
+ def _dynamic_arguments(suffix: str) -> list[str]:
737
+ """Return the current call's arguments without consuming later calls."""
738
+ depth = 0
739
+ quote = ""
740
+ current: list[str] = []
741
+ arguments: list[str] = []
742
+ for index, char in enumerate(suffix):
743
+ if quote:
744
+ current.append(char)
745
+ if char == quote and (index == 0 or suffix[index - 1] != "\\"):
746
+ quote = ""
747
+ continue
748
+ if char in {"'", '"', "`"}:
749
+ quote = char
750
+ current.append(char)
751
+ elif char in "([{":
752
+ depth += 1
753
+ current.append(char)
754
+ elif char in ")]}":
755
+ if char == ")" and depth == 0:
756
+ value = "".join(current).strip()
757
+ if value:
758
+ arguments.append(value)
759
+ break
760
+ depth = max(0, depth - 1)
761
+ current.append(char)
762
+ elif char == "," and depth == 0:
763
+ arguments.append("".join(current).strip())
764
+ current = []
765
+ else:
766
+ current.append(char)
767
+ return arguments
768
+
769
+
770
+ def _resolve_dynamic_file(
771
+ source_path: str,
772
+ value: str,
773
+ file_paths: Iterable[str],
774
+ *,
775
+ root: Path | None = None,
776
+ ) -> str | None:
777
+ available = set(file_paths)
778
+ raw = value.replace("\\", "/").split("?", 1)[0]
779
+ if raw.startswith("."):
780
+ base = (Path(source_path).parent / raw).as_posix()
781
+ else:
782
+ # Alias / absolute-style specs (``@/components/...``).
783
+ if root is not None and not raw.startswith("/"):
784
+ try:
785
+ from devcouncil.indexing.repo_mapper import RepoMapper
786
+
787
+ mapper = RepoMapper(root)
788
+ mapper._last_file_set = available
789
+ hit = mapper._resolve_js_spec(source_path, raw, available)
790
+ if hit:
791
+ return hit
792
+ except Exception:
793
+ pass
794
+ base = raw.lstrip("/")
795
+ candidates = [base]
796
+ candidates.extend(f"{base}{suffix}" for suffix in (
797
+ ".py", ".pyi", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
798
+ ".go", ".rs", ".java", ".cs", ".swift", ".kt", ".rb", ".php",
799
+ ))
800
+ candidates.extend(f"{base}/index{suffix}" for suffix in (
801
+ ".ts", ".tsx", ".js", ".jsx", ".py",
802
+ ))
803
+ return next((candidate for candidate in candidates if candidate in available), None)
804
+
805
+
806
+ def _route_handler_id(
807
+ nodes: list[GraphNode],
808
+ symbols: dict[str, list[GraphNode]],
809
+ path: str,
810
+ line: int,
811
+ suffix: str,
812
+ owner_id: str,
813
+ imported_paths: set[str],
814
+ aliases: Mapping[str, str],
815
+ abstract_state: AbstractState,
816
+ ) -> str | None:
817
+ identifiers = re.findall(r"[A-Za-z_$][\w$]*", suffix)
818
+ for identifier in reversed(identifiers):
819
+ value = abstract_state.get(identifier)
820
+ resolved_names = set(value.callables) | set(value.types)
821
+ if len(resolved_names) > 1:
822
+ return None
823
+ candidate_names = resolved_names or {identifier}
824
+ resolved: dict[str, GraphNode] = {}
825
+ had_candidates = False
826
+ for candidate_name in candidate_names:
827
+ had_candidates = had_candidates or bool(symbols.get(candidate_name))
828
+ for target in _resolved_symbol_targets(
829
+ symbols,
830
+ candidate_name,
831
+ path,
832
+ imported_paths=imported_paths,
833
+ aliases=aliases,
834
+ ):
835
+ resolved[target.id] = target
836
+ if len(resolved) == 1:
837
+ return next(iter(resolved))
838
+ if len(resolved) > 1 or had_candidates:
839
+ return None
840
+ if owner_id != path:
841
+ return owner_id
842
+ following = [
843
+ node
844
+ for node in nodes
845
+ if node.path == path
846
+ and node.kind in {NodeKind.FUNCTION, NodeKind.METHOD, NodeKind.CLASS}
847
+ and line <= node.line <= line + 8
848
+ ]
849
+ return min(following, key=lambda node: node.line).id if following else None
850
+
851
+
852
+ def _registration_owner(
853
+ nodes: list[GraphNode],
854
+ path: str,
855
+ owner_id: str,
856
+ target_id: str | None,
857
+ ) -> str:
858
+ """Avoid self-cycles for decorator/annotation-style registrations."""
859
+ if target_id is None or owner_id != target_id:
860
+ return owner_id
861
+ target = next((node for node in nodes if node.id == target_id), None)
862
+ qualname = str((target.extras if target else {}).get("qualname") or "")
863
+ if "." in qualname:
864
+ parent = qualname.rsplit(".", 1)[0]
865
+ parent_id = next(
866
+ (
867
+ node.id
868
+ for node in nodes
869
+ if node.path == path
870
+ and str(node.extras.get("qualname") or node.name) == parent
871
+ ),
872
+ None,
873
+ )
874
+ if parent_id is not None:
875
+ return parent_id
876
+ return path
877
+
878
+
879
+ def _file_route_path(path: str) -> str:
880
+ for prefix in _FILE_ROUTE_PREFIXES:
881
+ if path.startswith(prefix):
882
+ path = path[len(prefix):]
883
+ break
884
+ stem = str(Path(path).with_suffix("")).replace("\\", "/")
885
+ stem = re.sub(r"/(?:index|page)$", "", stem)
886
+ stem = stem.replace("[...", "*").replace("[", ":").replace("]", "")
887
+ return "/" + stem.strip("/")