agentevolve-optimizer 0.5.0__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 (414) hide show
  1. agent_evolve/__init__.py +722 -0
  2. agent_evolve/agentic.py +2800 -0
  3. agent_evolve/api.py +767 -0
  4. agent_evolve/application/__init__.py +1580 -0
  5. agent_evolve/application/action_allocation.py +744 -0
  6. agent_evolve/application/action_allocation_frame.py +347 -0
  7. agent_evolve/application/action_allocation_frame_commit.py +185 -0
  8. agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
  9. agent_evolve/application/action_allocation_frame_v3.py +338 -0
  10. agent_evolve/application/action_archive_value.py +497 -0
  11. agent_evolve/application/action_evidence_consistency.py +455 -0
  12. agent_evolve/application/action_forecast_partitioning.py +1471 -0
  13. agent_evolve/application/action_metric_projection.py +211 -0
  14. agent_evolve/application/action_role_value.py +680 -0
  15. agent_evolve/application/action_score_authorities.py +363 -0
  16. agent_evolve/application/action_structural_signature.py +116 -0
  17. agent_evolve/application/action_target_realization.py +402 -0
  18. agent_evolve/application/agentic_evolution.py +7734 -0
  19. agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
  20. agent_evolve/application/anchor_residual_identification.py +463 -0
  21. agent_evolve/application/archive_conditioned_action_target.py +208 -0
  22. agent_evolve/application/artifact_journal.py +246 -0
  23. agent_evolve/application/artifact_replay.py +347 -0
  24. agent_evolve/application/budgeted_optimizer.py +1828 -0
  25. agent_evolve/application/calibrated_campaign.py +485 -0
  26. agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
  27. agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
  28. agent_evolve/application/campaign_capacity_recourse.py +254 -0
  29. agent_evolve/application/campaign_contextual_outcomes.py +119 -0
  30. agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
  31. agent_evolve/application/campaign_evidence_registry.py +262 -0
  32. agent_evolve/application/campaign_execution.py +2537 -0
  33. agent_evolve/application/campaign_generation_audit.py +942 -0
  34. agent_evolve/application/campaign_learning.py +1812 -0
  35. agent_evolve/application/campaign_learning_runtime.py +1977 -0
  36. agent_evolve/application/campaign_search_phase.py +227 -0
  37. agent_evolve/application/campaign_selector_context_extension.py +220 -0
  38. agent_evolve/application/campaign_variation_envelope.py +649 -0
  39. agent_evolve/application/campaign_variation_trace.py +451 -0
  40. agent_evolve/application/candidate_archive_consequence.py +128 -0
  41. agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
  42. agent_evolve/application/composite_outcome_updater.py +145 -0
  43. agent_evolve/application/composition_portfolio_selection.py +363 -0
  44. agent_evolve/application/concurrent_stage.py +144 -0
  45. agent_evolve/application/contextual_action_allocation.py +181 -0
  46. agent_evolve/application/contextual_campaign_outcomes.py +267 -0
  47. agent_evolve/application/contextual_campaign_planning.py +1366 -0
  48. agent_evolve/application/contextual_delayed_credit.py +651 -0
  49. agent_evolve/application/contextual_search_controller.py +2374 -0
  50. agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
  51. agent_evolve/application/decision_metric_projection.py +112 -0
  52. agent_evolve/application/derived_action_semantics.py +129 -0
  53. agent_evolve/application/detailed_evaluation.py +449 -0
  54. agent_evolve/application/earned_lineage.py +1011 -0
  55. agent_evolve/application/effective_choice_audit.py +484 -0
  56. agent_evolve/application/empirical_consequence_calibration.py +908 -0
  57. agent_evolve/application/evaluation_accounting.py +325 -0
  58. agent_evolve/application/evaluation_cache.py +199 -0
  59. agent_evolve/application/evaluation_escrow.py +547 -0
  60. agent_evolve/application/evaluation_recourse.py +253 -0
  61. agent_evolve/application/event_recorder.py +151 -0
  62. agent_evolve/application/evolution_campaign.py +1840 -0
  63. agent_evolve/application/executable_hypothesis.py +323 -0
  64. agent_evolve/application/factorial_branch_pilot.py +772 -0
  65. agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
  66. agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
  67. agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
  68. agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
  69. agent_evolve/application/finite_action_selection.py +188 -0
  70. agent_evolve/application/finite_action_set.py +306 -0
  71. agent_evolve/application/finite_action_transition.py +537 -0
  72. agent_evolve/application/finite_variation_eligibility.py +296 -0
  73. agent_evolve/application/forecast_geometry_portfolio.py +799 -0
  74. agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
  75. agent_evolve/application/front_proximity_admission.py +311 -0
  76. agent_evolve/application/front_proximity_parent_basis.py +458 -0
  77. agent_evolve/application/frozen_hurdle_score.py +659 -0
  78. agent_evolve/application/g3_causal_screen.py +2257 -0
  79. agent_evolve/application/g3_causal_validation.py +1046 -0
  80. agent_evolve/application/g3_postseal_curation.py +818 -0
  81. agent_evolve/application/gated_agentic_generator.py +205 -0
  82. agent_evolve/application/generation_feedback.py +293 -0
  83. agent_evolve/application/generative_proposal_journal.py +185 -0
  84. agent_evolve/application/geometry_conditional_elasticity.py +453 -0
  85. agent_evolve/application/global_wave_action_allocation.py +1151 -0
  86. agent_evolve/application/head_mass_conditional_seat.py +268 -0
  87. agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
  88. agent_evolve/application/identifiable_reflection_learning.py +395 -0
  89. agent_evolve/application/identifiable_reflection_request.py +364 -0
  90. agent_evolve/application/in_memory_residual_archive.py +341 -0
  91. agent_evolve/application/insight_memory.py +1804 -0
  92. agent_evolve/application/live_runtime_manifest.py +758 -0
  93. agent_evolve/application/llm_task_queue.py +769 -0
  94. agent_evolve/application/matched_finite_action_block.py +409 -0
  95. agent_evolve/application/materialized_action_broker.py +2328 -0
  96. agent_evolve/application/materialized_action_constraints.py +83 -0
  97. agent_evolve/application/materialized_variation.py +211 -0
  98. agent_evolve/application/multi_option_evolution.py +1536 -0
  99. agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
  100. agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
  101. agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
  102. agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
  103. agent_evolve/application/outcome_relation.py +193 -0
  104. agent_evolve/application/paired_allocation_comparison.py +241 -0
  105. agent_evolve/application/paired_block_schedule.py +127 -0
  106. agent_evolve/application/parent_measurement.py +226 -0
  107. agent_evolve/application/pareto_archive.py +811 -0
  108. agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
  109. agent_evolve/application/portfolio_evolution.py +2950 -0
  110. agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
  111. agent_evolve/application/portfolio_memory_attribution.py +581 -0
  112. agent_evolve/application/portfolio_memory_dose.py +788 -0
  113. agent_evolve/application/portfolio_memory_matched_control.py +938 -0
  114. agent_evolve/application/portfolio_memory_transfer.py +297 -0
  115. agent_evolve/application/portfolio_optimization_memory.py +363 -0
  116. agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
  117. agent_evolve/application/portfolio_projection.py +335 -0
  118. agent_evolve/application/portfolio_recombination.py +2032 -0
  119. agent_evolve/application/post_evolution_reflection.py +834 -0
  120. agent_evolve/application/postcommit_rank_authority.py +245 -0
  121. agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
  122. agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
  123. agent_evolve/application/prequential_residual_exploration.py +343 -0
  124. agent_evolve/application/prequential_score_portfolio.py +954 -0
  125. agent_evolve/application/projections.py +292 -0
  126. agent_evolve/application/protected_action_committee.py +1027 -0
  127. agent_evolve/application/protected_branch_pilot.py +376 -0
  128. agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
  129. agent_evolve/application/provider_replay.py +910 -0
  130. agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
  131. agent_evolve/application/recombination_residual_expert.py +403 -0
  132. agent_evolve/application/reflection_workflow.py +571 -0
  133. agent_evolve/application/region_conditional_credit.py +911 -0
  134. agent_evolve/application/residual_campaign_runtime.py +531 -0
  135. agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
  136. agent_evolve/application/residual_headroom_ledger.py +1544 -0
  137. agent_evolve/application/residual_learning_transaction.py +396 -0
  138. agent_evolve/application/residual_portfolio_evolution.py +1228 -0
  139. agent_evolve/application/residual_reachability.py +749 -0
  140. agent_evolve/application/residual_stage_credit.py +499 -0
  141. agent_evolve/application/same_prefix_paired_audit.py +1580 -0
  142. agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
  143. agent_evolve/application/sequential_lineage_allocation.py +1017 -0
  144. agent_evolve/application/sequential_market_replay.py +1395 -0
  145. agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
  146. agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
  147. agent_evolve/application/single_score_action_allocation.py +299 -0
  148. agent_evolve/application/source_exposure_allocation.py +906 -0
  149. agent_evolve/application/staged_memory.py +210 -0
  150. agent_evolve/application/stratified_cold_start_allocation.py +732 -0
  151. agent_evolve/application/support_guarded_hurdle_score.py +549 -0
  152. agent_evolve/application/target_conditioned_action_forecast.py +595 -0
  153. agent_evolve/application/target_conditioned_campaign.py +566 -0
  154. agent_evolve/application/treatment_assignment.py +201 -0
  155. agent_evolve/application/trusted_objective_evidence.py +217 -0
  156. agent_evolve/application/two_stage_action_evolution.py +1131 -0
  157. agent_evolve/application/v8lite_allocation_policy.py +1083 -0
  158. agent_evolve/application/v9_candidate_policy.py +1303 -0
  159. agent_evolve/bootstrap.py +108 -0
  160. agent_evolve/campaign_presets.py +517 -0
  161. agent_evolve/campaign_profiles.py +452 -0
  162. agent_evolve/campaign_variation_topology.py +288 -0
  163. agent_evolve/campaign_workload.py +950 -0
  164. agent_evolve/cli.py +797 -0
  165. agent_evolve/contract.py +241 -0
  166. agent_evolve/core/__init__.py +91 -0
  167. agent_evolve/core/action_semantics.py +411 -0
  168. agent_evolve/core/authored.py +105 -0
  169. agent_evolve/core/formatting.py +286 -0
  170. agent_evolve/core/optimization_semantics.py +324 -0
  171. agent_evolve/core/problem.py +167 -0
  172. agent_evolve/core/results.py +323 -0
  173. agent_evolve/core/stats.py +70 -0
  174. agent_evolve/core/telemetry.py +100 -0
  175. agent_evolve/domain/__init__.py +89 -0
  176. agent_evolve/domain/artifact.py +162 -0
  177. agent_evolve/domain/durable_text.py +68 -0
  178. agent_evolve/domain/event.py +1454 -0
  179. agent_evolve/domain/finite_action_set.py +426 -0
  180. agent_evolve/domain/finite_variation.py +526 -0
  181. agent_evolve/domain/generative_emission.py +559 -0
  182. agent_evolve/domain/ids.py +163 -0
  183. agent_evolve/domain/inline_text.py +106 -0
  184. agent_evolve/domain/insight.py +27 -0
  185. agent_evolve/domain/lineage.py +737 -0
  186. agent_evolve/domain/llm_task_queue.py +960 -0
  187. agent_evolve/domain/outcome.py +96 -0
  188. agent_evolve/domain/patch.py +854 -0
  189. agent_evolve/domain/typed_json.py +542 -0
  190. agent_evolve/domain/variation_space.py +158 -0
  191. agent_evolve/driver.py +1014 -0
  192. agent_evolve/harness/__init__.py +29 -0
  193. agent_evolve/harness/base.py +242 -0
  194. agent_evolve/harness/directives.py +163 -0
  195. agent_evolve/harness/generative_seal.py +479 -0
  196. agent_evolve/harness/registry.py +41 -0
  197. agent_evolve/infrastructure/__init__.py +39 -0
  198. agent_evolve/infrastructure/artifacts/__init__.py +6 -0
  199. agent_evolve/infrastructure/artifacts/_verification.py +67 -0
  200. agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
  201. agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
  202. agent_evolve/infrastructure/asyncio_runtime.py +109 -0
  203. agent_evolve/infrastructure/authored_runtime.py +188 -0
  204. agent_evolve/infrastructure/authored_worker.py +171 -0
  205. agent_evolve/infrastructure/clock.py +53 -0
  206. agent_evolve/infrastructure/events/__init__.py +6 -0
  207. agent_evolve/infrastructure/events/_validation.py +89 -0
  208. agent_evolve/infrastructure/events/in_memory.py +56 -0
  209. agent_evolve/infrastructure/events/jsonl.py +193 -0
  210. agent_evolve/infrastructure/exception_provenance.py +215 -0
  211. agent_evolve/infrastructure/ids.py +118 -0
  212. agent_evolve/infrastructure/lineage_codec.py +1836 -0
  213. agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
  214. agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
  215. agent_evolve/infrastructure/resource_lease.py +370 -0
  216. agent_evolve/infrastructure/sanitization/__init__.py +8 -0
  217. agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
  218. agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
  219. agent_evolve/infrastructure/stream_liveness.py +383 -0
  220. agent_evolve/infrastructure/subprocess_boundary.py +136 -0
  221. agent_evolve/integrations/__init__.py +1 -0
  222. agent_evolve/integrations/botorch/__init__.py +28 -0
  223. agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
  224. agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
  225. agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
  226. agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
  227. agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
  228. agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
  229. agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
  230. agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
  231. agent_evolve/integrations/completion.py +242 -0
  232. agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
  233. agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
  234. agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
  235. agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
  236. agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
  237. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
  238. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
  239. agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
  240. agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
  241. agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
  242. agent_evolve/integrations/pydantic_ai/harness.py +159 -0
  243. agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
  244. agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
  245. agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
  246. agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
  247. agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
  248. agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
  249. agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
  250. agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
  251. agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
  252. agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
  253. agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
  254. agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
  255. agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
  256. agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
  257. agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
  258. agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
  259. agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
  260. agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
  261. agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
  262. agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
  263. agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
  264. agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
  265. agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
  266. agent_evolve/integrations/pymoo_adapter.py +242 -0
  267. agent_evolve/policies/__init__.py +17 -0
  268. agent_evolve/policies/check.py +469 -0
  269. agent_evolve/policies/emit_scaffold.py +451 -0
  270. agent_evolve/policies/feedback/__init__.py +37 -0
  271. agent_evolve/policies/feedback/held_out_asn.py +1325 -0
  272. agent_evolve/policies/genetic.py +607 -0
  273. agent_evolve/policies/llm_backoff.py +183 -0
  274. agent_evolve/policies/llm_chooser.py +226 -0
  275. agent_evolve/policies/llm_generator.py +1760 -0
  276. agent_evolve/policies/llm_init.py +267 -0
  277. agent_evolve/policies/llm_operator.py +109 -0
  278. agent_evolve/policies/llm_prior.py +194 -0
  279. agent_evolve/policies/llm_surrogate.py +334 -0
  280. agent_evolve/policies/measurement_evidence.py +704 -0
  281. agent_evolve/policies/memory/__init__.py +223 -0
  282. agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
  283. agent_evolve/policies/memory/compatibility_matching.py +593 -0
  284. agent_evolve/policies/memory/global_falsification.py +1841 -0
  285. agent_evolve/policies/memory/prompt_shape.py +503 -0
  286. agent_evolve/policies/memory/randomized_subset.py +714 -0
  287. agent_evolve/policies/memory/staged_causal.py +1270 -0
  288. agent_evolve/policies/memory/treatment_compliance.py +759 -0
  289. agent_evolve/policies/objective_resolution/__init__.py +17 -0
  290. agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
  291. agent_evolve/policies/operator_portfolio.py +407 -0
  292. agent_evolve/policies/reguidance.py +1133 -0
  293. agent_evolve/policies/reward/__init__.py +83 -0
  294. agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
  295. agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
  296. agent_evolve/policies/reward/affine_hypervolume.py +490 -0
  297. agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
  298. agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
  299. agent_evolve/policies/reward/frozen_archive.py +360 -0
  300. agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
  301. agent_evolve/policies/search_state.py +208 -0
  302. agent_evolve/policies/selection/__init__.py +345 -0
  303. agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
  304. agent_evolve/policies/selection/affine_frontier_context.py +330 -0
  305. agent_evolve/policies/selection/affine_frontier_target.py +473 -0
  306. agent_evolve/policies/selection/archive_elite.py +1346 -0
  307. agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
  308. agent_evolve/policies/selection/calibrated_slate.py +1394 -0
  309. agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
  310. agent_evolve/policies/selection/common_candidate_pool.py +685 -0
  311. agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
  312. agent_evolve/policies/selection/disjoint_pairs.py +479 -0
  313. agent_evolve/policies/selection/elite_explorer.py +719 -0
  314. agent_evolve/policies/selection/finite_action.py +187 -0
  315. agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
  316. agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
  317. agent_evolve/policies/selection/forecast_calibration.py +922 -0
  318. agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
  319. agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
  320. agent_evolve/policies/selection/full_support_slate.py +91 -0
  321. agent_evolve/policies/selection/meaningful_direction.py +240 -0
  322. agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
  323. agent_evolve/policies/selection/model_anchored_slate.py +826 -0
  324. agent_evolve/policies/selection/phenotype_recourse.py +979 -0
  325. agent_evolve/policies/selection/proposal_support.py +368 -0
  326. agent_evolve/policies/selection/random_portfolio.py +254 -0
  327. agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
  328. agent_evolve/policies/selection/residual_frontier.py +463 -0
  329. agent_evolve/policies/selection/residual_frontier_target.py +605 -0
  330. agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
  331. agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
  332. agent_evolve/policies/selection/target_conditioned_features.py +812 -0
  333. agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
  334. agent_evolve/policies/selection/task_keyed_palette.py +906 -0
  335. agent_evolve/policies/semantics.py +147 -0
  336. agent_evolve/policies/structure.py +362 -0
  337. agent_evolve/policies/structured_output_budget.py +62 -0
  338. agent_evolve/policies/surrogate.py +696 -0
  339. agent_evolve/policies/variation/__init__.py +1 -0
  340. agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
  341. agent_evolve/policies/variation/crossover_inheritance.py +575 -0
  342. agent_evolve/policies/variation/disjoint_recombination.py +611 -0
  343. agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
  344. agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
  345. agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
  346. agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
  347. agent_evolve/policies/variation/typed_patch.py +1981 -0
  348. agent_evolve/policies/weighted_prior.py +394 -0
  349. agent_evolve/ports/__init__.py +383 -0
  350. agent_evolve/ports/action_allocation.py +733 -0
  351. agent_evolve/ports/action_allocation_frame.py +1153 -0
  352. agent_evolve/ports/action_allocation_frame_commit.py +294 -0
  353. agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
  354. agent_evolve/ports/action_allocation_frame_v3.py +995 -0
  355. agent_evolve/ports/action_forecast.py +1568 -0
  356. agent_evolve/ports/action_metric_projection.py +165 -0
  357. agent_evolve/ports/agentic_generator.py +1561 -0
  358. agent_evolve/ports/archive_context.py +136 -0
  359. agent_evolve/ports/artifact_sanitizer.py +44 -0
  360. agent_evolve/ports/artifact_store.py +225 -0
  361. agent_evolve/ports/clock.py +13 -0
  362. agent_evolve/ports/contextual_search_allocation.py +827 -0
  363. agent_evolve/ports/decision_metric_projection.py +258 -0
  364. agent_evolve/ports/event_store.py +55 -0
  365. agent_evolve/ports/executable_hypothesis.py +557 -0
  366. agent_evolve/ports/finite_acquisition.py +377 -0
  367. agent_evolve/ports/finite_acquisition_batch.py +296 -0
  368. agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
  369. agent_evolve/ports/finite_acquisition_json.py +247 -0
  370. agent_evolve/ports/finite_acquisition_space.py +168 -0
  371. agent_evolve/ports/finite_action_selection.py +348 -0
  372. agent_evolve/ports/finite_action_set.py +256 -0
  373. agent_evolve/ports/frontier_target.py +396 -0
  374. agent_evolve/ports/generation_failure.py +43 -0
  375. agent_evolve/ports/hard_feasibility.py +233 -0
  376. agent_evolve/ports/id_factory.py +34 -0
  377. agent_evolve/ports/llm_task_queue.py +93 -0
  378. agent_evolve/ports/objective_resolution.py +419 -0
  379. agent_evolve/ports/paired_allocation_comparison.py +401 -0
  380. agent_evolve/ports/paired_block_schedule.py +475 -0
  381. agent_evolve/ports/parent_measurement.py +336 -0
  382. agent_evolve/ports/portfolio_memory_dose.py +643 -0
  383. agent_evolve/ports/portfolio_selection.py +3169 -0
  384. agent_evolve/ports/postcommit_rank_authority.py +467 -0
  385. agent_evolve/ports/presented_action_evidence.py +794 -0
  386. agent_evolve/ports/resource_lease.py +162 -0
  387. agent_evolve/ports/structured_generator.py +734 -0
  388. agent_evolve/ports/structured_output_budget.py +120 -0
  389. agent_evolve/ports/subprocess_boundary.py +138 -0
  390. agent_evolve/ports/treatment_assignment.py +466 -0
  391. agent_evolve/ports/variation_catalog.py +76 -0
  392. agent_evolve/ports/variation_source.py +226 -0
  393. agent_evolve/proposal_mode.py +157 -0
  394. agent_evolve/proposers/__init__.py +10 -0
  395. agent_evolve/proposers/random_proposer.py +188 -0
  396. agent_evolve/provider_accounting.py +163 -0
  397. agent_evolve/py.typed +0 -0
  398. agent_evolve/reference_method.py +1570 -0
  399. agent_evolve/session/__init__.py +11 -0
  400. agent_evolve/session/authorship.py +864 -0
  401. agent_evolve/session/evaluate.py +236 -0
  402. agent_evolve/session/fidelity.py +237 -0
  403. agent_evolve/session/genetic_loop.py +742 -0
  404. agent_evolve/session/loop.py +803 -0
  405. agent_evolve/session/screening.py +671 -0
  406. agent_evolve/settings.py +376 -0
  407. agent_evolve/workload_kit.py +368 -0
  408. agent_evolve/workload_prompt.py +398 -0
  409. agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
  410. agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
  411. agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
  412. agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
  413. agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
  414. agentevolve_optimizer-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2374 @@
