agentinputlab 0.1.0a1__py3-none-any.whl

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 (469) hide show
  1. agentinputlab/__init__.py +28 -0
  2. agentinputlab/algorithms/__init__.py +6 -0
  3. agentinputlab/algorithms/acquire/__init__.py +145 -0
  4. agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
  5. agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
  6. agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
  7. agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
  8. agentinputlab/algorithms/acquire/hyde.py +296 -0
  9. agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
  10. agentinputlab/algorithms/acquire/hyde_external.py +148 -0
  11. agentinputlab/algorithms/acquire/ircot.py +28 -0
  12. agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
  13. agentinputlab/algorithms/acquire/ircot_native.py +355 -0
  14. agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
  15. agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
  16. agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
  17. agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
  18. agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
  19. agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
  20. agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
  21. agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
  22. agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
  23. agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
  24. agentinputlab/algorithms/clarify/__init__.py +61 -0
  25. agentinputlab/algorithms/clarify/_structured.py +64 -0
  26. agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
  27. agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
  28. agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
  29. agentinputlab/algorithms/clarify/future_turns.py +379 -0
  30. agentinputlab/algorithms/clarify/udcs.py +255 -0
  31. agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
  32. agentinputlab/algorithms/curate/__init__.py +65 -0
  33. agentinputlab/algorithms/curate/acon/__init__.py +27 -0
  34. agentinputlab/algorithms/curate/acon/common.py +284 -0
  35. agentinputlab/algorithms/curate/acon/common_support.py +136 -0
  36. agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
  37. agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
  38. agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
  39. agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
  40. agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
  41. agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
  42. agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
  43. agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
  44. agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
  45. agentinputlab/algorithms/curate/recomp/external.py +1129 -0
  46. agentinputlab/algorithms/curate/recomp/selective.py +304 -0
  47. agentinputlab/algorithms/decompose/__init__.py +107 -0
  48. agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
  49. agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
  50. agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
  51. agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
  52. agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
  53. agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
  54. agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
  55. agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
  56. agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
  57. agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
  58. agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
  59. agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
  60. agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
  61. agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
  62. agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
  63. agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
  64. agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
  65. agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
  66. agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
  67. agentinputlab/algorithms/reformulate/__init__.py +85 -0
  68. agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
  69. agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
  70. agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
  71. agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
  72. agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
  73. agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
  74. agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
  75. agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
  76. agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
  77. agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
  78. agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
  79. agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
  80. agentinputlab/algorithms/reformulate/rrr.py +696 -0
  81. agentinputlab/annotation/__init__.py +5 -0
  82. agentinputlab/annotation/independent.py +428 -0
  83. agentinputlab/annotation/server.py +233 -0
  84. agentinputlab/annotation/static/app.js +121 -0
  85. agentinputlab/annotation/static/index.html +60 -0
  86. agentinputlab/annotation/static/review.css +2 -0
  87. agentinputlab/annotation/static/review.html +10 -0
  88. agentinputlab/annotation/static/review.js +11 -0
  89. agentinputlab/annotation/static/styles.css +55 -0
  90. agentinputlab/annotation/store.py +444 -0
  91. agentinputlab/api.py +368 -0
  92. agentinputlab/artifacts/__init__.py +15 -0
  93. agentinputlab/artifacts/json_file.py +104 -0
  94. agentinputlab/artifacts/manifest.py +98 -0
  95. agentinputlab/artifacts/store.py +157 -0
  96. agentinputlab/backends/__init__.py +98 -0
  97. agentinputlab/backends/embedding/__init__.py +25 -0
  98. agentinputlab/backends/embedding/base.py +145 -0
  99. agentinputlab/backends/embedding/callable.py +75 -0
  100. agentinputlab/backends/embedding/replay.py +129 -0
  101. agentinputlab/backends/llm/__init__.py +31 -0
  102. agentinputlab/backends/llm/base.py +149 -0
  103. agentinputlab/backends/llm/callable.py +108 -0
  104. agentinputlab/backends/llm/decomp_completion.py +88 -0
  105. agentinputlab/backends/llm/final_content_stop.py +72 -0
  106. agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
  107. agentinputlab/backends/llm/openai_compatible.py +332 -0
  108. agentinputlab/backends/llm/transformers.py +171 -0
  109. agentinputlab/backends/retrieval/__init__.py +35 -0
  110. agentinputlab/backends/retrieval/base.py +50 -0
  111. agentinputlab/backends/retrieval/dense.py +777 -0
  112. agentinputlab/backends/retrieval/memory.py +119 -0
  113. agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
  114. agentinputlab/backends/user/__init__.py +15 -0
  115. agentinputlab/backends/user/base.py +66 -0
  116. agentinputlab/backends/user/callable.py +54 -0
  117. agentinputlab/backends/user/replay.py +89 -0
  118. agentinputlab/backends/vector.py +38 -0
  119. agentinputlab/benchmarks/__init__.py +246 -0
  120. agentinputlab/benchmarks/acquisition.py +106 -0
  121. agentinputlab/benchmarks/ale.py +540 -0
  122. agentinputlab/benchmarks/appworld_ul.py +669 -0
  123. agentinputlab/benchmarks/assets.py +226 -0
  124. agentinputlab/benchmarks/base.py +68 -0
  125. agentinputlab/benchmarks/beir_archive.py +235 -0
  126. agentinputlab/benchmarks/beir_sample.py +269 -0
  127. agentinputlab/benchmarks/bfcl_native.py +245 -0
  128. agentinputlab/benchmarks/bfcl_v3.py +607 -0
  129. agentinputlab/benchmarks/cache.py +218 -0
  130. agentinputlab/benchmarks/clariq.py +203 -0
  131. agentinputlab/benchmarks/data/registry.json +321 -0
  132. agentinputlab/benchmarks/data/smoke_assets.json +511 -0
  133. agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
  134. agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
  135. agentinputlab/benchmarks/execution.py +1111 -0
  136. agentinputlab/benchmarks/interactive.py +257 -0
  137. agentinputlab/benchmarks/ir_query.py +204 -0
  138. agentinputlab/benchmarks/longbench.py +203 -0
  139. agentinputlab/benchmarks/paired_replay.py +479 -0
  140. agentinputlab/benchmarks/preprocess.py +2313 -0
  141. agentinputlab/benchmarks/production.py +236 -0
  142. agentinputlab/benchmarks/qa.py +963 -0
  143. agentinputlab/benchmarks/registry.py +110 -0
  144. agentinputlab/benchmarks/retrieval.py +459 -0
  145. agentinputlab/benchmarks/runtime.py +509 -0
  146. agentinputlab/benchmarks/statebench.py +938 -0
  147. agentinputlab/benchmarks/tau3.py +725 -0
  148. agentinputlab/benchmarks/toolsandbox.py +626 -0
  149. agentinputlab/benchmarks/trec_dl_sample.py +263 -0
  150. agentinputlab/campaign.py +654 -0
  151. agentinputlab/campaign_runner.py +953 -0
  152. agentinputlab/cli/__init__.py +6 -0
  153. agentinputlab/cli/main.py +933 -0
  154. agentinputlab/connectors/__init__.py +46 -0
  155. agentinputlab/connectors/knowledge/__init__.py +4 -0
  156. agentinputlab/connectors/tools/__init__.py +30 -0
  157. agentinputlab/connectors/tools/base.py +57 -0
  158. agentinputlab/connectors/tools/mcp.py +222 -0
  159. agentinputlab/connectors/tools/memory.py +41 -0
  160. agentinputlab/connectors/tools/openapi.py +695 -0
  161. agentinputlab/connectors/tools/schema.py +102 -0
  162. agentinputlab/contracts/__init__.py +91 -0
  163. agentinputlab/contracts/availability.py +24 -0
  164. agentinputlab/contracts/dependency.py +58 -0
  165. agentinputlab/contracts/enums.py +82 -0
  166. agentinputlab/contracts/errors.py +63 -0
  167. agentinputlab/contracts/evidence.py +43 -0
  168. agentinputlab/contracts/intent.py +110 -0
  169. agentinputlab/contracts/lifecycle.py +164 -0
  170. agentinputlab/contracts/patch.py +167 -0
  171. agentinputlab/contracts/serialization.py +93 -0
  172. agentinputlab/contracts/state.py +127 -0
  173. agentinputlab/contracts/trace.py +59 -0
  174. agentinputlab/contracts/worker.py +42 -0
  175. agentinputlab/core/__init__.py +7 -0
  176. agentinputlab/data/__init__.py +32 -0
  177. agentinputlab/data/checksums.py +9 -0
  178. agentinputlab/data/downloader.py +235 -0
  179. agentinputlab/data/http.py +171 -0
  180. agentinputlab/data/index.py +21 -0
  181. agentinputlab/data/overlap.py +28 -0
  182. agentinputlab/data/preprocess.py +32 -0
  183. agentinputlab/data/storage.py +57 -0
  184. agentinputlab/data/trec_sample.py +427 -0
  185. agentinputlab/evaluation/__init__.py +5 -0
  186. agentinputlab/evaluation/outcomes.py +32 -0
  187. agentinputlab/experiments/__init__.py +278 -0
  188. agentinputlab/experiments/analysis.py +981 -0
  189. agentinputlab/experiments/artifacts.py +687 -0
  190. agentinputlab/experiments/automatic_measurement.py +678 -0
  191. agentinputlab/experiments/auxiliary_matrix.py +203 -0
  192. agentinputlab/experiments/bindings.py +430 -0
  193. agentinputlab/experiments/blueprints.py +147 -0
  194. agentinputlab/experiments/builders/__init__.py +45 -0
  195. agentinputlab/experiments/builders/executable_external_payload.py +423 -0
  196. agentinputlab/experiments/builders/external_process_arena.py +1018 -0
  197. agentinputlab/experiments/builders/registry_intervention.py +463 -0
  198. agentinputlab/experiments/cli.py +158 -0
  199. agentinputlab/experiments/controls.py +340 -0
  200. agentinputlab/experiments/driver_guard.py +205 -0
  201. agentinputlab/experiments/drivers/__init__.py +89 -0
  202. agentinputlab/experiments/drivers/external_arena.py +2908 -0
  203. agentinputlab/experiments/drivers/method_process.py +662 -0
  204. agentinputlab/experiments/drivers/parity_process.py +1484 -0
  205. agentinputlab/experiments/drivers/static_production.py +1574 -0
  206. agentinputlab/experiments/executor.py +448 -0
  207. agentinputlab/experiments/loader.py +605 -0
  208. agentinputlab/experiments/measurement.py +675 -0
  209. agentinputlab/experiments/paper_analysis.py +1818 -0
  210. agentinputlab/experiments/parity_identity.py +605 -0
  211. agentinputlab/experiments/plan.py +277 -0
  212. agentinputlab/experiments/production_builders.py +3739 -0
  213. agentinputlab/experiments/readiness.py +432 -0
  214. agentinputlab/experiments/schema.py +514 -0
  215. agentinputlab/experiments/secrets.py +177 -0
  216. agentinputlab/experiments/stateful_analysis.py +1738 -0
  217. agentinputlab/experiments/transfer_analysis.py +1316 -0
  218. agentinputlab/experiments/unseen_analysis.py +1025 -0
  219. agentinputlab/integrations/__init__.py +1 -0
  220. agentinputlab/integrations/action_space/__init__.py +1 -0
  221. agentinputlab/integrations/external_workers/__init__.py +76 -0
  222. agentinputlab/integrations/external_workers/ace.py +431 -0
  223. agentinputlab/integrations/external_workers/base.py +181 -0
  224. agentinputlab/integrations/external_workers/decomp_live.py +249 -0
  225. agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
  226. agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
  227. agentinputlab/integrations/external_workers/ircot.py +576 -0
  228. agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
  229. agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
  230. agentinputlab/integrations/external_workers/method.py +314 -0
  231. agentinputlab/integrations/external_workers/process.py +678 -0
  232. agentinputlab/integrations/external_workers/supervisor.py +1071 -0
  233. agentinputlab/integrations/full_architecture/__init__.py +1 -0
  234. agentinputlab/integrations/full_architecture/hyde.py +212 -0
  235. agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
  236. agentinputlab/integrations/live_history.py +222 -0
  237. agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
  238. agentinputlab/integrations/training/__init__.py +1 -0
  239. agentinputlab/metrics/__init__.py +50 -0
  240. agentinputlab/metrics/catalog.py +313 -0
  241. agentinputlab/metrics/harm.py +95 -0
  242. agentinputlab/metrics/paired.py +40 -0
  243. agentinputlab/metrics/qa.py +76 -0
  244. agentinputlab/metrics/text.py +33 -0
  245. agentinputlab/metrics/third_party.py +161 -0
  246. agentinputlab/models/__init__.py +4 -0
  247. agentinputlab/observability/__init__.py +56 -0
  248. agentinputlab/observability/budget.py +316 -0
  249. agentinputlab/observability/instrumentation.py +754 -0
  250. agentinputlab/observability/models.py +403 -0
  251. agentinputlab/observability/monitor.py +380 -0
  252. agentinputlab/optimizers/__init__.py +1 -0
  253. agentinputlab/optimizers/clarify/__init__.py +12 -0
  254. agentinputlab/optimizers/context/__init__.py +22 -0
  255. agentinputlab/optimizers/decompose/__init__.py +64 -0
  256. agentinputlab/optimizers/evidence/__init__.py +22 -0
  257. agentinputlab/optimizers/rewrite/__init__.py +30 -0
  258. agentinputlab/parity/__init__.py +5 -0
  259. agentinputlab/parity/base.py +206 -0
  260. agentinputlab/production_campaign.py +763 -0
  261. agentinputlab/py.typed +1 -0
  262. agentinputlab/recipes/__init__.py +333 -0
  263. agentinputlab/recipes/ace.py +418 -0
  264. agentinputlab/recipes/ace_bfcl.py +273 -0
  265. agentinputlab/recipes/ace_common_support.py +169 -0
  266. agentinputlab/recipes/ace_native_plan.py +322 -0
  267. agentinputlab/recipes/ace_online.py +711 -0
  268. agentinputlab/recipes/ace_retrieval.py +330 -0
  269. agentinputlab/recipes/ace_statebench.py +382 -0
  270. agentinputlab/recipes/ace_tau.py +463 -0
  271. agentinputlab/recipes/ace_toolsandbox.py +432 -0
  272. agentinputlab/recipes/acon_guideline.py +22 -0
  273. agentinputlab/recipes/adaptive_rag.py +822 -0
  274. agentinputlab/recipes/llmlingua2_inference.py +146 -0
  275. agentinputlab/recipes/llmlingua2_training.py +796 -0
  276. agentinputlab/recipes/mce.py +1486 -0
  277. agentinputlab/recipes/mce_final.py +335 -0
  278. agentinputlab/recipes/mce_text_fit.py +499 -0
  279. agentinputlab/recipes/mce_text_only.py +298 -0
  280. agentinputlab/recipes/qrag.py +729 -0
  281. agentinputlab/recipes/qrag_inference.py +364 -0
  282. agentinputlab/recipes/r2ag_features.py +236 -0
  283. agentinputlab/recipes/r2ag_inference.py +503 -0
  284. agentinputlab/recipes/r2ag_training.py +948 -0
  285. agentinputlab/recipes/recomp_abstractive.py +592 -0
  286. agentinputlab/recipes/recomp_extractive.py +525 -0
  287. agentinputlab/recipes/reformer.py +17 -0
  288. agentinputlab/recipes/rqrag.py +1516 -0
  289. agentinputlab/recipes/rqrag_common_support.py +193 -0
  290. agentinputlab/recipes/rqrag_completion.py +147 -0
  291. agentinputlab/recipes/skillopt.py +462 -0
  292. agentinputlab/recipes/skillopt_common_support.py +154 -0
  293. agentinputlab/recipes/skillopt_environment.py +165 -0
  294. agentinputlab/recipes/skillopt_environment_cli.py +5 -0
  295. agentinputlab/recipes/skillopt_retrieval.py +188 -0
  296. agentinputlab/recipes/smartsearch.py +897 -0
  297. agentinputlab/recipes/smartsearch_curriculum.py +283 -0
  298. agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
  299. agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
  300. agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
  301. agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
  302. agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
  303. agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
  304. agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
  305. agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
  306. agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
  307. agentinputlab/recipes/smartsearch_inference.py +524 -0
  308. agentinputlab/recipes/smartsearch_native_data.py +172 -0
  309. agentinputlab/recipes/smartsearch_policy.py +616 -0
  310. agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
  311. agentinputlab/recipes/smartsearch_sampling.py +232 -0
  312. agentinputlab/recipes/smartsearch_teacher.py +203 -0
  313. agentinputlab/recipes/smartsearch_training.py +265 -0
  314. agentinputlab/registry/__init__.py +46 -0
  315. agentinputlab/registry/beta.py +727 -0
  316. agentinputlab/registry/data/algorithms.json +1959 -0
  317. agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
  318. agentinputlab/registry/loader.py +100 -0
  319. agentinputlab/registry/result_manifest.py +184 -0
  320. agentinputlab/registry/runtime.py +195 -0
  321. agentinputlab/registry/schema.py +449 -0
  322. agentinputlab/runner/__init__.py +30 -0
  323. agentinputlab/runner/balance.py +248 -0
  324. agentinputlab/runner/cache.py +135 -0
  325. agentinputlab/runner/composite.py +130 -0
  326. agentinputlab/runner/engine.py +285 -0
  327. agentinputlab/runner/runtime_guard.py +268 -0
  328. agentinputlab/runner/worker.py +80 -0
  329. agentinputlab/runtime_bootstrap.py +288 -0
  330. agentinputlab/selectors/__init__.py +196 -0
  331. agentinputlab/selectors/advanced.py +1514 -0
  332. agentinputlab/selectors/artifacts.py +1087 -0
  333. agentinputlab/selectors/base.py +112 -0
  334. agentinputlab/selectors/campaigns.py +1893 -0
  335. agentinputlab/selectors/catalog.py +86 -0
  336. agentinputlab/selectors/discovery.py +1860 -0
  337. agentinputlab/selectors/evaluation.py +1348 -0
  338. agentinputlab/selectors/features.py +383 -0
  339. agentinputlab/selectors/learned.py +502 -0
  340. agentinputlab/selectors/no_op.py +28 -0
  341. agentinputlab/selectors/outcome_table.py +475 -0
  342. agentinputlab/selectors/producer.py +853 -0
  343. agentinputlab/selectors/rules.py +109 -0
  344. agentinputlab/selectors/runtime.py +420 -0
  345. agentinputlab/studies/__init__.py +16 -0
  346. agentinputlab/studies/design.py +165 -0
  347. agentinputlab/studies/paired.py +322 -0
  348. agentinputlab/studio/__init__.py +6 -0
  349. agentinputlab/studio/campaigns.py +819 -0
  350. agentinputlab/studio/engine.py +1408 -0
  351. agentinputlab/studio/model_backends.py +425 -0
  352. agentinputlab/studio/prepared.py +410 -0
  353. agentinputlab/studio/server.py +613 -0
  354. agentinputlab/studio/static/app.js +397 -0
  355. agentinputlab/studio/static/campaigns.html +175 -0
  356. agentinputlab/studio/static/campaigns.js +426 -0
  357. agentinputlab/studio/static/index.html +192 -0
  358. agentinputlab/studio/static/result.html +103 -0
  359. agentinputlab/studio/static/result.js +273 -0
  360. agentinputlab/studio/static/styles.css +467 -0
  361. agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
  362. agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
  363. agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
  364. agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
  365. agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
  366. agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  367. agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  368. agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
  369. agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
  370. workers/README.md +94 -0
  371. workers/ace/README.md +261 -0
  372. workers/ace/__init__.py +1 -0
  373. workers/ace/compatibility_manifest.json +39 -0
  374. workers/ace/driver.py +1682 -0
  375. workers/acon/README.md +215 -0
  376. workers/acon/__init__.py +5 -0
  377. workers/acon/compatibility_manifest.json +93 -0
  378. workers/acon/driver.py +1859 -0
  379. workers/acon/fitted.py +395 -0
  380. workers/acon/guideline.py +470 -0
  381. workers/acon/guideline_source_manifest.json +20 -0
  382. workers/adaptive_rag/README.md +96 -0
  383. workers/adaptive_rag/__init__.py +15 -0
  384. workers/adaptive_rag/compatibility_manifest.json +61 -0
  385. workers/adaptive_rag/driver.py +1470 -0
  386. workers/adaptive_rag/fixture_runtime.py +163 -0
  387. workers/adaptive_rag/runtime.py +252 -0
  388. workers/ask_or_assume/README.md +124 -0
  389. workers/ask_or_assume/__init__.py +1 -0
  390. workers/ask_or_assume/compatibility_manifest.json +36 -0
  391. workers/ask_or_assume/driver.py +1227 -0
  392. workers/decomp/README.md +133 -0
  393. workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
  394. workers/decomp/__init__.py +5 -0
  395. workers/decomp/compatibility_manifest.json +205 -0
  396. workers/decomp/driver.py +1015 -0
  397. workers/decomp/live.py +137 -0
  398. workers/decomp/runtime.py +231 -0
  399. workers/infocqr/README.md +106 -0
  400. workers/infocqr/__init__.py +1 -0
  401. workers/infocqr/compatibility_manifest.json +84 -0
  402. workers/infocqr/driver.py +1766 -0
  403. workers/ircot/README.md +167 -0
  404. workers/ircot/__init__.py +7 -0
  405. workers/ircot/compatibility_manifest.json +121 -0
  406. workers/ircot/controlled.py +457 -0
  407. workers/ircot/driver.py +1373 -0
  408. workers/ircot/fixture_bindings.json +39 -0
  409. workers/ircot/fixture_config.json +48 -0
  410. workers/ircot/fixture_prompt.txt +2 -0
  411. workers/ircot/native_qa.py +398 -0
  412. workers/ircot/qa_compatibility_manifest.json +57 -0
  413. workers/ircot/runtime.py +97 -0
  414. workers/llmcompiler/README.md +160 -0
  415. workers/llmcompiler/__init__.py +1 -0
  416. workers/llmcompiler/compatibility_manifest.json +45 -0
  417. workers/llmcompiler/driver.py +1514 -0
  418. workers/llmcompiler/export_prompts.py +74 -0
  419. workers/llmcompiler/live.py +174 -0
  420. workers/llmcompiler/patch_manifest.json +147 -0
  421. workers/mce/README.md +198 -0
  422. workers/mce/__init__.py +29 -0
  423. workers/mce/compatibility_manifest.json +115 -0
  424. workers/mce/driver.py +2585 -0
  425. workers/mce/final_eval.py +173 -0
  426. workers/official_arenas/README.md +25 -0
  427. workers/official_arenas/__init__.py +1 -0
  428. workers/official_arenas/capture_records.py +110 -0
  429. workers/official_arenas/driver.py +619 -0
  430. workers/official_arenas/protocol.py +334 -0
  431. workers/official_arenas/providers/__init__.py +1 -0
  432. workers/official_arenas/providers/_live_history.py +228 -0
  433. workers/official_arenas/providers/_message_compat.py +77 -0
  434. workers/official_arenas/providers/appworld_ul.py +196 -0
  435. workers/official_arenas/providers/fixture.py +112 -0
  436. workers/official_arenas/providers/statebench.py +960 -0
  437. workers/official_arenas/providers/tau3.py +1672 -0
  438. workers/official_arenas/providers/tau_retail.py +586 -0
  439. workers/official_arenas/providers/toolsandbox.py +1321 -0
  440. workers/official_arenas/providers/tooltalk.py +699 -0
  441. workers/official_arenas/retail_capture.py +188 -0
  442. workers/official_arenas/retail_driver.py +275 -0
  443. workers/qrag/README.md +172 -0
  444. workers/qrag/__init__.py +1 -0
  445. workers/qrag/compatibility_manifest.json +64 -0
  446. workers/qrag/driver.py +673 -0
  447. workers/r2ag/README.md +154 -0
  448. workers/r2ag/__init__.py +5 -0
  449. workers/r2ag/compatibility_manifest.json +55 -0
  450. workers/r2ag/driver.py +1287 -0
  451. workers/r2ag/feature_schema.json +8 -0
  452. workers/r2ag/runtime.py +589 -0
  453. workers/recomp/README.md +120 -0
  454. workers/recomp/__init__.py +1 -0
  455. workers/recomp/compatibility_manifest.json +43 -0
  456. workers/recomp/driver.py +1653 -0
  457. workers/reformer/README.md +125 -0
  458. workers/reformer/__init__.py +1 -0
  459. workers/reformer/compatibility_manifest.json +68 -0
  460. workers/reformer/driver.py +1459 -0
  461. workers/reformer/induction.py +457 -0
  462. workers/rrr/README.md +126 -0
  463. workers/rrr/__init__.py +1 -0
  464. workers/rrr/compatibility_manifest.json +57 -0
  465. workers/rrr/driver.py +1133 -0
  466. workers/smartsearch/README.md +331 -0
  467. workers/smartsearch/__init__.py +1 -0
  468. workers/smartsearch/compatibility_manifest.json +41 -0
  469. workers/smartsearch/driver.py +1643 -0
