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.
- agent_evolve/__init__.py +722 -0
- agent_evolve/agentic.py +2800 -0
- agent_evolve/api.py +767 -0
- agent_evolve/application/__init__.py +1580 -0
- agent_evolve/application/action_allocation.py +744 -0
- agent_evolve/application/action_allocation_frame.py +347 -0
- agent_evolve/application/action_allocation_frame_commit.py +185 -0
- agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
- agent_evolve/application/action_allocation_frame_v3.py +338 -0
- agent_evolve/application/action_archive_value.py +497 -0
- agent_evolve/application/action_evidence_consistency.py +455 -0
- agent_evolve/application/action_forecast_partitioning.py +1471 -0
- agent_evolve/application/action_metric_projection.py +211 -0
- agent_evolve/application/action_role_value.py +680 -0
- agent_evolve/application/action_score_authorities.py +363 -0
- agent_evolve/application/action_structural_signature.py +116 -0
- agent_evolve/application/action_target_realization.py +402 -0
- agent_evolve/application/agentic_evolution.py +7734 -0
- agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
- agent_evolve/application/anchor_residual_identification.py +463 -0
- agent_evolve/application/archive_conditioned_action_target.py +208 -0
- agent_evolve/application/artifact_journal.py +246 -0
- agent_evolve/application/artifact_replay.py +347 -0
- agent_evolve/application/budgeted_optimizer.py +1828 -0
- agent_evolve/application/calibrated_campaign.py +485 -0
- agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
- agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
- agent_evolve/application/campaign_capacity_recourse.py +254 -0
- agent_evolve/application/campaign_contextual_outcomes.py +119 -0
- agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
- agent_evolve/application/campaign_evidence_registry.py +262 -0
- agent_evolve/application/campaign_execution.py +2537 -0
- agent_evolve/application/campaign_generation_audit.py +942 -0
- agent_evolve/application/campaign_learning.py +1812 -0
- agent_evolve/application/campaign_learning_runtime.py +1977 -0
- agent_evolve/application/campaign_search_phase.py +227 -0
- agent_evolve/application/campaign_selector_context_extension.py +220 -0
- agent_evolve/application/campaign_variation_envelope.py +649 -0
- agent_evolve/application/campaign_variation_trace.py +451 -0
- agent_evolve/application/candidate_archive_consequence.py +128 -0
- agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
- agent_evolve/application/composite_outcome_updater.py +145 -0
- agent_evolve/application/composition_portfolio_selection.py +363 -0
- agent_evolve/application/concurrent_stage.py +144 -0
- agent_evolve/application/contextual_action_allocation.py +181 -0
- agent_evolve/application/contextual_campaign_outcomes.py +267 -0
- agent_evolve/application/contextual_campaign_planning.py +1366 -0
- agent_evolve/application/contextual_delayed_credit.py +651 -0
- agent_evolve/application/contextual_search_controller.py +2374 -0
- agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
- agent_evolve/application/decision_metric_projection.py +112 -0
- agent_evolve/application/derived_action_semantics.py +129 -0
- agent_evolve/application/detailed_evaluation.py +449 -0
- agent_evolve/application/earned_lineage.py +1011 -0
- agent_evolve/application/effective_choice_audit.py +484 -0
- agent_evolve/application/empirical_consequence_calibration.py +908 -0
- agent_evolve/application/evaluation_accounting.py +325 -0
- agent_evolve/application/evaluation_cache.py +199 -0
- agent_evolve/application/evaluation_escrow.py +547 -0
- agent_evolve/application/evaluation_recourse.py +253 -0
- agent_evolve/application/event_recorder.py +151 -0
- agent_evolve/application/evolution_campaign.py +1840 -0
- agent_evolve/application/executable_hypothesis.py +323 -0
- agent_evolve/application/factorial_branch_pilot.py +772 -0
- agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
- agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
- agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
- agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
- agent_evolve/application/finite_action_selection.py +188 -0
- agent_evolve/application/finite_action_set.py +306 -0
- agent_evolve/application/finite_action_transition.py +537 -0
- agent_evolve/application/finite_variation_eligibility.py +296 -0
- agent_evolve/application/forecast_geometry_portfolio.py +799 -0
- agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
- agent_evolve/application/front_proximity_admission.py +311 -0
- agent_evolve/application/front_proximity_parent_basis.py +458 -0
- agent_evolve/application/frozen_hurdle_score.py +659 -0
- agent_evolve/application/g3_causal_screen.py +2257 -0
- agent_evolve/application/g3_causal_validation.py +1046 -0
- agent_evolve/application/g3_postseal_curation.py +818 -0
- agent_evolve/application/gated_agentic_generator.py +205 -0
- agent_evolve/application/generation_feedback.py +293 -0
- agent_evolve/application/generative_proposal_journal.py +185 -0
- agent_evolve/application/geometry_conditional_elasticity.py +453 -0
- agent_evolve/application/global_wave_action_allocation.py +1151 -0
- agent_evolve/application/head_mass_conditional_seat.py +268 -0
- agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
- agent_evolve/application/identifiable_reflection_learning.py +395 -0
- agent_evolve/application/identifiable_reflection_request.py +364 -0
- agent_evolve/application/in_memory_residual_archive.py +341 -0
- agent_evolve/application/insight_memory.py +1804 -0
- agent_evolve/application/live_runtime_manifest.py +758 -0
- agent_evolve/application/llm_task_queue.py +769 -0
- agent_evolve/application/matched_finite_action_block.py +409 -0
- agent_evolve/application/materialized_action_broker.py +2328 -0
- agent_evolve/application/materialized_action_constraints.py +83 -0
- agent_evolve/application/materialized_variation.py +211 -0
- agent_evolve/application/multi_option_evolution.py +1536 -0
- agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
- agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
- agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
- agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
- agent_evolve/application/outcome_relation.py +193 -0
- agent_evolve/application/paired_allocation_comparison.py +241 -0
- agent_evolve/application/paired_block_schedule.py +127 -0
- agent_evolve/application/parent_measurement.py +226 -0
- agent_evolve/application/pareto_archive.py +811 -0
- agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
- agent_evolve/application/portfolio_evolution.py +2950 -0
- agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
- agent_evolve/application/portfolio_memory_attribution.py +581 -0
- agent_evolve/application/portfolio_memory_dose.py +788 -0
- agent_evolve/application/portfolio_memory_matched_control.py +938 -0
- agent_evolve/application/portfolio_memory_transfer.py +297 -0
- agent_evolve/application/portfolio_optimization_memory.py +363 -0
- agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
- agent_evolve/application/portfolio_projection.py +335 -0
- agent_evolve/application/portfolio_recombination.py +2032 -0
- agent_evolve/application/post_evolution_reflection.py +834 -0
- agent_evolve/application/postcommit_rank_authority.py +245 -0
- agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
- agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
- agent_evolve/application/prequential_residual_exploration.py +343 -0
- agent_evolve/application/prequential_score_portfolio.py +954 -0
- agent_evolve/application/projections.py +292 -0
- agent_evolve/application/protected_action_committee.py +1027 -0
- agent_evolve/application/protected_branch_pilot.py +376 -0
- agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
- agent_evolve/application/provider_replay.py +910 -0
- agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
- agent_evolve/application/recombination_residual_expert.py +403 -0
- agent_evolve/application/reflection_workflow.py +571 -0
- agent_evolve/application/region_conditional_credit.py +911 -0
- agent_evolve/application/residual_campaign_runtime.py +531 -0
- agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
- agent_evolve/application/residual_headroom_ledger.py +1544 -0
- agent_evolve/application/residual_learning_transaction.py +396 -0
- agent_evolve/application/residual_portfolio_evolution.py +1228 -0
- agent_evolve/application/residual_reachability.py +749 -0
- agent_evolve/application/residual_stage_credit.py +499 -0
- agent_evolve/application/same_prefix_paired_audit.py +1580 -0
- agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
- agent_evolve/application/sequential_lineage_allocation.py +1017 -0
- agent_evolve/application/sequential_market_replay.py +1395 -0
- agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
- agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
- agent_evolve/application/single_score_action_allocation.py +299 -0
- agent_evolve/application/source_exposure_allocation.py +906 -0
- agent_evolve/application/staged_memory.py +210 -0
- agent_evolve/application/stratified_cold_start_allocation.py +732 -0
- agent_evolve/application/support_guarded_hurdle_score.py +549 -0
- agent_evolve/application/target_conditioned_action_forecast.py +595 -0
- agent_evolve/application/target_conditioned_campaign.py +566 -0
- agent_evolve/application/treatment_assignment.py +201 -0
- agent_evolve/application/trusted_objective_evidence.py +217 -0
- agent_evolve/application/two_stage_action_evolution.py +1131 -0
- agent_evolve/application/v8lite_allocation_policy.py +1083 -0
- agent_evolve/application/v9_candidate_policy.py +1303 -0
- agent_evolve/bootstrap.py +108 -0
- agent_evolve/campaign_presets.py +517 -0
- agent_evolve/campaign_profiles.py +452 -0
- agent_evolve/campaign_variation_topology.py +288 -0
- agent_evolve/campaign_workload.py +950 -0
- agent_evolve/cli.py +797 -0
- agent_evolve/contract.py +241 -0
- agent_evolve/core/__init__.py +91 -0
- agent_evolve/core/action_semantics.py +411 -0
- agent_evolve/core/authored.py +105 -0
- agent_evolve/core/formatting.py +286 -0
- agent_evolve/core/optimization_semantics.py +324 -0
- agent_evolve/core/problem.py +167 -0
- agent_evolve/core/results.py +323 -0
- agent_evolve/core/stats.py +70 -0
- agent_evolve/core/telemetry.py +100 -0
- agent_evolve/domain/__init__.py +89 -0
- agent_evolve/domain/artifact.py +162 -0
- agent_evolve/domain/durable_text.py +68 -0
- agent_evolve/domain/event.py +1454 -0
- agent_evolve/domain/finite_action_set.py +426 -0
- agent_evolve/domain/finite_variation.py +526 -0
- agent_evolve/domain/generative_emission.py +559 -0
- agent_evolve/domain/ids.py +163 -0
- agent_evolve/domain/inline_text.py +106 -0
- agent_evolve/domain/insight.py +27 -0
- agent_evolve/domain/lineage.py +737 -0
- agent_evolve/domain/llm_task_queue.py +960 -0
- agent_evolve/domain/outcome.py +96 -0
- agent_evolve/domain/patch.py +854 -0
- agent_evolve/domain/typed_json.py +542 -0
- agent_evolve/domain/variation_space.py +158 -0
- agent_evolve/driver.py +1014 -0
- agent_evolve/harness/__init__.py +29 -0
- agent_evolve/harness/base.py +242 -0
- agent_evolve/harness/directives.py +163 -0
- agent_evolve/harness/generative_seal.py +479 -0
- agent_evolve/harness/registry.py +41 -0
- agent_evolve/infrastructure/__init__.py +39 -0
- agent_evolve/infrastructure/artifacts/__init__.py +6 -0
- agent_evolve/infrastructure/artifacts/_verification.py +67 -0
- agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
- agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
- agent_evolve/infrastructure/asyncio_runtime.py +109 -0
- agent_evolve/infrastructure/authored_runtime.py +188 -0
- agent_evolve/infrastructure/authored_worker.py +171 -0
- agent_evolve/infrastructure/clock.py +53 -0
- agent_evolve/infrastructure/events/__init__.py +6 -0
- agent_evolve/infrastructure/events/_validation.py +89 -0
- agent_evolve/infrastructure/events/in_memory.py +56 -0
- agent_evolve/infrastructure/events/jsonl.py +193 -0
- agent_evolve/infrastructure/exception_provenance.py +215 -0
- agent_evolve/infrastructure/ids.py +118 -0
- agent_evolve/infrastructure/lineage_codec.py +1836 -0
- agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
- agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
- agent_evolve/infrastructure/resource_lease.py +370 -0
- agent_evolve/infrastructure/sanitization/__init__.py +8 -0
- agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
- agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
- agent_evolve/infrastructure/stream_liveness.py +383 -0
- agent_evolve/infrastructure/subprocess_boundary.py +136 -0
- agent_evolve/integrations/__init__.py +1 -0
- agent_evolve/integrations/botorch/__init__.py +28 -0
- agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
- agent_evolve/integrations/completion.py +242 -0
- agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
- agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
- agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
- agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
- agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
- agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
- agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
- agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
- agent_evolve/integrations/pydantic_ai/harness.py +159 -0
- agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
- agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
- agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
- agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
- agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
- agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
- agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
- agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
- agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
- agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
- agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
- agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
- agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
- agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
- agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
- agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
- agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
- agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
- agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
- agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
- agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
- agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
- agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
- agent_evolve/integrations/pymoo_adapter.py +242 -0
- agent_evolve/policies/__init__.py +17 -0
- agent_evolve/policies/check.py +469 -0
- agent_evolve/policies/emit_scaffold.py +451 -0
- agent_evolve/policies/feedback/__init__.py +37 -0
- agent_evolve/policies/feedback/held_out_asn.py +1325 -0
- agent_evolve/policies/genetic.py +607 -0
- agent_evolve/policies/llm_backoff.py +183 -0
- agent_evolve/policies/llm_chooser.py +226 -0
- agent_evolve/policies/llm_generator.py +1760 -0
- agent_evolve/policies/llm_init.py +267 -0
- agent_evolve/policies/llm_operator.py +109 -0
- agent_evolve/policies/llm_prior.py +194 -0
- agent_evolve/policies/llm_surrogate.py +334 -0
- agent_evolve/policies/measurement_evidence.py +704 -0
- agent_evolve/policies/memory/__init__.py +223 -0
- agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
- agent_evolve/policies/memory/compatibility_matching.py +593 -0
- agent_evolve/policies/memory/global_falsification.py +1841 -0
- agent_evolve/policies/memory/prompt_shape.py +503 -0
- agent_evolve/policies/memory/randomized_subset.py +714 -0
- agent_evolve/policies/memory/staged_causal.py +1270 -0
- agent_evolve/policies/memory/treatment_compliance.py +759 -0
- agent_evolve/policies/objective_resolution/__init__.py +17 -0
- agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
- agent_evolve/policies/operator_portfolio.py +407 -0
- agent_evolve/policies/reguidance.py +1133 -0
- agent_evolve/policies/reward/__init__.py +83 -0
- agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
- agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
- agent_evolve/policies/reward/affine_hypervolume.py +490 -0
- agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
- agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
- agent_evolve/policies/reward/frozen_archive.py +360 -0
- agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
- agent_evolve/policies/search_state.py +208 -0
- agent_evolve/policies/selection/__init__.py +345 -0
- agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
- agent_evolve/policies/selection/affine_frontier_context.py +330 -0
- agent_evolve/policies/selection/affine_frontier_target.py +473 -0
- agent_evolve/policies/selection/archive_elite.py +1346 -0
- agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
- agent_evolve/policies/selection/calibrated_slate.py +1394 -0
- agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
- agent_evolve/policies/selection/common_candidate_pool.py +685 -0
- agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
- agent_evolve/policies/selection/disjoint_pairs.py +479 -0
- agent_evolve/policies/selection/elite_explorer.py +719 -0
- agent_evolve/policies/selection/finite_action.py +187 -0
- agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
- agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
- agent_evolve/policies/selection/forecast_calibration.py +922 -0
- agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
- agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
- agent_evolve/policies/selection/full_support_slate.py +91 -0
- agent_evolve/policies/selection/meaningful_direction.py +240 -0
- agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
- agent_evolve/policies/selection/model_anchored_slate.py +826 -0
- agent_evolve/policies/selection/phenotype_recourse.py +979 -0
- agent_evolve/policies/selection/proposal_support.py +368 -0
- agent_evolve/policies/selection/random_portfolio.py +254 -0
- agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
- agent_evolve/policies/selection/residual_frontier.py +463 -0
- agent_evolve/policies/selection/residual_frontier_target.py +605 -0
- agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
- agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
- agent_evolve/policies/selection/target_conditioned_features.py +812 -0
- agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
- agent_evolve/policies/selection/task_keyed_palette.py +906 -0
- agent_evolve/policies/semantics.py +147 -0
- agent_evolve/policies/structure.py +362 -0
- agent_evolve/policies/structured_output_budget.py +62 -0
- agent_evolve/policies/surrogate.py +696 -0
- agent_evolve/policies/variation/__init__.py +1 -0
- agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
- agent_evolve/policies/variation/crossover_inheritance.py +575 -0
- agent_evolve/policies/variation/disjoint_recombination.py +611 -0
- agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
- agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
- agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
- agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
- agent_evolve/policies/variation/typed_patch.py +1981 -0
- agent_evolve/policies/weighted_prior.py +394 -0
- agent_evolve/ports/__init__.py +383 -0
- agent_evolve/ports/action_allocation.py +733 -0
- agent_evolve/ports/action_allocation_frame.py +1153 -0
- agent_evolve/ports/action_allocation_frame_commit.py +294 -0
- agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
- agent_evolve/ports/action_allocation_frame_v3.py +995 -0
- agent_evolve/ports/action_forecast.py +1568 -0
- agent_evolve/ports/action_metric_projection.py +165 -0
- agent_evolve/ports/agentic_generator.py +1561 -0
- agent_evolve/ports/archive_context.py +136 -0
- agent_evolve/ports/artifact_sanitizer.py +44 -0
- agent_evolve/ports/artifact_store.py +225 -0
- agent_evolve/ports/clock.py +13 -0
- agent_evolve/ports/contextual_search_allocation.py +827 -0
- agent_evolve/ports/decision_metric_projection.py +258 -0
- agent_evolve/ports/event_store.py +55 -0
- agent_evolve/ports/executable_hypothesis.py +557 -0
- agent_evolve/ports/finite_acquisition.py +377 -0
- agent_evolve/ports/finite_acquisition_batch.py +296 -0
- agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
- agent_evolve/ports/finite_acquisition_json.py +247 -0
- agent_evolve/ports/finite_acquisition_space.py +168 -0
- agent_evolve/ports/finite_action_selection.py +348 -0
- agent_evolve/ports/finite_action_set.py +256 -0
- agent_evolve/ports/frontier_target.py +396 -0
- agent_evolve/ports/generation_failure.py +43 -0
- agent_evolve/ports/hard_feasibility.py +233 -0
- agent_evolve/ports/id_factory.py +34 -0
- agent_evolve/ports/llm_task_queue.py +93 -0
- agent_evolve/ports/objective_resolution.py +419 -0
- agent_evolve/ports/paired_allocation_comparison.py +401 -0
- agent_evolve/ports/paired_block_schedule.py +475 -0
- agent_evolve/ports/parent_measurement.py +336 -0
- agent_evolve/ports/portfolio_memory_dose.py +643 -0
- agent_evolve/ports/portfolio_selection.py +3169 -0
- agent_evolve/ports/postcommit_rank_authority.py +467 -0
- agent_evolve/ports/presented_action_evidence.py +794 -0
- agent_evolve/ports/resource_lease.py +162 -0
- agent_evolve/ports/structured_generator.py +734 -0
- agent_evolve/ports/structured_output_budget.py +120 -0
- agent_evolve/ports/subprocess_boundary.py +138 -0
- agent_evolve/ports/treatment_assignment.py +466 -0
- agent_evolve/ports/variation_catalog.py +76 -0
- agent_evolve/ports/variation_source.py +226 -0
- agent_evolve/proposal_mode.py +157 -0
- agent_evolve/proposers/__init__.py +10 -0
- agent_evolve/proposers/random_proposer.py +188 -0
- agent_evolve/provider_accounting.py +163 -0
- agent_evolve/py.typed +0 -0
- agent_evolve/reference_method.py +1570 -0
- agent_evolve/session/__init__.py +11 -0
- agent_evolve/session/authorship.py +864 -0
- agent_evolve/session/evaluate.py +236 -0
- agent_evolve/session/fidelity.py +237 -0
- agent_evolve/session/genetic_loop.py +742 -0
- agent_evolve/session/loop.py +803 -0
- agent_evolve/session/screening.py +671 -0
- agent_evolve/settings.py +376 -0
- agent_evolve/workload_kit.py +368 -0
- agent_evolve/workload_prompt.py +398 -0
- agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
- agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
- agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
- agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
- agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|