devcouncil 0.3.0 → 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
@@ -1,4 +1,8 @@
1
- """Semantic snapshots and diff classification."""
1
+ """Semantic snapshots and diff classification.
2
+
3
+ NOTE (Phase 4 legacy audit): task-scoped semantic diffs for verification — distinct
4
+ from the code symbol graph in ``indexing/graph/``. Keep; not overlapping dead-code.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -11,8 +15,10 @@ from pathlib import Path
11
15
 
12
16
  from devcouncil.indexing.ast_matcher import AstMatcher
13
17
  from devcouncil.indexing.lsp import LspInspector
18
+ from devcouncil.indexing.walk import iter_project_files
14
19
  from devcouncil.storage.db import get_db
15
20
  from devcouncil.storage.native import SemanticDiffRepository
21
+ from devcouncil.utils.json_persist import read_json, write_json
16
22
 
17
23
  _CONFIG_FILES = {
18
24
  "pyproject.toml",
@@ -39,7 +45,7 @@ class SemanticIndex:
39
45
  # matching, source-file hashing, import extraction, and LSP language detection);
40
46
  # each collector now filters this single in-memory list with its own predicate, so
41
47
  # the on-disk output is unchanged while the filesystem is walked a single time.
42
- all_files = [path for path in self.project_root.rglob("*") if path.is_file()]
48
+ all_files = list(iter_project_files(self.project_root))
43
49
  rel_files = [str(path.relative_to(self.project_root)) for path in all_files]
44
50
  symbols = self._collect_symbols(all_files)
45
51
  source_files, imports = self._collect_source_data(all_files)
@@ -56,7 +62,7 @@ class SemanticIndex:
56
62
  }
57
63
  path = self.snapshot_path(task_id, stage)
58
64
  path.parent.mkdir(parents=True, exist_ok=True)
59
- path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
65
+ write_json(path, payload)
60
66
  return path
61
67
 
62
68
  def diff(self, task_id: str) -> dict:
@@ -64,8 +70,8 @@ class SemanticIndex:
64
70
  after_path = self.snapshot_path(task_id, "after")
65
71
  if not after_path.exists():
66
72
  self.create_snapshot(task_id, "after")
67
- before = json.loads(before_path.read_text(encoding="utf-8")) if before_path.exists() else {}
68
- after = json.loads(after_path.read_text(encoding="utf-8"))
73
+ before = read_json(before_path) if before_path.exists() else {}
74
+ after = read_json(after_path) if after_path.exists() else {}
69
75
  classifications = self._classify(before, after)
70
76
  summary = ", ".join(item["type"] for item in classifications) or "no semantic changes"
71
77
  db = get_db(self.project_root)
@@ -107,7 +113,7 @@ class SemanticIndex:
107
113
  exactly — source entries are still sorted by path, imports stay in traversal order.
108
114
  """
109
115
  if files is None:
110
- files = [p for p in self.project_root.rglob("*") if p.is_file()]
116
+ files = list(iter_project_files(self.project_root))
111
117
  source_entries: list[dict] = []
112
118
  imports: list[dict] = []
113
119
  for path in files:
