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,1205 @@
1
+ """TS/JS/Go/Rust symbol + call extraction via tree-sitter with regex fallback.
2
+
3
+ devcouncil: allow-unwired — package-private; reached only via graph.cache/build.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+ from typing import List, Optional, Set
11
+
12
+ from devcouncil.indexing.graph.extract_python import (
13
+ ExtractedCall,
14
+ ExtractedImport,
15
+ ExtractedSymbol,
16
+ FileExtraction,
17
+ extract_rationales,
18
+ _enclosing_qualname,
19
+ )
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Regex fallbacks
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _JS_EXPORT_RE = re.compile(
26
+ r"(?m)^\s*export\s+(?:default\s+)?(?:async\s+)?"
27
+ r"(?:function|class|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)"
28
+ )
29
+ _JS_FUNCTION_RE = re.compile(
30
+ r"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\("
31
+ )
32
+ _JS_CLASS_RE = re.compile(
33
+ r"(?m)^\s*(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)"
34
+ )
35
+ _JS_CALL_RE = re.compile(
36
+ r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
37
+ r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
38
+ )
39
+ _JS_IMPORT_RE = re.compile(
40
+ r"""(?:import|export)\s[^'"]*?from\s*['"](?P<spec>[^'"]+)['"]"""
41
+ r"""|(?:require|import)\s*\(\s*['"](?P<spec2>[^'"]+)['"]\s*\)"""
42
+ )
43
+ _JS_WORKER_URL_RE = re.compile(
44
+ r"""(?:new\s+(?:Worker|SharedWorker)\s*\(\s*)?"""
45
+ r"""new\s+URL\s*\(\s*['"](?P<spec>[^'"]+)['"]\s*,\s*import\.meta\.url"""
46
+ )
47
+ _JS_BARE_IMPORT_RE = re.compile(r"""^\s*import\s*['"](?P<spec>[^'"]+)['"]""", re.M)
48
+ _JS_EXPORT_LIST_RE = re.compile(
49
+ r"^\s*export\s*\{([^}]+)\}"
50
+ r"|^\s*export\s+default\s+(?:async\s+)?(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)",
51
+ re.M,
52
+ )
53
+ _JS_RATIONALE_RE = re.compile(
54
+ r"^\s*(?://|/\*)\s*(NOTE|WHY)\s*:\s*(.+?)(?:\*/)?$"
55
+ r"|^\s*(?://|/\*).*\b(ADR[- ]?\d+)\b",
56
+ re.M | re.IGNORECASE,
57
+ )
58
+
59
+ _GO_FUNC_RE = re.compile(
60
+ r"(?m)^func\s+(?:\((?P<recv>[^)]+)\)\s+)?(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*\("
61
+ )
62
+ _GO_IMPORT_BLOCK_RE = re.compile(r"import\s*\((?P<body>[^)]*)\)", re.DOTALL)
63
+ _GO_IMPORT_SINGLE_RE = re.compile(
64
+ r"""^\s*import\s+(?:[A-Za-z_.]\w*\s+)?['"](?P<spec>[^'"]+)['"]""", re.M
65
+ )
66
+ _GO_IMPORT_SPEC_RE = re.compile(r"""['"](?P<spec>[^'"]+)['"]""")
67
+ _GO_CALL_RE = re.compile(
68
+ r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
69
+ r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
70
+ )
71
+ _GO_RECV_TYPE_RE = re.compile(r"\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*$")
72
+
73
+ _RUST_FN_RE = re.compile(
74
+ r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*[<(]"
75
+ )
76
+ _RUST_STRUCT_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)")
77
+ _RUST_ENUM_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_][A-Za-z0-9_]*)")
78
+ _RUST_TRAIT_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)")
79
+ _RUST_USE_RE = re.compile(r"(?m)^\s*(?:pub\s+)?use\s+([^;]+);")
80
+ _RUST_MOD_RE = re.compile(r"(?m)^\s*(?:pub\s+)?mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;")
81
+ _RUST_CALL_RE = re.compile(
82
+ r"\b([A-Za-z_][A-Za-z0-9_]*)\s*::\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
83
+ r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
84
+ r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
85
+ )
86
+ _RUST_RATIONALE_RE = re.compile(
87
+ r"^\s*//[!/]?\s*(NOTE|WHY)\s*:\s*(.+)$"
88
+ r"|^\s*//[!/]?.*\b(ADR[- ]?\d+)\b",
89
+ re.M | re.IGNORECASE,
90
+ )
91
+
92
+ _JS_SUFFIXES = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
93
+ _JS_CALL_SKIP = {
94
+ "if", "for", "while", "switch", "catch", "function", "return", "await", "new",
95
+ "typeof", "instanceof", "void", "delete", "yield", "import", "super",
96
+ }
97
+ # JSX/TSX component tags used as references (keeps components live without call edges).
98
+ # Member form (``<Foo.Bar``) must win over bare PascalCase so ``Nested`` is not
99
+ # captured alone when ``Nested.Thing`` is written.
100
+ _JSX_TAG_RE = re.compile(
101
+ r"<(?:"
102
+ r"([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)"
103
+ r"|([A-Z][A-Za-z0-9_]*)"
104
+ r")\b"
105
+ )
106
+ _GO_CALL_SKIP = {
107
+ "if", "for", "switch", "return", "go", "defer", "func", "make", "new",
108
+ "len", "cap", "append", "copy", "delete", "panic", "recover", "range",
109
+ }
110
+ _RUST_CALL_SKIP = {
111
+ "if", "for", "while", "loop", "match", "return", "unsafe", "async", "await",
112
+ "box", "move", "ref", "mut", "self", "Self", "super", "crate",
113
+ }
114
+
115
+
116
+ def _js_import_specs(source: str) -> List[str]:
117
+ specs: List[str] = []
118
+ for m in _JS_IMPORT_RE.finditer(source):
119
+ spec = m.group("spec") or m.group("spec2")
120
+ if spec:
121
+ specs.append(spec)
122
+ for m in _JS_WORKER_URL_RE.finditer(source):
123
+ spec = m.group("spec")
124
+ if spec:
125
+ specs.append(spec)
126
+ for m in _JS_BARE_IMPORT_RE.finditer(source):
127
+ specs.append(m.group("spec"))
128
+ return specs
129
+
130
+
131
+ def _go_import_specs_regex(source: str) -> List[str]:
132
+ specs: List[str] = []
133
+ for m in _GO_IMPORT_BLOCK_RE.finditer(source):
134
+ specs.extend(_GO_IMPORT_SPEC_RE.findall(m.group("body")))
135
+ for m in _GO_IMPORT_SINGLE_RE.finditer(source):
136
+ specs.append(m.group("spec"))
137
+ return specs
138
+
139
+
140
+ def _line_span(node) -> tuple[int, int]:
141
+ return int(node.start_point[0]) + 1, int(node.end_point[0]) + 1
142
+
143
+
144
+ def _node_text(node, source: bytes) -> str:
145
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
146
+
147
+
148
+ def _first_child(node, types: Set[str]):
149
+ for c in node.children:
150
+ if c.type in types:
151
+ return c
152
+ return None
153
+
154
+
155
+ def _first_named_text(node, source: bytes, types: Set[str]) -> str:
156
+ c = _first_child(node, types)
157
+ return _node_text(c, source) if c is not None else ""
158
+
159
+
160
+ def _walk(node):
161
+ yield node
162
+ for child in node.children:
163
+ yield from _walk(child)
164
+
165
+
166
+ def _parse_lang(language: str, source: str):
167
+ try:
168
+ from tree_sitter_language_pack import get_parser
169
+ except Exception:
170
+ return None, b""
171
+ try:
172
+ parser = get_parser(language)
173
+ raw = source.encode("utf-8")
174
+ return parser.parse(raw), raw
175
+ except Exception:
176
+ return None, b""
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # JS / TS regex fallback
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def _js_export_names_regex(source: str) -> Set[str]:
185
+ names: Set[str] = set()
186
+ for m in _JS_EXPORT_RE.finditer(source):
187
+ names.add(m.group(1))
188
+ for m in _JS_EXPORT_LIST_RE.finditer(source):
189
+ if m.group(1):
190
+ for part in m.group(1).split(","):
191
+ part = part.strip()
192
+ if not part:
193
+ continue
194
+ bits = re.split(r"\s+as\s+", part, flags=re.I)
195
+ local = bits[0].strip()
196
+ if local and local != "default" and re.match(r"^[A-Za-z_]", local):
197
+ names.add(local)
198
+ if m.group(2):
199
+ names.add(m.group(2))
200
+ return names
201
+
202
+
203
+ def _extract_js_rationales(source: str, symbols: List[ExtractedSymbol]) -> List[ExtractedSymbol]:
204
+ out: List[ExtractedSymbol] = []
205
+ for m in _JS_RATIONALE_RE.finditer(source):
206
+ line = source[: m.start()].count("\n") + 1
207
+ if m.group(1):
208
+ tag = m.group(1).upper()
209
+ text = (m.group(2) or "").strip().rstrip("*/").strip()
210
+ name = f"{tag}: {text}" if text else tag
211
+ else:
212
+ adr = (m.group(3) or "").strip()
213
+ name = f"ADR: {adr}"
214
+ enclosing = _enclosing_qualname(symbols, line)
215
+ out.append(
216
+ ExtractedSymbol(
217
+ kind="rationale",
218
+ name=name[:120],
219
+ qualname=f"$rationale:{line}",
220
+ line=line,
221
+ end_line=line,
222
+ bases=[enclosing] if enclosing else [],
223
+ )
224
+ )
225
+ return out
226
+
227
+
228
+ def _jsx_component_refs(source: str) -> List[str]:
229
+ """Bare component names from JSX/TSX tags (``<Foo />``, ``<Foo.Bar />``)."""
230
+ names: List[str] = []
231
+ seen: Set[str] = set()
232
+ for m in _JSX_TAG_RE.finditer(source):
233
+ # Member form: groups 1/2; bare PascalCase: group 3.
234
+ candidates = [m.group(2), m.group(1), m.group(3)]
235
+ for name in candidates:
236
+ if not name or name in seen:
237
+ continue
238
+ # Prefer component-like identifiers (PascalCase or trailing member).
239
+ if name[0].isupper() or name is m.group(2):
240
+ seen.add(name)
241
+ names.append(name)
242
+ return names
243
+
244
+
245
+ def _extract_js_regex(path: str, source: str) -> FileExtraction:
246
+ lang = "typescript" if path.endswith((".ts", ".tsx")) else "javascript"
247
+ out = FileExtraction(path=path, language=lang, imports=_js_import_specs(source))
248
+ exported_names = _js_export_names_regex(source)
249
+ seen: set[str] = set()
250
+ for m in _JS_EXPORT_RE.finditer(source):
251
+ name = m.group(1)
252
+ if name in seen:
253
+ continue
254
+ seen.add(name)
255
+ line = source[: m.start()].count("\n") + 1
256
+ kind = "class" if "class" in m.group(0) else "function"
257
+ out.symbols.append(
258
+ ExtractedSymbol(
259
+ kind=kind,
260
+ name=name,
261
+ qualname=name,
262
+ line=line,
263
+ end_line=line,
264
+ exported=True,
265
+ )
266
+ )
267
+ for m in _JS_FUNCTION_RE.finditer(source):
268
+ name = m.group(1)
269
+ if name in seen:
270
+ continue
271
+ seen.add(name)
272
+ line = source[: m.start()].count("\n") + 1
273
+ out.symbols.append(
274
+ ExtractedSymbol(
275
+ kind="function",
276
+ name=name,
277
+ qualname=name,
278
+ line=line,
279
+ end_line=line,
280
+ exported=name in exported_names,
281
+ )
282
+ )
283
+ for m in _JS_CLASS_RE.finditer(source):
284
+ name = m.group(1)
285
+ if name in seen:
286
+ continue
287
+ seen.add(name)
288
+ line = source[: m.start()].count("\n") + 1
289
+ out.symbols.append(
290
+ ExtractedSymbol(
291
+ kind="class",
292
+ name=name,
293
+ qualname=name,
294
+ line=line,
295
+ end_line=line,
296
+ exported=name in exported_names,
297
+ )
298
+ )
299
+ for m in _JS_CALL_RE.finditer(source):
300
+ if m.group(1) and m.group(2):
301
+ out.calls.append(
302
+ ExtractedCall(
303
+ name=m.group(2),
304
+ line=source[: m.start()].count("\n") + 1,
305
+ receiver=m.group(1),
306
+ qualname_hint=f"{m.group(1)}.{m.group(2)}",
307
+ )
308
+ )
309
+ elif m.group(3):
310
+ name = m.group(3)
311
+ if name in _JS_CALL_SKIP:
312
+ continue
313
+ out.calls.append(
314
+ ExtractedCall(
315
+ name=name,
316
+ line=source[: m.start()].count("\n") + 1,
317
+ qualname_hint=name,
318
+ )
319
+ )
320
+ # Mark export-list names
321
+ for sym in out.symbols:
322
+ if sym.name in exported_names:
323
+ sym.exported = True
324
+ out.all_exports = sorted(exported_names)
325
+ out.references = _jsx_component_refs(source)
326
+ out.symbols.extend(_extract_js_rationales(source, out.symbols))
327
+ return out
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # JS / TS tree-sitter
332
+ # ---------------------------------------------------------------------------
333
+
334
+
335
+ def _js_heritage(node, source: bytes) -> tuple[List[str], List[str]]:
336
+ bases: List[str] = []
337
+ implements: List[str] = []
338
+ heritage = _first_child(node, {"class_heritage"})
339
+ if heritage is None:
340
+ return bases, implements
341
+ for clause in heritage.children:
342
+ if clause.type == "extends_clause":
343
+ for c in clause.children:
344
+ if c.type in {"identifier", "type_identifier", "member_expression"}:
345
+ bases.append(_node_text(c, source))
346
+ elif clause.type == "implements_clause":
347
+ for c in clause.children:
348
+ if c.type in {"identifier", "type_identifier", "generic_type"}:
349
+ text = _node_text(c, source)
350
+ # strip type args: Foo<T> → Foo
351
+ implements.append(text.split("<", 1)[0].strip())
352
+ return bases, implements
353
+
354
+
355
+ def _collect_js_exports(root, source: bytes) -> tuple[Set[str], List[str], List[ExtractedImport]]:
356
+ """Return (exported local names, import specs, import details / reexports)."""
357
+ exported: Set[str] = set()
358
+ specs: List[str] = []
359
+ details: List[ExtractedImport] = []
360
+ reexports: List[str] = []
361
+
362
+ for node in root.children:
363
+ t = node.type
364
+ if t == "import_statement":
365
+ detail = ExtractedImport(module="")
366
+ for c in _walk(node):
367
+ if c.type == "string":
368
+ spec = _node_text(c, source).strip("'\"")
369
+ if spec:
370
+ specs.append(spec)
371
+ detail.module = spec
372
+ elif c.type == "import_specifier":
373
+ ids = [x for x in c.children if x.type == "identifier"]
374
+ if ids:
375
+ local = _node_text(ids[-1], source)
376
+ remote = _node_text(ids[0], source)
377
+ detail.names.append(remote)
378
+ detail.alias_map[local] = remote
379
+ elif c.type == "identifier" and c.parent is not None and c.parent.type == "import_clause":
380
+ name = _node_text(c, source)
381
+ detail.names.append(name)
382
+ detail.alias_map[name] = "default"
383
+ if detail.module or detail.names:
384
+ details.append(detail)
385
+ elif t == "export_statement":
386
+ # export { X } / export { X } from '...' / export default ...
387
+ has_from = any(c.type == "string" for c in node.children)
388
+ from_spec = ""
389
+ stmt_reexports: List[str] = [] # names re-exported by THIS statement
390
+ for c in node.children:
391
+ if c.type == "string":
392
+ from_spec = _node_text(c, source).strip("'\"")
393
+ if from_spec:
394
+ specs.append(from_spec)
395
+ for c in node.children:
396
+ if c.type == "export_clause":
397
+ for spec in c.children:
398
+ if spec.type != "export_specifier":
399
+ continue
400
+ ids = [x for x in spec.children if x.type == "identifier"]
401
+ if not ids:
402
+ continue
403
+ local = _node_text(ids[0], source)
404
+ exported.add(local)
405
+ if has_from:
406
+ reexports.append(local)
407
+ stmt_reexports.append(local)
408
+ elif c.type in {
409
+ "function_declaration",
410
+ "class_declaration",
411
+ "interface_declaration",
412
+ "type_alias_declaration",
413
+ "lexical_declaration",
414
+ "variable_declaration",
415
+ }:
416
+ name = _first_named_text(
417
+ c, source, {"identifier", "type_identifier", "property_identifier"}
418
+ )
419
+ if c.type == "lexical_declaration":
420
+ for vd in c.children:
421
+ if vd.type == "variable_declarator":
422
+ name = _first_named_text(vd, source, {"identifier"}) or name
423
+ if name:
424
+ exported.add(name)
425
+ elif c.type == "identifier" and any(x.type == "default" for x in node.children):
426
+ exported.add(_node_text(c, source))
427
+ if has_from and from_spec:
428
+ # Only names from THIS statement (the old accumulated-list slice
429
+ # cross-contaminated re-export names across statements).
430
+ details.append(
431
+ ExtractedImport(module=from_spec, names=stmt_reexports)
432
+ )
433
+
434
+ # Fix reexport details: rebuild from export-from statements cleanly
435
+ details = [d for d in details if d.module or d.names]
436
+ return exported, specs, details
437
+
438
+
439
+ def _extract_js_calls(root, source: bytes) -> List[ExtractedCall]:
440
+ calls: List[ExtractedCall] = []
441
+ for node in _walk(root):
442
+ if node.type != "call_expression":
443
+ continue
444
+ func = node.child_by_field_name("function") if hasattr(node, "child_by_field_name") else None
445
+ if func is None and node.children:
446
+ func = node.children[0]
447
+ if func is None:
448
+ continue
449
+ line = int(node.start_point[0]) + 1
450
+ if func.type == "identifier":
451
+ name = _node_text(func, source)
452
+ if name in _JS_CALL_SKIP:
453
+ continue
454
+ calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
455
+ elif func.type == "member_expression":
456
+ prop = _first_child(func, {"property_identifier"}) or _first_child(
457
+ func, {"identifier"}
458
+ )
459
+ if prop is None:
460
+ continue
461
+ name = _node_text(prop, source)
462
+ obj = func.children[0] if func.children else None
463
+ receiver = ""
464
+ if obj is not None and obj.type in {"identifier", "this", "super"}:
465
+ receiver = _node_text(obj, source)
466
+ elif obj is not None:
467
+ receiver = _node_text(obj, source).split(".")[-1]
468
+ hint = f"{receiver}.{name}" if receiver else name
469
+ calls.append(
470
+ ExtractedCall(name=name, line=line, receiver=receiver, qualname_hint=hint)
471
+ )
472
+ return calls
473
+
474
+
475
+ def _extract_js_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
476
+ from devcouncil.indexing.ts_imports import language_for_suffix
477
+
478
+ suffix = Path(path).suffix.lower()
479
+ lang_key = language_for_suffix(suffix)
480
+ if not lang_key:
481
+ return None
482
+ tree, raw = _parse_lang(lang_key, source)
483
+ if tree is None and lang_key == "typescript":
484
+ tree, raw = _parse_lang("tsx", source)
485
+ if tree is None:
486
+ return None
487
+
488
+ lang = "typescript" if suffix in {".ts", ".tsx"} else "javascript"
489
+ out = FileExtraction(path=path, language=lang)
490
+ root = tree.root_node
491
+ exported, specs, details = _collect_js_exports(root, raw)
492
+ # Also pick up require/import() via regex
493
+ for spec in _js_import_specs(source):
494
+ if spec not in specs:
495
+ specs.append(spec)
496
+ out.imports = specs
497
+ out.import_details = details
498
+ out.all_exports = sorted(exported)
499
+ out.reexports = []
500
+
501
+ # Re-scan export-from for reexports list
502
+ for node in root.children:
503
+ if node.type != "export_statement":
504
+ continue
505
+ if not any(c.type == "string" for c in node.children):
506
+ continue
507
+ for c in node.children:
508
+ if c.type != "export_clause":
509
+ continue
510
+ for spec in c.children:
511
+ if spec.type != "export_specifier":
512
+ continue
513
+ ids = [x for x in spec.children if x.type == "identifier"]
514
+ if ids:
515
+ out.reexports.append(_node_text(ids[0], raw))
516
+
517
+ seen_qual: Set[str] = set()
518
+
519
+ def add_sym(
520
+ kind: str,
521
+ name: str,
522
+ qualname: str,
523
+ node,
524
+ *,
525
+ bases: Optional[List[str]] = None,
526
+ implements: Optional[List[str]] = None,
527
+ force_exported: Optional[bool] = None,
528
+ ) -> None:
529
+ if not name or qualname in seen_qual:
530
+ return
531
+ seen_qual.add(qualname)
532
+ start, end = _line_span(node)
533
+ if force_exported is not None:
534
+ exp = force_exported
535
+ else:
536
+ exp = name in exported
537
+ out.symbols.append(
538
+ ExtractedSymbol(
539
+ kind=kind,
540
+ name=name,
541
+ qualname=qualname,
542
+ line=start,
543
+ end_line=end,
544
+ bases=bases or [],
545
+ implements=implements or [],
546
+ exported=exp,
547
+ )
548
+ )
549
+
550
+ def walk_decl(node, under_export: bool = False) -> None:
551
+ t = node.type
552
+ if t == "export_statement":
553
+ for c in node.children:
554
+ walk_decl(c, under_export=True)
555
+ return
556
+ if t == "function_declaration":
557
+ name = _first_named_text(node, raw, {"identifier"})
558
+ add_sym("function", name, name, node, force_exported=True if under_export else None)
559
+ return
560
+ if t == "class_declaration":
561
+ name = _first_named_text(node, raw, {"type_identifier", "identifier"})
562
+ bases, implements = _js_heritage(node, raw)
563
+ add_sym(
564
+ "class",
565
+ name,
566
+ name,
567
+ node,
568
+ bases=bases,
569
+ implements=implements,
570
+ force_exported=True if under_export else None,
571
+ )
572
+ body = _first_child(node, {"class_body"})
573
+ if body is not None and name:
574
+ for child in body.children:
575
+ if child.type != "method_definition":
576
+ continue
577
+ # skip constructors as methods with name constructor still recorded
578
+ mname = _first_named_text(
579
+ child, raw, {"property_identifier", "identifier"}
580
+ )
581
+ if not mname:
582
+ continue
583
+ add_sym("method", mname, f"{name}.{mname}", child, force_exported=False)
584
+ return
585
+ if t == "interface_declaration":
586
+ name = _first_named_text(node, raw, {"type_identifier", "identifier"})
587
+ bases, _impl = _js_heritage(node, raw)
588
+ # interfaces use extends_type_clause sometimes
589
+ for c in node.children:
590
+ if c.type in {"extends_type_clause", "extends_clause"}:
591
+ for gc in c.children:
592
+ if gc.type in {"identifier", "type_identifier"}:
593
+ bases.append(_node_text(gc, raw))
594
+ add_sym(
595
+ "interface",
596
+ name,
597
+ name,
598
+ node,
599
+ bases=bases,
600
+ force_exported=True if under_export else None,
601
+ )
602
+ return
603
+ if t == "type_alias_declaration":
604
+ name = _first_named_text(node, raw, {"type_identifier", "identifier"})
605
+ add_sym(
606
+ "type",
607
+ name,
608
+ name,
609
+ node,
610
+ force_exported=True if under_export else None,
611
+ )
612
+ return
613
+ if t in {"lexical_declaration", "variable_declaration"}:
614
+ for c in node.children:
615
+ if c.type != "variable_declarator":
616
+ continue
617
+ name = _first_named_text(c, raw, {"identifier"})
618
+ if not name:
619
+ continue
620
+ # Only count if value looks like a function/arrow, or is exported
621
+ val = None
622
+ for gc in c.children:
623
+ if gc.type not in {"identifier", "=", "type_annotation"}:
624
+ val = gc
625
+ break
626
+ is_fn = val is not None and val.type in {
627
+ "arrow_function",
628
+ "function",
629
+ "function_expression",
630
+ }
631
+ if is_fn or under_export or name in exported:
632
+ add_sym(
633
+ "function",
634
+ name,
635
+ name,
636
+ c,
637
+ force_exported=True if under_export or name in exported else False,
638
+ )
639
+ return
640
+
641
+ for child in root.children:
642
+ walk_decl(child)
643
+
644
+ # Mark any remaining exported names that were only listed via export { X }
645
+ by_name = {s.name: s for s in out.symbols if s.kind != "rationale"}
646
+ for name in exported:
647
+ if name in by_name:
648
+ by_name[name].exported = True
649
+
650
+ out.calls = _extract_js_calls(root, raw)
651
+ # JSX component tags as non-call references (regex covers TSX even when
652
+ # tree-sitter language was plain typescript).
653
+ refs = set(_jsx_component_refs(source))
654
+ for node in _walk(root):
655
+ if node.type not in {"jsx_opening_element", "jsx_self_closing_element"}:
656
+ continue
657
+ name_node = node.child_by_field_name("name") if hasattr(node, "child_by_field_name") else None
658
+ if name_node is None:
659
+ for c in node.children:
660
+ if c.type in {
661
+ "identifier",
662
+ "jsx_identifier",
663
+ "nested_identifier",
664
+ "member_expression",
665
+ }:
666
+ name_node = c
667
+ break
668
+ if name_node is None:
669
+ continue
670
+ text = _node_text(name_node, raw)
671
+ if "." in text:
672
+ text = text.rsplit(".", 1)[-1]
673
+ if text and text[0].isupper():
674
+ refs.add(text)
675
+ out.references = sorted(refs)
676
+ out.symbols.extend(_extract_js_rationales(source, out.symbols))
677
+ return out
678
+
679
+
680
+ # ---------------------------------------------------------------------------
681
+ # Go
682
+ # ---------------------------------------------------------------------------
683
+
684
+
685
+ def _go_receiver_type(recv_list, source: bytes) -> str:
686
+ """Extract type name from method receiver parameter_list."""
687
+ if recv_list is None:
688
+ return ""
689
+ for c in recv_list.children:
690
+ if c.type != "parameter_declaration":
691
+ continue
692
+ for gc in c.children:
693
+ if gc.type == "type_identifier":
694
+ return _node_text(gc, source)
695
+ if gc.type == "pointer_type":
696
+ tid = _first_child(gc, {"type_identifier"})
697
+ if tid is not None:
698
+ return _node_text(tid, source)
699
+ return _node_text(gc, source).lstrip("*").strip()
700
+ return ""
701
+
702
+
703
+ def _extract_go_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
704
+ tree, raw = _parse_lang("go", source)
705
+ if tree is None:
706
+ return None
707
+ out = FileExtraction(path=path, language="go")
708
+ try:
709
+ from devcouncil.indexing.ts_imports import extract_go_import_specs
710
+
711
+ specs = extract_go_import_specs(source)
712
+ if specs is not None:
713
+ out.imports = specs
714
+ else:
715
+ out.imports = _go_import_specs_regex(source)
716
+ except Exception:
717
+ out.imports = _go_import_specs_regex(source)
718
+
719
+ for node in _walk(tree.root_node):
720
+ if node.type == "function_declaration":
721
+ name = _first_named_text(node, raw, {"identifier"})
722
+ if not name:
723
+ continue
724
+ start, end = _line_span(node)
725
+ out.symbols.append(
726
+ ExtractedSymbol(
727
+ kind="function",
728
+ name=name,
729
+ qualname=name,
730
+ line=start,
731
+ end_line=end,
732
+ exported=name[:1].isupper(),
733
+ )
734
+ )
735
+ elif node.type == "method_declaration":
736
+ name = _first_named_text(node, raw, {"field_identifier"})
737
+ if not name:
738
+ continue
739
+ # First parameter_list is the receiver
740
+ recv_type = ""
741
+ for c in node.children:
742
+ if c.type == "parameter_list":
743
+ recv_type = _go_receiver_type(c, raw)
744
+ break
745
+ qual = f"{recv_type}.{name}" if recv_type else name
746
+ start, end = _line_span(node)
747
+ out.symbols.append(
748
+ ExtractedSymbol(
749
+ kind="method",
750
+ name=name,
751
+ qualname=qual,
752
+ line=start,
753
+ end_line=end,
754
+ exported=name[:1].isupper(),
755
+ )
756
+ )
757
+ elif node.type == "type_spec":
758
+ name = _first_named_text(node, raw, {"type_identifier", "identifier"})
759
+ if not name:
760
+ continue
761
+ kind = "type"
762
+ for c in node.children:
763
+ if c.type == "struct_type":
764
+ kind = "struct"
765
+ elif c.type == "interface_type":
766
+ kind = "interface"
767
+ start, end = _line_span(node)
768
+ out.symbols.append(
769
+ ExtractedSymbol(
770
+ kind=kind,
771
+ name=name,
772
+ qualname=name,
773
+ line=start,
774
+ end_line=end,
775
+ exported=name[:1].isupper(),
776
+ )
777
+ )
778
+ elif node.type == "call_expression":
779
+ func = node.children[0] if node.children else None
780
+ if func is None:
781
+ continue
782
+ line = int(node.start_point[0]) + 1
783
+ if func.type == "identifier":
784
+ name = _node_text(func, raw)
785
+ if name in _GO_CALL_SKIP:
786
+ continue
787
+ out.calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
788
+ elif func.type == "selector_expression":
789
+ field = _first_child(func, {"field_identifier"})
790
+ if field is None:
791
+ continue
792
+ name = _node_text(field, raw)
793
+ recv_node = func.children[0] if func.children else None
794
+ receiver = _node_text(recv_node, raw) if recv_node else ""
795
+ out.calls.append(
796
+ ExtractedCall(
797
+ name=name,
798
+ line=line,
799
+ receiver=receiver,
800
+ qualname_hint=f"{receiver}.{name}" if receiver else name,
801
+ )
802
+ )
803
+ out.symbols.extend(extract_rationales(source, out.symbols)) # # style rare in Go
804
+ return out
805
+
806
+
807
+ def _extract_go_regex(path: str, source: str) -> FileExtraction:
808
+ out = FileExtraction(path=path, language="go", imports=_go_import_specs_regex(source))
809
+ try:
810
+ from devcouncil.indexing.ts_imports import extract_go_import_specs
811
+
812
+ ts_specs = extract_go_import_specs(source)
813
+ if ts_specs is not None:
814
+ out.imports = ts_specs
815
+ except Exception:
816
+ pass
817
+ for m in _GO_FUNC_RE.finditer(source):
818
+ name = m.group("name")
819
+ recv = (m.group("recv") or "").strip()
820
+ line = source[: m.start()].count("\n") + 1
821
+ if recv:
822
+ rt = _GO_RECV_TYPE_RE.search(recv)
823
+ rtype = rt.group(1) if rt else ""
824
+ qual = f"{rtype}.{name}" if rtype else name
825
+ out.symbols.append(
826
+ ExtractedSymbol(
827
+ kind="method",
828
+ name=name,
829
+ qualname=qual,
830
+ line=line,
831
+ end_line=line,
832
+ exported=name[:1].isupper() if name else False,
833
+ )
834
+ )
835
+ else:
836
+ out.symbols.append(
837
+ ExtractedSymbol(
838
+ kind="function",
839
+ name=name,
840
+ qualname=name,
841
+ line=line,
842
+ end_line=line,
843
+ exported=name[:1].isupper() if name else False,
844
+ )
845
+ )
846
+ for m in _GO_CALL_RE.finditer(source):
847
+ if m.group(1) and m.group(2):
848
+ out.calls.append(
849
+ ExtractedCall(
850
+ name=m.group(2),
851
+ line=source[: m.start()].count("\n") + 1,
852
+ receiver=m.group(1),
853
+ qualname_hint=f"{m.group(1)}.{m.group(2)}",
854
+ )
855
+ )
856
+ elif m.group(3):
857
+ name = m.group(3)
858
+ if name in _GO_CALL_SKIP:
859
+ continue
860
+ out.calls.append(
861
+ ExtractedCall(
862
+ name=name,
863
+ line=source[: m.start()].count("\n") + 1,
864
+ qualname_hint=name,
865
+ )
866
+ )
867
+ return out
868
+
869
+
870
+ # ---------------------------------------------------------------------------
871
+ # Rust
872
+ # ---------------------------------------------------------------------------
873
+
874
+
875
+ def _extract_rust_rationales(source: str, symbols: List[ExtractedSymbol]) -> List[ExtractedSymbol]:
876
+ out: List[ExtractedSymbol] = []
877
+ for m in _RUST_RATIONALE_RE.finditer(source):
878
+ line = source[: m.start()].count("\n") + 1
879
+ if m.group(1):
880
+ tag = m.group(1).upper()
881
+ text = (m.group(2) or "").strip()
882
+ name = f"{tag}: {text}" if text else tag
883
+ else:
884
+ name = f"ADR: {(m.group(3) or '').strip()}"
885
+ enclosing = _enclosing_qualname(symbols, line)
886
+ out.append(
887
+ ExtractedSymbol(
888
+ kind="rationale",
889
+ name=name[:120],
890
+ qualname=f"$rationale:{line}",
891
+ line=line,
892
+ end_line=line,
893
+ bases=[enclosing] if enclosing else [],
894
+ )
895
+ )
896
+ return out
897
+
898
+
899
+ def _rust_is_pub(node) -> bool:
900
+ return any(c.type == "visibility_modifier" for c in node.children)
901
+
902
+
903
+ def _extract_rust_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
904
+ tree, raw = _parse_lang("rust", source)
905
+ if tree is None:
906
+ return None
907
+ out = FileExtraction(path=path, language="rust")
908
+ try:
909
+ from devcouncil.indexing.ts_imports import extract_rust_import_refs
910
+
911
+ refs = extract_rust_import_refs(source)
912
+ if refs is not None:
913
+ for ref in refs:
914
+ if ref.get("kind") == "mod":
915
+ name = ref.get("name") or ""
916
+ if name:
917
+ out.imports.append(f"mod:{name}")
918
+ elif ref.get("kind") == "use":
919
+ segs = ref.get("segments") or []
920
+ if segs:
921
+ out.imports.append("::".join(segs))
922
+ out.import_details.append(
923
+ ExtractedImport(
924
+ module="::".join(segs[:-1]) if len(segs) > 1 else "::".join(segs),
925
+ names=[segs[-1]] if segs else [],
926
+ alias_map={segs[-1]: "::".join(segs)} if segs else {},
927
+ )
928
+ )
929
+ except Exception:
930
+ pass
931
+
932
+ for node in tree.root_node.children:
933
+ t = node.type
934
+ if t == "function_item":
935
+ name = _first_named_text(node, raw, {"identifier"})
936
+ if not name:
937
+ continue
938
+ start, end = _line_span(node)
939
+ out.symbols.append(
940
+ ExtractedSymbol(
941
+ kind="function",
942
+ name=name,
943
+ qualname=name,
944
+ line=start,
945
+ end_line=end,
946
+ exported=_rust_is_pub(node),
947
+ )
948
+ )
949
+ elif t in {"struct_item", "enum_item", "trait_item", "type_item"}:
950
+ kind = {
951
+ "struct_item": "struct",
952
+ "enum_item": "enum",
953
+ "trait_item": "trait",
954
+ "type_item": "type",
955
+ }[t]
956
+ name = _first_named_text(node, raw, {"type_identifier", "identifier"})
957
+ if not name:
958
+ continue
959
+ start, end = _line_span(node)
960
+ out.symbols.append(
961
+ ExtractedSymbol(
962
+ kind=kind,
963
+ name=name,
964
+ qualname=name,
965
+ line=start,
966
+ end_line=end,
967
+ exported=_rust_is_pub(node),
968
+ )
969
+ )
970
+ if t == "trait_item":
971
+ body = _first_child(node, {"declaration_list"})
972
+ if body is not None:
973
+ for child in body.children:
974
+ if child.type not in {"function_item", "function_signature_item"}:
975
+ continue
976
+ mname = _first_named_text(child, raw, {"identifier"})
977
+ if not mname:
978
+ continue
979
+ ms, me = _line_span(child)
980
+ out.symbols.append(
981
+ ExtractedSymbol(
982
+ kind="method",
983
+ name=mname,
984
+ qualname=f"{name}.{mname}",
985
+ line=ms,
986
+ end_line=me,
987
+ exported=_rust_is_pub(node),
988
+ )
989
+ )
990
+ elif t == "impl_item":
991
+ # impl Type { ... } or impl Trait for Type { ... }
992
+ type_ids = [c for c in node.children if c.type == "type_identifier"]
993
+ trait_name = ""
994
+ type_name = ""
995
+ if any(c.type == "for" for c in node.children):
996
+ if len(type_ids) >= 2:
997
+ trait_name = _node_text(type_ids[0], raw)
998
+ type_name = _node_text(type_ids[1], raw)
999
+ elif len(type_ids) == 1:
1000
+ type_name = _node_text(type_ids[0], raw)
1001
+ elif type_ids:
1002
+ type_name = _node_text(type_ids[0], raw)
1003
+ if type_name and trait_name:
1004
+ # Record implements on a synthetic note via a type-level symbol update later
1005
+ # Ensure type exists; attach implements list by appending a marker symbol update
1006
+ existing = next((s for s in out.symbols if s.qualname == type_name), None)
1007
+ if existing is not None:
1008
+ if trait_name not in existing.implements:
1009
+ existing.implements.append(trait_name)
1010
+ else:
1011
+ start, end = _line_span(node)
1012
+ out.symbols.append(
1013
+ ExtractedSymbol(
1014
+ kind="struct",
1015
+ name=type_name,
1016
+ qualname=type_name,
1017
+ line=start,
1018
+ end_line=end,
1019
+ implements=[trait_name],
1020
+ exported=False,
1021
+ )
1022
+ )
1023
+ body = _first_child(node, {"declaration_list"})
1024
+ if body is not None and type_name:
1025
+ for child in body.children:
1026
+ if child.type != "function_item":
1027
+ continue
1028
+ mname = _first_named_text(child, raw, {"identifier"})
1029
+ if not mname:
1030
+ continue
1031
+ ms, me = _line_span(child)
1032
+ out.symbols.append(
1033
+ ExtractedSymbol(
1034
+ kind="method",
1035
+ name=mname,
1036
+ qualname=f"{type_name}.{mname}",
1037
+ line=ms,
1038
+ end_line=me,
1039
+ exported=_rust_is_pub(child),
1040
+ bases=[trait_name] if trait_name else [],
1041
+ )
1042
+ )
1043
+
1044
+ for node in _walk(tree.root_node):
1045
+ if node.type != "call_expression":
1046
+ continue
1047
+ func = node.children[0] if node.children else None
1048
+ if func is None:
1049
+ continue
1050
+ line = int(node.start_point[0]) + 1
1051
+ if func.type == "identifier":
1052
+ name = _node_text(func, raw)
1053
+ if name in _RUST_CALL_SKIP:
1054
+ continue
1055
+ out.calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
1056
+ elif func.type == "field_expression":
1057
+ field = _first_child(func, {"field_identifier"}) or _first_child(
1058
+ func, {"identifier"}
1059
+ )
1060
+ if field is None:
1061
+ continue
1062
+ name = _node_text(field, raw)
1063
+ recv = func.children[0] if func.children else None
1064
+ receiver = _node_text(recv, raw) if recv is not None else ""
1065
+ out.calls.append(
1066
+ ExtractedCall(
1067
+ name=name,
1068
+ line=line,
1069
+ receiver=receiver,
1070
+ qualname_hint=f"{receiver}.{name}" if receiver else name,
1071
+ )
1072
+ )
1073
+ elif func.type in {"scoped_identifier", "scoped_type_identifier"}:
1074
+ # Type::method / path::func
1075
+ name_node = _first_child(func, {"identifier"})
1076
+ # Prefer the rightmost identifier as the called name
1077
+ ids = [c for c in func.children if c.type in {"identifier", "type_identifier"}]
1078
+ if not ids:
1079
+ continue
1080
+ name = _node_text(ids[-1], raw)
1081
+ if name in _RUST_CALL_SKIP:
1082
+ continue
1083
+ receiver = _node_text(ids[-2], raw) if len(ids) >= 2 else ""
1084
+ if not receiver and name_node is not None:
1085
+ name = _node_text(name_node, raw)
1086
+ out.calls.append(
1087
+ ExtractedCall(
1088
+ name=name,
1089
+ line=line,
1090
+ receiver=receiver,
1091
+ qualname_hint=f"{receiver}.{name}" if receiver else name,
1092
+ )
1093
+ )
1094
+
1095
+ out.symbols.extend(_extract_rust_rationales(source, out.symbols))
1096
+ return out
1097
+
1098
+
1099
+ def _extract_rust_regex(path: str, source: str) -> FileExtraction:
1100
+ out = FileExtraction(path=path, language="rust")
1101
+ for m in _RUST_USE_RE.finditer(source):
1102
+ path_s = m.group(1).strip()
1103
+ out.imports.append(path_s.replace(" ", ""))
1104
+ for m in _RUST_MOD_RE.finditer(source):
1105
+ out.imports.append(f"mod:{m.group(1)}")
1106
+ for kind, rx in (
1107
+ ("struct", _RUST_STRUCT_RE),
1108
+ ("enum", _RUST_ENUM_RE),
1109
+ ("trait", _RUST_TRAIT_RE),
1110
+ ("function", _RUST_FN_RE),
1111
+ ):
1112
+ for m in rx.finditer(source):
1113
+ name = m.group(1)
1114
+ line = source[: m.start()].count("\n") + 1
1115
+ exported = "pub" in m.group(0)
1116
+ out.symbols.append(
1117
+ ExtractedSymbol(
1118
+ kind=kind,
1119
+ name=name,
1120
+ qualname=name,
1121
+ line=line,
1122
+ end_line=line,
1123
+ exported=exported,
1124
+ )
1125
+ )
1126
+ for m in _RUST_CALL_RE.finditer(source):
1127
+ if m.group(1) and m.group(2):
1128
+ # Type::method
1129
+ out.calls.append(
1130
+ ExtractedCall(
1131
+ name=m.group(2),
1132
+ line=source[: m.start()].count("\n") + 1,
1133
+ receiver=m.group(1),
1134
+ qualname_hint=f"{m.group(1)}.{m.group(2)}",
1135
+ )
1136
+ )
1137
+ elif m.group(3) and m.group(4):
1138
+ out.calls.append(
1139
+ ExtractedCall(
1140
+ name=m.group(4),
1141
+ line=source[: m.start()].count("\n") + 1,
1142
+ receiver=m.group(3),
1143
+ qualname_hint=f"{m.group(3)}.{m.group(4)}",
1144
+ )
1145
+ )
1146
+ elif m.group(5):
1147
+ name = m.group(5)
1148
+ if name in _RUST_CALL_SKIP:
1149
+ continue
1150
+ out.calls.append(
1151
+ ExtractedCall(
1152
+ name=name,
1153
+ line=source[: m.start()].count("\n") + 1,
1154
+ qualname_hint=name,
1155
+ )
1156
+ )
1157
+ out.symbols.extend(_extract_rust_rationales(source, out.symbols))
1158
+ return out
1159
+
1160
+
1161
+ # ---------------------------------------------------------------------------
1162
+ # Public API
1163
+ # ---------------------------------------------------------------------------
1164
+
1165
+
1166
+ def extract_ts_js(path: str, source: str) -> FileExtraction:
1167
+ """Extract from a TS/JS file (tree-sitter when available, else regex)."""
1168
+ ts = _extract_js_tree_sitter(path, source)
1169
+ if ts is not None:
1170
+ return ts
1171
+ return _extract_js_regex(path, source)
1172
+
1173
+
1174
+ def extract_go(path: str, source: str) -> FileExtraction:
1175
+ """Extract from a Go file (tree-sitter preferred)."""
1176
+ ts = _extract_go_tree_sitter(path, source)
1177
+ if ts is not None:
1178
+ return ts
1179
+ return _extract_go_regex(path, source)
1180
+
1181
+
1182
+ def extract_rust(path: str, source: str) -> FileExtraction:
1183
+ """Extract from a Rust file (tree-sitter preferred)."""
1184
+ ts = _extract_rust_tree_sitter(path, source)
1185
+ if ts is not None:
1186
+ return ts
1187
+ return _extract_rust_regex(path, source)
1188
+
1189
+
1190
+ def extract_file(path: str, source: str) -> FileExtraction:
1191
+ """Dispatch extraction by file suffix."""
1192
+ lower = path.lower()
1193
+ if lower.endswith(".py"):
1194
+ from devcouncil.indexing.graph.extract_python import extract_python
1195
+
1196
+ return extract_python(path, source)
1197
+ if any(lower.endswith(s) for s in _JS_SUFFIXES):
1198
+ return extract_ts_js(path, source)
1199
+ if lower.endswith(".go"):
1200
+ return extract_go(path, source)
1201
+ if lower.endswith(".rs"):
1202
+ return extract_rust(path, source)
1203
+ from devcouncil.codeintel.languages.generic_extractor import extract_generic
1204
+
1205
+ return extract_generic(path, source)