@@ -0,0 +1,1087 @@
1
+ """Safe JSON codecs and append-only storage for frozen selector policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import os
8
+ import re
9
+ import tempfile
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from types import MappingProxyType
14
+ from typing import Any
15
+
16
+ from agentinputlab.contracts.enums import SemanticFamily, StrEnum
17
+ from agentinputlab.contracts.errors import ArtifactError, ValidationError
18
+ from agentinputlab.contracts.serialization import canonical_json, content_hash, freeze_mapping
19
+
20
+ from .advanced import (
21
+ CostRiskAwareSelector,
22
+ GBDTTLearnerSelector,
23
+ KNNTrainingRow,
24
+ KNNUtilitySelector,
25
+ LogisticTLearnerSelector,
26
+ RidgeTLearnerSelector,
27
+ )
28
+ from .base import Selector
29
+ from .catalog import get_selector_card
30
+ from .learned import RidgeUtilitySelector
31
+ from .rules import RuleSelector
32
+ from .runtime import SelectorArtifactIdentity
33
+
34
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
35
+ _SAFE_SEGMENT = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
36
+ _CORE_FAMILIES = frozenset(
37
+ {
38
+ SemanticFamily.REFORMULATE,
39
+ SemanticFamily.DECOMPOSE,
40
+ SemanticFamily.ACQUIRE,
41
+ SemanticFamily.CLARIFY,
42
+ SemanticFamily.CURATE,
43
+ }
44
+ )
45
+ _SELECTOR_KIND_BY_TYPE = {
46
+ RuleSelector: "rule",
47
+ RidgeUtilitySelector: "ridge_utility",
48
+ RidgeTLearnerSelector: "ridge_t_learner",
49
+ LogisticTLearnerSelector: "logistic_t_learner",
50
+ GBDTTLearnerSelector: "gbdt_t_learner",
51
+ KNNUtilitySelector: "knn_utility",
52
+ CostRiskAwareSelector: "cost_risk_guardrail",
53
+ }
54
+ _CARD_BY_KIND = {
55
+ "rule": "rule.v1",
56
+ "ridge_utility": "ridge_utility.v1",
57
+ "ridge_t_learner": "ridge_t_learner.v1",
58
+ "logistic_t_learner": "logistic_t_learner.v1",
59
+ "gbdt_t_learner": "gbdt_t_learner.v1",
60
+ "knn_utility": "knn_utility.v1",
61
+ "cost_risk_guardrail": "cost_risk_guardrail.v1",
62
+ }
63
+
64
+
65
+ class SelectorArtifactStage(StrEnum):
66
+ FITTED = "fitted"
67
+ CALIBRATED = "calibrated"
68
+ FROZEN = "frozen"
69
+
70
+
71
+ def selector_model_hash(selector: Selector) -> str:
72
+ declared = getattr(selector, "model_hash", None)
73
+ if not isinstance(declared, str) or not _SHA256.fullmatch(declared):
74
+ raise ValidationError("selector must expose a lowercase SHA-256 model_hash")
75
+ return declared
76
+
77
+
78
+ def selector_card_id(selector: Selector) -> str:
79
+ for selector_type, kind in _SELECTOR_KIND_BY_TYPE.items():
80
+ if type(selector) is selector_type:
81
+ return _CARD_BY_KIND[kind]
82
+ raise ValidationError(f"unsupported selector artifact type: {type(selector).__name__}")
83
+
84
+
85
+ def selector_to_artifact(selector: Selector) -> dict[str, Any]:
86
+ """Encode one built-in selector without pickle or executable objects."""
87
+
88
+ kind = _kind_for_selector(selector)
89
+ payload: dict[str, Any]
90
+ if isinstance(selector, RuleSelector):
91
+ payload = {
92
+ "threshold": selector.threshold,
93
+ "margin": selector.margin,
94
+ }
95
+ elif isinstance(selector, RidgeUtilitySelector):
96
+ payload = {
97
+ "coefficients": _encode_family_vectors(selector.coefficients),
98
+ "release_by_family": _encode_releases(selector.release_by_family),
99
+ "min_gain": selector.min_gain,
100
+ "margin": selector.margin,
101
+ }
102
+ elif isinstance(selector, RidgeTLearnerSelector):
103
+ payload = {
104
+ "outcome_coefficients": _encode_family_vectors(selector.outcome_coefficients),
105
+ "release_by_family": _encode_releases(selector.release_by_family),
106
+ "ridge": selector.ridge,
107
+ "min_gain": selector.min_gain,
108
+ "margin": selector.margin,
109
+ }
110
+ elif isinstance(selector, LogisticTLearnerSelector):
111
+ payload = {
112
+ "outcome_coefficients": _encode_family_vectors(selector.outcome_coefficients),
113
+ "release_by_family": _encode_releases(selector.release_by_family),
114
+ "l2": selector.l2,
115
+ "learning_rate": selector.learning_rate,
116
+ "iterations": selector.iterations,
117
+ "min_gain": selector.min_gain,
118
+ "margin": selector.margin,
119
+ }
120
+ elif isinstance(selector, GBDTTLearnerSelector):
121
+ payload = selector.to_artifact()
122
+ elif isinstance(selector, KNNUtilitySelector):
123
+ payload = {
124
+ "training_rows": [
125
+ {
126
+ "feature_vector": list(row.feature_vector),
127
+ "gains": {
128
+ family.value: value
129
+ for family, value in sorted(
130
+ row.gains.items(),
131
+ key=lambda item: item[0].value,
132
+ )
133
+ },
134
+ }
135
+ for row in selector.training_rows
136
+ ],
137
+ "release_by_family": _encode_releases(selector.release_by_family),
138
+ "k": selector.k,
139
+ "distance_power": selector.distance_power,
140
+ "min_gain": selector.min_gain,
141
+ "margin": selector.margin,
142
+ }
143
+ elif isinstance(selector, CostRiskAwareSelector):
144
+ payload = {
145
+ "base_selector": selector_to_artifact(selector.base_selector),
146
+ "release_by_family": _encode_releases(selector.release_by_family),
147
+ "cost_by_family": _encode_family_scalars(selector.cost_by_family),
148
+ "risk_by_family": _encode_family_scalars(selector.risk_by_family),
149
+ "max_cost": selector.max_cost,
150
+ "max_risk": selector.max_risk,
151
+ "cost_weight": selector.cost_weight,
152
+ "risk_weight": selector.risk_weight,
153
+ "min_gain": selector.min_gain,
154
+ "margin": selector.margin,
155
+ }
156
+ else: # pragma: no cover - guarded by _kind_for_selector
157
+ raise ValidationError(f"unsupported selector artifact type: {type(selector).__name__}")
158
+ return {
159
+ "schema_version": 1,
160
+ "selector_kind": kind,
161
+ "payload": payload,
162
+ }
163
+
164
+
165
+ def selector_from_artifact(value: Mapping[str, Any]) -> Selector:
166
+ """Strictly decode a built-in selector from canonical JSON-shaped data."""
167
+
168
+ artifact = _strict_mapping(
169
+ value,
170
+ expected={"schema_version", "selector_kind", "payload"},
171
+ label="selector artifact",
172
+ )
173
+ if _integer(artifact["schema_version"], label="schema_version") != 1:
174
+ raise ValidationError("unsupported selector artifact schema")
175
+ kind = _text(artifact["selector_kind"], label="selector_kind")
176
+ if kind not in _CARD_BY_KIND:
177
+ raise ValidationError(f"unsupported selector artifact kind: {kind}")
178
+ payload_value = artifact["payload"]
179
+ if not isinstance(payload_value, Mapping):
180
+ raise ValidationError("selector artifact payload must be an object")
181
+
182
+ try:
183
+ if kind == "rule":
184
+ payload = _strict_mapping(
185
+ payload_value,
186
+ expected={"threshold", "margin"},
187
+ label="rule selector payload",
188
+ )
189
+ selector: Selector = RuleSelector(
190
+ threshold=_number(payload["threshold"], label="threshold"),
191
+ margin=_number(payload["margin"], label="margin"),
192
+ )
193
+ elif kind == "ridge_utility":
194
+ payload = _strict_mapping(
195
+ payload_value,
196
+ expected={
197
+ "coefficients",
198
+ "release_by_family",
199
+ "min_gain",
200
+ "margin",
201
+ },
202
+ label="ridge utility payload",
203
+ )
204
+ selector = RidgeUtilitySelector(
205
+ _decode_family_vectors(payload["coefficients"], include_no_op=False),
206
+ _decode_releases(payload["release_by_family"]),
207
+ min_gain=_number(payload["min_gain"], label="min_gain"),
208
+ margin=_number(payload["margin"], label="margin"),
209
+ )
210
+ elif kind == "ridge_t_learner":
211
+ payload = _strict_mapping(
212
+ payload_value,
213
+ expected={
214
+ "outcome_coefficients",
215
+ "release_by_family",
216
+ "ridge",
217
+ "min_gain",
218
+ "margin",
219
+ },
220
+ label="ridge T-learner payload",
221
+ )
222
+ selector = RidgeTLearnerSelector(
223
+ _decode_family_vectors(
224
+ payload["outcome_coefficients"],
225
+ include_no_op=True,
226
+ ),
227
+ _decode_releases(payload["release_by_family"]),
228
+ ridge=_number(payload["ridge"], label="ridge"),
229
+ min_gain=_number(payload["min_gain"], label="min_gain"),
230
+ margin=_number(payload["margin"], label="margin"),
231
+ )
232
+ elif kind == "logistic_t_learner":
233
+ payload = _strict_mapping(
234
+ payload_value,
235
+ expected={
236
+ "outcome_coefficients",
237
+ "release_by_family",
238
+ "l2",
239
+ "learning_rate",
240
+ "iterations",
241
+ "min_gain",
242
+ "margin",
243
+ },
244
+ label="logistic T-learner payload",
245
+ )
246
+ selector = LogisticTLearnerSelector(
247
+ _decode_family_vectors(
248
+ payload["outcome_coefficients"],
249
+ include_no_op=True,
250
+ ),
251
+ _decode_releases(payload["release_by_family"]),
252
+ l2=_number(payload["l2"], label="l2"),
253
+ learning_rate=_number(
254
+ payload["learning_rate"],
255
+ label="learning_rate",
256
+ ),
257
+ iterations=_integer(payload["iterations"], label="iterations"),
258
+ min_gain=_number(payload["min_gain"], label="min_gain"),
259
+ margin=_number(payload["margin"], label="margin"),
260
+ )
261
+ elif kind == "gbdt_t_learner":
262
+ selector = GBDTTLearnerSelector.from_artifact(payload_value)
263
+ elif kind == "knn_utility":
264
+ payload = _strict_mapping(
265
+ payload_value,
266
+ expected={
267
+ "training_rows",
268
+ "release_by_family",
269
+ "k",
270
+ "distance_power",
271
+ "min_gain",
272
+ "margin",
273
+ },
274
+ label="KNN selector payload",
275
+ )
276
+ raw_rows = _array(payload["training_rows"], label="training_rows")
277
+ rows: list[KNNTrainingRow] = []
278
+ for raw_row in raw_rows:
279
+ row = _strict_mapping(
280
+ raw_row,
281
+ expected={"feature_vector", "gains"},
282
+ label="KNN training row",
283
+ )
284
+ rows.append(
285
+ KNNTrainingRow(
286
+ tuple(
287
+ _number(item, label="feature_vector item")
288
+ for item in _array(
289
+ row["feature_vector"],
290
+ label="feature_vector",
291
+ )
292
+ ),
293
+ _decode_family_scalars(
294
+ row["gains"],
295
+ include_no_op=False,
296
+ label="KNN gains",
297
+ ),
298
+ )
299
+ )
300
+ selector = KNNUtilitySelector(
301
+ tuple(rows),
302
+ _decode_releases(payload["release_by_family"]),
303
+ k=_integer(payload["k"], label="k"),
304
+ distance_power=_number(
305
+ payload["distance_power"],
306
+ label="distance_power",
307
+ ),
308
+ min_gain=_number(payload["min_gain"], label="min_gain"),
309
+ margin=_number(payload["margin"], label="margin"),
310
+ )
311
+ else:
312
+ payload = _strict_mapping(
313
+ payload_value,
314
+ expected={
315
+ "base_selector",
316
+ "release_by_family",
317
+ "cost_by_family",
318
+ "risk_by_family",
319
+ "max_cost",
320
+ "max_risk",
321
+ "cost_weight",
322
+ "risk_weight",
323
+ "min_gain",
324
+ "margin",
325
+ },
326
+ label="cost/risk selector payload",
327
+ )
328
+ raw_base = payload["base_selector"]
329
+ if not isinstance(raw_base, Mapping):
330
+ raise ValidationError("base_selector must be an object")
331
+ selector = CostRiskAwareSelector(
332
+ selector_from_artifact(raw_base),
333
+ _decode_releases(payload["release_by_family"]),
334
+ _decode_family_scalars(
335
+ payload["cost_by_family"],
336
+ include_no_op=False,
337
+ label="cost_by_family",
338
+ ),
339
+ _decode_family_scalars(
340
+ payload["risk_by_family"],
341
+ include_no_op=False,
342
+ label="risk_by_family",
343
+ ),
344
+ max_cost=_number(payload["max_cost"], label="max_cost"),
345
+ max_risk=_number(payload["max_risk"], label="max_risk"),
346
+ cost_weight=_number(payload["cost_weight"], label="cost_weight"),
347
+ risk_weight=_number(payload["risk_weight"], label="risk_weight"),
348
+ min_gain=_number(payload["min_gain"], label="min_gain"),
349
+ margin=_number(payload["margin"], label="margin"),
350
+ )
351
+ except (TypeError, ValueError) as exc:
352
+ if isinstance(exc, ValidationError):
353
+ raise
354
+ raise ValidationError(f"invalid {kind} selector artifact") from exc
355
+
356
+ if selector_card_id(selector) != _CARD_BY_KIND[kind]:
357
+ raise ValidationError("decoded selector type differs from its artifact kind")
358
+ return selector
359
+
360
+
361
+ @dataclass(frozen=True, slots=True)
362
+ class SelectorArtifactBundle:
363
+ """One content-addressed selector plus all inference-critical bindings."""
364
+
365
+ selector_card_id: str
366
+ stage: SelectorArtifactStage
367
+ revision: str
368
+ model_payload: Mapping[str, Any]
369
+ model_hash: str
370
+ extractor_id: str
371
+ extractor_revision: str
372
+ extractor_hash: str
373
+ feature_schema_id: str
374
+ feature_schema_hash: str
375
+ representative_by_family: Mapping[SemanticFamily, str]
376
+ training_table_sha256: str
377
+ split_provenance_hash: str
378
+ comparison_profile_hash: str
379
+ calibration_receipt: Mapping[str, Any] = field(default_factory=dict)
380
+ schema_version: int = 1
381
+ fit_lineage: Mapping[str, Any] = field(default_factory=dict)
382
+
383
+ def __post_init__(self) -> None:
384
+ if self.schema_version != 1 or isinstance(self.schema_version, bool):
385
+ raise ValidationError("unsupported selector bundle schema")
386
+ try:
387
+ get_selector_card(self.selector_card_id)
388
+ except KeyError as exc:
389
+ raise ValidationError("selector bundle references an unknown selector card") from exc
390
+ if not isinstance(self.stage, SelectorArtifactStage):
391
+ raise ValidationError("selector bundle stage must be SelectorArtifactStage")
392
+ if (
393
+ not isinstance(self.revision, str)
394
+ or not _SAFE_SEGMENT.fullmatch(self.revision)
395
+ or ".." in self.revision
396
+ ):
397
+ raise ValidationError("selector bundle revision must be a safe flat identifier")
398
+ for label in (
399
+ "extractor_id",
400
+ "extractor_revision",
401
+ "feature_schema_id",
402
+ ):
403
+ value = getattr(self, label)
404
+ if not isinstance(value, str) or not value.strip():
405
+ raise ValidationError(f"selector bundle {label} must be non-empty")
406
+ for label in (
407
+ "model_hash",
408
+ "extractor_hash",
409
+ "feature_schema_hash",
410
+ "training_table_sha256",
411
+ "split_provenance_hash",
412
+ "comparison_profile_hash",
413
+ ):
414
+ value = getattr(self, label)
415
+ if not isinstance(value, str) or not _SHA256.fullmatch(value):
416
+ raise ValidationError(f"selector bundle {label} must be SHA-256")
417
+ try:
418
+ normalized_payload = json.loads(canonical_json(self.model_payload))
419
+ normalized_receipt = json.loads(canonical_json(self.calibration_receipt))
420
+ normalized_lineage = json.loads(canonical_json(self.fit_lineage))
421
+ frozen_payload = freeze_mapping(normalized_payload)
422
+ receipt = freeze_mapping(normalized_receipt)
423
+ lineage = freeze_mapping(normalized_lineage)
424
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
425
+ raise ValidationError("selector bundle values must be canonical JSON") from exc
426
+ representatives = _validated_representatives(self.representative_by_family)
427
+ selector = selector_from_artifact(normalized_payload)
428
+ if selector_card_id(selector) != self.selector_card_id:
429
+ raise ValidationError("selector bundle card and model payload differ")
430
+ if selector_model_hash(selector) != self.model_hash:
431
+ raise ValidationError("selector bundle model hash does not match its payload")
432
+ declared_releases = getattr(selector, "release_by_family", None)
433
+ if isinstance(declared_releases, Mapping) and dict(declared_releases) != representatives:
434
+ raise ValidationError(
435
+ "selector bundle representatives differ from model release mapping"
436
+ )
437
+ if self.stage is SelectorArtifactStage.FITTED and receipt:
438
+ raise ValidationError("fitted selector bundles cannot contain calibration receipts")
439
+ if self.stage is not SelectorArtifactStage.FITTED and not receipt:
440
+ raise ValidationError(
441
+ "calibrated/frozen selector bundles require a calibration receipt"
442
+ )
443
+ _validate_fit_lineage(
444
+ lineage,
445
+ selector=selector,
446
+ selector_card=self.selector_card_id,
447
+ )
448
+ object.__setattr__(self, "model_payload", frozen_payload)
449
+ object.__setattr__(self, "calibration_receipt", receipt)
450
+ object.__setattr__(self, "fit_lineage", lineage)
451
+ object.__setattr__(
452
+ self,
453
+ "representative_by_family",
454
+ MappingProxyType(representatives),
455
+ )
456
+
457
+ @classmethod
458
+ def create(
459
+ cls,
460
+ selector: Selector,
461
+ *,
462
+ stage: SelectorArtifactStage,
463
+ revision: str,
464
+ extractor_id: str,
465
+ extractor_revision: str,
466
+ extractor_hash: str,
467
+ feature_schema_id: str,
468
+ feature_schema_hash: str,
469
+ representative_by_family: Mapping[SemanticFamily, str],
470
+ training_table_sha256: str,
471
+ split_provenance_hash: str,
472
+ comparison_profile_hash: str,
473
+ calibration_receipt: Mapping[str, Any] | None = None,
474
+ fit_lineage: Mapping[str, Any] | None = None,
475
+ ) -> SelectorArtifactBundle:
476
+ return cls(
477
+ selector_card_id=selector_card_id(selector),
478
+ stage=stage,
479
+ revision=revision,
480
+ model_payload=selector_to_artifact(selector),
481
+ model_hash=selector_model_hash(selector),
482
+ extractor_id=extractor_id,
483
+ extractor_revision=extractor_revision,
484
+ extractor_hash=extractor_hash,
485
+ feature_schema_id=feature_schema_id,
486
+ feature_schema_hash=feature_schema_hash,
487
+ representative_by_family=representative_by_family,
488
+ training_table_sha256=training_table_sha256,
489
+ split_provenance_hash=split_provenance_hash,
490
+ comparison_profile_hash=comparison_profile_hash,
491
+ calibration_receipt=calibration_receipt or {},
492
+ fit_lineage=fit_lineage or {},
493
+ )
494
+
495
+ @property
496
+ def bundle_hash(self) -> str:
497
+ return content_hash(self.to_artifact())
498
+
499
+ def selector(self) -> Selector:
500
+ normalized = json.loads(canonical_json(self.model_payload))
501
+ return selector_from_artifact(normalized)
502
+
503
+ def to_artifact(self) -> dict[str, Any]:
504
+ artifact = {
505
+ "schema_version": self.schema_version,
506
+ "selector_card_id": self.selector_card_id,
507
+ "stage": self.stage.value,
508
+ "revision": self.revision,
509
+ "model_payload": self.model_payload,
510
+ "model_hash": self.model_hash,
511
+ "extractor": {
512
+ "id": self.extractor_id,
513
+ "revision": self.extractor_revision,
514
+ "sha256": self.extractor_hash,
515
+ },
516
+ "feature_schema": {
517
+ "id": self.feature_schema_id,
518
+ "sha256": self.feature_schema_hash,
519
+ },
520
+ "representative_by_family": _encode_releases(self.representative_by_family),
521
+ "training_table_sha256": self.training_table_sha256,
522
+ "split_provenance_hash": self.split_provenance_hash,
523
+ "comparison_profile_hash": self.comparison_profile_hash,
524
+ "calibration_receipt": self.calibration_receipt,
525
+ }
526
+ if self.fit_lineage:
527
+ artifact["fit_lineage"] = self.fit_lineage
528
+ return artifact
529
+
530
+ @classmethod
531
+ def from_artifact(cls, value: Mapping[str, Any]) -> SelectorArtifactBundle:
532
+ artifact = _string_mapping(value, label="selector bundle")
533
+ required_fields = {
534
+ "schema_version",
535
+ "selector_card_id",
536
+ "stage",
537
+ "revision",
538
+ "model_payload",
539
+ "model_hash",
540
+ "extractor",
541
+ "feature_schema",
542
+ "representative_by_family",
543
+ "training_table_sha256",
544
+ "split_provenance_hash",
545
+ "comparison_profile_hash",
546
+ "calibration_receipt",
547
+ }
548
+ if set(artifact) not in (required_fields, required_fields | {"fit_lineage"}):
549
+ raise ValidationError(
550
+ "selector bundle fields differ: expected the legacy fields with "
551
+ "optional fit_lineage"
552
+ )
553
+ extractor = _strict_mapping(
554
+ artifact["extractor"],
555
+ expected={"id", "revision", "sha256"},
556
+ label="selector bundle extractor",
557
+ )
558
+ schema = _strict_mapping(
559
+ artifact["feature_schema"],
560
+ expected={"id", "sha256"},
561
+ label="selector bundle feature schema",
562
+ )
563
+ model_payload = artifact["model_payload"]
564
+ calibration_receipt = artifact["calibration_receipt"]
565
+ fit_lineage = artifact.get("fit_lineage", {})
566
+ if (
567
+ not isinstance(model_payload, Mapping)
568
+ or not isinstance(calibration_receipt, Mapping)
569
+ or not isinstance(fit_lineage, Mapping)
570
+ ):
571
+ raise ValidationError("selector bundle model/receipt values must be objects")
572
+ try:
573
+ stage = SelectorArtifactStage(_text(artifact["stage"], label="stage"))
574
+ except ValueError as exc:
575
+ raise ValidationError("selector bundle stage is unsupported") from exc
576
+ return cls(
577
+ schema_version=_integer(
578
+ artifact["schema_version"],
579
+ label="schema_version",
580
+ ),
581
+ selector_card_id=_text(
582
+ artifact["selector_card_id"],
583
+ label="selector_card_id",
584
+ ),
585
+ stage=stage,
586
+ revision=_text(artifact["revision"], label="revision"),
587
+ model_payload=model_payload,
588
+ model_hash=_text(artifact["model_hash"], label="model_hash"),
589
+ extractor_id=_text(extractor["id"], label="extractor.id"),
590
+ extractor_revision=_text(
591
+ extractor["revision"],
592
+ label="extractor.revision",
593
+ ),
594
+ extractor_hash=_text(extractor["sha256"], label="extractor.sha256"),
595
+ feature_schema_id=_text(schema["id"], label="feature_schema.id"),
596
+ feature_schema_hash=_text(
597
+ schema["sha256"],
598
+ label="feature_schema.sha256",
599
+ ),
600
+ representative_by_family=_decode_releases(artifact["representative_by_family"]),
601
+ training_table_sha256=_text(
602
+ artifact["training_table_sha256"],
603
+ label="training_table_sha256",
604
+ ),
605
+ split_provenance_hash=_text(
606
+ artifact["split_provenance_hash"],
607
+ label="split_provenance_hash",
608
+ ),
609
+ comparison_profile_hash=_text(
610
+ artifact["comparison_profile_hash"],
611
+ label="comparison_profile_hash",
612
+ ),
613
+ calibration_receipt=calibration_receipt,
614
+ fit_lineage=fit_lineage,
615
+ )
616
+
617
+
618
+ def _validate_fit_lineage(
619
+ value: Mapping[str, Any],
620
+ *,
621
+ selector: Selector,
622
+ selector_card: str,
623
+ ) -> None:
624
+ if not value:
625
+ return
626
+ lineage = _string_mapping(value, label="selector fit_lineage")
627
+ common_fields = {
628
+ "schema_version",
629
+ "kind",
630
+ "discovery",
631
+ "fit_config_sha256",
632
+ }
633
+ kind = _text(lineage.get("kind"), label="fit_lineage.kind")
634
+ if kind == "selector_fit":
635
+ if set(lineage) != common_fields:
636
+ raise ValidationError("selector_fit lineage fields differ")
637
+ if selector_card == _CARD_BY_KIND["cost_risk_guardrail"]:
638
+ raise ValidationError("cost/risk guardrail lineage must bind its exact base bundle")
639
+ elif kind == "cost_risk_guardrail_base_bundle":
640
+ if set(lineage) != common_fields | {"base_selector_bundle"}:
641
+ raise ValidationError("cost/risk selector fit_lineage fields differ")
642
+ if selector_card != _CARD_BY_KIND["cost_risk_guardrail"] or not isinstance(
643
+ selector,
644
+ CostRiskAwareSelector,
645
+ ):
646
+ raise ValidationError("guardrail base lineage is valid only for a cost/risk selector")
647
+ else:
648
+ raise ValidationError("selector fit_lineage kind is unsupported")
649
+ if _integer(lineage["schema_version"], label="fit_lineage.schema_version") != 1:
650
+ raise ValidationError("unsupported selector fit_lineage schema")
651
+ discovery = _strict_mapping(
652
+ lineage["discovery"],
653
+ expected={"revision", "sha256"},
654
+ label="selector fit_lineage discovery",
655
+ )
656
+ for label, value_to_check in (
657
+ ("fit_lineage.fit_config_sha256", lineage["fit_config_sha256"]),
658
+ ("fit_lineage.discovery.sha256", discovery["sha256"]),
659
+ ):
660
+ if not isinstance(value_to_check, str) or not _SHA256.fullmatch(value_to_check):
661
+ raise ValidationError(f"{label} must be SHA-256")
662
+ value_to_check = discovery["revision"]
663
+ if (
664
+ not isinstance(value_to_check, str)
665
+ or not _SAFE_SEGMENT.fullmatch(value_to_check)
666
+ or ".." in value_to_check
667
+ ):
668
+ raise ValidationError("fit_lineage.discovery.revision must be a safe flat identifier")
669
+ if kind == "selector_fit":
670
+ return
671
+
672
+ base = _strict_mapping(
673
+ lineage["base_selector_bundle"],
674
+ expected={
675
+ "selector_id",
676
+ "revision",
677
+ "sha256",
678
+ "stage",
679
+ "model_hash",
680
+ },
681
+ label="selector fit_lineage base_selector_bundle",
682
+ )
683
+ for label, value_to_check in (
684
+ ("fit_lineage.base_selector_bundle.sha256", base["sha256"]),
685
+ ("fit_lineage.base_selector_bundle.model_hash", base["model_hash"]),
686
+ ):
687
+ if not isinstance(value_to_check, str) or not _SHA256.fullmatch(value_to_check):
688
+ raise ValidationError(f"{label} must be SHA-256")
689
+ for label, value_to_check in (
690
+ ("fit_lineage.base_selector_bundle.selector_id", base["selector_id"]),
691
+ ("fit_lineage.base_selector_bundle.revision", base["revision"]),
692
+ ):
693
+ if (
694
+ not isinstance(value_to_check, str)
695
+ or not _SAFE_SEGMENT.fullmatch(value_to_check)
696
+ or ".." in value_to_check
697
+ ):
698
+ raise ValidationError(f"{label} must be a safe flat identifier")
699
+ try:
700
+ base_stage = SelectorArtifactStage(
701
+ _text(base["stage"], label="fit_lineage.base_selector_bundle.stage")
702
+ )
703
+ except ValueError as exc:
704
+ raise ValidationError("fit_lineage base selector stage is unsupported") from exc
705
+ if base_stage not in {
706
+ SelectorArtifactStage.FITTED,
707
+ SelectorArtifactStage.CALIBRATED,
708
+ SelectorArtifactStage.FROZEN,
709
+ }: # pragma: no cover - exhaustive enum guard
710
+ raise ValidationError("fit_lineage base selector stage is unsupported")
711
+ embedded_base = selector.base_selector
712
+ if base["selector_id"] != selector_card_id(embedded_base):
713
+ raise ValidationError("fit_lineage base selector card differs from the embedded base model")
714
+ if base["model_hash"] != selector_model_hash(embedded_base):
715
+ raise ValidationError("fit_lineage base model hash differs from the embedded base model")
716
+
717
+
718
+ @dataclass(slots=True)
719
+ class SelectorArtifactStore:
720
+ """Content-addressed selector storage without mutable aliases or ``latest``."""
721
+
722
+ root: Path
723
+
724
+ def __init__(self, root: str | Path) -> None:
725
+ self.root = Path(root).expanduser().resolve()
726
+ self.root.mkdir(parents=True, exist_ok=True)
727
+
728
+ def path_for(self, identity: SelectorArtifactIdentity) -> Path:
729
+ for label, value in (
730
+ ("selector_id", identity.selector_id),
731
+ ("revision", identity.revision),
732
+ ):
733
+ if not _SAFE_SEGMENT.fullmatch(value) or ".." in value:
734
+ raise ArtifactError(f"selector artifact {label} is not a safe path segment")
735
+ candidate = (
736
+ self.root / identity.selector_id / identity.revision / identity.sha256 / "bundle.json"
737
+ ).resolve()
738
+ if self.root not in candidate.parents:
739
+ raise ArtifactError("selector artifact path escaped store root")
740
+ return candidate
741
+
742
+ def save(
743
+ self,
744
+ bundle: SelectorArtifactBundle,
745
+ *,
746
+ previous_identity: SelectorArtifactIdentity | None = None,
747
+ ) -> SelectorArtifactIdentity:
748
+ if not isinstance(bundle, SelectorArtifactBundle):
749
+ raise TypeError("selector artifact store requires SelectorArtifactBundle")
750
+ self._validate_transition(bundle, previous_identity)
751
+ identity = SelectorArtifactIdentity(
752
+ selector_id=bundle.selector_card_id,
753
+ revision=bundle.revision,
754
+ sha256=bundle.bundle_hash,
755
+ )
756
+ destination = self.path_for(identity)
757
+ revision_root = destination.parent.parent
758
+ if revision_root.exists():
759
+ existing_hashes = {
760
+ path.name
761
+ for path in revision_root.iterdir()
762
+ if path.is_dir() and _SHA256.fullmatch(path.name)
763
+ }
764
+ if existing_hashes - {identity.sha256}:
765
+ raise ArtifactError(
766
+ "selector artifact revision is append-only and already has different content"
767
+ )
768
+ payload = (canonical_json(bundle.to_artifact()) + "\n").encode("utf-8")
769
+ if destination.exists():
770
+ if destination.read_bytes() != payload:
771
+ raise ArtifactError("stored selector artifact bytes differ from bundle identity")
772
+ self.load(identity)
773
+ return identity
774
+ destination.parent.mkdir(parents=True, exist_ok=True)
775
+ descriptor, temporary_name = tempfile.mkstemp(
776
+ prefix=".partial-",
777
+ dir=destination.parent,
778
+ )
779
+ try:
780
+ with os.fdopen(descriptor, "wb") as handle:
781
+ handle.write(payload)
782
+ handle.flush()
783
+ os.fsync(handle.fileno())
784
+ try:
785
+ os.link(temporary_name, destination)
786
+ except FileExistsError:
787
+ if destination.read_bytes() != payload:
788
+ raise ArtifactError(
789
+ "concurrent selector artifact materialization differs"
790
+ ) from None
791
+ finally:
792
+ if os.path.exists(temporary_name):
793
+ os.unlink(temporary_name)
794
+ self.load(identity)
795
+ return identity
796
+
797
+ def load(
798
+ self,
799
+ identity: SelectorArtifactIdentity,
800
+ *,
801
+ require_stage: SelectorArtifactStage | None = None,
802
+ ) -> tuple[Selector, SelectorArtifactBundle]:
803
+ if not isinstance(identity, SelectorArtifactIdentity):
804
+ raise TypeError("selector artifact load requires SelectorArtifactIdentity")
805
+ if require_stage is not None and not isinstance(require_stage, SelectorArtifactStage):
806
+ raise TypeError("require_stage must be SelectorArtifactStage or None")
807
+ path = self.path_for(identity)
808
+ if not path.is_file():
809
+ raise ArtifactError(
810
+ f"selector artifact missing: {identity.selector_id}@{identity.revision}"
811
+ )
812
+ try:
813
+ raw = json.loads(path.read_text(encoding="utf-8"))
814
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
815
+ raise ArtifactError("selector artifact is not valid UTF-8 JSON") from exc
816
+ if not isinstance(raw, Mapping):
817
+ raise ArtifactError("selector artifact root must be an object")
818
+ try:
819
+ bundle = SelectorArtifactBundle.from_artifact(raw)
820
+ except (TypeError, ValueError) as exc:
821
+ raise ArtifactError("selector artifact bundle failed validation") from exc
822
+ if (
823
+ bundle.selector_card_id != identity.selector_id
824
+ or bundle.revision != identity.revision
825
+ or bundle.bundle_hash != identity.sha256
826
+ ):
827
+ raise ArtifactError("selector artifact identity does not match stored content")
828
+ if require_stage is not None and bundle.stage is not require_stage:
829
+ raise ArtifactError(
830
+ f"selector artifact stage is {bundle.stage.value}, "
831
+ f"not required {require_stage.value}"
832
+ )
833
+ return bundle.selector(), bundle
834
+
835
+ def _validate_transition(
836
+ self,
837
+ bundle: SelectorArtifactBundle,
838
+ previous_identity: SelectorArtifactIdentity | None,
839
+ ) -> None:
840
+ if bundle.stage is SelectorArtifactStage.FITTED:
841
+ if previous_identity is not None:
842
+ raise ArtifactError("fitted selector artifact cannot have a predecessor")
843
+ return
844
+ if previous_identity is None:
845
+ raise ArtifactError(
846
+ f"{bundle.stage.value} selector artifact requires its exact predecessor"
847
+ )
848
+ _, previous = self.load(previous_identity)
849
+ expected_previous = (
850
+ SelectorArtifactStage.FITTED
851
+ if bundle.stage is SelectorArtifactStage.CALIBRATED
852
+ else SelectorArtifactStage.CALIBRATED
853
+ )
854
+ if previous.stage is not expected_previous:
855
+ raise ArtifactError(
856
+ f"selector transition requires {expected_previous.value} predecessor"
857
+ )
858
+ if previous.selector_card_id != bundle.selector_card_id:
859
+ raise ArtifactError("selector transition cannot change selector card")
860
+ if previous.revision == bundle.revision:
861
+ raise ArtifactError("selector lifecycle stages require distinct revisions")
862
+ binding_fields = (
863
+ "extractor_id",
864
+ "extractor_revision",
865
+ "extractor_hash",
866
+ "feature_schema_id",
867
+ "feature_schema_hash",
868
+ "representative_by_family",
869
+ "training_table_sha256",
870
+ "split_provenance_hash",
871
+ "comparison_profile_hash",
872
+ "fit_lineage",
873
+ )
874
+ if any(getattr(previous, name) != getattr(bundle, name) for name in binding_fields):
875
+ raise ArtifactError("selector transition changed a frozen data/runtime binding")
876
+ if bundle.stage is SelectorArtifactStage.CALIBRATED and _calibration_invariant_payload(
877
+ previous.model_payload
878
+ ) != _calibration_invariant_payload(bundle.model_payload):
879
+ raise ArtifactError(
880
+ "selector calibration changed fitted model parameters; only the "
881
+ "predefined calibration/guardrail fields may change"
882
+ )
883
+ if bundle.stage is SelectorArtifactStage.FROZEN and (
884
+ previous.model_hash != bundle.model_hash
885
+ or previous.model_payload != bundle.model_payload
886
+ or previous.calibration_receipt != bundle.calibration_receipt
887
+ ):
888
+ raise ArtifactError("freezing cannot change a calibrated selector policy")
889
+
890
+
891
+ def _calibration_invariant_payload(value: Mapping[str, Any]) -> dict[str, Any]:
892
+ """Return the portion of a selector artifact that calibration cannot change."""
893
+
894
+ normalized = json.loads(canonical_json(value))
895
+ if not isinstance(normalized, dict): # pragma: no cover - bundle validation invariant
896
+ raise ArtifactError("selector model payload must be an object")
897
+ kind = normalized.get("selector_kind")
898
+ payload = normalized.get("payload")
899
+ if not isinstance(kind, str) or not isinstance(payload, dict):
900
+ raise ArtifactError("selector model payload has an invalid calibration schema")
901
+ direct_fields = {
902
+ "rule": {"threshold", "margin"},
903
+ "ridge_utility": {"min_gain", "margin"},
904
+ "ridge_t_learner": {"min_gain", "margin"},
905
+ "logistic_t_learner": {"min_gain", "margin"},
906
+ "knn_utility": {"min_gain", "margin"},
907
+ "cost_risk_guardrail": {
908
+ "max_cost",
909
+ "max_risk",
910
+ "min_gain",
911
+ "margin",
912
+ },
913
+ }
914
+ if kind == "gbdt_t_learner":
915
+ abstention = payload.get("abstention")
916
+ if not isinstance(abstention, dict):
917
+ raise ArtifactError("GBDT selector payload lacks abstention fields")
918
+ for field in ("min_gain", "margin"):
919
+ abstention.pop(field, None)
920
+ return normalized
921
+ try:
922
+ allowed = direct_fields[kind]
923
+ except KeyError as exc: # pragma: no cover - selector codec rejects unknown kinds
924
+ raise ArtifactError(f"unsupported selector calibration kind: {kind}") from exc
925
+ for field in allowed:
926
+ payload.pop(field, None)
927
+ return normalized
928
+
929
+
930
+ def _kind_for_selector(selector: Selector) -> str:
931
+ try:
932
+ return _SELECTOR_KIND_BY_TYPE[type(selector)]
933
+ except KeyError as exc:
934
+ raise ValidationError(
935
+ f"unsupported selector artifact type: {type(selector).__name__}"
936
+ ) from exc
937
+
938
+
939
+ def _encode_family_vectors(
940
+ values: Mapping[SemanticFamily, tuple[float, ...]],
941
+ ) -> dict[str, list[float]]:
942
+ return {
943
+ family.value: list(vector)
944
+ for family, vector in sorted(values.items(), key=lambda item: item[0].value)
945
+ }
946
+
947
+
948
+ def _encode_releases(values: Mapping[SemanticFamily, str]) -> dict[str, str]:
949
+ return {
950
+ family.value: release
951
+ for family, release in sorted(values.items(), key=lambda item: item[0].value)
952
+ }
953
+
954
+
955
+ def _encode_family_scalars(
956
+ values: Mapping[SemanticFamily, float],
957
+ ) -> dict[str, float]:
958
+ return {
959
+ family.value: value
960
+ for family, value in sorted(values.items(), key=lambda item: item[0].value)
961
+ }
962
+
963
+
964
+ def _decode_family_vectors(
965
+ value: object,
966
+ *,
967
+ include_no_op: bool,
968
+ ) -> dict[SemanticFamily, tuple[float, ...]]:
969
+ mapping = _string_mapping(value, label="family vectors")
970
+ return {
971
+ _family(name, include_no_op=include_no_op): tuple(
972
+ _number(item, label=f"{name} coefficient")
973
+ for item in _array(vector, label=f"{name} coefficients")
974
+ )
975
+ for name, vector in mapping.items()
976
+ }
977
+
978
+
979
+ def _decode_releases(value: object) -> dict[SemanticFamily, str]:
980
+ mapping = _string_mapping(value, label="release_by_family")
981
+ releases = {
982
+ _family(name, include_no_op=False): _text(
983
+ release,
984
+ label=f"{name} release",
985
+ )
986
+ for name, release in mapping.items()
987
+ }
988
+ return _validated_representatives(releases)
989
+
990
+
991
+ def _decode_family_scalars(
992
+ value: object,
993
+ *,
994
+ include_no_op: bool,
995
+ label: str,
996
+ ) -> dict[SemanticFamily, float]:
997
+ mapping = _string_mapping(value, label=label)
998
+ return {
999
+ _family(name, include_no_op=include_no_op): _number(
1000
+ scalar,
1001
+ label=f"{label}.{name}",
1002
+ )
1003
+ for name, scalar in mapping.items()
1004
+ }
1005
+
1006
+
1007
+ def _validated_representatives(
1008
+ values: Mapping[SemanticFamily, str],
1009
+ ) -> dict[SemanticFamily, str]:
1010
+ copied = dict(values)
1011
+ if any(
1012
+ family not in _CORE_FAMILIES
1013
+ or not isinstance(release, str)
1014
+ or not release.strip()
1015
+ or release != release.strip()
1016
+ for family, release in copied.items()
1017
+ ):
1018
+ raise ValidationError(
1019
+ "selector representatives require core families and trimmed release IDs"
1020
+ )
1021
+ return copied
1022
+
1023
+
1024
+ def _family(value: str, *, include_no_op: bool) -> SemanticFamily:
1025
+ try:
1026
+ family = SemanticFamily(value)
1027
+ except ValueError as exc:
1028
+ raise ValidationError(f"unknown selector family: {value!r}") from exc
1029
+ allowed = _CORE_FAMILIES | ({SemanticFamily.NO_OP} if include_no_op else set())
1030
+ if family not in allowed:
1031
+ raise ValidationError(f"family {value!r} is not valid in this selector artifact")
1032
+ return family
1033
+
1034
+
1035
+ def _strict_mapping(
1036
+ value: object,
1037
+ *,
1038
+ expected: set[str],
1039
+ label: str,
1040
+ ) -> dict[str, Any]:
1041
+ mapping = _string_mapping(value, label=label)
1042
+ if set(mapping) != expected:
1043
+ raise ValidationError(
1044
+ f"{label} fields differ: expected {sorted(expected)!r}, got {sorted(mapping)!r}"
1045
+ )
1046
+ return mapping
1047
+
1048
+
1049
+ def _string_mapping(value: object, *, label: str) -> dict[str, Any]:
1050
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
1051
+ raise ValidationError(f"{label} must be an object with string keys")
1052
+ return dict(value)
1053
+
1054
+
1055
+ def _array(value: object, *, label: str) -> list[Any]:
1056
+ if not isinstance(value, list):
1057
+ raise ValidationError(f"{label} must be a JSON array")
1058
+ return value
1059
+
1060
+
1061
+ def _text(value: object, *, label: str) -> str:
1062
+ if not isinstance(value, str) or not value.strip() or value != value.strip():
1063
+ raise ValidationError(f"{label} must be a trimmed non-empty string")
1064
+ return value
1065
+
1066
+
1067
+ def _integer(value: object, *, label: str) -> int:
1068
+ if isinstance(value, bool) or not isinstance(value, int):
1069
+ raise ValidationError(f"{label} must be a JSON integer")
1070
+ return value
1071
+
1072
+
1073
+ def _number(value: object, *, label: str) -> float:
1074
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
1075
+ raise ValidationError(f"{label} must be a finite JSON number")
1076
+ return float(value)
1077
+
1078
+
1079
+ __all__ = [
1080
+ "SelectorArtifactBundle",
1081
+ "SelectorArtifactStage",
1082
+ "SelectorArtifactStore",
1083
+ "selector_card_id",
1084
+ "selector_from_artifact",
1085
+ "selector_model_hash",
1086
+ "selector_to_artifact",
1087
+ ]