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,2032 @@
1
+ """Replay-safe exploit/coverage recombination over one ranked portfolio wave.
2
+
3
+ This application service contains no benchmark semantics and makes no provider
4
+ calls. It accounts for every ranked child, excludes candidate-attributable
5
+ infeasibilities with authenticated no-resampling evidence, enumerates every
6
+ pair of remaining scored children, retains only exact disjoint
7
+ ancestor-relative unions, applies the existing exploit/coverage pair policy,
8
+ and evaluates the selected engine-materialized unions concurrently.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import math
16
+ import re
17
+ from collections.abc import Mapping
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from itertools import combinations
21
+ from typing import Literal
22
+
23
+ from agent_evolve.application.agentic_evolution import (
24
+ EvolutionCandidate,
25
+ InvocationOutcome,
26
+ InvocationPlan,
27
+ MaterializedInvocation,
28
+ OperatorKind,
29
+ ProposalAuthority,
30
+ RewardPolicyBinding,
31
+ )
32
+ from agent_evolve.application.materialized_variation import (
33
+ materialized_disjoint_invocation,
34
+ )
35
+ from agent_evolve.application.evolution_campaign import ArchiveUtilitySnapshot
36
+ from agent_evolve.application.outcome_relation import OutcomeRelation
37
+ from agent_evolve.application.portfolio_evolution import (
38
+ MaterializedPortfolioEngine,
39
+ PortfolioCandidateFailureEvidence,
40
+ PortfolioMemberDisposition,
41
+ PortfolioVariationWaveRequest,
42
+ PortfolioVariationWaveResult,
43
+ )
44
+ from agent_evolve.domain.ids import CandidateId, OperatorInvocationId
45
+ from agent_evolve.domain.patch import (
46
+ ArrayIndex,
47
+ JsonPath,
48
+ ObjectKey,
49
+ canonical_path_bytes,
50
+ require_sha256,
51
+ )
52
+ from agent_evolve.domain.typed_json import typed_json_equal, typed_json_sha256
53
+ from agent_evolve.policies.selection.disjoint_pairs import (
54
+ DisjointBranchFacts,
55
+ DisjointPairSelectionDecision,
56
+ DisjointParentPairPolicy,
57
+ ReplayVerifiedDisjointPair,
58
+ )
59
+ from agent_evolve.policies.selection.frozen_archive_pairs import (
60
+ ArchiveAwareDisjointPairSelectionDecision,
61
+ ArchiveAwareDisjointParentPairPolicy,
62
+ FrozenArchiveBranchUtility,
63
+ FrozenArchiveSourcePairUtility,
64
+ FrozenArchiveSourceUtilityContext,
65
+ FrozenArchiveSourceUtilityReceipt,
66
+ ObservedSourceBranch,
67
+ )
68
+ from agent_evolve.policies.selection.task_keyed_palette import PathFamilyExposure
69
+ from agent_evolve.policies.variation.disjoint_recombination import (
70
+ POLICY_ID as DISJOINT_PATCH_POLICY_ID,
71
+ )
72
+ from agent_evolve.policies.variation.disjoint_recombination import (
73
+ POLICY_VERSION as DISJOINT_PATCH_POLICY_VERSION,
74
+ )
75
+ from agent_evolve.policies.variation.disjoint_recombination import (
76
+ DisjointPatchMaterialization,
77
+ DisjointPatchRecombinationError,
78
+ DisjointPatchRecombiner,
79
+ )
80
+ from agent_evolve.policies.variation.typed_patch import derive_patch
81
+ from agent_evolve.ports.agentic_generator import SourceAttribution
82
+ from agent_evolve.ports.id_factory import IdFactory
83
+
84
+
85
+ _TOKEN = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
86
+ _WAVE_DOMAIN = b"agent-evolve:portfolio-recombination-wave:v1\x00"
87
+ _MEMBER_DOMAIN = b"agent-evolve:portfolio-recombination-member:v1\x00"
88
+ _SOURCE_EXCLUSION_DOMAIN = (
89
+ b"agent-evolve:portfolio-recombination-source-exclusion:v1\x00"
90
+ )
91
+ _NO_PAIR_DOMAIN = b"agent-evolve:portfolio-recombination-no-pair:v1\x00"
92
+ _PREPARATION_DOMAIN = b"agent-evolve:prepared-portfolio-recombination:v1\x00"
93
+ _PREPARED_MEMBER_DOMAIN = (
94
+ b"agent-evolve:evaluated-prepared-portfolio-recombination-member:v1\x00"
95
+ )
96
+
97
+
98
+ def _canonical_json(record: dict[str, object]) -> bytes:
99
+ return json.dumps(
100
+ record,
101
+ ensure_ascii=True,
102
+ allow_nan=False,
103
+ separators=(",", ":"),
104
+ sort_keys=True,
105
+ ).encode("ascii", errors="strict")
106
+
107
+
108
+ def _hash_record(domain: bytes, record: dict[str, object]) -> str:
109
+ return hashlib.sha256(domain + _canonical_json(record)).hexdigest()
110
+
111
+
112
+ def _require_token(value: str, name: str) -> None:
113
+ if type(value) is not str or _TOKEN.fullmatch(value) is None:
114
+ raise ValueError(f"{name} must use the closed lowercase token grammar")
115
+
116
+
117
+ def _path_text(path: JsonPath) -> str:
118
+ parts = ["$"]
119
+ for segment in path.segments:
120
+ if type(segment) is ObjectKey:
121
+ parts.append(f".{segment.value}")
122
+ elif type(segment) is ArrayIndex:
123
+ parts.append(f"[{segment.value}]")
124
+ else: # pragma: no cover - JsonPath closes the segment union.
125
+ raise AssertionError("unsupported JSON-path segment")
126
+ return "".join(parts)
127
+
128
+
129
+ def _canonical_exposures(
130
+ values: tuple[PathFamilyExposure, ...],
131
+ ) -> tuple[PathFamilyExposure, ...]:
132
+ if type(values) is not tuple or any(
133
+ type(value) is not PathFamilyExposure for value in values
134
+ ):
135
+ raise TypeError("path_family_exposures must contain exact values")
136
+ for value in values:
137
+ value.revalidate()
138
+ keys = tuple((canonical_path_bytes(value.path), value.family) for value in values)
139
+ if len(set(keys)) != len(keys):
140
+ raise ValueError("path/family exposure cells must be unique")
141
+ expected = tuple(
142
+ sorted(
143
+ values, key=lambda value: (canonical_path_bytes(value.path), value.family)
144
+ )
145
+ )
146
+ if values != expected:
147
+ raise ValueError("path_family_exposures must use canonical order")
148
+ return values
149
+
150
+
151
+ def frozen_archive_source_utility_context(
152
+ snapshot: ArchiveUtilitySnapshot,
153
+ ) -> FrozenArchiveSourceUtilityContext:
154
+ """Project the generic campaign snapshot into pair-policy authority."""
155
+
156
+ if type(snapshot) is not ArchiveUtilitySnapshot:
157
+ raise TypeError("snapshot must be an exact ArchiveUtilitySnapshot")
158
+ ArchiveUtilitySnapshot.__post_init__(snapshot)
159
+ return FrozenArchiveSourceUtilityContext(
160
+ utility_id=snapshot.utility_id,
161
+ utility_version=snapshot.utility_version,
162
+ utility_definition_sha256=snapshot.definition_sha256,
163
+ benchmark_sha256=snapshot.benchmark_sha256,
164
+ archive_cutoff_sha256=snapshot.archive_sha256,
165
+ archive_snapshot_sha256=snapshot.snapshot_sha256,
166
+ snapshot_generation=snapshot.generation,
167
+ )
168
+
169
+
170
+ def portfolio_recombination_observed_sources(
171
+ source_result: PortfolioVariationWaveResult,
172
+ ) -> tuple[ObservedSourceBranch, ...]:
173
+ """Project scored source branches into utility evidence.
174
+
175
+ Candidate-attributable infeasibilities remain part of the ranked
176
+ intention-to-treat wave, but they have no decision-objective vector and
177
+ therefore cannot receive archive utility or become recombination parents.
178
+ Their separate authenticated exclusion receipts are emitted by the
179
+ recombination wave.
180
+ """
181
+
182
+ if type(source_result) is not PortfolioVariationWaveResult:
183
+ raise TypeError("source_result must be an exact PortfolioVariationWaveResult")
184
+ PortfolioVariationWaveResult.__post_init__(source_result)
185
+ return tuple(
186
+ ObservedSourceBranch(
187
+ source_rank=member.materialization.rank,
188
+ candidate_id=member.materialization.candidate_id,
189
+ candidate_configuration_sha256=(
190
+ member.materialization.child_configuration_sha256
191
+ ),
192
+ source_outcome_sha256=member.outcome_sha256,
193
+ )
194
+ for member in source_result.receipt.members
195
+ if member.disposition is PortfolioMemberDisposition.SCORED
196
+ )
197
+
198
+
199
+ def bind_portfolio_recombination_source_utilities(
200
+ *,
201
+ snapshot: ArchiveUtilitySnapshot,
202
+ source_wave: PortfolioVariationWaveRequest,
203
+ source_result: PortfolioVariationWaveResult,
204
+ marginal_utilities: Mapping[CandidateId, float],
205
+ exact_pair_utilities: Mapping[tuple[CandidateId, CandidateId], float],
206
+ ) -> FrozenArchiveSourceUtilityReceipt:
207
+ """Commit injected source and exact pair utilities without benchmark semantics.
208
+
209
+ The caller-owned utility port computes the numbers. This boundary only
210
+ authenticates their archive cutoff, complete source universe, and canonical
211
+ pair universe; it never names or interprets workload objectives.
212
+ """
213
+
214
+ if type(source_wave) is not PortfolioVariationWaveRequest:
215
+ raise TypeError("source_wave must be an exact PortfolioVariationWaveRequest")
216
+ PortfolioVariationWaveRequest.__post_init__(source_wave)
217
+ if type(source_result) is not PortfolioVariationWaveResult:
218
+ raise TypeError("source_result must be an exact PortfolioVariationWaveResult")
219
+ PortfolioVariationWaveResult.__post_init__(source_result)
220
+ receipt = source_result.receipt
221
+ selection = source_wave.selection_request
222
+ if (
223
+ receipt.selection_call_id != selection.call_id
224
+ or receipt.request_sha256 != selection.request_sha256
225
+ or receipt.parent_candidate_id != source_wave.parent.candidate_id
226
+ or receipt.parent_configuration_sha256
227
+ != source_wave.parent.occurrence.configuration_hash
228
+ or receipt.generation != source_wave.generation
229
+ ):
230
+ raise ValueError("source result differs from its exact wave request")
231
+ context = frozen_archive_source_utility_context(snapshot)
232
+ if context.snapshot_generation != source_wave.generation:
233
+ raise ValueError("archive utility snapshot is stale for the source wave")
234
+ sources = portfolio_recombination_observed_sources(source_result)
235
+ source_ids = tuple(value.candidate_id for value in sources)
236
+ if not isinstance(marginal_utilities, Mapping):
237
+ raise TypeError("marginal_utilities must be a mapping")
238
+ marginal_snapshot = dict(marginal_utilities)
239
+ if set(marginal_snapshot) != set(source_ids) or any(
240
+ type(value) is not CandidateId for value in marginal_snapshot
241
+ ):
242
+ raise ValueError("marginal utilities differ from the exact source universe")
243
+ if not isinstance(exact_pair_utilities, Mapping):
244
+ raise TypeError("exact_pair_utilities must be a mapping")
245
+ pair_snapshot = dict(exact_pair_utilities)
246
+ expected_pairs = tuple(combinations(sorted(source_ids), 2))
247
+ if set(pair_snapshot) != set(expected_pairs) or any(
248
+ type(pair) is not tuple
249
+ or len(pair) != 2
250
+ or any(type(value) is not CandidateId for value in pair)
251
+ for pair in pair_snapshot
252
+ ):
253
+ raise ValueError("exact pair utilities differ from the complete source pairs")
254
+ contract = selection.finite_variation_contract
255
+ return FrozenArchiveSourceUtilityReceipt(
256
+ context=context,
257
+ source_wave_receipt_sha256=receipt.receipt_sha256,
258
+ source_request_sha256=selection.request_sha256,
259
+ source_decision_sha256=receipt.decision_sha256,
260
+ source_contract_sha256=contract.identity_sha256,
261
+ source_generation=source_wave.generation,
262
+ branches=tuple(
263
+ FrozenArchiveBranchUtility(
264
+ source=source,
265
+ marginal_utility=marginal_snapshot[source.candidate_id],
266
+ )
267
+ for source in sources
268
+ ),
269
+ pair_utilities=tuple(
270
+ FrozenArchiveSourcePairUtility(
271
+ pair_ids=pair_ids,
272
+ exact_joint_utility=pair_snapshot[pair_ids],
273
+ )
274
+ for pair_ids in expected_pairs
275
+ ),
276
+ )
277
+
278
+
279
+ @dataclass(frozen=True, slots=True)
280
+ class PortfolioRecombinationWaveRequest:
281
+ """Exact source wave, ancestor, generation, and optional exposure state."""
282
+
283
+ source_wave: PortfolioVariationWaveRequest
284
+ source_result: PortfolioVariationWaveResult
285
+ ancestor: EvolutionCandidate
286
+ generation: int
287
+ label_prefix: str
288
+ phase: str = "portfolio_recombination"
289
+ path_family_exposures: tuple[PathFamilyExposure, ...] = ()
290
+ source_archive_snapshot: ArchiveUtilitySnapshot | None = None
291
+ source_utilities: FrozenArchiveSourceUtilityReceipt | None = None
292
+
293
+ def __post_init__(self) -> None:
294
+ if type(self.source_wave) is not PortfolioVariationWaveRequest:
295
+ raise TypeError("source_wave must be an exact request")
296
+ PortfolioVariationWaveRequest.__post_init__(self.source_wave)
297
+ if type(self.source_result) is not PortfolioVariationWaveResult:
298
+ raise TypeError("source_result must be an exact result")
299
+ PortfolioVariationWaveResult.__post_init__(self.source_result)
300
+ if type(self.ancestor) is not EvolutionCandidate:
301
+ raise TypeError("ancestor must be an exact EvolutionCandidate")
302
+ EvolutionCandidate.__post_init__(self.ancestor)
303
+ if self.ancestor != self.source_wave.parent:
304
+ raise ValueError("ancestor differs from the source portfolio parent")
305
+ receipt = self.source_result.receipt
306
+ selection = self.source_wave.selection_request
307
+ if (
308
+ receipt.selection_call_id != selection.call_id
309
+ or receipt.request_sha256 != selection.request_sha256
310
+ or receipt.parent_candidate_id != self.ancestor.candidate_id
311
+ or receipt.parent_configuration_sha256
312
+ != self.ancestor.occurrence.configuration_hash
313
+ or receipt.generation != self.source_wave.generation
314
+ ):
315
+ raise ValueError("source result differs from its exact wave request")
316
+ contract = selection.finite_variation_contract
317
+ for member, candidate in zip(
318
+ receipt.members,
319
+ self.source_result.candidates,
320
+ strict=True,
321
+ ):
322
+ materialization = member.materialization
323
+ option = contract.resolve(materialization.option_id)
324
+ if (
325
+ materialization.option_identity_sha256 != option.identity_sha256
326
+ or materialization.child_configuration_sha256
327
+ != option.child_configuration_sha256
328
+ or candidate.candidate_id != materialization.candidate_id
329
+ or not typed_json_equal(
330
+ candidate.configuration, option.child_configuration
331
+ )
332
+ ):
333
+ raise ValueError(
334
+ "source branch differs from its finite option evidence"
335
+ )
336
+ if type(self.generation) is not int or self.generation <= max(
337
+ candidate.generation for candidate in self.source_result.candidates
338
+ ):
339
+ raise ValueError("recombination generation must follow every source child")
340
+ _require_token(self.label_prefix, "label_prefix")
341
+ _require_token(self.phase, "phase")
342
+ _canonical_exposures(self.path_family_exposures)
343
+ if (self.source_archive_snapshot is None) != (self.source_utilities is None):
344
+ raise ValueError(
345
+ "source_archive_snapshot and source_utilities must be supplied together"
346
+ )
347
+ if self.source_archive_snapshot is not None:
348
+ if type(self.source_archive_snapshot) is not ArchiveUtilitySnapshot:
349
+ raise TypeError(
350
+ "source_archive_snapshot must be exact ArchiveUtilitySnapshot or None"
351
+ )
352
+ ArchiveUtilitySnapshot.__post_init__(self.source_archive_snapshot)
353
+ if type(self.source_utilities) is not FrozenArchiveSourceUtilityReceipt:
354
+ raise TypeError(
355
+ "source_utilities must be exact FrozenArchiveSourceUtilityReceipt"
356
+ )
357
+ expected_context = frozen_archive_source_utility_context(
358
+ self.source_archive_snapshot
359
+ )
360
+ self.source_utilities.require_exact_context(
361
+ context=expected_context,
362
+ source_wave_receipt_sha256=receipt.receipt_sha256,
363
+ source_request_sha256=selection.request_sha256,
364
+ source_decision_sha256=receipt.decision_sha256,
365
+ source_contract_sha256=contract.identity_sha256,
366
+ source_generation=self.source_wave.generation,
367
+ source_branches=portfolio_recombination_observed_sources(
368
+ self.source_result
369
+ ),
370
+ )
371
+
372
+
373
+ class PortfolioRecombinationSourceExclusionReason(str, Enum):
374
+ """Closed reason a ranked ITT member cannot be a recombination parent."""
375
+
376
+ CANDIDATE_INFEASIBLE = "candidate_infeasible"
377
+
378
+
379
+ @dataclass(frozen=True, slots=True)
380
+ class PortfolioRecombinationSourceExclusionReceipt:
381
+ """Authenticated non-resampling exclusion of one ranked source member."""
382
+
383
+ rank: int
384
+ candidate_id: CandidateId
385
+ candidate_configuration_sha256: str
386
+ option_id: str
387
+ option_identity_sha256: str
388
+ family: str
389
+ source_outcome_sha256: str
390
+ candidate_failure: PortfolioCandidateFailureEvidence
391
+ reason: PortfolioRecombinationSourceExclusionReason = (
392
+ PortfolioRecombinationSourceExclusionReason.CANDIDATE_INFEASIBLE
393
+ )
394
+
395
+ def __post_init__(self) -> None:
396
+ if type(self.rank) is not int or self.rank <= 0:
397
+ raise ValueError("rank must be a positive exact integer")
398
+ if type(self.candidate_id) is not CandidateId:
399
+ raise TypeError("candidate_id must be exact")
400
+ CandidateId.__post_init__(self.candidate_id)
401
+ for name in (
402
+ "candidate_configuration_sha256",
403
+ "option_identity_sha256",
404
+ "source_outcome_sha256",
405
+ ):
406
+ require_sha256(getattr(self, name), name)
407
+ if type(self.option_id) is not str or not self.option_id:
408
+ raise ValueError("option_id must be non-empty")
409
+ _require_token(self.family, "family")
410
+ if type(self.candidate_failure) is not PortfolioCandidateFailureEvidence:
411
+ raise TypeError("candidate_failure must be exact")
412
+ PortfolioCandidateFailureEvidence.__post_init__(self.candidate_failure)
413
+ if type(self.reason) is not PortfolioRecombinationSourceExclusionReason:
414
+ raise TypeError("reason must be exact")
415
+
416
+ def _unsigned_record(self) -> dict[str, object]:
417
+ self.__post_init__()
418
+ return {
419
+ "schema_version": 1,
420
+ "rank": self.rank,
421
+ "candidate_id": self.candidate_id.value,
422
+ "candidate_configuration_sha256": self.candidate_configuration_sha256,
423
+ "option_id": self.option_id,
424
+ "option_identity_sha256": self.option_identity_sha256,
425
+ "family": self.family,
426
+ "source_outcome_sha256": self.source_outcome_sha256,
427
+ "reason": self.reason.value,
428
+ "candidate_failure": self.candidate_failure.to_record(),
429
+ "resampled": False,
430
+ }
431
+
432
+ @property
433
+ def exclusion_sha256(self) -> str:
434
+ return _hash_record(_SOURCE_EXCLUSION_DOMAIN, self._unsigned_record())
435
+
436
+ def to_record(self) -> dict[str, object]:
437
+ return {
438
+ **self._unsigned_record(),
439
+ "exclusion_sha256": self.exclusion_sha256,
440
+ }
441
+
442
+
443
+ class PortfolioRecombinationNoPairReason(str, Enum):
444
+ """Closed terminal meanings for an empty recombination evaluation wave."""
445
+
446
+ INSUFFICIENT_SCORED_SOURCES = "insufficient_scored_sources"
447
+ NO_REPLAY_SAFE_DISJOINT_PAIR = "no_replay_safe_disjoint_pair"
448
+
449
+
450
+ @dataclass(frozen=True, slots=True)
451
+ class PortfolioRecombinationNoPairReceipt:
452
+ """Typed proof that no recombination child could be selected."""
453
+
454
+ reason: PortfolioRecombinationNoPairReason
455
+ scored_source_count: int
456
+ excluded_source_count: int
457
+ enumerated_pair_count: int
458
+ replay_safe_pair_count: int
459
+
460
+ def __post_init__(self) -> None:
461
+ if type(self.reason) is not PortfolioRecombinationNoPairReason:
462
+ raise TypeError("reason must be exact")
463
+ for name in (
464
+ "scored_source_count",
465
+ "excluded_source_count",
466
+ "enumerated_pair_count",
467
+ "replay_safe_pair_count",
468
+ ):
469
+ value = getattr(self, name)
470
+ if type(value) is not int or value < 0:
471
+ raise ValueError(f"{name} must be an exact non-negative integer")
472
+ expected_pairs = self.scored_source_count * (
473
+ self.scored_source_count - 1
474
+ ) // 2
475
+ if self.enumerated_pair_count != expected_pairs:
476
+ raise ValueError("enumerated_pair_count does not close the source universe")
477
+ if self.replay_safe_pair_count > self.enumerated_pair_count:
478
+ raise ValueError("replay-safe count exceeds enumerated pair count")
479
+ if self.replay_safe_pair_count != 0:
480
+ raise ValueError("a no-pair receipt cannot hide a replay-safe pair")
481
+ if self.reason is PortfolioRecombinationNoPairReason.INSUFFICIENT_SCORED_SOURCES:
482
+ if self.scored_source_count >= 2:
483
+ raise ValueError("insufficient-source reason requires fewer than two")
484
+ elif self.scored_source_count < 2 or self.enumerated_pair_count == 0:
485
+ raise ValueError("no-safe-pair reason requires an enumerated pair universe")
486
+
487
+ def _unsigned_record(self) -> dict[str, object]:
488
+ self.__post_init__()
489
+ return {
490
+ "schema_version": 1,
491
+ "reason": self.reason.value,
492
+ "scored_source_count": self.scored_source_count,
493
+ "excluded_source_count": self.excluded_source_count,
494
+ "enumerated_pair_count": self.enumerated_pair_count,
495
+ "replay_safe_pair_count": self.replay_safe_pair_count,
496
+ "evaluation_wave_dispatched": False,
497
+ }
498
+
499
+ @property
500
+ def no_pair_sha256(self) -> str:
501
+ return _hash_record(_NO_PAIR_DOMAIN, self._unsigned_record())
502
+
503
+ def to_record(self) -> dict[str, object]:
504
+ return {**self._unsigned_record(), "no_pair_sha256": self.no_pair_sha256}
505
+
506
+
507
+ @dataclass(frozen=True, slots=True)
508
+ class PortfolioRecombinationBranchBinding:
509
+ """Rank, option family, reward, paths, and exposure for one source branch."""
510
+
511
+ rank: int
512
+ candidate_id: CandidateId
513
+ candidate_configuration_sha256: str
514
+ option_id: str
515
+ option_identity_sha256: str
516
+ family: str
517
+ role: str
518
+ reward: float
519
+ changed_paths: tuple[str, ...]
520
+ path_family_exposure: int
521
+ source_outcome_sha256: str
522
+
523
+ def __post_init__(self) -> None:
524
+ if type(self.rank) is not int or self.rank <= 0:
525
+ raise ValueError("rank must be a positive exact integer")
526
+ if type(self.candidate_id) is not CandidateId:
527
+ raise TypeError("candidate_id must be exact")
528
+ CandidateId.__post_init__(self.candidate_id)
529
+ for name in (
530
+ "candidate_configuration_sha256",
531
+ "option_identity_sha256",
532
+ "source_outcome_sha256",
533
+ ):
534
+ require_sha256(getattr(self, name), name)
535
+ if type(self.option_id) is not str or not self.option_id:
536
+ raise ValueError("option_id must be non-empty")
537
+ _require_token(self.family, "family")
538
+ _require_token(self.role, "role")
539
+ if self.role != f"rank_{self.rank:04d}":
540
+ raise ValueError("branch role must exactly encode its source rank")
541
+ if type(self.reward) is not float or not math.isfinite(self.reward):
542
+ raise TypeError("reward must be a finite canonical float")
543
+ if (
544
+ type(self.changed_paths) is not tuple
545
+ or not self.changed_paths
546
+ or any(
547
+ type(path) is not str or not path.startswith("$.")
548
+ for path in self.changed_paths
549
+ )
550
+ or self.changed_paths != tuple(sorted(set(self.changed_paths)))
551
+ ):
552
+ raise ValueError("changed_paths must be non-empty, unique, and canonical")
553
+ if type(self.path_family_exposure) is not int or not (
554
+ 0 <= self.path_family_exposure <= (1 << 63) - 1
555
+ ):
556
+ raise ValueError("path_family_exposure must be an exact non-negative int63")
557
+
558
+ @property
559
+ def facts(self) -> DisjointBranchFacts:
560
+ return DisjointBranchFacts(
561
+ candidate_id=self.candidate_id,
562
+ reward=self.reward,
563
+ role=self.role,
564
+ family=self.family,
565
+ path_family_exposure=self.path_family_exposure,
566
+ )
567
+
568
+ def to_record(self) -> dict[str, object]:
569
+ self.__post_init__()
570
+ return {
571
+ "rank": self.rank,
572
+ "candidate_id": self.candidate_id.value,
573
+ "candidate_configuration_sha256": self.candidate_configuration_sha256,
574
+ "option_id": self.option_id,
575
+ "option_identity_sha256": self.option_identity_sha256,
576
+ "family": self.family,
577
+ "role": self.role,
578
+ "reward_hex": self.reward.hex(),
579
+ "changed_paths": list(self.changed_paths),
580
+ "path_family_exposure": self.path_family_exposure,
581
+ "source_outcome_sha256": self.source_outcome_sha256,
582
+ }
583
+
584
+
585
+ @dataclass(frozen=True, slots=True)
586
+ class PortfolioPairAttemptReceipt:
587
+ """One member of the complete n-choose-2 mechanical enumeration ledger."""
588
+
589
+ left_candidate_id: CandidateId
590
+ right_candidate_id: CandidateId
591
+ target_candidate_id: CandidateId
592
+ replay_safe: bool
593
+ target_configuration_sha256: str | None = None
594
+ materialization_receipt_sha256: str | None = None
595
+ union_patch_sha256: str | None = None
596
+
597
+ def __post_init__(self) -> None:
598
+ for name in (
599
+ "left_candidate_id",
600
+ "right_candidate_id",
601
+ "target_candidate_id",
602
+ ):
603
+ value = getattr(self, name)
604
+ if type(value) is not CandidateId:
605
+ raise TypeError(f"{name} must be exact")
606
+ CandidateId.__post_init__(value)
607
+ if self.left_candidate_id >= self.right_candidate_id:
608
+ raise ValueError("pair branch IDs must use canonical order")
609
+ if self.target_candidate_id in {
610
+ self.left_candidate_id,
611
+ self.right_candidate_id,
612
+ }:
613
+ raise ValueError("pair target ID must differ from its branches")
614
+ if type(self.replay_safe) is not bool:
615
+ raise TypeError("replay_safe must be bool")
616
+ optional = (
617
+ self.target_configuration_sha256,
618
+ self.materialization_receipt_sha256,
619
+ self.union_patch_sha256,
620
+ )
621
+ if self.replay_safe != all(value is not None for value in optional):
622
+ raise ValueError("safe status must agree with materialization evidence")
623
+ for name in (
624
+ "target_configuration_sha256",
625
+ "materialization_receipt_sha256",
626
+ "union_patch_sha256",
627
+ ):
628
+ value = getattr(self, name)
629
+ if value is not None:
630
+ require_sha256(value, name)
631
+
632
+ @property
633
+ def pair_ids(self) -> tuple[CandidateId, CandidateId]:
634
+ return self.left_candidate_id, self.right_candidate_id
635
+
636
+ def to_record(self) -> dict[str, object]:
637
+ self.__post_init__()
638
+ return {
639
+ "left_candidate_id": self.left_candidate_id.value,
640
+ "right_candidate_id": self.right_candidate_id.value,
641
+ "target_candidate_id": self.target_candidate_id.value,
642
+ "replay_safe": self.replay_safe,
643
+ "target_configuration_sha256": self.target_configuration_sha256,
644
+ "materialization_receipt_sha256": self.materialization_receipt_sha256,
645
+ "union_patch_sha256": self.union_patch_sha256,
646
+ }
647
+
648
+
649
+ PortfolioRecombinationRole = Literal["exploit", "coverage"]
650
+ PortfolioRecombinationPairDecision = (
651
+ DisjointPairSelectionDecision | ArchiveAwareDisjointPairSelectionDecision
652
+ )
653
+
654
+
655
+ @dataclass(frozen=True, slots=True)
656
+ class PortfolioRecombinationMemberReceipt:
657
+ """Closed pair-selection, materialization, and terminal-outcome join.
658
+
659
+ A replay-safe materialization can still be rejected by a workload
660
+ evaluator. Candidate-attributable infeasibility is therefore a complete
661
+ member outcome, not a partial recombination wave or an orchestration
662
+ failure. The disposition mirrors direct portfolio variation so the core
663
+ applies one workload-independent terminal-outcome contract to both paths.
664
+ """
665
+
666
+ selection_role: PortfolioRecombinationRole
667
+ pair_ids: tuple[CandidateId, CandidateId]
668
+ source_ranks: tuple[int, int]
669
+ source_option_ids: tuple[str, str]
670
+ source_families: tuple[str, str]
671
+ target_candidate_id: CandidateId
672
+ target_configuration_sha256: str
673
+ materialization_receipt_sha256: str
674
+ union_patch_sha256: str
675
+ parent_patch_sha256s: tuple[str, str]
676
+ source_attribution: tuple[SourceAttribution, ...]
677
+ operator_invocation_id: OperatorInvocationId
678
+ reward_definition_sha256: str
679
+ reward: float
680
+ parent_relations: tuple[OutcomeRelation, ...]
681
+ detailed_evaluation_sha256: str | None
682
+ dominates_any_parent: bool
683
+ better_than_any_parent: bool
684
+ disposition: PortfolioMemberDisposition = PortfolioMemberDisposition.SCORED
685
+ candidate_failure: PortfolioCandidateFailureEvidence | None = None
686
+
687
+ def __post_init__(self) -> None:
688
+ if self.selection_role not in {"exploit", "coverage"}:
689
+ raise ValueError("selection_role must be exploit or coverage")
690
+ if (
691
+ type(self.pair_ids) is not tuple
692
+ or len(self.pair_ids) != 2
693
+ or any(type(value) is not CandidateId for value in self.pair_ids)
694
+ ):
695
+ raise TypeError("pair_ids must contain two exact CandidateId values")
696
+ if self.pair_ids[0] >= self.pair_ids[1]:
697
+ raise ValueError("pair_ids must use canonical order")
698
+ if (
699
+ type(self.source_ranks) is not tuple
700
+ or len(self.source_ranks) != 2
701
+ or any(type(value) is not int or value <= 0 for value in self.source_ranks)
702
+ ):
703
+ raise ValueError("source_ranks must contain two positive exact integers")
704
+ for name in ("source_option_ids", "source_families"):
705
+ values = getattr(self, name)
706
+ if (
707
+ type(values) is not tuple
708
+ or len(values) != 2
709
+ or any(type(value) is not str or not value for value in values)
710
+ ):
711
+ raise ValueError(f"{name} must contain two non-empty strings")
712
+ for family in self.source_families:
713
+ _require_token(family, "source family")
714
+ if type(self.target_candidate_id) is not CandidateId:
715
+ raise TypeError("target_candidate_id must be exact")
716
+ CandidateId.__post_init__(self.target_candidate_id)
717
+ if self.target_candidate_id in self.pair_ids:
718
+ raise ValueError("target candidate must differ from both parents")
719
+ for name in (
720
+ "target_configuration_sha256",
721
+ "materialization_receipt_sha256",
722
+ "union_patch_sha256",
723
+ "reward_definition_sha256",
724
+ ):
725
+ require_sha256(getattr(self, name), name)
726
+ if (
727
+ type(self.parent_patch_sha256s) is not tuple
728
+ or len(self.parent_patch_sha256s) != 2
729
+ ):
730
+ raise TypeError("parent_patch_sha256s must contain two hashes")
731
+ for value in self.parent_patch_sha256s:
732
+ require_sha256(value, "parent patch SHA-256")
733
+ if type(self.source_attribution) is not tuple or any(
734
+ type(value) is not SourceAttribution for value in self.source_attribution
735
+ ):
736
+ raise TypeError("source_attribution must contain exact values")
737
+ for value in self.source_attribution:
738
+ SourceAttribution.__post_init__(value)
739
+ if type(self.operator_invocation_id) is not OperatorInvocationId:
740
+ raise TypeError("operator_invocation_id must be exact")
741
+ OperatorInvocationId.__post_init__(self.operator_invocation_id)
742
+ if type(self.reward) is not float or not math.isfinite(self.reward):
743
+ raise TypeError("reward must be a finite canonical float")
744
+ if type(self.parent_relations) is not tuple or any(
745
+ type(value) is not OutcomeRelation for value in self.parent_relations
746
+ ):
747
+ raise TypeError("parent_relations must contain exact values")
748
+ if self.detailed_evaluation_sha256 is not None:
749
+ require_sha256(
750
+ self.detailed_evaluation_sha256,
751
+ "detailed_evaluation_sha256",
752
+ )
753
+ if (
754
+ type(self.dominates_any_parent) is not bool
755
+ or type(self.better_than_any_parent) is not bool
756
+ ):
757
+ raise TypeError("outcome comparison projections must be bool")
758
+ if type(self.disposition) is not PortfolioMemberDisposition:
759
+ raise TypeError("disposition must be a PortfolioMemberDisposition")
760
+ failure = self.candidate_failure
761
+ if self.disposition is PortfolioMemberDisposition.SCORED:
762
+ if len(self.parent_relations) != 2:
763
+ raise ValueError("scored recombination must compare to both parents")
764
+ if failure is not None:
765
+ raise ValueError(
766
+ "scored recombination cannot carry candidate failure evidence"
767
+ )
768
+ else:
769
+ if type(failure) is not PortfolioCandidateFailureEvidence:
770
+ raise TypeError(
771
+ "candidate-infeasible recombination requires exact failure evidence"
772
+ )
773
+ PortfolioCandidateFailureEvidence.__post_init__(failure)
774
+ if self.detailed_evaluation_sha256 != failure.detailed_evaluation_sha256:
775
+ raise ValueError(
776
+ "recombination failure identifies another detailed evaluation"
777
+ )
778
+ if self.parent_relations:
779
+ raise ValueError(
780
+ "candidate-infeasible recombination cannot publish parent relations"
781
+ )
782
+ if self.dominates_any_parent or self.better_than_any_parent:
783
+ raise ValueError(
784
+ "candidate-infeasible recombination cannot publish improvement flags"
785
+ )
786
+
787
+ def _unsigned_record(self) -> dict[str, object]:
788
+ self.__post_init__()
789
+ return {
790
+ "schema_version": 2,
791
+ "selection_role": self.selection_role,
792
+ "pair_ids": [value.value for value in self.pair_ids],
793
+ "source_ranks": list(self.source_ranks),
794
+ "source_option_ids": list(self.source_option_ids),
795
+ "source_families": list(self.source_families),
796
+ "target_candidate_id": self.target_candidate_id.value,
797
+ "target_configuration_sha256": self.target_configuration_sha256,
798
+ "materialization_receipt_sha256": (self.materialization_receipt_sha256),
799
+ "union_patch_sha256": self.union_patch_sha256,
800
+ "parent_patch_sha256s": list(self.parent_patch_sha256s),
801
+ "source_attribution": [
802
+ {"path": value.path, "source": value.source}
803
+ for value in self.source_attribution
804
+ ],
805
+ "operator_invocation_id": self.operator_invocation_id.value,
806
+ "reward_definition_sha256": self.reward_definition_sha256,
807
+ "disposition": self.disposition.value,
808
+ "candidate_valid": (
809
+ self.disposition is PortfolioMemberDisposition.SCORED
810
+ ),
811
+ "reward_hex": self.reward.hex(),
812
+ "parent_relations": [value.value for value in self.parent_relations],
813
+ "detailed_evaluation_sha256": self.detailed_evaluation_sha256,
814
+ "candidate_failure": (
815
+ None
816
+ if self.candidate_failure is None
817
+ else self.candidate_failure.to_record()
818
+ ),
819
+ "dominates_any_parent": self.dominates_any_parent,
820
+ "better_than_any_parent": self.better_than_any_parent,
821
+ }
822
+
823
+ @property
824
+ def outcome_sha256(self) -> str:
825
+ return _hash_record(_MEMBER_DOMAIN, self._unsigned_record())
826
+
827
+ def to_record(self) -> dict[str, object]:
828
+ return {**self._unsigned_record(), "outcome_sha256": self.outcome_sha256}
829
+
830
+
831
+ @dataclass(frozen=True, slots=True)
832
+ class PortfolioRecombinationWaveReceipt:
833
+ """Complete pair universe, deterministic decision, and selected outcomes."""
834
+
835
+ source_wave_receipt_sha256: str
836
+ source_request_sha256: str
837
+ source_decision_sha256: str
838
+ source_contract_sha256: str
839
+ ancestor_candidate_id: CandidateId
840
+ ancestor_configuration_sha256: str
841
+ generation: int
842
+ branches: tuple[PortfolioRecombinationBranchBinding, ...]
843
+ source_exclusions: tuple[PortfolioRecombinationSourceExclusionReceipt, ...]
844
+ path_family_exposures: tuple[PathFamilyExposure, ...]
845
+ pair_attempts: tuple[PortfolioPairAttemptReceipt, ...]
846
+ pair_decision: PortfolioRecombinationPairDecision
847
+ members: tuple[PortfolioRecombinationMemberReceipt, ...]
848
+ selection_limit: int = 2
849
+ no_pair: PortfolioRecombinationNoPairReceipt | None = None
850
+
851
+ def __post_init__(self) -> None:
852
+ for name in (
853
+ "source_wave_receipt_sha256",
854
+ "source_request_sha256",
855
+ "source_decision_sha256",
856
+ "source_contract_sha256",
857
+ "ancestor_configuration_sha256",
858
+ ):
859
+ require_sha256(getattr(self, name), name)
860
+ if type(self.ancestor_candidate_id) is not CandidateId:
861
+ raise TypeError("ancestor_candidate_id must be exact")
862
+ CandidateId.__post_init__(self.ancestor_candidate_id)
863
+ if type(self.generation) is not int or self.generation <= 0:
864
+ raise ValueError("generation must be positive")
865
+ if type(self.branches) is not tuple or any(
866
+ type(value) is not PortfolioRecombinationBranchBinding
867
+ for value in self.branches
868
+ ):
869
+ raise TypeError("branches must contain exact bindings")
870
+ for value in self.branches:
871
+ PortfolioRecombinationBranchBinding.__post_init__(value)
872
+ branch_ranks = tuple(value.rank for value in self.branches)
873
+ if branch_ranks != tuple(sorted(set(branch_ranks))):
874
+ raise ValueError("branch bindings must remain in canonical ranked order")
875
+ branch_ids = tuple(value.candidate_id for value in self.branches)
876
+ if len(set(branch_ids)) != len(branch_ids):
877
+ raise ValueError("branch candidate IDs must be unique")
878
+ if type(self.source_exclusions) is not tuple or any(
879
+ type(value) is not PortfolioRecombinationSourceExclusionReceipt
880
+ for value in self.source_exclusions
881
+ ):
882
+ raise TypeError("source_exclusions must contain exact receipts")
883
+ for value in self.source_exclusions:
884
+ PortfolioRecombinationSourceExclusionReceipt.__post_init__(value)
885
+ exclusion_ranks = tuple(value.rank for value in self.source_exclusions)
886
+ if exclusion_ranks != tuple(sorted(set(exclusion_ranks))):
887
+ raise ValueError("source exclusions must use canonical ranked order")
888
+ source_ranks = tuple(sorted((*branch_ranks, *exclusion_ranks)))
889
+ if source_ranks != tuple(range(1, len(source_ranks) + 1)):
890
+ raise ValueError(
891
+ "scored branches and exclusions must exactly cover ranked ITT members"
892
+ )
893
+ exclusion_ids = tuple(value.candidate_id for value in self.source_exclusions)
894
+ if len(set(exclusion_ids)) != len(exclusion_ids) or set(branch_ids).intersection(
895
+ exclusion_ids
896
+ ):
897
+ raise ValueError("source candidate IDs must be unique across dispositions")
898
+ _canonical_exposures(self.path_family_exposures)
899
+ exposure_by_cell = {
900
+ (_path_text(value.path), value.family): value.count
901
+ for value in self.path_family_exposures
902
+ }
903
+ for branch in self.branches:
904
+ expected_exposure = sum(
905
+ exposure_by_cell.get((path, branch.family), 0)
906
+ for path in branch.changed_paths
907
+ )
908
+ if branch.path_family_exposure != expected_exposure:
909
+ raise ValueError(
910
+ "branch exposure differs from the exact path/family snapshot"
911
+ )
912
+ if type(self.pair_attempts) is not tuple or any(
913
+ type(value) is not PortfolioPairAttemptReceipt
914
+ for value in self.pair_attempts
915
+ ):
916
+ raise TypeError("pair_attempts must contain exact receipts")
917
+ for value in self.pair_attempts:
918
+ PortfolioPairAttemptReceipt.__post_init__(value)
919
+ expected_pair_ids = tuple(combinations(sorted(branch_ids), 2))
920
+ if tuple(value.pair_ids for value in self.pair_attempts) != expected_pair_ids:
921
+ raise ValueError("pair_attempts must enumerate the complete pair universe")
922
+ target_ids = tuple(value.target_candidate_id for value in self.pair_attempts)
923
+ if (
924
+ len(set(target_ids)) != len(target_ids)
925
+ or self.ancestor_candidate_id in set(target_ids)
926
+ or set(target_ids) & set(branch_ids)
927
+ ):
928
+ raise ValueError("pair attempt target IDs collide with wave occurrences")
929
+ if type(self.pair_decision) not in {
930
+ DisjointPairSelectionDecision,
931
+ ArchiveAwareDisjointPairSelectionDecision,
932
+ }:
933
+ raise TypeError("pair_decision must be an exact supported decision")
934
+ self.pair_decision.revalidate()
935
+ branch_by_id = {value.candidate_id: value for value in self.branches}
936
+ safe_by_ids = {
937
+ value.pair_ids: value for value in self.pair_attempts if value.replay_safe
938
+ }
939
+ rows_by_ids = {
940
+ value.pair_ids: value for value in self.pair_decision.eligible_rows
941
+ }
942
+ if set(safe_by_ids) != set(rows_by_ids):
943
+ raise ValueError("safe pair attempts differ from pair-policy eligibility")
944
+ for pair_ids, row in rows_by_ids.items():
945
+ attempt = safe_by_ids[pair_ids]
946
+ if (
947
+ row.pair.left != branch_by_id[pair_ids[0]].facts
948
+ or row.pair.right != branch_by_id[pair_ids[1]].facts
949
+ or row.pair.target_configuration_sha256
950
+ != attempt.target_configuration_sha256
951
+ or row.pair.materialization_receipt_sha256
952
+ != attempt.materialization_receipt_sha256
953
+ ):
954
+ raise ValueError("eligible pair facts differ from enumeration evidence")
955
+ if type(self.pair_decision) is ArchiveAwareDisjointPairSelectionDecision:
956
+ source_utilities = self.pair_decision.source_utilities
957
+ if source_utilities.source_generation >= self.generation:
958
+ raise ValueError(
959
+ "archive-aware source utility must precede recombination generation"
960
+ )
961
+ source_utilities.require_exact_context(
962
+ context=source_utilities.context,
963
+ source_wave_receipt_sha256=self.source_wave_receipt_sha256,
964
+ source_request_sha256=self.source_request_sha256,
965
+ source_decision_sha256=self.source_decision_sha256,
966
+ source_contract_sha256=self.source_contract_sha256,
967
+ source_generation=source_utilities.source_generation,
968
+ source_branches=tuple(
969
+ ObservedSourceBranch(
970
+ source_rank=value.rank,
971
+ candidate_id=value.candidate_id,
972
+ candidate_configuration_sha256=(
973
+ value.candidate_configuration_sha256
974
+ ),
975
+ source_outcome_sha256=value.source_outcome_sha256,
976
+ )
977
+ for value in self.branches
978
+ ),
979
+ )
980
+ no_pair = self.no_pair
981
+ if rows_by_ids:
982
+ if no_pair is not None:
983
+ raise ValueError("a replay-safe pair cannot carry no-pair evidence")
984
+ else:
985
+ if type(no_pair) is not PortfolioRecombinationNoPairReceipt:
986
+ raise TypeError("an empty safe-pair universe requires no-pair evidence")
987
+ PortfolioRecombinationNoPairReceipt.__post_init__(no_pair)
988
+ expected_reason = (
989
+ PortfolioRecombinationNoPairReason.INSUFFICIENT_SCORED_SOURCES
990
+ if len(self.branches) < 2
991
+ else PortfolioRecombinationNoPairReason.NO_REPLAY_SAFE_DISJOINT_PAIR
992
+ )
993
+ if (
994
+ no_pair.reason is not expected_reason
995
+ or no_pair.scored_source_count != len(self.branches)
996
+ or no_pair.excluded_source_count != len(self.source_exclusions)
997
+ or no_pair.enumerated_pair_count != len(self.pair_attempts)
998
+ or no_pair.replay_safe_pair_count != 0
999
+ ):
1000
+ raise ValueError("no-pair evidence differs from the exact pair universe")
1001
+ if type(self.members) is not tuple or any(
1002
+ type(value) is not PortfolioRecombinationMemberReceipt
1003
+ for value in self.members
1004
+ ):
1005
+ raise TypeError("members must contain exact receipts")
1006
+ if type(self.selection_limit) is not int or self.selection_limit not in {1, 2}:
1007
+ raise ValueError("selection_limit must be one or two")
1008
+ for value in self.members:
1009
+ PortfolioRecombinationMemberReceipt.__post_init__(value)
1010
+ expected_roles: list[tuple[str, tuple[CandidateId, CandidateId]]] = []
1011
+ if self.pair_decision.exploit_pair_ids is not None:
1012
+ expected_roles.append(("exploit", self.pair_decision.exploit_pair_ids))
1013
+ if self.pair_decision.coverage_pair_ids is not None:
1014
+ expected_roles.append(("coverage", self.pair_decision.coverage_pair_ids))
1015
+ expected_roles = expected_roles[: self.selection_limit]
1016
+ if tuple(
1017
+ (value.selection_role, value.pair_ids) for value in self.members
1018
+ ) != tuple(expected_roles):
1019
+ raise ValueError("member roles differ from the exact pair decision")
1020
+ for member in self.members:
1021
+ attempt = safe_by_ids[member.pair_ids]
1022
+ left, right = (branch_by_id[value] for value in member.pair_ids)
1023
+ if (
1024
+ member.source_ranks != (left.rank, right.rank)
1025
+ or member.source_option_ids != (left.option_id, right.option_id)
1026
+ or member.source_families != (left.family, right.family)
1027
+ or member.target_candidate_id != attempt.target_candidate_id
1028
+ or member.target_configuration_sha256
1029
+ != attempt.target_configuration_sha256
1030
+ or member.materialization_receipt_sha256
1031
+ != attempt.materialization_receipt_sha256
1032
+ or member.union_patch_sha256 != attempt.union_patch_sha256
1033
+ ):
1034
+ raise ValueError("selected member differs from branch/pair evidence")
1035
+ if len({value.target_candidate_id for value in self.members}) != len(
1036
+ self.members
1037
+ ) or len({value.operator_invocation_id for value in self.members}) != len(
1038
+ self.members
1039
+ ):
1040
+ raise ValueError("selected recombination members collide")
1041
+
1042
+ def _unsigned_record(self) -> dict[str, object]:
1043
+ self.__post_init__()
1044
+ record: dict[str, object] = {
1045
+ "schema_version": 1,
1046
+ "source_wave_receipt_sha256": self.source_wave_receipt_sha256,
1047
+ "source_request_sha256": self.source_request_sha256,
1048
+ "source_decision_sha256": self.source_decision_sha256,
1049
+ "source_contract_sha256": self.source_contract_sha256,
1050
+ "ancestor_candidate_id": self.ancestor_candidate_id.value,
1051
+ "ancestor_configuration_sha256": self.ancestor_configuration_sha256,
1052
+ "generation": self.generation,
1053
+ "branches": [value.to_record() for value in self.branches],
1054
+ "path_family_exposures": [
1055
+ value.to_trace_record() for value in self.path_family_exposures
1056
+ ],
1057
+ "pair_universe_size": len(self.pair_attempts),
1058
+ "pair_attempts": [value.to_record() for value in self.pair_attempts],
1059
+ "pair_decision": self.pair_decision.to_trace_record(),
1060
+ "selected_member_count": len(self.members),
1061
+ "concurrent_materialized_evaluation_wave": bool(self.members),
1062
+ "members": [value.to_record() for value in self.members],
1063
+ }
1064
+ if self.source_exclusions or self.no_pair is not None:
1065
+ record.update(
1066
+ {
1067
+ "schema_version": 2,
1068
+ "source_exclusions": [
1069
+ value.to_record() for value in self.source_exclusions
1070
+ ],
1071
+ "ranked_source_count": len(self.branches)
1072
+ + len(self.source_exclusions),
1073
+ "scored_source_count": len(self.branches),
1074
+ "excluded_source_count": len(self.source_exclusions),
1075
+ "no_pair": (
1076
+ None if self.no_pair is None else self.no_pair.to_record()
1077
+ ),
1078
+ }
1079
+ )
1080
+ infeasible_members = tuple(
1081
+ value
1082
+ for value in self.members
1083
+ if value.disposition is PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
1084
+ )
1085
+ if infeasible_members:
1086
+ record.update(
1087
+ {
1088
+ "schema_version": 3,
1089
+ "scored_member_count": len(self.members)
1090
+ - len(infeasible_members),
1091
+ "candidate_infeasible_member_count": len(infeasible_members),
1092
+ "candidate_infeasibility_recourse": (
1093
+ "retain_selected_itt_reject_from_archive_no_resampling"
1094
+ ),
1095
+ }
1096
+ )
1097
+ if self.selection_limit != 2:
1098
+ record.update(
1099
+ {
1100
+ "schema_version": 4,
1101
+ "selection_limit": self.selection_limit,
1102
+ }
1103
+ )
1104
+ return record
1105
+
1106
+ @property
1107
+ def receipt_sha256(self) -> str:
1108
+ return _hash_record(_WAVE_DOMAIN, self._unsigned_record())
1109
+
1110
+ def to_record(self) -> dict[str, object]:
1111
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
1112
+
1113
+
1114
+ @dataclass(frozen=True, slots=True)
1115
+ class PortfolioRecombinationWaveResult:
1116
+ """Planner-facing evidence plus exact evaluated recombination outcomes."""
1117
+
1118
+ receipt: PortfolioRecombinationWaveReceipt
1119
+ outcomes: tuple[InvocationOutcome, ...]
1120
+
1121
+ def __post_init__(self) -> None:
1122
+ if type(self.receipt) is not PortfolioRecombinationWaveReceipt:
1123
+ raise TypeError("receipt must be an exact recombination receipt")
1124
+ PortfolioRecombinationWaveReceipt.__post_init__(self.receipt)
1125
+ if type(self.outcomes) is not tuple or any(
1126
+ type(value) is not InvocationOutcome for value in self.outcomes
1127
+ ):
1128
+ raise TypeError("outcomes must contain exact InvocationOutcome values")
1129
+ if len(self.outcomes) != len(self.receipt.members):
1130
+ raise ValueError("outcomes differ from receipt member count")
1131
+ for member, outcome in zip(self.receipt.members, self.outcomes, strict=True):
1132
+ InvocationOutcome.__post_init__(outcome)
1133
+ candidate = outcome.candidate
1134
+ prepared = outcome.prepared
1135
+ if (
1136
+ outcome.failure_stage is not None
1137
+ or candidate is None
1138
+ or not candidate.operator_compliant
1139
+ or not candidate.evidence_compliant
1140
+ ):
1141
+ raise ValueError("result contains a failed or partial outcome")
1142
+ detailed = candidate.detailed_evaluation
1143
+ detailed_sha256 = None if detailed is None else detailed.evidence_sha256
1144
+ disposition = PortfolioMemberDisposition.SCORED
1145
+ candidate_failure: PortfolioCandidateFailureEvidence | None = None
1146
+ if not candidate.valid:
1147
+ if detailed is None or detailed.failure is None:
1148
+ raise ValueError(
1149
+ "candidate infeasibility requires detailed evaluator evidence"
1150
+ )
1151
+ candidate_failure = (
1152
+ PortfolioCandidateFailureEvidence.from_failure_record(
1153
+ detailed.failure,
1154
+ detailed_evaluation_sha256=detailed.evidence_sha256,
1155
+ )
1156
+ )
1157
+ disposition = PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
1158
+ plan = prepared.plan
1159
+ if (
1160
+ prepared.operator_invocation_id != member.operator_invocation_id
1161
+ or prepared.proposal_authority is not ProposalAuthority.ENGINE
1162
+ or prepared.call_id is not None
1163
+ or prepared.candidate_id != member.target_candidate_id
1164
+ or prepared.materialized_candidate_id != member.target_candidate_id
1165
+ or prepared.materialization_policy_id != DISJOINT_PATCH_POLICY_ID
1166
+ or prepared.materialization_policy_version
1167
+ != DISJOINT_PATCH_POLICY_VERSION
1168
+ or prepared.materialization_receipt_hash
1169
+ != member.materialization_receipt_sha256
1170
+ or prepared.variation_case.reward_definition_hash
1171
+ != member.reward_definition_sha256
1172
+ or plan.operator_kind is not OperatorKind.THREE_WAY_RECOMBINATION
1173
+ or tuple(value.candidate_id for value in plan.parents)
1174
+ != member.pair_ids
1175
+ or plan.common_ancestor is None
1176
+ or plan.common_ancestor.candidate_id
1177
+ != self.receipt.ancestor_candidate_id
1178
+ or plan.generation != self.receipt.generation
1179
+ or candidate.candidate_id != member.target_candidate_id
1180
+ or candidate.occurrence.configuration_hash
1181
+ != member.target_configuration_sha256
1182
+ or candidate.generation != self.receipt.generation
1183
+ or candidate.operator_kind is not OperatorKind.THREE_WAY_RECOMBINATION
1184
+ or candidate.parent_ids != member.pair_ids
1185
+ or candidate.common_ancestor_id != self.receipt.ancestor_candidate_id
1186
+ or candidate.parent_patch_hashes != member.parent_patch_sha256s
1187
+ or candidate.preservation_verified is not True
1188
+ or candidate.call_telemetry is not None
1189
+ or candidate.source_attribution != member.source_attribution
1190
+ or outcome.reward != member.reward
1191
+ or outcome.parent_relations != member.parent_relations
1192
+ or outcome.dominates_any_parent != member.dominates_any_parent
1193
+ or outcome.better_than_any_parent != member.better_than_any_parent
1194
+ or detailed_sha256 != member.detailed_evaluation_sha256
1195
+ or disposition is not member.disposition
1196
+ or candidate_failure != member.candidate_failure
1197
+ ):
1198
+ raise ValueError("outcome differs from its recombination receipt")
1199
+
1200
+ @property
1201
+ def candidates(self) -> tuple[EvolutionCandidate, ...]:
1202
+ values: list[EvolutionCandidate] = []
1203
+ for outcome in self.outcomes:
1204
+ candidate = outcome.candidate
1205
+ if candidate is None: # pragma: no cover - closed by __post_init__.
1206
+ raise AssertionError("validated outcome lost its candidate")
1207
+ values.append(candidate)
1208
+ return tuple(values)
1209
+
1210
+ @property
1211
+ def scored_candidates(self) -> tuple[EvolutionCandidate, ...]:
1212
+ """Return selected unions with complete decision-objective vectors."""
1213
+
1214
+ return tuple(
1215
+ candidate
1216
+ for member, candidate in zip(
1217
+ self.receipt.members,
1218
+ self.candidates,
1219
+ strict=True,
1220
+ )
1221
+ if member.disposition is PortfolioMemberDisposition.SCORED
1222
+ )
1223
+
1224
+ @property
1225
+ def infeasible_candidates(self) -> tuple[EvolutionCandidate, ...]:
1226
+ """Return selected unions rejected by workload evaluation evidence."""
1227
+
1228
+ return tuple(
1229
+ candidate
1230
+ for member, candidate in zip(
1231
+ self.receipt.members,
1232
+ self.candidates,
1233
+ strict=True,
1234
+ )
1235
+ if member.disposition is PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
1236
+ )
1237
+
1238
+
1239
+ @dataclass(frozen=True, slots=True)
1240
+ class PreparedPortfolioRecombinationWave:
1241
+ """Replay-complete recombination proposals before real evaluation."""
1242
+
1243
+ request: PortfolioRecombinationWaveRequest
1244
+ branches: tuple[PortfolioRecombinationBranchBinding, ...]
1245
+ source_exclusions: tuple[PortfolioRecombinationSourceExclusionReceipt, ...]
1246
+ pair_attempts: tuple[PortfolioPairAttemptReceipt, ...]
1247
+ pair_decision: PortfolioRecombinationPairDecision
1248
+ selected_roles_and_pairs: tuple[
1249
+ tuple[PortfolioRecombinationRole, tuple[CandidateId, CandidateId]],
1250
+ ...,
1251
+ ]
1252
+ invocations: tuple[MaterializedInvocation, ...]
1253
+ materializations: tuple[DisjointPatchMaterialization, ...]
1254
+ no_pair: PortfolioRecombinationNoPairReceipt | None
1255
+ selection_limit: int
1256
+ preparation_sha256: str = field(init=False)
1257
+
1258
+ def __post_init__(self) -> None:
1259
+ if type(self.request) is not PortfolioRecombinationWaveRequest:
1260
+ raise TypeError("request must be an exact recombination wave request")
1261
+ PortfolioRecombinationWaveRequest.__post_init__(self.request)
1262
+ if type(self.branches) is not tuple or any(
1263
+ type(value) is not PortfolioRecombinationBranchBinding
1264
+ for value in self.branches
1265
+ ):
1266
+ raise TypeError("branches must contain exact bindings")
1267
+ for value in self.branches:
1268
+ PortfolioRecombinationBranchBinding.__post_init__(value)
1269
+ if type(self.source_exclusions) is not tuple or any(
1270
+ type(value) is not PortfolioRecombinationSourceExclusionReceipt
1271
+ for value in self.source_exclusions
1272
+ ):
1273
+ raise TypeError("source_exclusions must contain exact receipts")
1274
+ for value in self.source_exclusions:
1275
+ PortfolioRecombinationSourceExclusionReceipt.__post_init__(value)
1276
+ if type(self.pair_attempts) is not tuple or any(
1277
+ type(value) is not PortfolioPairAttemptReceipt
1278
+ for value in self.pair_attempts
1279
+ ):
1280
+ raise TypeError("pair_attempts must contain exact receipts")
1281
+ for value in self.pair_attempts:
1282
+ PortfolioPairAttemptReceipt.__post_init__(value)
1283
+ if type(self.pair_decision) not in {
1284
+ DisjointPairSelectionDecision,
1285
+ ArchiveAwareDisjointPairSelectionDecision,
1286
+ }:
1287
+ raise TypeError("pair_decision must be an exact supported decision")
1288
+ self.pair_decision.revalidate()
1289
+ if type(self.selection_limit) is not int or self.selection_limit not in {1, 2}:
1290
+ raise ValueError("selection_limit must be one or two")
1291
+ if type(self.selected_roles_and_pairs) is not tuple:
1292
+ raise TypeError("selected_roles_and_pairs must be an exact tuple")
1293
+ expected: list[
1294
+ tuple[PortfolioRecombinationRole, tuple[CandidateId, CandidateId]]
1295
+ ] = []
1296
+ if self.pair_decision.exploit_pair_ids is not None:
1297
+ expected.append(("exploit", self.pair_decision.exploit_pair_ids))
1298
+ if self.pair_decision.coverage_pair_ids is not None:
1299
+ expected.append(("coverage", self.pair_decision.coverage_pair_ids))
1300
+ if self.selected_roles_and_pairs != tuple(expected[: self.selection_limit]):
1301
+ raise ValueError("prepared roles differ from the pair decision")
1302
+ if (
1303
+ type(self.invocations) is not tuple
1304
+ or type(self.materializations) is not tuple
1305
+ or len(self.invocations)
1306
+ != len(self.materializations)
1307
+ != len(self.selected_roles_and_pairs)
1308
+ ):
1309
+ raise ValueError("prepared invocations must exactly cover selected pairs")
1310
+ safe_attempts = {
1311
+ value.pair_ids: value for value in self.pair_attempts if value.replay_safe
1312
+ }
1313
+ target_ids: list[CandidateId] = []
1314
+ for (_role, pair_ids), invocation, materialization in zip(
1315
+ self.selected_roles_and_pairs,
1316
+ self.invocations,
1317
+ self.materializations,
1318
+ strict=True,
1319
+ ):
1320
+ if type(invocation) is not MaterializedInvocation:
1321
+ raise TypeError("invocations must contain exact values")
1322
+ MaterializedInvocation.__post_init__(invocation)
1323
+ if type(materialization) is not DisjointPatchMaterialization:
1324
+ raise TypeError("materializations must contain exact values")
1325
+ materialization.revalidate()
1326
+ try:
1327
+ attempt = safe_attempts[pair_ids]
1328
+ except KeyError as error:
1329
+ raise ValueError(
1330
+ "prepared selection is absent from safe pairs"
1331
+ ) from error
1332
+ if (
1333
+ tuple(value.candidate_id for value in invocation.plan.parents)
1334
+ != pair_ids
1335
+ or invocation.candidate_id != attempt.target_candidate_id
1336
+ or invocation.materialization_receipt_hash
1337
+ != materialization.receipt_sha256
1338
+ or invocation.materialization_receipt_hash
1339
+ != attempt.materialization_receipt_sha256
1340
+ or typed_json_sha256(invocation.draft.configuration)
1341
+ != attempt.target_configuration_sha256
1342
+ ):
1343
+ raise ValueError("prepared invocation differs from pair evidence")
1344
+ target_ids.append(invocation.candidate_id)
1345
+ if len(target_ids) != len(set(target_ids)):
1346
+ raise ValueError("prepared recombination targets collide")
1347
+ if self.selected_roles_and_pairs:
1348
+ if self.no_pair is not None:
1349
+ raise ValueError(
1350
+ "selected recombinations cannot carry no-pair evidence"
1351
+ )
1352
+ elif type(self.no_pair) is not PortfolioRecombinationNoPairReceipt:
1353
+ raise TypeError(
1354
+ "an empty recombination preparation requires no-pair evidence"
1355
+ )
1356
+ object.__setattr__(
1357
+ self,
1358
+ "preparation_sha256",
1359
+ _hash_record(_PREPARATION_DOMAIN, self._unsigned_record()),
1360
+ )
1361
+
1362
+ @property
1363
+ def target_candidate_ids(self) -> tuple[CandidateId, ...]:
1364
+ return tuple(value.candidate_id for value in self.invocations)
1365
+
1366
+ def _unsigned_record(self) -> dict[str, object]:
1367
+ request = self.request
1368
+ return {
1369
+ "schema_version": 1,
1370
+ "source_wave_receipt_sha256": (
1371
+ request.source_result.receipt.receipt_sha256
1372
+ ),
1373
+ "source_request_sha256": (
1374
+ request.source_wave.selection_request.request_sha256
1375
+ ),
1376
+ "ancestor_candidate_id": request.ancestor.candidate_id.value,
1377
+ "ancestor_configuration_sha256": (
1378
+ request.ancestor.occurrence.configuration_hash
1379
+ ),
1380
+ "generation": request.generation,
1381
+ "branch_candidate_ids": [
1382
+ value.candidate_id.value for value in self.branches
1383
+ ],
1384
+ "source_exclusion_sha256s": [
1385
+ value.exclusion_sha256 for value in self.source_exclusions
1386
+ ],
1387
+ "pair_attempt_sha256s": [
1388
+ _hash_record(
1389
+ _PREPARATION_DOMAIN,
1390
+ value.to_record(),
1391
+ )
1392
+ for value in self.pair_attempts
1393
+ ],
1394
+ "pair_decision": self.pair_decision.to_trace_record(),
1395
+ "selected": [
1396
+ {
1397
+ "role": role,
1398
+ "pair_ids": [value.value for value in pair_ids],
1399
+ "target_candidate_id": invocation.candidate_id.value,
1400
+ "target_configuration_sha256": typed_json_sha256(
1401
+ invocation.draft.configuration
1402
+ ),
1403
+ "materialization_receipt_sha256": (
1404
+ materialization.receipt_sha256
1405
+ ),
1406
+ }
1407
+ for (role, pair_ids), invocation, materialization in zip(
1408
+ self.selected_roles_and_pairs,
1409
+ self.invocations,
1410
+ self.materializations,
1411
+ strict=True,
1412
+ )
1413
+ ],
1414
+ "selection_limit": self.selection_limit,
1415
+ "no_pair_sha256": (
1416
+ None if self.no_pair is None else self.no_pair.no_pair_sha256
1417
+ ),
1418
+ "evaluation_performed": False,
1419
+ }
1420
+
1421
+ def to_record(self) -> dict[str, object]:
1422
+ self.__post_init__()
1423
+ return {
1424
+ **self._unsigned_record(),
1425
+ "preparation_sha256": self.preparation_sha256,
1426
+ }
1427
+
1428
+
1429
+ @dataclass(frozen=True, slots=True)
1430
+ class EvaluatedPreparedRecombinationMember:
1431
+ """One broker-selected prepared recombination and its real outcome."""
1432
+
1433
+ preparation_sha256: str
1434
+ selection_role: PortfolioRecombinationRole
1435
+ pair_ids: tuple[CandidateId, CandidateId]
1436
+ invocation: MaterializedInvocation
1437
+ materialization: DisjointPatchMaterialization
1438
+ outcome: InvocationOutcome
1439
+ receipt: PortfolioRecombinationMemberReceipt
1440
+ evaluation_sha256: str = field(init=False)
1441
+
1442
+ def __post_init__(self) -> None:
1443
+ require_sha256(self.preparation_sha256, "preparation_sha256")
1444
+ if self.selection_role not in {"exploit", "coverage"}:
1445
+ raise ValueError("selection_role must be exploit or coverage")
1446
+ if (
1447
+ type(self.pair_ids) is not tuple
1448
+ or len(self.pair_ids) != 2
1449
+ or any(type(value) is not CandidateId for value in self.pair_ids)
1450
+ ):
1451
+ raise TypeError("pair_ids must contain two exact candidate IDs")
1452
+ if type(self.invocation) is not MaterializedInvocation:
1453
+ raise TypeError("invocation must be exact")
1454
+ MaterializedInvocation.__post_init__(self.invocation)
1455
+ if type(self.materialization) is not DisjointPatchMaterialization:
1456
+ raise TypeError("materialization must be exact")
1457
+ self.materialization.revalidate()
1458
+ if type(self.outcome) is not InvocationOutcome:
1459
+ raise TypeError("outcome must be exact")
1460
+ InvocationOutcome.__post_init__(self.outcome)
1461
+ if type(self.receipt) is not PortfolioRecombinationMemberReceipt:
1462
+ raise TypeError("receipt must be exact")
1463
+ PortfolioRecombinationMemberReceipt.__post_init__(self.receipt)
1464
+ candidate = self.outcome.candidate
1465
+ if (
1466
+ candidate is None
1467
+ or self.receipt.selection_role != self.selection_role
1468
+ or self.receipt.pair_ids != self.pair_ids
1469
+ or self.invocation.candidate_id != self.receipt.target_candidate_id
1470
+ or candidate.candidate_id != self.receipt.target_candidate_id
1471
+ or self.materialization.receipt_sha256
1472
+ != self.receipt.materialization_receipt_sha256
1473
+ ):
1474
+ raise ValueError("evaluated prepared member has inconsistent joins")
1475
+ object.__setattr__(
1476
+ self,
1477
+ "evaluation_sha256",
1478
+ _hash_record(_PREPARED_MEMBER_DOMAIN, self._unsigned_record()),
1479
+ )
1480
+
1481
+ def _unsigned_record(self) -> dict[str, object]:
1482
+ return {
1483
+ "schema_version": 1,
1484
+ "preparation_sha256": self.preparation_sha256,
1485
+ "selection_role": self.selection_role,
1486
+ "pair_ids": [value.value for value in self.pair_ids],
1487
+ "target_candidate_id": self.invocation.candidate_id.value,
1488
+ "materialization_receipt_sha256": self.materialization.receipt_sha256,
1489
+ "member_outcome_sha256": self.receipt.outcome_sha256,
1490
+ }
1491
+
1492
+ def to_record(self) -> dict[str, object]:
1493
+ self.__post_init__()
1494
+ return {
1495
+ **self._unsigned_record(),
1496
+ "evaluation_sha256": self.evaluation_sha256,
1497
+ }
1498
+
1499
+
1500
+ @dataclass(slots=True)
1501
+ class PortfolioRecombination:
1502
+ """Enumerate safe disjoint unions and evaluate exploit/coverage selections."""
1503
+
1504
+ engine: MaterializedPortfolioEngine
1505
+ ids: IdFactory
1506
+ recombiner: DisjointPatchRecombiner = field(default_factory=DisjointPatchRecombiner)
1507
+ pair_policy: DisjointParentPairPolicy = field(
1508
+ default_factory=DisjointParentPairPolicy
1509
+ )
1510
+ archive_pair_policy: ArchiveAwareDisjointParentPairPolicy = field(
1511
+ default_factory=ArchiveAwareDisjointParentPairPolicy
1512
+ )
1513
+ selection_limit: int = 2
1514
+
1515
+ def __post_init__(self) -> None:
1516
+ if not isinstance(self.engine, MaterializedPortfolioEngine):
1517
+ raise TypeError("engine must implement run_materialized_invocations")
1518
+ if not isinstance(self.ids, IdFactory):
1519
+ raise TypeError("ids must implement IdFactory")
1520
+ if type(self.recombiner) is not DisjointPatchRecombiner:
1521
+ raise TypeError("recombiner must be an exact DisjointPatchRecombiner")
1522
+ DisjointPatchRecombiner.__post_init__(self.recombiner)
1523
+ if type(self.pair_policy) is not DisjointParentPairPolicy:
1524
+ raise TypeError("pair_policy must be exact")
1525
+ if type(self.archive_pair_policy) is not ArchiveAwareDisjointParentPairPolicy:
1526
+ raise TypeError("archive_pair_policy must be exact")
1527
+ if type(self.selection_limit) is not int or self.selection_limit not in {1, 2}:
1528
+ raise ValueError("selection_limit must be one or two")
1529
+
1530
+ @staticmethod
1531
+ def _branch_bindings(
1532
+ request: PortfolioRecombinationWaveRequest,
1533
+ ) -> tuple[
1534
+ tuple[PortfolioRecombinationBranchBinding, ...],
1535
+ tuple[PortfolioRecombinationSourceExclusionReceipt, ...],
1536
+ ]:
1537
+ contract = request.source_wave.selection_request.finite_variation_contract
1538
+ exposures = {
1539
+ (_path_text(value.path), value.family): value.count
1540
+ for value in request.path_family_exposures
1541
+ }
1542
+ values: list[PortfolioRecombinationBranchBinding] = []
1543
+ exclusions: list[PortfolioRecombinationSourceExclusionReceipt] = []
1544
+ for member in request.source_result.receipt.members:
1545
+ materialization = member.materialization
1546
+ option = contract.resolve(materialization.option_id)
1547
+ if member.disposition is PortfolioMemberDisposition.CANDIDATE_INFEASIBLE:
1548
+ failure = member.candidate_failure
1549
+ if type(failure) is not PortfolioCandidateFailureEvidence:
1550
+ raise AssertionError(
1551
+ "validated infeasible source lost candidate failure evidence"
1552
+ )
1553
+ exclusions.append(
1554
+ PortfolioRecombinationSourceExclusionReceipt(
1555
+ rank=materialization.rank,
1556
+ candidate_id=materialization.candidate_id,
1557
+ candidate_configuration_sha256=(
1558
+ materialization.child_configuration_sha256
1559
+ ),
1560
+ option_id=materialization.option_id,
1561
+ option_identity_sha256=(
1562
+ materialization.option_identity_sha256
1563
+ ),
1564
+ family=option.family,
1565
+ source_outcome_sha256=member.outcome_sha256,
1566
+ candidate_failure=failure,
1567
+ )
1568
+ )
1569
+ continue
1570
+ exposure = sum(
1571
+ exposures.get((path, option.family), 0)
1572
+ for path in materialization.changed_paths
1573
+ )
1574
+ values.append(
1575
+ PortfolioRecombinationBranchBinding(
1576
+ rank=materialization.rank,
1577
+ candidate_id=materialization.candidate_id,
1578
+ candidate_configuration_sha256=(
1579
+ materialization.child_configuration_sha256
1580
+ ),
1581
+ option_id=materialization.option_id,
1582
+ option_identity_sha256=materialization.option_identity_sha256,
1583
+ family=option.family,
1584
+ role=f"rank_{materialization.rank:04d}",
1585
+ reward=member.reward,
1586
+ changed_paths=materialization.changed_paths,
1587
+ path_family_exposure=exposure,
1588
+ source_outcome_sha256=member.outcome_sha256,
1589
+ )
1590
+ )
1591
+ return tuple(values), tuple(exclusions)
1592
+
1593
+ def _enumerate_pairs(
1594
+ self,
1595
+ request: PortfolioRecombinationWaveRequest,
1596
+ branches: tuple[PortfolioRecombinationBranchBinding, ...],
1597
+ ) -> tuple[
1598
+ tuple[PortfolioPairAttemptReceipt, ...],
1599
+ tuple[ReplayVerifiedDisjointPair, ...],
1600
+ dict[tuple[CandidateId, CandidateId], DisjointPatchMaterialization],
1601
+ ]:
1602
+ candidate_by_id = {
1603
+ value.candidate_id: value
1604
+ for value in request.source_result.scored_candidates
1605
+ }
1606
+ branch_by_id = {value.candidate_id: value for value in branches}
1607
+ source_ids = set(candidate_by_id)
1608
+ allocated_ids: set[CandidateId] = set()
1609
+ attempts: list[PortfolioPairAttemptReceipt] = []
1610
+ eligible: list[ReplayVerifiedDisjointPair] = []
1611
+ materializations: dict[
1612
+ tuple[CandidateId, CandidateId], DisjointPatchMaterialization
1613
+ ] = {}
1614
+ for left_id, right_id in combinations(sorted(source_ids), 2):
1615
+ target_id = self.ids.new_candidate_id()
1616
+ if (
1617
+ target_id in source_ids
1618
+ or target_id == request.ancestor.candidate_id
1619
+ or (target_id in allocated_ids)
1620
+ ):
1621
+ raise ValueError("recombination target candidate IDs collide")
1622
+ allocated_ids.add(target_id)
1623
+ left = candidate_by_id[left_id]
1624
+ right = candidate_by_id[right_id]
1625
+ try:
1626
+ materialization = self.recombiner.materialize(
1627
+ ancestor=request.ancestor.configuration,
1628
+ ancestor_candidate_id=request.ancestor.candidate_id,
1629
+ left=left.configuration,
1630
+ left_candidate_id=left_id,
1631
+ right=right.configuration,
1632
+ right_candidate_id=right_id,
1633
+ target_candidate_id=target_id,
1634
+ )
1635
+ except DisjointPatchRecombinationError:
1636
+ attempts.append(
1637
+ PortfolioPairAttemptReceipt(
1638
+ left_candidate_id=left_id,
1639
+ right_candidate_id=right_id,
1640
+ target_candidate_id=target_id,
1641
+ replay_safe=False,
1642
+ )
1643
+ )
1644
+ continue
1645
+ target_sha256 = typed_json_sha256(materialization.configuration)
1646
+ attempt = PortfolioPairAttemptReceipt(
1647
+ left_candidate_id=left_id,
1648
+ right_candidate_id=right_id,
1649
+ target_candidate_id=target_id,
1650
+ replay_safe=True,
1651
+ target_configuration_sha256=target_sha256,
1652
+ materialization_receipt_sha256=materialization.receipt_sha256,
1653
+ union_patch_sha256=materialization.union_patch.patch_hash,
1654
+ )
1655
+ pair = ReplayVerifiedDisjointPair(
1656
+ left=branch_by_id[left_id].facts,
1657
+ right=branch_by_id[right_id].facts,
1658
+ target_configuration_sha256=target_sha256,
1659
+ materialization_receipt_sha256=materialization.receipt_sha256,
1660
+ )
1661
+ attempts.append(attempt)
1662
+ eligible.append(pair)
1663
+ materializations[(left_id, right_id)] = materialization
1664
+ return tuple(attempts), tuple(eligible), materializations
1665
+
1666
+ @staticmethod
1667
+ def _join_outcome(
1668
+ *,
1669
+ role: PortfolioRecombinationRole,
1670
+ invocation: MaterializedInvocation,
1671
+ materialization: DisjointPatchMaterialization,
1672
+ branches: dict[CandidateId, PortfolioRecombinationBranchBinding],
1673
+ outcome: InvocationOutcome,
1674
+ ) -> PortfolioRecombinationMemberReceipt:
1675
+ if type(outcome) is not InvocationOutcome:
1676
+ raise TypeError("engine outcomes must be exact InvocationOutcome values")
1677
+ InvocationOutcome.__post_init__(outcome)
1678
+ candidate = outcome.candidate
1679
+ prepared = outcome.prepared
1680
+ pair_ids = tuple(parent.candidate_id for parent in invocation.plan.parents)
1681
+ if len(pair_ids) != 2:
1682
+ raise AssertionError("recombination invocation lost its two parents")
1683
+ if (
1684
+ prepared.plan != invocation.plan
1685
+ or prepared.proposal_authority is not ProposalAuthority.ENGINE
1686
+ or prepared.call_id is not None
1687
+ or prepared.candidate_id != invocation.candidate_id
1688
+ or prepared.materialized_candidate_id != invocation.candidate_id
1689
+ or prepared.materialization_policy_id != materialization.policy_id
1690
+ or prepared.materialization_policy_version != materialization.policy_version
1691
+ or prepared.materialization_receipt_hash != materialization.receipt_sha256
1692
+ ):
1693
+ raise ValueError("engine outcome differs from materialized recombination")
1694
+ if (
1695
+ outcome.failure_stage is not None
1696
+ or candidate is None
1697
+ or not candidate.operator_compliant
1698
+ or not candidate.evidence_compliant
1699
+ ):
1700
+ raise ValueError("recombination wave contains a failed or partial member")
1701
+ draft = invocation.draft
1702
+ expected_attribution = draft.source_attribution
1703
+ expected_parent_patches = tuple(
1704
+ derive_patch(
1705
+ parent.configuration,
1706
+ materialization.configuration,
1707
+ base_candidate_id=parent.candidate_id,
1708
+ target_candidate_id=invocation.candidate_id,
1709
+ ).patch_hash
1710
+ for parent in invocation.plan.parents
1711
+ )
1712
+ if (
1713
+ candidate.candidate_id != invocation.candidate_id
1714
+ or not typed_json_equal(
1715
+ candidate.configuration, materialization.configuration
1716
+ )
1717
+ or candidate.generation != invocation.plan.generation
1718
+ or candidate.operator_kind is not OperatorKind.THREE_WAY_RECOMBINATION
1719
+ or candidate.parent_ids != pair_ids
1720
+ or candidate.common_ancestor_id
1721
+ != materialization.classification.ancestor_candidate_id
1722
+ or candidate.parent_patch_hashes != expected_parent_patches
1723
+ or candidate.preservation_verified is not True
1724
+ or candidate.call_telemetry is not None
1725
+ or candidate.source_attribution != expected_attribution
1726
+ ):
1727
+ raise ValueError("candidate differs from exact disjoint materialization")
1728
+ detailed = candidate.detailed_evaluation
1729
+ disposition = PortfolioMemberDisposition.SCORED
1730
+ candidate_failure: PortfolioCandidateFailureEvidence | None = None
1731
+ if not candidate.valid:
1732
+ if detailed is None or detailed.failure is None:
1733
+ raise ValueError(
1734
+ "candidate infeasibility requires detailed evaluator evidence"
1735
+ )
1736
+ candidate_failure = PortfolioCandidateFailureEvidence.from_failure_record(
1737
+ detailed.failure,
1738
+ detailed_evaluation_sha256=detailed.evidence_sha256,
1739
+ )
1740
+ disposition = PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
1741
+ if outcome.parent_relations:
1742
+ raise ValueError(
1743
+ "candidate infeasibility cannot publish parent relations"
1744
+ )
1745
+ if outcome.dominates_any_parent or outcome.better_than_any_parent:
1746
+ raise ValueError(
1747
+ "candidate infeasibility cannot publish improvement flags"
1748
+ )
1749
+ left, right = (branches[value] for value in pair_ids)
1750
+ return PortfolioRecombinationMemberReceipt(
1751
+ selection_role=role,
1752
+ pair_ids=pair_ids,
1753
+ source_ranks=(left.rank, right.rank),
1754
+ source_option_ids=(left.option_id, right.option_id),
1755
+ source_families=(left.family, right.family),
1756
+ target_candidate_id=candidate.candidate_id,
1757
+ target_configuration_sha256=candidate.occurrence.configuration_hash,
1758
+ materialization_receipt_sha256=materialization.receipt_sha256,
1759
+ union_patch_sha256=materialization.union_patch.patch_hash,
1760
+ parent_patch_sha256s=expected_parent_patches,
1761
+ source_attribution=expected_attribution,
1762
+ operator_invocation_id=prepared.operator_invocation_id,
1763
+ reward_definition_sha256=prepared.variation_case.reward_definition_hash,
1764
+ reward=outcome.reward,
1765
+ parent_relations=outcome.parent_relations,
1766
+ detailed_evaluation_sha256=(
1767
+ None if detailed is None else detailed.evidence_sha256
1768
+ ),
1769
+ dominates_any_parent=outcome.dominates_any_parent,
1770
+ better_than_any_parent=outcome.better_than_any_parent,
1771
+ disposition=disposition,
1772
+ candidate_failure=candidate_failure,
1773
+ )
1774
+
1775
+ def prepare(
1776
+ self,
1777
+ request: PortfolioRecombinationWaveRequest,
1778
+ ) -> PreparedPortfolioRecombinationWave:
1779
+ """Materialize the bounded exploit/coverage slate without evaluating it."""
1780
+
1781
+ if type(request) is not PortfolioRecombinationWaveRequest:
1782
+ raise TypeError("request must be an exact recombination wave request")
1783
+ PortfolioRecombinationWaveRequest.__post_init__(request)
1784
+ branches, source_exclusions = self._branch_bindings(request)
1785
+ attempts, eligible, materializations_by_pair = self._enumerate_pairs(
1786
+ request,
1787
+ branches,
1788
+ )
1789
+ decision: PortfolioRecombinationPairDecision
1790
+ if request.source_utilities is None:
1791
+ decision = self.pair_policy.select(eligible)
1792
+ else:
1793
+ decision = self.archive_pair_policy.select(
1794
+ eligible,
1795
+ source_utilities=request.source_utilities,
1796
+ )
1797
+ selected: list[
1798
+ tuple[PortfolioRecombinationRole, tuple[CandidateId, CandidateId]]
1799
+ ] = []
1800
+ if decision.exploit_pair_ids is not None:
1801
+ selected.append(("exploit", decision.exploit_pair_ids))
1802
+ if decision.coverage_pair_ids is not None:
1803
+ selected.append(("coverage", decision.coverage_pair_ids))
1804
+ selected = selected[: self.selection_limit]
1805
+
1806
+ candidate_by_id = {
1807
+ value.candidate_id: value
1808
+ for value in request.source_result.scored_candidates
1809
+ }
1810
+ invocations: list[MaterializedInvocation] = []
1811
+ selected_materializations: list[DisjointPatchMaterialization] = []
1812
+ for role, pair_ids in selected:
1813
+ materialization = materializations_by_pair[pair_ids]
1814
+ invocations.append(
1815
+ materialized_disjoint_invocation(
1816
+ plan=InvocationPlan(
1817
+ operator_kind=OperatorKind.THREE_WAY_RECOMBINATION,
1818
+ parents=tuple(candidate_by_id[value] for value in pair_ids),
1819
+ generation=request.generation,
1820
+ label=f"{request.label_prefix}.{role}",
1821
+ common_ancestor=request.ancestor,
1822
+ phase=request.phase,
1823
+ ),
1824
+ materialization=materialization,
1825
+ )
1826
+ )
1827
+ selected_materializations.append(materialization)
1828
+ no_pair = (
1829
+ None
1830
+ if eligible
1831
+ else PortfolioRecombinationNoPairReceipt(
1832
+ reason=(
1833
+ PortfolioRecombinationNoPairReason.INSUFFICIENT_SCORED_SOURCES
1834
+ if len(branches) < 2
1835
+ else PortfolioRecombinationNoPairReason.NO_REPLAY_SAFE_DISJOINT_PAIR
1836
+ ),
1837
+ scored_source_count=len(branches),
1838
+ excluded_source_count=len(source_exclusions),
1839
+ enumerated_pair_count=len(attempts),
1840
+ replay_safe_pair_count=0,
1841
+ )
1842
+ )
1843
+ return PreparedPortfolioRecombinationWave(
1844
+ request=request,
1845
+ branches=branches,
1846
+ source_exclusions=source_exclusions,
1847
+ pair_attempts=attempts,
1848
+ pair_decision=decision,
1849
+ selected_roles_and_pairs=tuple(selected),
1850
+ invocations=tuple(invocations),
1851
+ materializations=tuple(selected_materializations),
1852
+ no_pair=no_pair,
1853
+ selection_limit=self.selection_limit,
1854
+ )
1855
+
1856
+ async def evaluate_prepared_members(
1857
+ self,
1858
+ prepared: PreparedPortfolioRecombinationWave,
1859
+ selected_candidate_ids: tuple[CandidateId, ...],
1860
+ *,
1861
+ reward_binding: RewardPolicyBinding | None = None,
1862
+ ) -> tuple[EvaluatedPreparedRecombinationMember, ...]:
1863
+ """Evaluate only a downstream-selected subset of prepared unions."""
1864
+
1865
+ if type(prepared) is not PreparedPortfolioRecombinationWave:
1866
+ raise TypeError("prepared must be an exact recombination preparation")
1867
+ prepared.__post_init__()
1868
+ if type(selected_candidate_ids) is not tuple or not selected_candidate_ids:
1869
+ raise ValueError("selected_candidate_ids must be a non-empty exact tuple")
1870
+ if any(type(value) is not CandidateId for value in selected_candidate_ids):
1871
+ raise TypeError(
1872
+ "selected_candidate_ids must contain exact CandidateId values"
1873
+ )
1874
+ for value in selected_candidate_ids:
1875
+ CandidateId.__post_init__(value)
1876
+ if len(set(selected_candidate_ids)) != len(selected_candidate_ids):
1877
+ raise ValueError("selected_candidate_ids cannot contain duplicates")
1878
+ if reward_binding is not None:
1879
+ if type(reward_binding) is not RewardPolicyBinding:
1880
+ raise TypeError("reward_binding must be exact or None")
1881
+ RewardPolicyBinding.__post_init__(reward_binding)
1882
+
1883
+ rows = {
1884
+ invocation.candidate_id: (role, pair_ids, invocation, materialization)
1885
+ for (role, pair_ids), invocation, materialization in zip(
1886
+ prepared.selected_roles_and_pairs,
1887
+ prepared.invocations,
1888
+ prepared.materializations,
1889
+ strict=True,
1890
+ )
1891
+ }
1892
+ try:
1893
+ selected = tuple(rows[value] for value in selected_candidate_ids)
1894
+ except KeyError as error:
1895
+ raise ValueError(
1896
+ "evaluation selected outside prepared recombinations"
1897
+ ) from error
1898
+ invocations = tuple(value[2] for value in selected)
1899
+ outcomes = await self.engine.run_materialized_invocations(
1900
+ invocations,
1901
+ reward_binding=reward_binding,
1902
+ )
1903
+ if type(outcomes) is not tuple or len(outcomes) != len(invocations):
1904
+ raise ValueError("engine returned a partial recombination outcome wave")
1905
+ branch_by_id = {value.candidate_id: value for value in prepared.branches}
1906
+ values: list[EvaluatedPreparedRecombinationMember] = []
1907
+ for (role, pair_ids, invocation, materialization), outcome in zip(
1908
+ selected,
1909
+ outcomes,
1910
+ strict=True,
1911
+ ):
1912
+ receipt = self._join_outcome(
1913
+ role=role,
1914
+ invocation=invocation,
1915
+ materialization=materialization,
1916
+ branches=branch_by_id,
1917
+ outcome=outcome,
1918
+ )
1919
+ values.append(
1920
+ EvaluatedPreparedRecombinationMember(
1921
+ preparation_sha256=prepared.preparation_sha256,
1922
+ selection_role=role,
1923
+ pair_ids=pair_ids,
1924
+ invocation=invocation,
1925
+ materialization=materialization,
1926
+ outcome=outcome,
1927
+ receipt=receipt,
1928
+ )
1929
+ )
1930
+ return tuple(values)
1931
+
1932
+ @staticmethod
1933
+ def _result_from_complete_evaluation(
1934
+ prepared: PreparedPortfolioRecombinationWave,
1935
+ evaluated: tuple[EvaluatedPreparedRecombinationMember, ...],
1936
+ ) -> PortfolioRecombinationWaveResult:
1937
+ if type(prepared) is not PreparedPortfolioRecombinationWave:
1938
+ raise TypeError("prepared must be exact")
1939
+ prepared.__post_init__()
1940
+ if type(evaluated) is not tuple or any(
1941
+ type(value) is not EvaluatedPreparedRecombinationMember
1942
+ for value in evaluated
1943
+ ):
1944
+ raise TypeError("evaluated must contain exact prepared members")
1945
+ if tuple(value.invocation.candidate_id for value in evaluated) != (
1946
+ prepared.target_candidate_ids
1947
+ ):
1948
+ raise ValueError("complete result must evaluate every prepared member")
1949
+ request = prepared.request
1950
+ receipt = PortfolioRecombinationWaveReceipt(
1951
+ source_wave_receipt_sha256=(
1952
+ request.source_result.receipt.receipt_sha256
1953
+ ),
1954
+ source_request_sha256=(
1955
+ request.source_wave.selection_request.request_sha256
1956
+ ),
1957
+ source_decision_sha256=request.source_result.receipt.decision_sha256,
1958
+ source_contract_sha256=(
1959
+ request.source_wave.selection_request
1960
+ .finite_variation_contract.identity_sha256
1961
+ ),
1962
+ ancestor_candidate_id=request.ancestor.candidate_id,
1963
+ ancestor_configuration_sha256=(
1964
+ request.ancestor.occurrence.configuration_hash
1965
+ ),
1966
+ generation=request.generation,
1967
+ branches=prepared.branches,
1968
+ source_exclusions=prepared.source_exclusions,
1969
+ path_family_exposures=request.path_family_exposures,
1970
+ pair_attempts=prepared.pair_attempts,
1971
+ pair_decision=prepared.pair_decision,
1972
+ members=tuple(value.receipt for value in evaluated),
1973
+ selection_limit=prepared.selection_limit,
1974
+ no_pair=prepared.no_pair,
1975
+ )
1976
+ return PortfolioRecombinationWaveResult(
1977
+ receipt=receipt,
1978
+ outcomes=tuple(value.outcome for value in evaluated),
1979
+ )
1980
+
1981
+ async def run(
1982
+ self,
1983
+ request: PortfolioRecombinationWaveRequest,
1984
+ *,
1985
+ reward_binding: RewardPolicyBinding | None = None,
1986
+ ) -> PortfolioRecombinationWaveResult:
1987
+ """Enumerate all source pairs and evaluate a bounded deterministic slate.
1988
+
1989
+ The pair policy can nominate an exploit pair and a coverage pair, while
1990
+ the enclosing campaign protocol owns the offspring envelope. Keeping
1991
+ that envelope explicit here lets generic campaign shapes request one
1992
+ recombination child without evaluating and then discarding a second
1993
+ child outside their preregistered budget.
1994
+ """
1995
+
1996
+ prepared = self.prepare(request)
1997
+ if prepared.target_candidate_ids:
1998
+ evaluated = await self.evaluate_prepared_members(
1999
+ prepared,
2000
+ prepared.target_candidate_ids,
2001
+ reward_binding=reward_binding,
2002
+ )
2003
+ else:
2004
+ evaluated = ()
2005
+ return self._result_from_complete_evaluation(prepared, evaluated)
2006
+
2007
+
2008
+ __all__ = [
2009
+ "ArchiveAwareDisjointPairSelectionDecision",
2010
+ "ArchiveAwareDisjointParentPairPolicy",
2011
+ "FrozenArchiveBranchUtility",
2012
+ "FrozenArchiveSourcePairUtility",
2013
+ "FrozenArchiveSourceUtilityContext",
2014
+ "FrozenArchiveSourceUtilityReceipt",
2015
+ "ObservedSourceBranch",
2016
+ "PortfolioPairAttemptReceipt",
2017
+ "PortfolioRecombination",
2018
+ "PortfolioRecombinationBranchBinding",
2019
+ "PortfolioRecombinationMemberReceipt",
2020
+ "PortfolioRecombinationNoPairReason",
2021
+ "PortfolioRecombinationNoPairReceipt",
2022
+ "PortfolioRecombinationSourceExclusionReason",
2023
+ "PortfolioRecombinationSourceExclusionReceipt",
2024
+ "PortfolioRecombinationWaveReceipt",
2025
+ "PortfolioRecombinationWaveRequest",
2026
+ "PortfolioRecombinationWaveResult",
2027
+ "PreparedPortfolioRecombinationWave",
2028
+ "EvaluatedPreparedRecombinationMember",
2029
+ "bind_portfolio_recombination_source_utilities",
2030
+ "frozen_archive_source_utility_context",
2031
+ "portfolio_recombination_observed_sources",
2032
+ ]