1
+ """Authenticated phase-aware credit and allocation for generic search sources.
2
+
3
+ The controller operates only on workload-neutral, normalized observations. It
4
+ does not inspect candidate fields, objective names, model prose, or provider
5
+ metadata. Sources (for example model, restart, global coverage, or memory) and
6
+ operators (for example atomic or recombination families) share the same typed
7
+ posterior machinery.
8
+
9
+ The controller is intentionally deterministic. It uses the empirical mean of
10
+ bounded normalized archive return and one distribution-free uncertainty slot
11
+ instead of stochastic Thompson sampling, which makes every decision exactly
12
+ replayable while preserving the scale of small but real archive improvements.
13
+ Feasibility, persistence, descendant yield, and allocation realizability remain
14
+ separate diagnostic channels, but are not added to return with arbitrary
15
+ weights.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import math
23
+ import re
24
+ from dataclasses import dataclass, field, replace
25
+ from enum import Enum
26
+ from itertools import product
27
+
28
+ from agent_evolve.domain.ids import CandidateId
29
+ from agent_evolve.domain.patch import require_sha256
30
+ from agent_evolve.ports.contextual_search_allocation import (
31
+ ContextualArmCountCapability,
32
+ ContextualLaneJointCountCapability,
33
+ ContextualPortfolioAllocationContract,
34
+ ContextualPortfolioAllocationRealization,
35
+ )
36
+
37
+
38
+ _TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,255}$")
39
+ CONTEXTUAL_SEARCH_CONTROLLER_ID = "phase_aware_contextual_source_operator"
40
+ CONTEXTUAL_SEARCH_CONTROLLER_VERSION = 9
41
+ CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256 = hashlib.sha256(
42
+ b"agent-evolve:phase-aware-contextual-source-operator:v9;"
43
+ b"evidence=authenticated-prior-only-normalized-outcomes;"
44
+ b"selection-currency=normalized-archive-return;"
45
+ b"diagnostics=positive-marginal,utility-share,feasibility,"
46
+ b"stage-front-survival,final-persistence,descendant,allocation-realizability;"
47
+ b"delayed-credit=append-only-source-joined-prior-cutoff;"
48
+ b"allocation-realization=objective-workload-blind-request-overlap;"
49
+ b"return-estimator=bounded-empirical-mean;"
50
+ b"uncertainty=distribution-free-maximum-standard-error;deterministic=true;"
51
+ b"phases=basin-acquisition,basin-expansion,composition,terminal-conversion;"
52
+ b"terminal-information-bonus=zero;one-nonterminal-exploration-slot=true;"
53
+ b"allocation=posterior-mean-proportional-plus-one-uncertainty-slot;"
54
+ b"cold-start=prior-proportional-plus-one-exploration;"
55
+ b"source-axis=sealed-finite-variation-source-not-reconciliation-origin;"
56
+ b"source-incumbent-prior-mass=0.50;operator-incumbent-prior-mass=0.75;"
57
+ b"empirical-capability=prior-realized-stage-count-witnesses;"
58
+ b"capability-projection=minimum-l1-then-maximum-posterior-score;"
59
+ b"capability-is-witness-not-current-guarantee=true;"
60
+ b"prospective-joint-capability=finite-contract-lane-product;"
61
+ b"joint-projection=maximum-feasible-exploration-retention-then-minimum-"
62
+ b"source-operator-l1-then-posterior-score;"
63
+ b"joint-exploration-recourse=deterministic-realizable-challenger;"
64
+ b"joint-slicing=exact-witnessed-lane-vector;"
65
+ b"durable-evidence=expanded-authenticated-query-and-prior-snapshot;"
66
+ b"workload-model-provider-identifiers=false"
67
+ ).hexdigest()
68
+ _OBSERVATION_DOMAIN = b"agent-evolve:contextual-search-observation:v2\x00"
69
+ _DELAYED_CREDIT_DOMAIN = b"agent-evolve:contextual-search-delayed-credit:v2\x00"
70
+ _SNAPSHOT_DOMAIN = b"agent-evolve:contextual-search-snapshot:v2\x00"
71
+ _QUERY_DOMAIN = b"agent-evolve:contextual-search-query:v3\x00"
72
+ _DECISION_DOMAIN = b"agent-evolve:contextual-search-decision:v3\x00"
73
+ _ALLOCATION_SLICE_DOMAIN = b"agent-evolve:contextual-search-allocation-slice:v1\x00"
74
+ _STAGE_ALLOCATION_DOMAIN = b"agent-evolve:contextual-search-stage-allocation:v1\x00"
75
+ _COMPLETION_AUDIT_DOMAIN = b"agent-evolve:contextual-search-completion-audit:v1\x00"
76
+
77
+
78
+ def _canonical_json(value: object) -> bytes:
79
+ return json.dumps(
80
+ value,
81
+ allow_nan=False,
82
+ ensure_ascii=True,
83
+ separators=(",", ":"),
84
+ sort_keys=True,
85
+ ).encode("ascii", errors="strict")
86
+
87
+
88
+ def _hash(domain: bytes, value: object) -> str:
89
+ return hashlib.sha256(domain + _canonical_json(value)).hexdigest()
90
+
91
+
92
+ def _require_token(value: str, *, name: str) -> None:
93
+ if type(value) is not str or _TOKEN.fullmatch(value) is None:
94
+ raise ValueError(f"{name} must use the closed token grammar")
95
+
96
+
97
+ def _require_probability(value: float, *, name: str) -> None:
98
+ if type(value) is not float or not math.isfinite(value):
99
+ raise TypeError(f"{name} must be a finite canonical float")
100
+ if not 0.0 <= value <= 1.0:
101
+ raise ValueError(f"{name} must lie in [0, 1]")
102
+
103
+
104
+ def _canonical_tokens(values: tuple[str, ...], *, name: str) -> None:
105
+ if type(values) is not tuple or not values:
106
+ raise ValueError(f"{name} must be a non-empty exact tuple")
107
+ for value in values:
108
+ _require_token(value, name=name)
109
+ if values != tuple(sorted(set(values))):
110
+ raise ValueError(f"{name} must be unique and canonical")
111
+
112
+
113
+ class SearchPhase(str, Enum):
114
+ BASIN_ACQUISITION = "basin_acquisition"
115
+ BASIN_EXPANSION = "basin_expansion"
116
+ COMPOSITION = "composition"
117
+ TERMINAL_CONVERSION = "terminal_conversion"
118
+
119
+
120
+ class SearchArmKind(str, Enum):
121
+ SOURCE = "source"
122
+ OPERATOR = "operator"
123
+
124
+
125
+ @dataclass(frozen=True, slots=True)
126
+ class ContextualSearchObservation:
127
+ """One evaluated action with normalized, separately typed credit channels."""
128
+
129
+ campaign_scope_sha256: str
130
+ wave_index: int
131
+ source_id: str
132
+ operator_id: str
133
+ option_identity_sha256: str
134
+ parent_context_sha256: str
135
+ feasible: bool
136
+ positive_marginal_utility: bool
137
+ normalized_marginal_utility: float
138
+ marginal_utility_share: float
139
+ stage_front_persisted: bool | None = None
140
+ final_front_persisted: bool | None = None
141
+ useful_descendant_observed: bool | None = None
142
+ source_distance: float = 0.0
143
+ candidate_id: CandidateId | None = None
144
+ observation_sha256: str = field(init=False)
145
+
146
+ def __post_init__(self) -> None:
147
+ for name in (
148
+ "campaign_scope_sha256",
149
+ "option_identity_sha256",
150
+ "parent_context_sha256",
151
+ ):
152
+ require_sha256(getattr(self, name), name)
153
+ if type(self.wave_index) is not int or self.wave_index <= 0:
154
+ raise ValueError("wave_index must be a positive exact integer")
155
+ _require_token(self.source_id, name="source_id")
156
+ _require_token(self.operator_id, name="operator_id")
157
+ for name in ("feasible", "positive_marginal_utility"):
158
+ if type(getattr(self, name)) is not bool:
159
+ raise TypeError(f"{name} must be an exact bool")
160
+ for name in (
161
+ "stage_front_persisted",
162
+ "final_front_persisted",
163
+ "useful_descendant_observed",
164
+ ):
165
+ value = getattr(self, name)
166
+ if value is not None and type(value) is not bool:
167
+ raise TypeError(f"{name} must be an exact bool or None")
168
+ _require_probability(
169
+ self.normalized_marginal_utility,
170
+ name="normalized_marginal_utility",
171
+ )
172
+ _require_probability(
173
+ self.marginal_utility_share,
174
+ name="marginal_utility_share",
175
+ )
176
+ _require_probability(self.source_distance, name="source_distance")
177
+ if self.candidate_id is not None:
178
+ if type(self.candidate_id) is not CandidateId:
179
+ raise TypeError("candidate_id must be exact CandidateId or None")
180
+ CandidateId.__post_init__(self.candidate_id)
181
+ if not self.feasible and (
182
+ self.positive_marginal_utility
183
+ or self.normalized_marginal_utility != 0.0
184
+ or self.marginal_utility_share != 0.0
185
+ or self.stage_front_persisted is True
186
+ or self.final_front_persisted is True
187
+ or self.useful_descendant_observed is True
188
+ ):
189
+ raise ValueError("infeasible observations cannot carry positive yield")
190
+ if self.positive_marginal_utility != (
191
+ self.normalized_marginal_utility > 0.0 and self.marginal_utility_share > 0.0
192
+ ):
193
+ raise ValueError("positive verdict differs from its utility channels")
194
+ object.__setattr__(
195
+ self,
196
+ "observation_sha256",
197
+ _hash(_OBSERVATION_DOMAIN, self._unsigned_record()),
198
+ )
199
+
200
+ def _unsigned_record(self) -> dict[str, object]:
201
+ return {
202
+ "schema_version": 2,
203
+ "campaign_scope_sha256": self.campaign_scope_sha256,
204
+ "wave_index": self.wave_index,
205
+ "source_id": self.source_id,
206
+ "operator_id": self.operator_id,
207
+ "option_identity_sha256": self.option_identity_sha256,
208
+ "parent_context_sha256": self.parent_context_sha256,
209
+ "feasible": self.feasible,
210
+ "positive_marginal_utility": self.positive_marginal_utility,
211
+ "normalized_marginal_utility_hex": (self.normalized_marginal_utility.hex()),
212
+ "marginal_utility_share_hex": self.marginal_utility_share.hex(),
213
+ "stage_front_persisted": self.stage_front_persisted,
214
+ "final_front_persisted": self.final_front_persisted,
215
+ "useful_descendant_observed": self.useful_descendant_observed,
216
+ "source_distance_hex": self.source_distance.hex(),
217
+ "candidate_id": (
218
+ None if self.candidate_id is None else self.candidate_id.value
219
+ ),
220
+ }
221
+
222
+ def to_record(self) -> dict[str, object]:
223
+ self.__post_init__()
224
+ return {
225
+ **self._unsigned_record(),
226
+ "observation_sha256": self.observation_sha256,
227
+ }
228
+
229
+
230
+ @dataclass(frozen=True, slots=True)
231
+ class ContextualSearchDelayedCredit:
232
+ """A later outcome joined to one immutable evaluated-action observation."""
233
+
234
+ campaign_scope_sha256: str
235
+ source_observation_sha256: str
236
+ available_at_wave_index: int
237
+ stage_front_persisted: bool | None = None
238
+ final_front_persisted: bool | None = None
239
+ useful_descendant_observed: bool | None = None
240
+ credit_sha256: str = field(init=False)
241
+
242
+ def __post_init__(self) -> None:
243
+ require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
244
+ require_sha256(
245
+ self.source_observation_sha256,
246
+ "source_observation_sha256",
247
+ )
248
+ if (
249
+ type(self.available_at_wave_index) is not int
250
+ or self.available_at_wave_index <= 0
251
+ ):
252
+ raise ValueError("available_at_wave_index must be positive")
253
+ for name in (
254
+ "stage_front_persisted",
255
+ "final_front_persisted",
256
+ "useful_descendant_observed",
257
+ ):
258
+ value = getattr(self, name)
259
+ if value is not None and type(value) is not bool:
260
+ raise TypeError(f"{name} must be an exact bool or None")
261
+ if (
262
+ self.stage_front_persisted is None
263
+ and self.final_front_persisted is None
264
+ and self.useful_descendant_observed is None
265
+ ):
266
+ raise ValueError("delayed credit must adjudicate at least one channel")
267
+ object.__setattr__(
268
+ self,
269
+ "credit_sha256",
270
+ _hash(_DELAYED_CREDIT_DOMAIN, self._unsigned_record()),
271
+ )
272
+
273
+ def _unsigned_record(self) -> dict[str, object]:
274
+ return {
275
+ "schema_version": 2,
276
+ "campaign_scope_sha256": self.campaign_scope_sha256,
277
+ "source_observation_sha256": self.source_observation_sha256,
278
+ "available_at_wave_index": self.available_at_wave_index,
279
+ "stage_front_persisted": self.stage_front_persisted,
280
+ "final_front_persisted": self.final_front_persisted,
281
+ "useful_descendant_observed": self.useful_descendant_observed,
282
+ }
283
+
284
+ def to_record(self) -> dict[str, object]:
285
+ self.__post_init__()
286
+ return {**self._unsigned_record(), "credit_sha256": self.credit_sha256}
287
+
288
+
289
+ @dataclass(frozen=True, slots=True)
290
+ class ContextualArmPosterior:
291
+ """Independent evidence channels for one source or operator arm."""
292
+
293
+ kind: SearchArmKind
294
+ arm_id: str
295
+ observation_count: int
296
+ positive_count: int
297
+ feasible_count: int
298
+ stage_persistence_observation_count: int
299
+ stage_persistence_positive_count: int
300
+ persistence_observation_count: int
301
+ persistence_positive_count: int
302
+ descendant_observation_count: int
303
+ descendant_positive_count: int
304
+ allocation_requested_slot_count: int
305
+ allocation_realized_overlap_count: int
306
+ allocation_projection_count: int
307
+ normalized_marginal_utility_sum: float
308
+ marginal_utility_share_sum: float
309
+ mean_source_distance: float
310
+
311
+ def __post_init__(self) -> None:
312
+ if type(self.kind) is not SearchArmKind:
313
+ raise TypeError("kind must be exact SearchArmKind")
314
+ _require_token(self.arm_id, name="arm_id")
315
+ for name in (
316
+ "observation_count",
317
+ "positive_count",
318
+ "feasible_count",
319
+ "stage_persistence_observation_count",
320
+ "stage_persistence_positive_count",
321
+ "persistence_observation_count",
322
+ "persistence_positive_count",
323
+ "descendant_observation_count",
324
+ "descendant_positive_count",
325
+ "allocation_requested_slot_count",
326
+ "allocation_realized_overlap_count",
327
+ "allocation_projection_count",
328
+ ):
329
+ value = getattr(self, name)
330
+ if type(value) is not int or value < 0:
331
+ raise ValueError(f"{name} must be a non-negative exact integer")
332
+ if self.positive_count > self.observation_count:
333
+ raise ValueError("positive count exceeds observations")
334
+ if self.feasible_count > self.observation_count:
335
+ raise ValueError("feasible count exceeds observations")
336
+ if self.stage_persistence_positive_count > (
337
+ self.stage_persistence_observation_count
338
+ ):
339
+ raise ValueError("stage persistence positives exceed observations")
340
+ if self.persistence_positive_count > self.persistence_observation_count:
341
+ raise ValueError("persistence positives exceed observations")
342
+ if self.descendant_positive_count > self.descendant_observation_count:
343
+ raise ValueError("descendant positives exceed observations")
344
+ if self.allocation_realized_overlap_count > (
345
+ self.allocation_requested_slot_count
346
+ ):
347
+ raise ValueError("allocation overlap exceeds requested slots")
348
+ if (
349
+ self.allocation_requested_slot_count == 0
350
+ and self.allocation_projection_count != 0
351
+ ):
352
+ raise ValueError("allocation projections require requested slots")
353
+ for name in (
354
+ "normalized_marginal_utility_sum",
355
+ "marginal_utility_share_sum",
356
+ ):
357
+ value = getattr(self, name)
358
+ if (
359
+ type(value) is not float
360
+ or not math.isfinite(value)
361
+ or not 0.0 <= value <= float(self.observation_count)
362
+ ):
363
+ raise ValueError(f"{name} is invalid")
364
+ _require_probability(self.mean_source_distance, name="mean_source_distance")
365
+
366
+ @staticmethod
367
+ def _beta_mean(successes: int, observations: int) -> float:
368
+ return (1.0 + successes) / (2.0 + observations)
369
+
370
+ @staticmethod
371
+ def _beta_variance(successes: int, observations: int) -> float:
372
+ alpha = 1.0 + successes
373
+ beta = 1.0 + observations - successes
374
+ total = alpha + beta
375
+ return (alpha * beta) / (total * total * (total + 1.0))
376
+
377
+ @property
378
+ def positive_probability(self) -> float:
379
+ return self._beta_mean(self.positive_count, self.observation_count)
380
+
381
+ @property
382
+ def positive_uncertainty(self) -> float:
383
+ return math.sqrt(
384
+ self._beta_variance(self.positive_count, self.observation_count)
385
+ )
386
+
387
+ @property
388
+ def return_probability(self) -> float:
389
+ """Empirical mean of bounded normalized archive return.
390
+
391
+ Infeasible evaluated actions already contribute zero utility, so
392
+ feasibility is a return gate rather than an independently rewarded
393
+ feature. An unseen arm receives the neutral cold-start value only
394
+ until its first real observation; no unit pseudo-count is allowed to
395
+ dwarf the small archive gains common in expensive optimization.
396
+ """
397
+
398
+ if self.observation_count == 0:
399
+ return 0.5
400
+ return self.normalized_marginal_utility_sum / self.observation_count
401
+
402
+ @property
403
+ def return_uncertainty(self) -> float:
404
+ # A variable bounded in [0, 1] has standard deviation at most 1/2.
405
+ # This conservative standard-error bound is scale free and, because
406
+ # only one explicit exploration slot consumes it, cannot inflate the
407
+ # entire exploitation allocation.
408
+ return 0.5 / math.sqrt(max(1, self.observation_count))
409
+
410
+ @property
411
+ def feasibility_probability(self) -> float:
412
+ return self._beta_mean(self.feasible_count, self.observation_count)
413
+
414
+ @property
415
+ def persistence_probability(self) -> float:
416
+ return self._beta_mean(
417
+ self.persistence_positive_count,
418
+ self.persistence_observation_count,
419
+ )
420
+
421
+ @property
422
+ def stage_persistence_probability(self) -> float:
423
+ return self._beta_mean(
424
+ self.stage_persistence_positive_count,
425
+ self.stage_persistence_observation_count,
426
+ )
427
+
428
+ @property
429
+ def descendant_probability(self) -> float:
430
+ return self._beta_mean(
431
+ self.descendant_positive_count,
432
+ self.descendant_observation_count,
433
+ )
434
+
435
+ @property
436
+ def allocation_realizability_probability(self) -> float:
437
+ return self._beta_mean(
438
+ self.allocation_realized_overlap_count,
439
+ self.allocation_requested_slot_count,
440
+ )
441
+
442
+ @property
443
+ def mean_marginal_utility_share(self) -> float:
444
+ if self.observation_count == 0:
445
+ return 0.0
446
+ return self.marginal_utility_share_sum / self.observation_count
447
+
448
+ @property
449
+ def mean_normalized_marginal_utility(self) -> float:
450
+ if self.observation_count == 0:
451
+ return 0.0
452
+ return self.normalized_marginal_utility_sum / self.observation_count
453
+
454
+ def to_record(self) -> dict[str, object]:
455
+ self.__post_init__()
456
+ return {
457
+ "kind": self.kind.value,
458
+ "arm_id": self.arm_id,
459
+ "observation_count": self.observation_count,
460
+ "positive_count": self.positive_count,
461
+ "feasible_count": self.feasible_count,
462
+ "stage_persistence_observation_count": (
463
+ self.stage_persistence_observation_count
464
+ ),
465
+ "stage_persistence_positive_count": (self.stage_persistence_positive_count),
466
+ "persistence_observation_count": self.persistence_observation_count,
467
+ "persistence_positive_count": self.persistence_positive_count,
468
+ "descendant_observation_count": self.descendant_observation_count,
469
+ "descendant_positive_count": self.descendant_positive_count,
470
+ "allocation_requested_slot_count": (self.allocation_requested_slot_count),
471
+ "allocation_realized_overlap_count": (
472
+ self.allocation_realized_overlap_count
473
+ ),
474
+ "allocation_projection_count": self.allocation_projection_count,
475
+ "normalized_marginal_utility_sum_hex": (
476
+ self.normalized_marginal_utility_sum.hex()
477
+ ),
478
+ "marginal_utility_share_sum_hex": (self.marginal_utility_share_sum.hex()),
479
+ "mean_source_distance_hex": self.mean_source_distance.hex(),
480
+ "posterior": {
481
+ "positive_probability_hex": self.positive_probability.hex(),
482
+ "positive_uncertainty_hex": self.positive_uncertainty.hex(),
483
+ "feasibility_probability_hex": self.feasibility_probability.hex(),
484
+ "stage_persistence_probability_hex": (
485
+ self.stage_persistence_probability.hex()
486
+ ),
487
+ "persistence_probability_hex": self.persistence_probability.hex(),
488
+ "descendant_probability_hex": self.descendant_probability.hex(),
489
+ "allocation_realizability_probability_hex": (
490
+ self.allocation_realizability_probability.hex()
491
+ ),
492
+ "mean_normalized_marginal_utility_hex": (
493
+ self.mean_normalized_marginal_utility.hex()
494
+ ),
495
+ "return_probability_hex": self.return_probability.hex(),
496
+ "return_uncertainty_hex": self.return_uncertainty.hex(),
497
+ "mean_marginal_utility_share_hex": (
498
+ self.mean_marginal_utility_share.hex()
499
+ ),
500
+ },
501
+ }
502
+
503
+
504
+ def _posterior(
505
+ observations: tuple[ContextualSearchObservation, ...],
506
+ *,
507
+ kind: SearchArmKind,
508
+ arm_id: str,
509
+ delayed_credits: tuple[ContextualSearchDelayedCredit, ...] = (),
510
+ allocation_realizations: tuple[ContextualPortfolioAllocationRealization, ...] = (),
511
+ ) -> ContextualArmPosterior:
512
+ selected = tuple(
513
+ value
514
+ for value in observations
515
+ if (value.source_id if kind is SearchArmKind.SOURCE else value.operator_id)
516
+ == arm_id
517
+ )
518
+ delayed_by_channel = {
519
+ (value.source_observation_sha256, name): getattr(value, name)
520
+ for value in delayed_credits
521
+ for name in (
522
+ "stage_front_persisted",
523
+ "final_front_persisted",
524
+ "useful_descendant_observed",
525
+ )
526
+ if getattr(value, name) is not None
527
+ }
528
+
529
+ def delayed_value(
530
+ value: ContextualSearchObservation,
531
+ name: str,
532
+ ) -> bool | None:
533
+ immediate = getattr(value, name)
534
+ if immediate is not None:
535
+ return immediate
536
+ return delayed_by_channel.get((value.observation_sha256, name))
537
+
538
+ stage_persistence = tuple(
539
+ delayed_value(value, "stage_front_persisted") for value in selected
540
+ )
541
+ stage_persistence = tuple(value for value in stage_persistence if value is not None)
542
+ persistence = tuple(
543
+ delayed_value(value, "final_front_persisted") for value in selected
544
+ )
545
+ persistence = tuple(value for value in persistence if value is not None)
546
+ descendants = tuple(
547
+ delayed_value(value, "useful_descendant_observed") for value in selected
548
+ )
549
+ descendants = tuple(value for value in descendants if value is not None)
550
+ allocation_rows = tuple(
551
+ value
552
+ for value in allocation_realizations
553
+ if dict(
554
+ value.requested_source_target_counts
555
+ if kind is SearchArmKind.SOURCE
556
+ else value.requested_operator_target_counts
557
+ ).get(arm_id, 0)
558
+ > 0
559
+ )
560
+ requested_slots = sum(
561
+ dict(
562
+ value.requested_source_target_counts
563
+ if kind is SearchArmKind.SOURCE
564
+ else value.requested_operator_target_counts
565
+ )[arm_id]
566
+ for value in allocation_rows
567
+ )
568
+ realized_overlap = sum(
569
+ min(
570
+ dict(
571
+ value.requested_source_target_counts
572
+ if kind is SearchArmKind.SOURCE
573
+ else value.requested_operator_target_counts
574
+ )[arm_id],
575
+ dict(
576
+ value.realized_source_target_counts
577
+ if kind is SearchArmKind.SOURCE
578
+ else value.realized_operator_target_counts
579
+ )[arm_id],
580
+ )
581
+ for value in allocation_rows
582
+ )
583
+ return ContextualArmPosterior(
584
+ kind=kind,
585
+ arm_id=arm_id,
586
+ observation_count=len(selected),
587
+ positive_count=sum(value.positive_marginal_utility for value in selected),
588
+ feasible_count=sum(value.feasible for value in selected),
589
+ stage_persistence_observation_count=len(stage_persistence),
590
+ stage_persistence_positive_count=sum(
591
+ value is True for value in stage_persistence
592
+ ),
593
+ persistence_observation_count=len(persistence),
594
+ persistence_positive_count=sum(value is True for value in persistence),
595
+ descendant_observation_count=len(descendants),
596
+ descendant_positive_count=sum(value is True for value in descendants),
597
+ allocation_requested_slot_count=requested_slots,
598
+ allocation_realized_overlap_count=realized_overlap,
599
+ allocation_projection_count=len(allocation_rows),
600
+ normalized_marginal_utility_sum=float(
601
+ sum(value.normalized_marginal_utility for value in selected)
602
+ ),
603
+ marginal_utility_share_sum=float(
604
+ sum(value.marginal_utility_share for value in selected)
605
+ ),
606
+ mean_source_distance=(
607
+ 0.0
608
+ if not selected
609
+ else float(sum(value.source_distance for value in selected) / len(selected))
610
+ ),
611
+ )
612
+
613
+
614
+ @dataclass(frozen=True, slots=True)
615
+ class ContextualSearchSnapshot:
616
+ campaign_scope_sha256: str
617
+ cutoff_wave_index_exclusive: int
618
+ observation_sha256s: tuple[str, ...]
619
+ delayed_credit_sha256s: tuple[str, ...]
620
+ allocation_realization_sha256s: tuple[str, ...]
621
+ source_posteriors: tuple[ContextualArmPosterior, ...]
622
+ operator_posteriors: tuple[ContextualArmPosterior, ...]
623
+ snapshot_sha256: str = field(init=False)
624
+
625
+ def __post_init__(self) -> None:
626
+ require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
627
+ if (
628
+ type(self.cutoff_wave_index_exclusive) is not int
629
+ or self.cutoff_wave_index_exclusive <= 0
630
+ ):
631
+ raise ValueError("cutoff_wave_index_exclusive must be positive")
632
+ if self.observation_sha256s != tuple(sorted(set(self.observation_sha256s))):
633
+ raise ValueError("observation hashes must be unique and canonical")
634
+ for value in self.observation_sha256s:
635
+ require_sha256(value, "observation_sha256")
636
+ if self.delayed_credit_sha256s != tuple(
637
+ sorted(set(self.delayed_credit_sha256s))
638
+ ):
639
+ raise ValueError("delayed credit hashes must be unique and canonical")
640
+ for value in self.delayed_credit_sha256s:
641
+ require_sha256(value, "delayed_credit_sha256")
642
+ if self.allocation_realization_sha256s != tuple(
643
+ sorted(set(self.allocation_realization_sha256s))
644
+ ):
645
+ raise ValueError(
646
+ "allocation realization hashes must be unique and canonical"
647
+ )
648
+ for value in self.allocation_realization_sha256s:
649
+ require_sha256(value, "allocation_realization_sha256")
650
+ for name, kind in (
651
+ ("source_posteriors", SearchArmKind.SOURCE),
652
+ ("operator_posteriors", SearchArmKind.OPERATOR),
653
+ ):
654
+ values = getattr(self, name)
655
+ if type(values) is not tuple or any(
656
+ type(value) is not ContextualArmPosterior for value in values
657
+ ):
658
+ raise TypeError(f"{name} must contain exact posteriors")
659
+ for value in values:
660
+ value.__post_init__()
661
+ if value.kind is not kind:
662
+ raise ValueError(f"{name} contains a foreign arm kind")
663
+ if tuple(value.arm_id for value in values) != tuple(
664
+ sorted({value.arm_id for value in values})
665
+ ):
666
+ raise ValueError(f"{name} must use canonical unique arm IDs")
667
+ object.__setattr__(
668
+ self,
669
+ "snapshot_sha256",
670
+ _hash(_SNAPSHOT_DOMAIN, self._unsigned_record()),
671
+ )
672
+
673
+ def _unsigned_record(self) -> dict[str, object]:
674
+ return {
675
+ "schema_version": 2,
676
+ "campaign_scope_sha256": self.campaign_scope_sha256,
677
+ "cutoff_wave_index_exclusive": self.cutoff_wave_index_exclusive,
678
+ "observation_sha256s": list(self.observation_sha256s),
679
+ "delayed_credit_sha256s": list(self.delayed_credit_sha256s),
680
+ "allocation_realization_sha256s": list(self.allocation_realization_sha256s),
681
+ "source_posteriors": [
682
+ value.to_record() for value in self.source_posteriors
683
+ ],
684
+ "operator_posteriors": [
685
+ value.to_record() for value in self.operator_posteriors
686
+ ],
687
+ "policy": {
688
+ "policy_id": CONTEXTUAL_SEARCH_CONTROLLER_ID,
689
+ "policy_version": CONTEXTUAL_SEARCH_CONTROLLER_VERSION,
690
+ "definition_sha256": CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256,
691
+ },
692
+ }
693
+
694
+ def to_record(self) -> dict[str, object]:
695
+ self.__post_init__()
696
+ return {**self._unsigned_record(), "snapshot_sha256": self.snapshot_sha256}
697
+
698
+
699
+ @dataclass(slots=True)
700
+ class ContextualSearchLedger:
701
+ """Append-only observation ledger with strict prior-wave snapshots."""
702
+
703
+ observations: list[ContextualSearchObservation] = field(default_factory=list)
704
+ delayed_credits: list[ContextualSearchDelayedCredit] = field(default_factory=list)
705
+ allocation_realizations: list[ContextualPortfolioAllocationRealization] = field(
706
+ default_factory=list
707
+ )
708
+
709
+ def append_batch(
710
+ self,
711
+ observations: tuple[ContextualSearchObservation, ...],
712
+ ) -> None:
713
+ if (
714
+ type(observations) is not tuple
715
+ or not observations
716
+ or any(
717
+ type(value) is not ContextualSearchObservation for value in observations
718
+ )
719
+ ):
720
+ raise ValueError("observations must contain exact controller observations")
721
+ for value in observations:
722
+ value.__post_init__()
723
+ combined = (*self.observations, *observations)
724
+ identities = tuple(value.observation_sha256 for value in combined)
725
+ if len(set(identities)) != len(identities):
726
+ raise ValueError("controller ledger cannot repeat an observation")
727
+ if tuple(
728
+ (value.wave_index, value.observation_sha256) for value in combined
729
+ ) != tuple(
730
+ sorted((value.wave_index, value.observation_sha256) for value in combined)
731
+ ):
732
+ raise ValueError("controller observations must be appended canonically")
733
+ scopes = {value.campaign_scope_sha256 for value in combined}
734
+ if len(scopes) != 1:
735
+ raise ValueError("controller ledger cannot mix campaign scopes")
736
+ self.observations.extend(observations)
737
+
738
+ def append_delayed_credit_batch(
739
+ self,
740
+ credits: tuple[ContextualSearchDelayedCredit, ...],
741
+ ) -> None:
742
+ if (
743
+ type(credits) is not tuple
744
+ or not credits
745
+ or any(
746
+ type(value) is not ContextualSearchDelayedCredit for value in credits
747
+ )
748
+ ):
749
+ raise ValueError("credits must contain exact delayed-credit events")
750
+ observations = {value.observation_sha256: value for value in self.observations}
751
+ combined = (*self.delayed_credits, *credits)
752
+ if len({value.credit_sha256 for value in combined}) != len(combined):
753
+ raise ValueError("controller ledger cannot repeat delayed credit")
754
+ if tuple(
755
+ (value.available_at_wave_index, value.credit_sha256) for value in combined
756
+ ) != tuple(
757
+ sorted(
758
+ (value.available_at_wave_index, value.credit_sha256)
759
+ for value in combined
760
+ )
761
+ ):
762
+ raise ValueError("delayed credits must be appended canonically")
763
+ occupied: set[tuple[str, str]] = set()
764
+ for value in combined:
765
+ value.__post_init__()
766
+ source = observations.get(value.source_observation_sha256)
767
+ if source is None:
768
+ raise ValueError("delayed credit cites an unknown observation")
769
+ if source.campaign_scope_sha256 != value.campaign_scope_sha256:
770
+ raise ValueError("delayed credit crosses campaign scopes")
771
+ if value.available_at_wave_index <= source.wave_index:
772
+ raise ValueError("delayed credit predates its source observation")
773
+ for name in (
774
+ "stage_front_persisted",
775
+ "final_front_persisted",
776
+ "useful_descendant_observed",
777
+ ):
778
+ delayed = getattr(value, name)
779
+ if delayed is None:
780
+ continue
781
+ if getattr(source, name) is not None:
782
+ raise ValueError("delayed credit overwrites immediate evidence")
783
+ key = (source.observation_sha256, name)
784
+ if key in occupied:
785
+ raise ValueError("delayed credit repeats one evidence channel")
786
+ occupied.add(key)
787
+ self.delayed_credits.extend(credits)
788
+
789
+ def append_allocation_realization_batch(
790
+ self,
791
+ realizations: tuple[ContextualPortfolioAllocationRealization, ...],
792
+ ) -> None:
793
+ if (
794
+ type(realizations) is not tuple
795
+ or not realizations
796
+ or any(
797
+ type(value) is not ContextualPortfolioAllocationRealization
798
+ for value in realizations
799
+ )
800
+ ):
801
+ raise ValueError("realizations must contain exact allocation evidence")
802
+ for value in realizations:
803
+ value.__post_init__()
804
+ combined = (*self.allocation_realizations, *realizations)
805
+ identities = tuple(value.realization_sha256 for value in combined)
806
+ if len(set(identities)) != len(identities):
807
+ raise ValueError(
808
+ "controller ledger cannot repeat an allocation realization"
809
+ )
810
+ if tuple(
811
+ (value.controller_wave_index, value.slice_id, value.realization_sha256)
812
+ for value in combined
813
+ ) != tuple(
814
+ sorted(
815
+ (
816
+ value.controller_wave_index,
817
+ value.slice_id,
818
+ value.realization_sha256,
819
+ )
820
+ for value in combined
821
+ )
822
+ ):
823
+ raise ValueError("allocation realizations must be appended canonically")
824
+ scopes = {value.campaign_scope_sha256 for value in combined}
825
+ if len(scopes) != 1:
826
+ raise ValueError("controller ledger cannot mix realization scopes")
827
+ self.allocation_realizations.extend(realizations)
828
+
829
+ def snapshot(
830
+ self,
831
+ *,
832
+ campaign_scope_sha256: str,
833
+ cutoff_wave_index_exclusive: int,
834
+ available_source_ids: tuple[str, ...],
835
+ available_operator_ids: tuple[str, ...],
836
+ ) -> ContextualSearchSnapshot:
837
+ require_sha256(campaign_scope_sha256, "campaign_scope_sha256")
838
+ if (
839
+ type(cutoff_wave_index_exclusive) is not int
840
+ or cutoff_wave_index_exclusive <= 0
841
+ ):
842
+ raise ValueError("cutoff_wave_index_exclusive must be positive")
843
+ _canonical_tokens(available_source_ids, name="available_source_ids")
844
+ _canonical_tokens(available_operator_ids, name="available_operator_ids")
845
+ selected = tuple(
846
+ value
847
+ for value in self.observations
848
+ if value.campaign_scope_sha256 == campaign_scope_sha256
849
+ and value.wave_index < cutoff_wave_index_exclusive
850
+ )
851
+ selected_hashes = {value.observation_sha256 for value in selected}
852
+ delayed = tuple(
853
+ value
854
+ for value in self.delayed_credits
855
+ if value.campaign_scope_sha256 == campaign_scope_sha256
856
+ and value.available_at_wave_index <= cutoff_wave_index_exclusive
857
+ and value.source_observation_sha256 in selected_hashes
858
+ )
859
+ realizations = tuple(
860
+ value
861
+ for value in self.allocation_realizations
862
+ if value.campaign_scope_sha256 == campaign_scope_sha256
863
+ and value.controller_wave_index < cutoff_wave_index_exclusive
864
+ )
865
+ return ContextualSearchSnapshot(
866
+ campaign_scope_sha256=campaign_scope_sha256,
867
+ cutoff_wave_index_exclusive=cutoff_wave_index_exclusive,
868
+ observation_sha256s=tuple(
869
+ sorted(value.observation_sha256 for value in selected)
870
+ ),
871
+ delayed_credit_sha256s=tuple(
872
+ sorted(value.credit_sha256 for value in delayed)
873
+ ),
874
+ allocation_realization_sha256s=tuple(
875
+ sorted(value.realization_sha256 for value in realizations)
876
+ ),
877
+ source_posteriors=tuple(
878
+ _posterior(
879
+ selected,
880
+ kind=SearchArmKind.SOURCE,
881
+ arm_id=value,
882
+ delayed_credits=delayed,
883
+ allocation_realizations=realizations,
884
+ )
885
+ for value in available_source_ids
886
+ ),
887
+ operator_posteriors=tuple(
888
+ _posterior(
889
+ selected,
890
+ kind=SearchArmKind.OPERATOR,
891
+ arm_id=value,
892
+ delayed_credits=delayed,
893
+ allocation_realizations=realizations,
894
+ )
895
+ for value in available_operator_ids
896
+ ),
897
+ )
898
+
899
+
900
+ @dataclass(frozen=True, slots=True)
901
+ class ContextualSearchCompletionAudit:
902
+ """Generic post-campaign integrity audit for the adaptive loop."""
903
+
904
+ campaign_scope_sha256: str
905
+ expected_wave_count: int
906
+ expected_post_recombination_wave_indices: tuple[int, ...]
907
+ expected_observation_count: int
908
+ expected_stage_credit_count: int
909
+ expected_allocation_realization_count: int
910
+ observation_count: int
911
+ allocation_realization_count: int
912
+ delayed_credit_count: int
913
+ candidate_bound_observation_count: int
914
+ unique_candidate_count: int
915
+ observation_wave_indices: tuple[int, ...]
916
+ allocation_wave_indices: tuple[int, ...]
917
+ stage_credit_wave_indices: tuple[int, ...]
918
+ stage_credit_source_sha256s: tuple[str, ...]
919
+ final_credit_source_sha256s: tuple[str, ...]
920
+ descendant_credit_source_sha256s: tuple[str, ...]
921
+ descendant_credit_wave_indices: tuple[int, ...]
922
+ audit_sha256: str = field(init=False)
923
+
924
+ def __post_init__(self) -> None:
925
+ require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
926
+ for name in (
927
+ "expected_wave_count",
928
+ "expected_observation_count",
929
+ "expected_allocation_realization_count",
930
+ ):
931
+ if type(getattr(self, name)) is not int or getattr(self, name) <= 0:
932
+ raise ValueError(f"{name} must be positive")
933
+ for name in (
934
+ "observation_count",
935
+ "expected_stage_credit_count",
936
+ "allocation_realization_count",
937
+ "delayed_credit_count",
938
+ "candidate_bound_observation_count",
939
+ "unique_candidate_count",
940
+ ):
941
+ if type(getattr(self, name)) is not int or getattr(self, name) < 0:
942
+ raise ValueError(f"{name} must be non-negative")
943
+ for name in (
944
+ "observation_wave_indices",
945
+ "allocation_wave_indices",
946
+ "expected_post_recombination_wave_indices",
947
+ "stage_credit_wave_indices",
948
+ "descendant_credit_wave_indices",
949
+ ):
950
+ values = getattr(self, name)
951
+ if type(values) is not tuple or any(
952
+ type(value) is not int or value <= 0 for value in values
953
+ ):
954
+ raise ValueError(f"{name} must contain positive exact integers")
955
+ if values != tuple(sorted(set(values))):
956
+ raise ValueError(f"{name} must be unique and canonical")
957
+ if not set(self.expected_post_recombination_wave_indices).issubset(
958
+ self.expected_wave_indices
959
+ ):
960
+ raise ValueError(
961
+ "post-recombination wave indices escape the expected horizon"
962
+ )
963
+ for name in (
964
+ "stage_credit_source_sha256s",
965
+ "final_credit_source_sha256s",
966
+ "descendant_credit_source_sha256s",
967
+ ):
968
+ values = getattr(self, name)
969
+ if type(values) is not tuple or values != tuple(sorted(set(values))):
970
+ raise ValueError(f"{name} must be a canonical exact tuple")
971
+ for value in values:
972
+ require_sha256(value, name)
973
+ object.__setattr__(
974
+ self,
975
+ "audit_sha256",
976
+ _hash(_COMPLETION_AUDIT_DOMAIN, self._unsigned_record()),
977
+ )
978
+
979
+ @property
980
+ def expected_wave_indices(self) -> tuple[int, ...]:
981
+ return tuple(range(1, self.expected_wave_count + 1))
982
+
983
+ @property
984
+ def healthy(self) -> bool:
985
+ return (
986
+ self.observation_count == self.expected_observation_count
987
+ and self.allocation_realization_count
988
+ == self.expected_allocation_realization_count
989
+ and self.delayed_credit_count
990
+ == self.expected_stage_credit_count + self.expected_observation_count
991
+ and self.candidate_bound_observation_count
992
+ == self.expected_observation_count
993
+ and self.unique_candidate_count == self.expected_observation_count
994
+ and self.observation_wave_indices == self.expected_wave_indices
995
+ and self.allocation_wave_indices == self.expected_wave_indices
996
+ and self.stage_credit_wave_indices
997
+ == self.expected_post_recombination_wave_indices
998
+ and len(self.stage_credit_source_sha256s)
999
+ == self.expected_stage_credit_count
1000
+ and len(self.final_credit_source_sha256s) == self.expected_observation_count
1001
+ )
1002
+
1003
+ def _unsigned_record(self) -> dict[str, object]:
1004
+ return {
1005
+ "schema_version": 1,
1006
+ "campaign_scope_sha256": self.campaign_scope_sha256,
1007
+ "expected_wave_count": self.expected_wave_count,
1008
+ "expected_post_recombination_wave_indices": list(
1009
+ self.expected_post_recombination_wave_indices
1010
+ ),
1011
+ "expected_observation_count": self.expected_observation_count,
1012
+ "expected_stage_credit_count": self.expected_stage_credit_count,
1013
+ "expected_allocation_realization_count": (
1014
+ self.expected_allocation_realization_count
1015
+ ),
1016
+ "observation_count": self.observation_count,
1017
+ "allocation_realization_count": self.allocation_realization_count,
1018
+ "delayed_credit_count": self.delayed_credit_count,
1019
+ "candidate_bound_observation_count": (
1020
+ self.candidate_bound_observation_count
1021
+ ),
1022
+ "unique_candidate_count": self.unique_candidate_count,
1023
+ "observation_wave_indices": list(self.observation_wave_indices),
1024
+ "allocation_wave_indices": list(self.allocation_wave_indices),
1025
+ "stage_credit_wave_indices": list(self.stage_credit_wave_indices),
1026
+ "stage_credit_source_sha256s": list(self.stage_credit_source_sha256s),
1027
+ "final_credit_source_sha256s": list(self.final_credit_source_sha256s),
1028
+ "descendant_credit_source_sha256s": list(
1029
+ self.descendant_credit_source_sha256s
1030
+ ),
1031
+ "descendant_credit_wave_indices": list(self.descendant_credit_wave_indices),
1032
+ "healthy": self.healthy,
1033
+ }
1034
+
1035
+ def to_record(self) -> dict[str, object]:
1036
+ self.__post_init__()
1037
+ return {**self._unsigned_record(), "audit_sha256": self.audit_sha256}
1038
+
1039
+
1040
+ def audit_completed_contextual_search_ledger(
1041
+ ledger: ContextualSearchLedger,
1042
+ *,
1043
+ campaign_scope_sha256: str,
1044
+ expected_wave_count: int,
1045
+ expected_post_recombination_wave_indices: tuple[int, ...],
1046
+ expected_observation_count: int,
1047
+ expected_allocation_realization_count: int,
1048
+ ) -> ContextualSearchCompletionAudit:
1049
+ """Verify candidate binding and both delayed horizons after finalization."""
1050
+
1051
+ if type(ledger) is not ContextualSearchLedger:
1052
+ raise TypeError("ledger must be exact ContextualSearchLedger")
1053
+ require_sha256(campaign_scope_sha256, "campaign_scope_sha256")
1054
+ observations = tuple(
1055
+ value
1056
+ for value in ledger.observations
1057
+ if value.campaign_scope_sha256 == campaign_scope_sha256
1058
+ )
1059
+ realizations = tuple(
1060
+ value
1061
+ for value in ledger.allocation_realizations
1062
+ if value.campaign_scope_sha256 == campaign_scope_sha256
1063
+ )
1064
+ credits = tuple(
1065
+ value
1066
+ for value in ledger.delayed_credits
1067
+ if value.campaign_scope_sha256 == campaign_scope_sha256
1068
+ )
1069
+ observation_by_sha256 = {value.observation_sha256: value for value in observations}
1070
+
1071
+ def sources(name: str) -> tuple[str, ...]:
1072
+ return tuple(
1073
+ sorted(
1074
+ value.source_observation_sha256
1075
+ for value in credits
1076
+ if getattr(value, name) is not None
1077
+ )
1078
+ )
1079
+
1080
+ stage_sources = sources("stage_front_persisted")
1081
+ descendant_sources = sources("useful_descendant_observed")
1082
+ candidate_ids = tuple(
1083
+ value.candidate_id for value in observations if value.candidate_id is not None
1084
+ )
1085
+ return ContextualSearchCompletionAudit(
1086
+ campaign_scope_sha256=campaign_scope_sha256,
1087
+ expected_wave_count=expected_wave_count,
1088
+ expected_post_recombination_wave_indices=(
1089
+ expected_post_recombination_wave_indices
1090
+ ),
1091
+ expected_observation_count=expected_observation_count,
1092
+ expected_stage_credit_count=sum(
1093
+ value.wave_index in expected_post_recombination_wave_indices
1094
+ for value in observations
1095
+ ),
1096
+ expected_allocation_realization_count=(expected_allocation_realization_count),
1097
+ observation_count=len(observations),
1098
+ allocation_realization_count=len(realizations),
1099
+ delayed_credit_count=len(credits),
1100
+ candidate_bound_observation_count=len(candidate_ids),
1101
+ unique_candidate_count=len(set(candidate_ids)),
1102
+ observation_wave_indices=tuple(
1103
+ sorted({value.wave_index for value in observations})
1104
+ ),
1105
+ allocation_wave_indices=tuple(
1106
+ sorted({value.controller_wave_index for value in realizations})
1107
+ ),
1108
+ stage_credit_source_sha256s=stage_sources,
1109
+ final_credit_source_sha256s=sources("final_front_persisted"),
1110
+ descendant_credit_source_sha256s=descendant_sources,
1111
+ stage_credit_wave_indices=tuple(
1112
+ sorted(
1113
+ {
1114
+ observation_by_sha256[value].wave_index
1115
+ for value in stage_sources
1116
+ if value in observation_by_sha256
1117
+ }
1118
+ )
1119
+ ),
1120
+ descendant_credit_wave_indices=tuple(
1121
+ sorted(
1122
+ {
1123
+ observation_by_sha256[value].wave_index
1124
+ for value in descendant_sources
1125
+ if value in observation_by_sha256
1126
+ }
1127
+ )
1128
+ ),
1129
+ )
1130
+
1131
+
1132
+ @dataclass(frozen=True, slots=True)
1133
+ class ContextualSearchQuery:
1134
+ campaign_scope_sha256: str
1135
+ wave_index: int
1136
+ total_portfolio_waves: int
1137
+ real_evaluation_slots: int
1138
+ available_source_ids: tuple[str, ...]
1139
+ available_operator_ids: tuple[str, ...]
1140
+ incumbent_source_id: str
1141
+ incumbent_operator_id: str
1142
+ archive_front_size: int
1143
+ recent_normalized_archive_gains: tuple[float, ...] = ()
1144
+ composition_evidence_available: bool = False
1145
+ source_count_capability: ContextualArmCountCapability | None = None
1146
+ operator_count_capability: ContextualArmCountCapability | None = None
1147
+ joint_count_capabilities: tuple[ContextualLaneJointCountCapability, ...] = ()
1148
+ query_sha256: str = field(init=False)
1149
+
1150
+ def __post_init__(self) -> None:
1151
+ require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
1152
+ if type(self.wave_index) is not int or self.wave_index <= 0:
1153
+ raise ValueError("wave_index must be positive")
1154
+ if (
1155
+ type(self.total_portfolio_waves) is not int
1156
+ or self.total_portfolio_waves <= 0
1157
+ or self.wave_index > self.total_portfolio_waves
1158
+ ):
1159
+ raise ValueError("total_portfolio_waves must cover the current wave")
1160
+ if (
1161
+ type(self.real_evaluation_slots) is not int
1162
+ or self.real_evaluation_slots <= 0
1163
+ ):
1164
+ raise ValueError("real_evaluation_slots must be positive")
1165
+ _canonical_tokens(self.available_source_ids, name="available_source_ids")
1166
+ _canonical_tokens(self.available_operator_ids, name="available_operator_ids")
1167
+ _require_token(self.incumbent_source_id, name="incumbent_source_id")
1168
+ if self.incumbent_source_id not in self.available_source_ids:
1169
+ raise ValueError("incumbent source must be available")
1170
+ _require_token(self.incumbent_operator_id, name="incumbent_operator_id")
1171
+ if self.incumbent_operator_id not in self.available_operator_ids:
1172
+ raise ValueError("incumbent operator must be available")
1173
+ if type(self.archive_front_size) is not int or self.archive_front_size <= 0:
1174
+ raise ValueError("archive_front_size must be positive")
1175
+ if type(self.recent_normalized_archive_gains) is not tuple:
1176
+ raise TypeError("recent_normalized_archive_gains must be an exact tuple")
1177
+ for value in self.recent_normalized_archive_gains:
1178
+ _require_probability(value, name="recent_normalized_archive_gain")
1179
+ if len(self.recent_normalized_archive_gains) > 4:
1180
+ raise ValueError("recent gain history is bounded to four stages")
1181
+ if type(self.composition_evidence_available) is not bool:
1182
+ raise TypeError("composition_evidence_available must be an exact bool")
1183
+ for name, kind, arm_ids in (
1184
+ (
1185
+ "source_count_capability",
1186
+ "source",
1187
+ self.available_source_ids,
1188
+ ),
1189
+ (
1190
+ "operator_count_capability",
1191
+ "operator",
1192
+ self.available_operator_ids,
1193
+ ),
1194
+ ):
1195
+ capability = getattr(self, name)
1196
+ if capability is None:
1197
+ continue
1198
+ if type(capability) is not ContextualArmCountCapability:
1199
+ raise TypeError(f"{name} must be an exact capability or None")
1200
+ capability.__post_init__()
1201
+ if (
1202
+ capability.kind != kind
1203
+ or capability.arm_ids != arm_ids
1204
+ or capability.evaluation_slots != self.real_evaluation_slots
1205
+ ):
1206
+ raise ValueError(f"{name} differs from the controller query")
1207
+ if any(
1208
+ witness.controller_wave_index >= self.wave_index
1209
+ for witness in capability.witnesses
1210
+ ):
1211
+ raise ValueError(f"{name} contains current/future-wave evidence")
1212
+ capabilities = self.joint_count_capabilities
1213
+ if type(capabilities) is not tuple or any(
1214
+ type(value) is not ContextualLaneJointCountCapability
1215
+ for value in capabilities
1216
+ ):
1217
+ raise TypeError(
1218
+ "joint_count_capabilities must contain exact lane capabilities"
1219
+ )
1220
+ for value in capabilities:
1221
+ value.__post_init__()
1222
+ if (
1223
+ value.source_arm_ids != self.available_source_ids
1224
+ or value.operator_arm_ids != self.available_operator_ids
1225
+ ):
1226
+ raise ValueError("joint capability differs from controller arms")
1227
+ if capabilities:
1228
+ if tuple(value.slice_id for value in capabilities) != tuple(
1229
+ sorted({value.slice_id for value in capabilities})
1230
+ ):
1231
+ raise ValueError(
1232
+ "joint lane capabilities must be unique and canonical"
1233
+ )
1234
+ if sum(value.evaluation_slots for value in capabilities) != (
1235
+ self.real_evaluation_slots
1236
+ ):
1237
+ raise ValueError(
1238
+ "joint lane capabilities must cover every evaluation slot"
1239
+ )
1240
+ object.__setattr__(
1241
+ self,
1242
+ "query_sha256",
1243
+ _hash(_QUERY_DOMAIN, self._unsigned_record()),
1244
+ )
1245
+
1246
+ @property
1247
+ def remaining_portfolio_waves(self) -> int:
1248
+ return self.total_portfolio_waves - self.wave_index + 1
1249
+
1250
+ def _unsigned_record(self) -> dict[str, object]:
1251
+ return {
1252
+ "schema_version": 3,
1253
+ "campaign_scope_sha256": self.campaign_scope_sha256,
1254
+ "wave_index": self.wave_index,
1255
+ "total_portfolio_waves": self.total_portfolio_waves,
1256
+ "remaining_portfolio_waves": self.remaining_portfolio_waves,
1257
+ "real_evaluation_slots": self.real_evaluation_slots,
1258
+ "available_source_ids": list(self.available_source_ids),
1259
+ "available_operator_ids": list(self.available_operator_ids),
1260
+ "incumbent_source_id": self.incumbent_source_id,
1261
+ "incumbent_operator_id": self.incumbent_operator_id,
1262
+ "archive_front_size": self.archive_front_size,
1263
+ "recent_normalized_archive_gain_hex": [
1264
+ value.hex() for value in self.recent_normalized_archive_gains
1265
+ ],
1266
+ "composition_evidence_available": self.composition_evidence_available,
1267
+ "source_count_capability": (
1268
+ None
1269
+ if self.source_count_capability is None
1270
+ else self.source_count_capability.to_record()
1271
+ ),
1272
+ "operator_count_capability": (
1273
+ None
1274
+ if self.operator_count_capability is None
1275
+ else self.operator_count_capability.to_record()
1276
+ ),
1277
+ "joint_count_capabilities": [
1278
+ value.to_record() for value in self.joint_count_capabilities
1279
+ ],
1280
+ }
1281
+
1282
+ def to_record(self) -> dict[str, object]:
1283
+ self.__post_init__()
1284
+ return {**self._unsigned_record(), "query_sha256": self.query_sha256}
1285
+
1286
+
1287
+ @dataclass(frozen=True, slots=True)
1288
+ class ContextualArmAllocation:
1289
+ kind: SearchArmKind
1290
+ arm_id: str
1291
+ target_slots: int
1292
+ score: float
1293
+ allocation_probability: float
1294
+ exploration_slot: bool
1295
+ unconstrained_target_slots: int
1296
+ empirical_capability_projected: bool
1297
+ prospective_joint_capability_projected: bool = False
1298
+ prospective_joint_exploration_projected: bool = False
1299
+
1300
+ def __post_init__(self) -> None:
1301
+ if type(self.kind) is not SearchArmKind:
1302
+ raise TypeError("kind must be exact SearchArmKind")
1303
+ _require_token(self.arm_id, name="arm_id")
1304
+ if type(self.target_slots) is not int or self.target_slots < 0:
1305
+ raise ValueError("target_slots must be a non-negative exact integer")
1306
+ if type(self.score) is not float or not math.isfinite(self.score):
1307
+ raise TypeError("score must be a finite canonical float")
1308
+ _require_probability(
1309
+ self.allocation_probability,
1310
+ name="allocation_probability",
1311
+ )
1312
+ if type(self.exploration_slot) is not bool:
1313
+ raise TypeError("exploration_slot must be an exact bool")
1314
+ if self.exploration_slot and self.target_slots == 0:
1315
+ raise ValueError("exploration marker requires a target slot")
1316
+ if (
1317
+ type(self.unconstrained_target_slots) is not int
1318
+ or self.unconstrained_target_slots < 0
1319
+ ):
1320
+ raise ValueError(
1321
+ "unconstrained_target_slots must be a non-negative exact integer"
1322
+ )
1323
+ if type(self.empirical_capability_projected) is not bool:
1324
+ raise TypeError("empirical_capability_projected must be an exact bool")
1325
+ if type(self.prospective_joint_capability_projected) is not bool:
1326
+ raise TypeError(
1327
+ "prospective_joint_capability_projected must be an exact bool"
1328
+ )
1329
+ if type(self.prospective_joint_exploration_projected) is not bool:
1330
+ raise TypeError(
1331
+ "prospective_joint_exploration_projected must be an exact bool"
1332
+ )
1333
+ if (
1334
+ self.prospective_joint_exploration_projected
1335
+ and not self.prospective_joint_capability_projected
1336
+ ):
1337
+ raise ValueError(
1338
+ "exploration projection requires joint-capability projection"
1339
+ )
1340
+ if not (
1341
+ self.empirical_capability_projected
1342
+ or self.prospective_joint_capability_projected
1343
+ ) and (
1344
+ self.target_slots != self.unconstrained_target_slots
1345
+ ):
1346
+ raise ValueError("unprojected allocation changed its unconstrained target")
1347
+
1348
+ def to_record(self) -> dict[str, object]:
1349
+ self.__post_init__()
1350
+ return {
1351
+ "kind": self.kind.value,
1352
+ "arm_id": self.arm_id,
1353
+ "target_slots": self.target_slots,
1354
+ "score_hex": self.score.hex(),
1355
+ "allocation_probability_hex": self.allocation_probability.hex(),
1356
+ "exploration_slot": self.exploration_slot,
1357
+ "unconstrained_target_slots": self.unconstrained_target_slots,
1358
+ "empirical_capability_projected": self.empirical_capability_projected,
1359
+ "prospective_joint_capability_projected": (
1360
+ self.prospective_joint_capability_projected
1361
+ ),
1362
+ "prospective_joint_exploration_projected": (
1363
+ self.prospective_joint_exploration_projected
1364
+ ),
1365
+ }
1366
+
1367
+
1368
+ @dataclass(frozen=True, slots=True)
1369
+ class ContextualSearchDecision:
1370
+ query: ContextualSearchQuery
1371
+ snapshot: ContextualSearchSnapshot
1372
+ phase: SearchPhase
1373
+ source_allocations: tuple[ContextualArmAllocation, ...]
1374
+ operator_allocations: tuple[ContextualArmAllocation, ...]
1375
+ joint_capability_selection: tuple[tuple[str, str], ...] = ()
1376
+ decision_sha256: str = field(init=False)
1377
+
1378
+ def __post_init__(self) -> None:
1379
+ if type(self.query) is not ContextualSearchQuery:
1380
+ raise TypeError("query must be exact ContextualSearchQuery")
1381
+ self.query.__post_init__()
1382
+ if type(self.snapshot) is not ContextualSearchSnapshot:
1383
+ raise TypeError("snapshot must be exact ContextualSearchSnapshot")
1384
+ self.snapshot.__post_init__()
1385
+ if (
1386
+ self.snapshot.campaign_scope_sha256 != self.query.campaign_scope_sha256
1387
+ or self.snapshot.cutoff_wave_index_exclusive != self.query.wave_index
1388
+ ):
1389
+ raise ValueError("controller snapshot differs from its query cutoff")
1390
+ snapshot_realizations = set(self.snapshot.allocation_realization_sha256s)
1391
+ for capability in (
1392
+ self.query.source_count_capability,
1393
+ self.query.operator_count_capability,
1394
+ ):
1395
+ if capability is not None and not set(
1396
+ capability.allocation_realization_sha256s
1397
+ ).issubset(snapshot_realizations):
1398
+ raise ValueError(
1399
+ "controller capability cites evidence outside its snapshot"
1400
+ )
1401
+ if type(self.phase) is not SearchPhase:
1402
+ raise TypeError("phase must be exact SearchPhase")
1403
+ for name, kind, available in (
1404
+ (
1405
+ "source_allocations",
1406
+ SearchArmKind.SOURCE,
1407
+ self.query.available_source_ids,
1408
+ ),
1409
+ (
1410
+ "operator_allocations",
1411
+ SearchArmKind.OPERATOR,
1412
+ self.query.available_operator_ids,
1413
+ ),
1414
+ ):
1415
+ values = getattr(self, name)
1416
+ if type(values) is not tuple or any(
1417
+ type(value) is not ContextualArmAllocation for value in values
1418
+ ):
1419
+ raise TypeError(f"{name} must contain exact allocations")
1420
+ for value in values:
1421
+ value.__post_init__()
1422
+ if value.kind is not kind:
1423
+ raise ValueError(f"{name} contains a foreign arm kind")
1424
+ if tuple(value.arm_id for value in values) != available:
1425
+ raise ValueError(f"{name} must cover available arms canonically")
1426
+ if sum(value.target_slots for value in values) != (
1427
+ self.query.real_evaluation_slots
1428
+ ):
1429
+ raise ValueError(f"{name} must allocate every real evaluation slot")
1430
+ if not math.isclose(
1431
+ sum(value.allocation_probability for value in values),
1432
+ 1.0,
1433
+ rel_tol=0.0,
1434
+ abs_tol=1e-12,
1435
+ ):
1436
+ raise ValueError(f"{name} allocation probabilities must sum to one")
1437
+ expected_exploration = (
1438
+ 0
1439
+ if self.phase is SearchPhase.TERMINAL_CONVERSION
1440
+ else min(1, len(values) - 1)
1441
+ )
1442
+ if sum(value.exploration_slot for value in values) != expected_exploration:
1443
+ raise ValueError(f"{name} has an invalid exploration-slot count")
1444
+ selection = self.joint_capability_selection
1445
+ if type(selection) is not tuple or any(
1446
+ type(value) is not tuple
1447
+ or len(value) != 2
1448
+ or type(value[0]) is not str
1449
+ or type(value[1]) is not str
1450
+ for value in selection
1451
+ ):
1452
+ raise TypeError(
1453
+ "joint_capability_selection must contain exact slice/hash pairs"
1454
+ )
1455
+ capabilities = self.query.joint_count_capabilities
1456
+ if not capabilities:
1457
+ if selection:
1458
+ raise ValueError(
1459
+ "decision selected a joint vector without query capabilities"
1460
+ )
1461
+ else:
1462
+ if tuple(slice_id for slice_id, _ in selection) != tuple(
1463
+ value.slice_id for value in capabilities
1464
+ ):
1465
+ raise ValueError(
1466
+ "joint capability selection must exactly cover query lanes"
1467
+ )
1468
+ selected_vectors = tuple(
1469
+ capability.resolve_vector(vector_sha256)
1470
+ for capability, (slice_id, vector_sha256) in zip(
1471
+ capabilities,
1472
+ selection,
1473
+ strict=True,
1474
+ )
1475
+ )
1476
+ if any(
1477
+ capability.slice_id != slice_id
1478
+ for capability, (slice_id, _) in zip(
1479
+ capabilities,
1480
+ selection,
1481
+ strict=True,
1482
+ )
1483
+ ):
1484
+ raise ValueError("joint selection lane differs from its capability")
1485
+ for count_name, allocations in (
1486
+ ("source_target_counts", self.source_allocations),
1487
+ ("operator_target_counts", self.operator_allocations),
1488
+ ):
1489
+ aggregate = {
1490
+ allocation.arm_id: sum(
1491
+ dict(getattr(vector, count_name))[allocation.arm_id]
1492
+ for vector in selected_vectors
1493
+ )
1494
+ for allocation in allocations
1495
+ }
1496
+ expected = {
1497
+ allocation.arm_id: allocation.target_slots
1498
+ for allocation in allocations
1499
+ }
1500
+ if aggregate != expected:
1501
+ raise ValueError(
1502
+ "joint capability selection differs from decision targets"
1503
+ )
1504
+ object.__setattr__(
1505
+ self,
1506
+ "decision_sha256",
1507
+ _hash(_DECISION_DOMAIN, self._unsigned_record()),
1508
+ )
1509
+
1510
+ def _unsigned_record(self) -> dict[str, object]:
1511
+ return {
1512
+ "schema_version": 3,
1513
+ "query_sha256": self.query.query_sha256,
1514
+ "snapshot_sha256": self.snapshot.snapshot_sha256,
1515
+ "phase": self.phase.value,
1516
+ "source_allocations": [
1517
+ value.to_record() for value in self.source_allocations
1518
+ ],
1519
+ "operator_allocations": [
1520
+ value.to_record() for value in self.operator_allocations
1521
+ ],
1522
+ "joint_capability_selection": [
1523
+ {"slice_id": slice_id, "vector_sha256": vector_sha256}
1524
+ for slice_id, vector_sha256 in self.joint_capability_selection
1525
+ ],
1526
+ "terminal_information_bonus": (
1527
+ 0.0 if self.phase is SearchPhase.TERMINAL_CONVERSION else None
1528
+ ),
1529
+ "policy": {
1530
+ "policy_id": CONTEXTUAL_SEARCH_CONTROLLER_ID,
1531
+ "policy_version": CONTEXTUAL_SEARCH_CONTROLLER_VERSION,
1532
+ "definition_sha256": CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256,
1533
+ },
1534
+ }
1535
+
1536
+ def to_record(self) -> dict[str, object]:
1537
+ self.__post_init__()
1538
+ return {
1539
+ **self._unsigned_record(),
1540
+ "query": self.query.to_record(),
1541
+ "snapshot": self.snapshot.to_record(),
1542
+ "decision_sha256": self.decision_sha256,
1543
+ }
1544
+
1545
+
1546
+ @dataclass(frozen=True, slots=True)
1547
+ class ContextualPortfolioAllocationSlice:
1548
+ """One exact request-local slice of a stage-global controller decision."""
1549
+
1550
+ campaign_scope_sha256: str
1551
+ query_sha256: str
1552
+ decision_sha256: str
1553
+ wave_index: int
1554
+ phase: SearchPhase
1555
+ slice_id: str
1556
+ evaluation_slots: int
1557
+ source_target_counts: tuple[tuple[str, int], ...]
1558
+ operator_target_counts: tuple[tuple[str, int], ...]
1559
+ minimum_single_path_interventions: int = 0
1560
+ minimum_disjoint_parent_patch_pairs: int = 0
1561
+ feasibility_witness_option_identity_sha256s: tuple[str, ...] = ()
1562
+ slice_sha256: str = field(init=False)
1563
+
1564
+ def __post_init__(self) -> None:
1565
+ for name in (
1566
+ "campaign_scope_sha256",
1567
+ "query_sha256",
1568
+ "decision_sha256",
1569
+ ):
1570
+ require_sha256(getattr(self, name), name)
1571
+ if type(self.wave_index) is not int or self.wave_index <= 0:
1572
+ raise ValueError("wave_index must be positive")
1573
+ if type(self.phase) is not SearchPhase:
1574
+ raise TypeError("phase must be exact SearchPhase")
1575
+ _require_token(self.slice_id, name="slice_id")
1576
+ if type(self.evaluation_slots) is not int or self.evaluation_slots <= 0:
1577
+ raise ValueError("evaluation_slots must be positive")
1578
+ if (
1579
+ type(self.minimum_single_path_interventions) is not int
1580
+ or not 0
1581
+ <= self.minimum_single_path_interventions
1582
+ <= self.evaluation_slots
1583
+ ):
1584
+ raise ValueError(
1585
+ "minimum_single_path_interventions must lie in slice capacity"
1586
+ )
1587
+ maximum_pairs = self.evaluation_slots * (self.evaluation_slots - 1) // 2
1588
+ if (
1589
+ type(self.minimum_disjoint_parent_patch_pairs) is not int
1590
+ or not 0
1591
+ <= self.minimum_disjoint_parent_patch_pairs
1592
+ <= maximum_pairs
1593
+ ):
1594
+ raise ValueError(
1595
+ "minimum_disjoint_parent_patch_pairs must lie in slice pair capacity"
1596
+ )
1597
+ for name in ("source_target_counts", "operator_target_counts"):
1598
+ values = getattr(self, name)
1599
+ if type(values) is not tuple or not values:
1600
+ raise ValueError(f"{name} must be a non-empty exact tuple")
1601
+ for arm_id, count in values:
1602
+ _require_token(arm_id, name=f"{name}.arm_id")
1603
+ if type(count) is not int or count < 0:
1604
+ raise ValueError(f"{name} counts must be non-negative")
1605
+ if values != tuple(sorted(values)) or len(
1606
+ {arm_id for arm_id, _ in values}
1607
+ ) != len(values):
1608
+ raise ValueError(f"{name} must use canonical unique arms")
1609
+ if sum(count for _, count in values) != self.evaluation_slots:
1610
+ raise ValueError(f"{name} must allocate every slice slot")
1611
+ witnesses = self.feasibility_witness_option_identity_sha256s
1612
+ if type(witnesses) is not tuple:
1613
+ raise TypeError(
1614
+ "feasibility_witness_option_identity_sha256s must be an exact tuple"
1615
+ )
1616
+ if witnesses:
1617
+ if len(witnesses) != self.evaluation_slots or witnesses != tuple(
1618
+ sorted(set(witnesses))
1619
+ ):
1620
+ raise ValueError(
1621
+ "allocation-slice feasibility witness must contain one canonical "
1622
+ "unique option identity per evaluation slot"
1623
+ )
1624
+ for value in witnesses:
1625
+ require_sha256(value, "feasibility_witness_option_identity_sha256")
1626
+ object.__setattr__(
1627
+ self,
1628
+ "slice_sha256",
1629
+ _hash(_ALLOCATION_SLICE_DOMAIN, self._unsigned_record()),
1630
+ )
1631
+
1632
+ def _unsigned_record(self) -> dict[str, object]:
1633
+ record: dict[str, object] = {
1634
+ "schema_version": (
1635
+ 4
1636
+ if self.feasibility_witness_option_identity_sha256s
1637
+ else 3
1638
+ if self.minimum_disjoint_parent_patch_pairs
1639
+ else 2
1640
+ if self.minimum_single_path_interventions
1641
+ else 1
1642
+ ),
1643
+ "campaign_scope_sha256": self.campaign_scope_sha256,
1644
+ "query_sha256": self.query_sha256,
1645
+ "decision_sha256": self.decision_sha256,
1646
+ "wave_index": self.wave_index,
1647
+ "phase": self.phase.value,
1648
+ "slice_id": self.slice_id,
1649
+ "evaluation_slots": self.evaluation_slots,
1650
+ "source_target_counts": [
1651
+ list(value) for value in self.source_target_counts
1652
+ ],
1653
+ "operator_target_counts": [
1654
+ list(value) for value in self.operator_target_counts
1655
+ ],
1656
+ }
1657
+ if self.minimum_single_path_interventions:
1658
+ record["minimum_single_path_interventions"] = (
1659
+ self.minimum_single_path_interventions
1660
+ )
1661
+ record["intervention_axis"] = (
1662
+ "exact_parent_relative_changed_json_path_count"
1663
+ )
1664
+ if self.minimum_disjoint_parent_patch_pairs:
1665
+ record["minimum_disjoint_parent_patch_pairs"] = (
1666
+ self.minimum_disjoint_parent_patch_pairs
1667
+ )
1668
+ record["offspring_opportunity_axis"] = (
1669
+ "pairwise_disjoint_parent_relative_patch_pairs"
1670
+ )
1671
+ if self.feasibility_witness_option_identity_sha256s:
1672
+ record["feasibility_witness_option_identity_sha256s"] = list(
1673
+ self.feasibility_witness_option_identity_sha256s
1674
+ )
1675
+ record["feasibility_witness_semantics"] = (
1676
+ "current_finite_contract_exact_joint_count_and_structural_witness"
1677
+ )
1678
+ return record
1679
+
1680
+ def to_record(self) -> dict[str, object]:
1681
+ self.__post_init__()
1682
+ return {**self._unsigned_record(), "slice_sha256": self.slice_sha256}
1683
+
1684
+ def target_count(self, kind: SearchArmKind, arm_id: str) -> int:
1685
+ if type(kind) is not SearchArmKind:
1686
+ raise TypeError("kind must be exact SearchArmKind")
1687
+ _require_token(arm_id, name="arm_id")
1688
+ values = (
1689
+ self.source_target_counts
1690
+ if kind is SearchArmKind.SOURCE
1691
+ else self.operator_target_counts
1692
+ )
1693
+ try:
1694
+ return dict(values)[arm_id]
1695
+ except KeyError as error:
1696
+ raise ValueError("arm is absent from this allocation slice") from error
1697
+
1698
+ def to_contract(
1699
+ self,
1700
+ *,
1701
+ campaign_generation: int,
1702
+ ) -> ContextualPortfolioAllocationContract:
1703
+ if type(campaign_generation) is not int or campaign_generation <= 0:
1704
+ raise ValueError("campaign_generation must be positive")
1705
+ return ContextualPortfolioAllocationContract(
1706
+ campaign_scope_sha256=self.campaign_scope_sha256,
1707
+ query_sha256=self.query_sha256,
1708
+ decision_sha256=self.decision_sha256,
1709
+ campaign_generation=campaign_generation,
1710
+ controller_wave_index=self.wave_index,
1711
+ phase_id=self.phase.value,
1712
+ slice_id=self.slice_id,
1713
+ evaluation_slots=self.evaluation_slots,
1714
+ source_target_counts=self.source_target_counts,
1715
+ operator_target_counts=self.operator_target_counts,
1716
+ minimum_single_path_interventions=(
1717
+ self.minimum_single_path_interventions
1718
+ ),
1719
+ minimum_disjoint_parent_patch_pairs=(
1720
+ self.minimum_disjoint_parent_patch_pairs
1721
+ ),
1722
+ feasibility_witness_option_identity_sha256s=(
1723
+ self.feasibility_witness_option_identity_sha256s
1724
+ ),
1725
+ )
1726
+
1727
+
1728
+ @dataclass(frozen=True, slots=True)
1729
+ class ContextualSearchStageAllocation:
1730
+ """Authenticated decomposition of one decision across concurrent parents."""
1731
+
1732
+ decision: ContextualSearchDecision
1733
+ slices: tuple[ContextualPortfolioAllocationSlice, ...]
1734
+ allocation_sha256: str = field(init=False)
1735
+
1736
+ def __post_init__(self) -> None:
1737
+ if type(self.decision) is not ContextualSearchDecision:
1738
+ raise TypeError("decision must be exact ContextualSearchDecision")
1739
+ self.decision.__post_init__()
1740
+ if (
1741
+ type(self.slices) is not tuple
1742
+ or not self.slices
1743
+ or any(
1744
+ type(value) is not ContextualPortfolioAllocationSlice
1745
+ for value in self.slices
1746
+ )
1747
+ ):
1748
+ raise ValueError("slices must contain exact allocation slices")
1749
+ for value in self.slices:
1750
+ value.__post_init__()
1751
+ if (
1752
+ value.campaign_scope_sha256 != self.decision.query.campaign_scope_sha256
1753
+ or value.query_sha256 != self.decision.query.query_sha256
1754
+ or value.decision_sha256 != self.decision.decision_sha256
1755
+ or value.wave_index != self.decision.query.wave_index
1756
+ or value.phase is not self.decision.phase
1757
+ ):
1758
+ raise ValueError("allocation slice differs from its decision")
1759
+ if tuple(value.slice_id for value in self.slices) != tuple(
1760
+ sorted({value.slice_id for value in self.slices})
1761
+ ):
1762
+ raise ValueError("allocation slices must be unique and canonical")
1763
+ if self.decision.joint_capability_selection:
1764
+ selected_vectors = tuple(
1765
+ capability.resolve_vector(vector_sha256)
1766
+ for capability, (slice_id, vector_sha256) in zip(
1767
+ self.decision.query.joint_count_capabilities,
1768
+ self.decision.joint_capability_selection,
1769
+ strict=True,
1770
+ )
1771
+ if capability.slice_id == slice_id
1772
+ )
1773
+ if len(selected_vectors) != len(self.slices) or any(
1774
+ allocation.feasibility_witness_option_identity_sha256s
1775
+ != vector.feasibility_witness_option_identity_sha256s
1776
+ for allocation, vector in zip(
1777
+ self.slices,
1778
+ selected_vectors,
1779
+ strict=True,
1780
+ )
1781
+ ):
1782
+ raise ValueError(
1783
+ "allocation slices differ from their selected joint witnesses"
1784
+ )
1785
+ elif any(
1786
+ value.feasibility_witness_option_identity_sha256s
1787
+ for value in self.slices
1788
+ ):
1789
+ raise ValueError(
1790
+ "allocation slice carries a witness without joint capability selection"
1791
+ )
1792
+ if sum(value.evaluation_slots for value in self.slices) != (
1793
+ self.decision.query.real_evaluation_slots
1794
+ ):
1795
+ raise ValueError("allocation slices do not cover the stage")
1796
+ for name, allocations in (
1797
+ ("source_target_counts", self.decision.source_allocations),
1798
+ ("operator_target_counts", self.decision.operator_allocations),
1799
+ ):
1800
+ aggregate = {
1801
+ allocation.arm_id: sum(
1802
+ dict(getattr(value, name))[allocation.arm_id]
1803
+ for value in self.slices
1804
+ )
1805
+ for allocation in allocations
1806
+ }
1807
+ expected = {
1808
+ allocation.arm_id: allocation.target_slots for allocation in allocations
1809
+ }
1810
+ if aggregate != expected:
1811
+ raise ValueError("allocation slices differ from stage targets")
1812
+ object.__setattr__(
1813
+ self,
1814
+ "allocation_sha256",
1815
+ _hash(_STAGE_ALLOCATION_DOMAIN, self._unsigned_record()),
1816
+ )
1817
+
1818
+ def _unsigned_record(self) -> dict[str, object]:
1819
+ return {
1820
+ "schema_version": 1,
1821
+ "decision_sha256": self.decision.decision_sha256,
1822
+ "slice_sha256s": [value.slice_sha256 for value in self.slices],
1823
+ }
1824
+
1825
+ def to_record(self) -> dict[str, object]:
1826
+ self.__post_init__()
1827
+ return {
1828
+ **self._unsigned_record(),
1829
+ "decision": self.decision.to_record(),
1830
+ "slices": [value.to_record() for value in self.slices],
1831
+ "allocation_sha256": self.allocation_sha256,
1832
+ }
1833
+
1834
+
1835
+ def _slice_arm_counts(
1836
+ allocations: tuple[ContextualArmAllocation, ...],
1837
+ slice_slots: tuple[int, ...],
1838
+ ) -> tuple[tuple[tuple[str, int], ...], ...]:
1839
+ remaining = {value.arm_id: value.target_slots for value in allocations}
1840
+ remaining_slots = sum(slice_slots)
1841
+ rows: list[tuple[tuple[str, int], ...]] = []
1842
+ for index, slots in enumerate(slice_slots):
1843
+ if index == len(slice_slots) - 1:
1844
+ counts = dict(remaining)
1845
+ else:
1846
+ desired = {
1847
+ arm_id: count * slots / remaining_slots
1848
+ for arm_id, count in remaining.items()
1849
+ }
1850
+ counts = {arm_id: math.floor(value) for arm_id, value in desired.items()}
1851
+ for _ in range(slots - sum(counts.values())):
1852
+ arm_id = sorted(
1853
+ remaining,
1854
+ key=lambda value: (
1855
+ -(desired[value] - counts[value]),
1856
+ value,
1857
+ ),
1858
+ )[0]
1859
+ counts[arm_id] += 1
1860
+ if sum(counts.values()) != slots or any(
1861
+ counts[arm_id] > remaining[arm_id] for arm_id in remaining
1862
+ ):
1863
+ raise AssertionError("deficit rounding produced an invalid slice")
1864
+ rows.append(tuple(sorted(counts.items())))
1865
+ remaining = {arm_id: remaining[arm_id] - counts[arm_id] for arm_id in remaining}
1866
+ remaining_slots -= slots
1867
+ if remaining_slots != 0 or any(remaining.values()):
1868
+ raise AssertionError("allocation slicing left an unassigned target")
1869
+ return tuple(rows)
1870
+
1871
+
1872
+ def slice_contextual_search_decision(
1873
+ decision: ContextualSearchDecision,
1874
+ *,
1875
+ slice_ids: tuple[str, ...],
1876
+ evaluation_slots: tuple[int, ...],
1877
+ ) -> ContextualSearchStageAllocation:
1878
+ """Split stage-global marginals without workload or provider knowledge."""
1879
+
1880
+ if type(decision) is not ContextualSearchDecision:
1881
+ raise TypeError("decision must be exact ContextualSearchDecision")
1882
+ decision.__post_init__()
1883
+ _canonical_tokens(slice_ids, name="slice_ids")
1884
+ if (
1885
+ type(evaluation_slots) is not tuple
1886
+ or len(evaluation_slots) != len(slice_ids)
1887
+ or any(type(value) is not int or value <= 0 for value in evaluation_slots)
1888
+ ):
1889
+ raise ValueError("evaluation_slots must positively cover every slice")
1890
+ if sum(evaluation_slots) != decision.query.real_evaluation_slots:
1891
+ raise ValueError("slice capacity differs from the controller query")
1892
+ if decision.joint_capability_selection:
1893
+ capabilities = decision.query.joint_count_capabilities
1894
+ if tuple(value.slice_id for value in capabilities) != slice_ids:
1895
+ raise ValueError("requested slices differ from joint capabilities")
1896
+ if tuple(value.evaluation_slots for value in capabilities) != evaluation_slots:
1897
+ raise ValueError("requested slice widths differ from joint capabilities")
1898
+ vectors = tuple(
1899
+ capability.resolve_vector(vector_sha256)
1900
+ for capability, (selected_slice_id, vector_sha256) in zip(
1901
+ capabilities,
1902
+ decision.joint_capability_selection,
1903
+ strict=True,
1904
+ )
1905
+ if capability.slice_id == selected_slice_id
1906
+ )
1907
+ if len(vectors) != len(capabilities):
1908
+ raise ValueError("joint capability selection has a foreign slice")
1909
+ source_rows = tuple(value.source_target_counts for value in vectors)
1910
+ operator_rows = tuple(value.operator_target_counts for value in vectors)
1911
+ minimum_single_path_rows = tuple(
1912
+ value.minimum_single_path_interventions for value in capabilities
1913
+ )
1914
+ minimum_disjoint_pair_rows = tuple(
1915
+ value.minimum_disjoint_parent_patch_pairs for value in capabilities
1916
+ )
1917
+ feasibility_witness_rows = tuple(
1918
+ value.feasibility_witness_option_identity_sha256s for value in vectors
1919
+ )
1920
+ else:
1921
+ source_rows = _slice_arm_counts(decision.source_allocations, evaluation_slots)
1922
+ operator_rows = _slice_arm_counts(
1923
+ decision.operator_allocations,
1924
+ evaluation_slots,
1925
+ )
1926
+ minimum_single_path_rows = tuple(0 for _ in slice_ids)
1927
+ minimum_disjoint_pair_rows = tuple(0 for _ in slice_ids)
1928
+ feasibility_witness_rows = tuple(() for _ in slice_ids)
1929
+ return ContextualSearchStageAllocation(
1930
+ decision=decision,
1931
+ slices=tuple(
1932
+ ContextualPortfolioAllocationSlice(
1933
+ campaign_scope_sha256=decision.query.campaign_scope_sha256,
1934
+ query_sha256=decision.query.query_sha256,
1935
+ decision_sha256=decision.decision_sha256,
1936
+ wave_index=decision.query.wave_index,
1937
+ phase=decision.phase,
1938
+ slice_id=slice_id,
1939
+ evaluation_slots=slots,
1940
+ source_target_counts=source_counts,
1941
+ operator_target_counts=operator_counts,
1942
+ minimum_single_path_interventions=(
1943
+ minimum_single_path_interventions
1944
+ ),
1945
+ minimum_disjoint_parent_patch_pairs=(
1946
+ minimum_disjoint_parent_patch_pairs
1947
+ ),
1948
+ feasibility_witness_option_identity_sha256s=(
1949
+ feasibility_witness_option_identity_sha256s
1950
+ ),
1951
+ )
1952
+ for (
1953
+ slice_id,
1954
+ slots,
1955
+ source_counts,
1956
+ operator_counts,
1957
+ minimum_single_path_interventions,
1958
+ minimum_disjoint_parent_patch_pairs,
1959
+ feasibility_witness_option_identity_sha256s,
1960
+ ) in zip(
1961
+ slice_ids,
1962
+ evaluation_slots,
1963
+ source_rows,
1964
+ operator_rows,
1965
+ minimum_single_path_rows,
1966
+ minimum_disjoint_pair_rows,
1967
+ feasibility_witness_rows,
1968
+ strict=True,
1969
+ )
1970
+ ),
1971
+ )
1972
+
1973
+
1974
+ def _phase(
1975
+ query: ContextualSearchQuery,
1976
+ snapshot: ContextualSearchSnapshot,
1977
+ ) -> SearchPhase:
1978
+ if query.remaining_portfolio_waves == 1:
1979
+ return SearchPhase.TERMINAL_CONVERSION
1980
+ if not snapshot.observation_sha256s:
1981
+ return SearchPhase.BASIN_ACQUISITION
1982
+ if query.composition_evidence_available:
1983
+ return SearchPhase.COMPOSITION
1984
+ return SearchPhase.BASIN_EXPANSION
1985
+
1986
+
1987
+ def _score(posterior: ContextualArmPosterior, phase: SearchPhase) -> float:
1988
+ """Return the exploitation value in the single archive-return currency.
1989
+
1990
+ Every evaluated infeasible or zero-yield action is already a zero in the
1991
+ fractional-Beta return posterior. Persistence, descendant incidence,
1992
+ source distance, and allocation overlap remain available for diagnosis and
1993
+ future resolved-return construction; rewarding them separately would count
1994
+ the same causal outcome more than once. Nonterminal uncertainty receives
1995
+ exactly one separately marked exploration slot in ``_allocate``. Folding
1996
+ it into every proportional target as well would pay for information twice.
1997
+ """
1998
+
1999
+ del phase
2000
+ return posterior.return_probability
2001
+
2002
+
2003
+ def _allocate(
2004
+ posteriors: tuple[ContextualArmPosterior, ...],
2005
+ *,
2006
+ slots: int,
2007
+ phase: SearchPhase,
2008
+ incumbent_arm_id: str | None,
2009
+ incumbent_prior_mass: float,
2010
+ empirical_capability: ContextualArmCountCapability | None,
2011
+ ) -> tuple[ContextualArmAllocation, ...]:
2012
+ if not posteriors:
2013
+ raise ValueError("controller allocation requires arm posteriors")
2014
+ if type(slots) is not int or slots <= 0:
2015
+ raise ValueError("slots must be positive")
2016
+ _require_probability(incumbent_prior_mass, name="incumbent_prior_mass")
2017
+ if len(posteriors) > 1 and not 0.0 < incumbent_prior_mass < 1.0:
2018
+ raise ValueError(
2019
+ "multi-arm incumbent prior mass must lie strictly inside (0, 1)"
2020
+ )
2021
+ scores = {value.arm_id: _score(value, phase) for value in posteriors}
2022
+ counts = {value.arm_id: 0 for value in posteriors}
2023
+ if incumbent_arm_id is not None and incumbent_arm_id not in {
2024
+ value.arm_id for value in posteriors
2025
+ }:
2026
+ raise ValueError("incumbent arm is absent from controller posteriors")
2027
+ incumbent_prior = {
2028
+ value.arm_id: (
2029
+ 1.0 / len(posteriors)
2030
+ if incumbent_arm_id is None
2031
+ else 1.0
2032
+ if len(posteriors) == 1
2033
+ else incumbent_prior_mass
2034
+ if value.arm_id == incumbent_arm_id
2035
+ else (1.0 - incumbent_prior_mass) / (len(posteriors) - 1)
2036
+ )
2037
+ for value in posteriors
2038
+ }
2039
+ if phase is SearchPhase.BASIN_ACQUISITION and incumbent_arm_id is not None:
2040
+ leader_id = incumbent_arm_id
2041
+ else:
2042
+ leader_id = sorted(
2043
+ posteriors,
2044
+ key=lambda value: (
2045
+ -scores[value.arm_id],
2046
+ value.observation_count,
2047
+ value.arm_id,
2048
+ ),
2049
+ )[0].arm_id
2050
+ exploration_id: str | None = None
2051
+ if phase is not SearchPhase.TERMINAL_CONVERSION and len(posteriors) > 1:
2052
+ exploration_id = sorted(
2053
+ (value for value in posteriors if value.arm_id != leader_id),
2054
+ key=lambda value: (
2055
+ -value.return_uncertainty,
2056
+ value.observation_count,
2057
+ value.arm_id,
2058
+ ),
2059
+ )[0].arm_id
2060
+ counts[exploration_id] += 1
2061
+
2062
+ remaining = slots - sum(counts.values())
2063
+ cold_start = phase is SearchPhase.BASIN_ACQUISITION and all(
2064
+ value.observation_count == 0 for value in posteriors
2065
+ )
2066
+ if cold_start:
2067
+ probabilities = incumbent_prior
2068
+ else:
2069
+ # Beta(1,1) is already the explicit cold-start prior. A second
2070
+ # temperature or hand-sized prior mixture would count prior mass twice.
2071
+ total_score = sum(scores.values())
2072
+ if total_score <= 0.0: # Defensive only: fractional-Beta means are > 0.
2073
+ probabilities = incumbent_prior
2074
+ else:
2075
+ probabilities = {
2076
+ value.arm_id: scores[value.arm_id] / total_score
2077
+ for value in posteriors
2078
+ }
2079
+ desired = {
2080
+ value.arm_id: slots * probabilities[value.arm_id] for value in posteriors
2081
+ }
2082
+ # Fill against the full-generation target after reserving exploration;
2083
+ # otherwise the forced slot would be counted twice for uncertain arms.
2084
+ for _ in range(remaining):
2085
+ chosen = sorted(
2086
+ posteriors,
2087
+ key=lambda value: (
2088
+ -(desired[value.arm_id] - counts[value.arm_id]),
2089
+ -scores[value.arm_id],
2090
+ value.arm_id,
2091
+ ),
2092
+ )[0]
2093
+ counts[chosen.arm_id] += 1
2094
+ unconstrained_counts = dict(counts)
2095
+ capability_projected = False
2096
+ if empirical_capability is not None:
2097
+ empirical_capability.__post_init__()
2098
+ arm_ids = tuple(value.arm_id for value in posteriors)
2099
+ if (
2100
+ empirical_capability.arm_ids != arm_ids
2101
+ or empirical_capability.evaluation_slots != slots
2102
+ ):
2103
+ raise ValueError("empirical capability differs from allocation arms")
2104
+ candidates = []
2105
+ for vector in empirical_capability.feasible_count_vectors:
2106
+ candidate = dict(vector)
2107
+ if exploration_id is not None and candidate[exploration_id] == 0:
2108
+ continue
2109
+ candidates.append(candidate)
2110
+ if candidates:
2111
+ counts = min(
2112
+ candidates,
2113
+ key=lambda candidate: (
2114
+ sum(
2115
+ abs(candidate[arm_id] - unconstrained_counts[arm_id])
2116
+ for arm_id in arm_ids
2117
+ ),
2118
+ -sum(scores[arm_id] * candidate[arm_id] for arm_id in arm_ids),
2119
+ tuple(candidate[arm_id] for arm_id in arm_ids),
2120
+ ),
2121
+ )
2122
+ capability_projected = counts != unconstrained_counts
2123
+ return tuple(
2124
+ ContextualArmAllocation(
2125
+ kind=value.kind,
2126
+ arm_id=value.arm_id,
2127
+ target_slots=counts[value.arm_id],
2128
+ score=float(scores[value.arm_id]),
2129
+ allocation_probability=float(probabilities[value.arm_id]),
2130
+ exploration_slot=value.arm_id == exploration_id,
2131
+ unconstrained_target_slots=unconstrained_counts[value.arm_id],
2132
+ empirical_capability_projected=capability_projected,
2133
+ )
2134
+ for value in posteriors
2135
+ )
2136
+
2137
+
2138
+ def _project_joint_capability_product(
2139
+ source_allocations: tuple[ContextualArmAllocation, ...],
2140
+ operator_allocations: tuple[ContextualArmAllocation, ...],
2141
+ capabilities: tuple[ContextualLaneJointCountCapability, ...],
2142
+ ) -> tuple[
2143
+ tuple[ContextualArmAllocation, ...],
2144
+ tuple[ContextualArmAllocation, ...],
2145
+ tuple[tuple[str, str], ...],
2146
+ ]:
2147
+ """Project independent marginals onto exact current lane capabilities."""
2148
+
2149
+ if not capabilities:
2150
+ return source_allocations, operator_allocations, ()
2151
+ for value in capabilities:
2152
+ value.__post_init__()
2153
+ source_ids = tuple(value.arm_id for value in source_allocations)
2154
+ operator_ids = tuple(value.arm_id for value in operator_allocations)
2155
+ if any(
2156
+ value.source_arm_ids != source_ids or value.operator_arm_ids != operator_ids
2157
+ for value in capabilities
2158
+ ):
2159
+ raise ValueError("joint capabilities differ from controller allocations")
2160
+ preferred_source = {
2161
+ value.arm_id: value.target_slots for value in source_allocations
2162
+ }
2163
+ preferred_operator = {
2164
+ value.arm_id: value.target_slots for value in operator_allocations
2165
+ }
2166
+ source_scores = {value.arm_id: value.score for value in source_allocations}
2167
+ operator_scores = {value.arm_id: value.score for value in operator_allocations}
2168
+ source_exploration = {
2169
+ value.arm_id for value in source_allocations if value.exploration_slot
2170
+ }
2171
+ operator_exploration = {
2172
+ value.arm_id for value in operator_allocations if value.exploration_slot
2173
+ }
2174
+ candidates: list[
2175
+ tuple[
2176
+ tuple[object, ...],
2177
+ tuple[object, ...],
2178
+ dict[str, int],
2179
+ dict[str, int],
2180
+ ]
2181
+ ] = []
2182
+ for vectors in product(*(value.feasible_vectors for value in capabilities)):
2183
+ source_counts = {
2184
+ arm_id: sum(
2185
+ dict(vector.source_target_counts)[arm_id] for vector in vectors
2186
+ )
2187
+ for arm_id in source_ids
2188
+ }
2189
+ operator_counts = {
2190
+ arm_id: sum(
2191
+ dict(vector.operator_target_counts)[arm_id] for vector in vectors
2192
+ )
2193
+ for arm_id in operator_ids
2194
+ }
2195
+ source_exploration_loss = sum(
2196
+ source_counts[value] == 0 for value in source_exploration
2197
+ )
2198
+ operator_exploration_loss = sum(
2199
+ operator_counts[value] == 0 for value in operator_exploration
2200
+ )
2201
+ source_l1 = sum(
2202
+ abs(source_counts[value] - preferred_source[value]) for value in source_ids
2203
+ )
2204
+ operator_l1 = sum(
2205
+ abs(operator_counts[value] - preferred_operator[value])
2206
+ for value in operator_ids
2207
+ )
2208
+ posterior_score = sum(
2209
+ source_scores[value] * source_counts[value] for value in source_ids
2210
+ ) + sum(
2211
+ operator_scores[value] * operator_counts[value] for value in operator_ids
2212
+ )
2213
+ key: tuple[object, ...] = (
2214
+ source_exploration_loss + operator_exploration_loss,
2215
+ source_exploration_loss,
2216
+ operator_exploration_loss,
2217
+ source_l1 + operator_l1,
2218
+ source_l1,
2219
+ operator_l1,
2220
+ -posterior_score,
2221
+ tuple(value.vector_sha256 for value in vectors),
2222
+ )
2223
+ candidates.append((key, vectors, source_counts, operator_counts))
2224
+ if not candidates: # pragma: no cover - lane capability constructors close this.
2225
+ raise ValueError("joint lane capabilities have no feasible product")
2226
+ _, vectors, selected_source, selected_operator = min(
2227
+ candidates,
2228
+ key=lambda value: value[0],
2229
+ )
2230
+
2231
+ def projected(
2232
+ allocations: tuple[ContextualArmAllocation, ...],
2233
+ selected: dict[str, int],
2234
+ ) -> tuple[ContextualArmAllocation, ...]:
2235
+ requested_exploration = tuple(
2236
+ value.arm_id for value in allocations if value.exploration_slot
2237
+ )
2238
+ realized_exploration: str | None = None
2239
+ if requested_exploration:
2240
+ requested = requested_exploration[0]
2241
+ if selected[requested] > 0:
2242
+ realized_exploration = requested
2243
+ else:
2244
+ eligible = tuple(
2245
+ value for value in allocations if selected[value.arm_id] > 0
2246
+ )
2247
+ if not eligible: # pragma: no cover - selected counts sum positive.
2248
+ raise AssertionError("joint projection selected no realized arm")
2249
+ # Infer the exploitation leader from the unconstrained dose, then
2250
+ # move the information marker to the least-entitled realizable
2251
+ # challenger. This is deterministic and consumes no workload,
2252
+ # model, provider, or outcome identifier.
2253
+ leader = sorted(
2254
+ allocations,
2255
+ key=lambda value: (
2256
+ -value.unconstrained_target_slots,
2257
+ -value.allocation_probability,
2258
+ -value.score,
2259
+ value.arm_id,
2260
+ ),
2261
+ )[0]
2262
+ challengers = tuple(
2263
+ value for value in eligible if value.arm_id != leader.arm_id
2264
+ )
2265
+ realized_exploration = sorted(
2266
+ challengers if challengers else eligible,
2267
+ key=lambda value: (
2268
+ value.unconstrained_target_slots,
2269
+ value.allocation_probability,
2270
+ value.score,
2271
+ value.arm_id,
2272
+ ),
2273
+ )[0].arm_id
2274
+ return tuple(
2275
+ replace(
2276
+ value,
2277
+ target_slots=selected[value.arm_id],
2278
+ exploration_slot=value.arm_id == realized_exploration,
2279
+ prospective_joint_capability_projected=(
2280
+ value.prospective_joint_capability_projected
2281
+ or selected[value.arm_id] != value.target_slots
2282
+ or value.exploration_slot
2283
+ != (value.arm_id == realized_exploration)
2284
+ ),
2285
+ prospective_joint_exploration_projected=(
2286
+ value.exploration_slot
2287
+ != (value.arm_id == realized_exploration)
2288
+ ),
2289
+ )
2290
+ for value in allocations
2291
+ )
2292
+
2293
+ return (
2294
+ projected(source_allocations, selected_source),
2295
+ projected(operator_allocations, selected_operator),
2296
+ tuple(
2297
+ (capability.slice_id, vector.vector_sha256)
2298
+ for capability, vector in zip(capabilities, vectors, strict=True)
2299
+ ),
2300
+ )
2301
+
2302
+
2303
+ @dataclass(frozen=True, slots=True)
2304
+ class PhaseAwareContextualSearchController:
2305
+ """Compute one prior-only, phase-aware source/operator decision."""
2306
+
2307
+ def decide(
2308
+ self,
2309
+ query: ContextualSearchQuery,
2310
+ snapshot: ContextualSearchSnapshot,
2311
+ ) -> ContextualSearchDecision:
2312
+ if type(query) is not ContextualSearchQuery:
2313
+ raise TypeError("query must be exact ContextualSearchQuery")
2314
+ if type(snapshot) is not ContextualSearchSnapshot:
2315
+ raise TypeError("snapshot must be exact ContextualSearchSnapshot")
2316
+ query.__post_init__()
2317
+ snapshot.__post_init__()
2318
+ phase = _phase(query, snapshot)
2319
+ source_allocations = _allocate(
2320
+ snapshot.source_posteriors,
2321
+ slots=query.real_evaluation_slots,
2322
+ phase=phase,
2323
+ incumbent_arm_id=query.incumbent_source_id,
2324
+ incumbent_prior_mass=0.50,
2325
+ empirical_capability=query.source_count_capability,
2326
+ )
2327
+ operator_allocations = _allocate(
2328
+ snapshot.operator_posteriors,
2329
+ slots=query.real_evaluation_slots,
2330
+ phase=phase,
2331
+ incumbent_arm_id=query.incumbent_operator_id,
2332
+ incumbent_prior_mass=0.75,
2333
+ empirical_capability=query.operator_count_capability,
2334
+ )
2335
+ (
2336
+ source_allocations,
2337
+ operator_allocations,
2338
+ joint_capability_selection,
2339
+ ) = _project_joint_capability_product(
2340
+ source_allocations,
2341
+ operator_allocations,
2342
+ query.joint_count_capabilities,
2343
+ )
2344
+ return ContextualSearchDecision(
2345
+ query=query,
2346
+ snapshot=snapshot,
2347
+ phase=phase,
2348
+ source_allocations=source_allocations,
2349
+ operator_allocations=operator_allocations,
2350
+ joint_capability_selection=joint_capability_selection,
2351
+ )
2352
+
2353
+
2354
+ __all__ = [
2355
+ "CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256",
2356
+ "CONTEXTUAL_SEARCH_CONTROLLER_ID",
2357
+ "CONTEXTUAL_SEARCH_CONTROLLER_VERSION",
2358
+ "ContextualArmAllocation",
2359
+ "ContextualArmPosterior",
2360
+ "ContextualPortfolioAllocationSlice",
2361
+ "ContextualSearchCompletionAudit",
2362
+ "ContextualSearchDecision",
2363
+ "ContextualSearchDelayedCredit",
2364
+ "ContextualSearchLedger",
2365
+ "ContextualSearchObservation",
2366
+ "ContextualSearchQuery",
2367
+ "ContextualSearchSnapshot",
2368
+ "ContextualSearchStageAllocation",
2369
+ "PhaseAwareContextualSearchController",
2370
+ "SearchArmKind",
2371
+ "SearchPhase",
2372
+ "audit_completed_contextual_search_ledger",
2373
+ "slice_contextual_search_decision",
2374
+ ]