@@ -0,0 +1,163 @@
1
+ """Pure helpers for reasoning about subsystem areas from a loaded ``repo_map.json``.
2
+
3
+ Shared by the prompt builder's impact block and the subsystem-boundary verification
4
+ gate so both agree on how a path maps to an area, which area is which area's neighbor,
5
+ and what a change "touches". Dependency-free and side-effect-free: everything takes an
6
+ already-parsed repo-map dict (or its ``subsystems``/``dependents`` slices) and returns
7
+ plain data. Never raises on malformed input — a missing/odd map degrades to "unknown".
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Iterable, Mapping, Sequence
13
+
14
+
15
+ def _norm(path: str) -> str:
16
+ return str(path).replace("\\", "/")
17
+
18
+
19
+ def subsystem_areas(data: Mapping | None) -> list[str]:
20
+ """All subsystem ``area`` prefixes declared in the map (longest first)."""
21
+ subs = (data or {}).get("subsystems") or []
22
+ areas = [str(s.get("area")) for s in subs if isinstance(s, dict) and s.get("area")]
23
+ return sorted(set(areas), key=len, reverse=True)
24
+
25
+
26
+ def area_for_path(path: str, data: Mapping | None) -> str | None:
27
+ """The subsystem area a path belongs to (longest matching area prefix), or ``None``.
28
+
29
+ Falls back to the per-file ``area`` recorded in the map's ``files`` list when no
30
+ subsystem prefix matches, so files outside a declared subsystem still resolve.
31
+ """
32
+ norm = _norm(path)
33
+ for area in subsystem_areas(data):
34
+ if norm == area or norm.startswith(area + "/"):
35
+ return area
36
+ files = (data or {}).get("files") or []
37
+ for entry in files:
38
+ if isinstance(entry, dict) and _norm(entry.get("path", "")) == norm:
39
+ file_area = entry.get("area")
40
+ return str(file_area) if file_area else None
41
+ return None
42
+
43
+
44
+ def neighbors_for_area(area: str | None, data: Mapping | None) -> list[str]:
45
+ """Declared neighboring areas for ``area`` (empty when unknown)."""
46
+ if not area:
47
+ return []
48
+ for sub in (data or {}).get("subsystems") or []:
49
+ if isinstance(sub, dict) and str(sub.get("area")) == area:
50
+ return [str(n) for n in (sub.get("neighbors") or [])]
51
+ return []
52
+
53
+
54
+ def are_neighbors(area_a: str | None, area_b: str | None, data: Mapping | None) -> bool:
55
+ """True when the two areas are the same or declared neighbors (in either direction)."""
56
+ if not area_a or not area_b:
57
+ return True # unknown side → don't flag
58
+ if area_a == area_b:
59
+ return True
60
+ if area_b in neighbors_for_area(area_a, data):
61
+ return True
62
+ if area_a in neighbors_for_area(area_b, data):
63
+ return True
64
+ return False
65
+
66
+
67
+ def dependents_of(path: str, data: Mapping | None) -> list[str]:
68
+ """Files that import ``path`` (reverse-import blast radius), from the map.
69
+
70
+ The list may be truncated by the mapper's ``_DEPENDENTS_MAX``; see
71
+ :func:`dependents_total_of` for the full importer count when truncated.
72
+ """
73
+ dependents = (data or {}).get("dependents") or {}
74
+ if not isinstance(dependents, dict):
75
+ return []
76
+ return [str(p) for p in (dependents.get(_norm(path)) or [])]
77
+
78
+
79
+ def dependents_total_of(path: str, data: Mapping | None) -> int | None:
80
+ """Full importer count when ``dependents`` was truncated; else ``None``.
81
+
82
+ When this returns an int greater than ``len(dependents_of(...))``, the listed
83
+ dependents are a sample — not the complete blast radius.
84
+ """
85
+ totals = (data or {}).get("dependents_total") or {}
86
+ if not isinstance(totals, dict):
87
+ return None
88
+ raw = totals.get(_norm(path))
89
+ if raw is None:
90
+ return None
91
+ try:
92
+ n = int(raw)
93
+ except (TypeError, ValueError):
94
+ return None
95
+ return n if n > 0 else None
96
+
97
+
98
+ def unwired_candidates_of(data: Mapping | None) -> list[str]:
99
+ """Files with zero inbound edges that aren't entry roots/exempt (capped list)."""
100
+ vals = (data or {}).get("unwired_candidates") or []
101
+ if not isinstance(vals, list):
102
+ return []
103
+ return [str(p) for p in vals]
104
+
105
+
106
+ def unreachable_of(data: Mapping | None) -> list[str]:
107
+ """Files not reachable by BFS from any entry root (capped list)."""
108
+ vals = (data or {}).get("unreachable_files") or []
109
+ if not isinstance(vals, list):
110
+ return []
111
+ return [str(p) for p in vals]
112
+
113
+
114
+ def dead_symbol_candidates_of(data: Mapping | None) -> list[str]:
115
+ """``path:line name`` entries for unused public top-level symbols (capped)."""
116
+ vals = (data or {}).get("dead_symbol_candidates") or []
117
+ if not isinstance(vals, list):
118
+ return []
119
+ return [str(p) for p in vals]
120
+
121
+
122
+ def is_entry_root(path: str, data: Mapping | None) -> bool:
123
+ """True when ``path`` is listed in the map's ``entry_roots``."""
124
+ roots = (data or {}).get("entry_roots") or []
125
+ if not isinstance(roots, list):
126
+ return False
127
+ norm = _norm(path)
128
+ return any(_norm(str(r)) == norm for r in roots)
129
+
130
+
131
+ def areas_touched(paths: Iterable[str], data: Mapping | None) -> list[str]:
132
+ """The distinct subsystem areas the given paths live in (sorted)."""
133
+ seen: set[str] = set()
134
+ for path in paths:
135
+ area = area_for_path(path, data)
136
+ if area:
137
+ seen.add(area)
138
+ return sorted(seen)
139
+
140
+
141
+ def impact_targets(path: str, data: Mapping | None) -> tuple[list[str], list[str]]:
142
+ """Return ``(dependent_files, neighbor_areas)`` a change to ``path`` touches."""
143
+ area = area_for_path(path, data)
144
+ return dependents_of(path, data), neighbors_for_area(area, data)
145
+
146
+
147
+ def cross_boundary_pairs(
148
+ paths: Sequence[str],
149
+ data: Mapping | None,
150
+ ) -> list[tuple[str, str]]:
151
+ """Pairs of touched areas that are neither identical nor declared neighbors.
152
+
153
+ Each pair ``(a, b)`` is emitted once (sorted) and signals an edit that spans two
154
+ subsystems the map does not consider adjacent — a candidate architecture-drift
155
+ boundary crossing.
156
+ """
157
+ areas = areas_touched(paths, data)
158
+ crossings: set[tuple[str, str]] = set()
159
+ for i, area_a in enumerate(areas):
160
+ for area_b in areas[i + 1:]:
161
+ if not are_neighbors(area_a, area_b, data):
162
+ crossings.add(tuple(sorted((area_a, area_b)))) # type: ignore[arg-type]
163
+ return sorted(crossings)
@@ -0,0 +1,343 @@
1
+ """Tree-sitter import / symbol extraction for Rust, Go, and JS/TS.
2
+
3
+ ``tree-sitter`` and ``tree-sitter-language-pack`` are core dependencies. Helpers
4
+ still return ``None`` on parse failure so callers can keep regex/AST fallbacks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from functools import lru_cache
11
+ from typing import Iterable, List, Optional, Sequence, Tuple
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # language name for tree-sitter-language-pack → file suffixes that use it
16
+ _LANG_BY_SUFFIX = {
17
+ ".rs": "rust",
18
+ ".go": "go",
19
+ ".ts": "typescript",
20
+ ".tsx": "tsx",
21
+ ".js": "javascript",
22
+ ".jsx": "javascript",
23
+ ".mjs": "javascript",
24
+ ".cjs": "javascript",
25
+ }
26
+
27
+ _SymbolHit = Tuple[str, str, int, str] # kind, name, line, text
28
+
29
+
30
+ def tree_sitter_available() -> bool:
31
+ """True when both the core binding and language pack import cleanly."""
32
+ try:
33
+ import tree_sitter # noqa: F401
34
+ from tree_sitter_language_pack import get_parser # noqa: F401
35
+ except Exception:
36
+ return False
37
+ return True
38
+
39
+
40
+ @lru_cache(maxsize=16)
41
+ def _parser_for(language: str):
42
+ if not tree_sitter_available():
43
+ return None
44
+ try:
45
+ from tree_sitter_language_pack import get_parser
46
+
47
+ return get_parser(language)
48
+ except Exception:
49
+ logger.debug("tree-sitter parser unavailable for %s", language, exc_info=True)
50
+ return None
51
+
52
+
53
+ def _parse(language: str, source: bytes):
54
+ parser = _parser_for(language)
55
+ if parser is None:
56
+ return None
57
+ try:
58
+ return parser.parse(source)
59
+ except Exception:
60
+ logger.debug("tree-sitter parse failed for %s", language, exc_info=True)
61
+ return None
62
+
63
+
64
+ def _node_text(node, source: bytes) -> str:
65
+ return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
66
+
67
+
68
+ def _walk(node) -> Iterable:
69
+ yield node
70
+ for child in node.children:
71
+ yield from _walk(child)
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Go imports
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ def extract_go_import_specs(source: str | bytes) -> Optional[List[str]]:
80
+ """File-level Go import path strings, or None when tree-sitter is unavailable."""
81
+ if not tree_sitter_available():
82
+ return None
83
+ raw = source.encode("utf-8") if isinstance(source, str) else source
84
+ tree = _parse("go", raw)
85
+ if tree is None:
86
+ return None
87
+ specs: List[str] = []
88
+ for node in _walk(tree.root_node):
89
+ if node.type != "interpreted_string_literal":
90
+ continue
91
+ # Only count string literals under import_spec.
92
+ parent = node.parent
93
+ while parent is not None and parent.type not in {"import_spec", "source_file"}:
94
+ parent = parent.parent
95
+ if parent is None or parent.type != "import_spec":
96
+ continue
97
+ text = _node_text(node, raw).strip()
98
+ if len(text) >= 2 and text[0] in "\"`" and text[-1] == text[0]:
99
+ text = text[1:-1]
100
+ if text:
101
+ specs.append(text)
102
+ return specs
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Rust mod / use
107
+ # ---------------------------------------------------------------------------
108
+
109
+
110
+ def _rust_path_segments(node, source: bytes) -> List[str]:
111
+ """Flatten a use/mod path node into identifier segments (skip crate/super/self)."""
112
+ segs: List[str] = []
113
+
114
+ def collect(n) -> None:
115
+ t = n.type
116
+ if t in {"identifier", "type_identifier", "field_identifier"}:
117
+ segs.append(_node_text(n, source))
118
+ return
119
+ if t in {"crate", "super", "self"}:
120
+ segs.append(t)
121
+ return
122
+ if t == "use_wildcard":
123
+ for c in n.children:
124
+ collect(c)
125
+ return
126
+ if t in {"scoped_identifier", "scoped_type_identifier", "scoped_use_list", "use_list", "use_as_clause"}:
127
+ for c in n.children:
128
+ if c.type in {",", "{", "}", "::", "as", "*"}:
129
+ continue
130
+ collect(c)
131
+ return
132
+ for c in n.children:
133
+ collect(c)
134
+
135
+ collect(node)
136
+ return segs
137
+
138
+
139
+ def extract_rust_import_refs(source: str | bytes) -> Optional[List[dict]]:
140
+ """Extract Rust ``mod`` / ``use`` references.
141
+
142
+ Returns a list of dicts:
143
+ ``{"kind": "mod", "name": "foo"}`` for ``mod foo;`` (file-level, no body)
144
+ ``{"kind": "use", "segments": ["crate", "foo", "Bar"]}`` for use paths
145
+
146
+ ``None`` when tree-sitter is unavailable.
147
+ """
148
+ if not tree_sitter_available():
149
+ return None
150
+ raw = source.encode("utf-8") if isinstance(source, str) else source
151
+ tree = _parse("rust", raw)
152
+ if tree is None:
153
+ return None
154
+ refs: List[dict] = []
155
+ for node in tree.root_node.children:
156
+ if node.type == "mod_item":
157
+ # File-level mod only when there is no declaration_list body.
158
+ has_body = any(c.type == "declaration_list" for c in node.children)
159
+ name = None
160
+ for c in node.children:
161
+ if c.type == "identifier":
162
+ name = _node_text(c, raw)
163
+ break
164
+ if name and not has_body:
165
+ refs.append({"kind": "mod", "name": name})
166
+ elif node.type == "use_declaration":
167
+ # Path is the non-keyword child after `use` / visibility.
168
+ path_node = None
169
+ for c in node.children:
170
+ if c.type in {"use", "visibility_modifier", ";"}:
171
+ continue
172
+ path_node = c
173
+ break
174
+ if path_node is None:
175
+ continue
176
+ segments = _rust_path_segments(path_node, raw)
177
+ if segments:
178
+ refs.append({"kind": "use", "segments": segments})
179
+ return refs
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Symbol matching (AstMatcher)
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def extract_symbols(language: str, source: str | bytes) -> Optional[List[_SymbolHit]]:
188
+ """Named top-level (and nested) definitions for ``language``.
189
+
190
+ ``language`` is an AstMatcher language id: typescript, javascript, go, rust.
191
+ Returns ``None`` when tree-sitter cannot parse (caller should fall back).
192
+ """
193
+ if not tree_sitter_available():
194
+ return None
195
+ lang_key = {
196
+ "typescript": "typescript",
197
+ "javascript": "javascript",
198
+ "go": "go",
199
+ "rust": "rust",
200
+ }.get(language)
201
+ if lang_key is None:
202
+ return None
203
+ raw = source.encode("utf-8") if isinstance(source, str) else source
204
+ # Prefer tsx grammar when JSX-looking; callers pass typescript for .tsx too.
205
+ tree = _parse(lang_key, raw)
206
+ if tree is None and language == "typescript":
207
+ tree = _parse("tsx", raw)
208
+ if tree is None:
209
+ return None
210
+
211
+ if language in {"typescript", "javascript"}:
212
+ return _symbols_js(tree.root_node, raw)
213
+ if language == "go":
214
+ return _symbols_go(tree.root_node, raw)
215
+ if language == "rust":
216
+ return _symbols_rust(tree.root_node, raw)
217
+ return None
218
+
219
+
220
+ def _line_of(node) -> int:
221
+ return int(node.start_point[0]) + 1
222
+
223
+
224
+ def _first_named(node, types: Sequence[str]) -> Optional[str]:
225
+ for c in node.children:
226
+ if c.type in types:
227
+ return str(c.text.decode("utf-8", errors="replace"))
228
+ return None
229
+
230
+
231
+ def _symbols_js(root, source: bytes) -> List[_SymbolHit]:
232
+ hits: List[_SymbolHit] = []
233
+
234
+ def consider(node, kind: str, name_types: Sequence[str] = ("identifier", "type_identifier")) -> None:
235
+ name = _first_named(node, name_types)
236
+ if not name:
237
+ return
238
+ line = _line_of(node)
239
+ text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip() if line else name
240
+ hits.append((kind, name, line, text))
241
+
242
+ def walk(node) -> None:
243
+ t = node.type
244
+ if t == "function_declaration":
245
+ consider(node, "function")
246
+ elif t == "class_declaration":
247
+ consider(node, "class", ("type_identifier", "identifier"))
248
+ elif t == "interface_declaration":
249
+ consider(node, "interface", ("type_identifier", "identifier"))
250
+ elif t == "type_alias_declaration":
251
+ consider(node, "type", ("type_identifier", "identifier"))
252
+ elif t == "lexical_declaration":
253
+ for c in node.children:
254
+ if c.type != "variable_declarator":
255
+ continue
256
+ name = _first_named(c, ("identifier",))
257
+ if not name:
258
+ continue
259
+ line = _line_of(c)
260
+ text = (
261
+ source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
262
+ if line
263
+ else name
264
+ )
265
+ hits.append(("function", name, line, text))
266
+ elif t == "export_statement":
267
+ for c in node.children:
268
+ walk(c)
269
+ return
270
+ for c in node.children:
271
+ walk(c)
272
+
273
+ walk(root)
274
+ return hits
275
+
276
+
277
+ def _symbols_go(root, source: bytes) -> List[_SymbolHit]:
278
+ hits: List[_SymbolHit] = []
279
+ for node in _walk(root):
280
+ if node.type == "function_declaration":
281
+ name = _first_named(node, ("identifier",))
282
+ if not name:
283
+ continue
284
+ line = _line_of(node)
285
+ text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
286
+ hits.append(("function", name, line, text))
287
+ elif node.type == "method_declaration":
288
+ name = None
289
+ for c in node.children:
290
+ if c.type == "field_identifier":
291
+ name = c.text.decode("utf-8", errors="replace")
292
+ break
293
+ if not name:
294
+ continue
295
+ line = _line_of(node)
296
+ text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
297
+ hits.append(("function", name, line, text))
298
+ elif node.type == "type_declaration":
299
+ for c in node.children:
300
+ if c.type != "type_spec":
301
+ continue
302
+ name = _first_named(c, ("type_identifier", "identifier"))
303
+ if not name:
304
+ continue
305
+ kind = "struct"
306
+ for gc in c.children:
307
+ if gc.type == "struct_type":
308
+ kind = "struct"
309
+ elif gc.type == "interface_type":
310
+ kind = "interface"
311
+ elif gc.type == "type_identifier":
312
+ continue
313
+ line = _line_of(c)
314
+ text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
315
+ hits.append((kind, name, line, text))
316
+ return hits
317
+
318
+
319
+ def _symbols_rust(root, source: bytes) -> List[_SymbolHit]:
320
+ hits: List[_SymbolHit] = []
321
+ kind_map = {
322
+ "function_item": "function",
323
+ "struct_item": "struct",
324
+ "enum_item": "enum",
325
+ "trait_item": "trait",
326
+ "type_item": "type",
327
+ }
328
+ for node in _walk(root):
329
+ kind = kind_map.get(node.type)
330
+ if not kind:
331
+ continue
332
+ name = _first_named(node, ("identifier", "type_identifier"))
333
+ if not name:
334
+ continue
335
+ line = _line_of(node)
336
+ text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
337
+ hits.append((kind, name, line, text))
338
+ return hits
339
+
340
+
341
+ def language_for_suffix(suffix: str) -> Optional[str]:
342
+ """Map a file suffix to a language-pack name, or None."""
343
+ return _LANG_BY_SUFFIX.get(suffix.lower())