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,906 @@
|
|
|
1
|
+
"""Task-keyed atomic palettes with complete, replayable selection facts.
|
|
2
|
+
|
|
3
|
+
The policy deliberately owns two choices which otherwise tend to leak into a
|
|
4
|
+
benchmark runner: which path to expose and which finite replacements to show.
|
|
5
|
+
An unforced path is selected from the minimum-exposure feasible paths with a
|
|
6
|
+
cryptographic task-key tie-break. Within that exact path, required options are
|
|
7
|
+
included first as constraints, the remaining capacity is filled by exposure
|
|
8
|
+
and an independent task-key tie-break, and the final provider-visible order is
|
|
9
|
+
another independent task-key permutation.
|
|
10
|
+
|
|
11
|
+
No incoming catalog, mapping, or concurrent-completion order participates in
|
|
12
|
+
any decision. The returned frozen value contains every path and option row
|
|
13
|
+
needed to audit or replay the choice.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import ClassVar, Sequence
|
|
24
|
+
|
|
25
|
+
from agent_evolve.domain.patch import (
|
|
26
|
+
ArrayIndex,
|
|
27
|
+
JsonPath,
|
|
28
|
+
ObjectKey,
|
|
29
|
+
canonical_path_bytes,
|
|
30
|
+
require_sha256,
|
|
31
|
+
validate_json_path,
|
|
32
|
+
)
|
|
33
|
+
from agent_evolve.domain.typed_json import canonical_typed_json_bytes
|
|
34
|
+
from agent_evolve.domain.variation_space import (
|
|
35
|
+
AtomicEditOption,
|
|
36
|
+
validate_atomic_edit_option,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
POLICY_ID = "task_keyed_atomic_palette"
|
|
41
|
+
POLICY_VERSION = 1
|
|
42
|
+
_TASK_ORDER_DOMAIN = b"agent-evolve:task-keyed-atomic-palette-order:v1\x00"
|
|
43
|
+
_CATALOG_DOMAIN = b"agent-evolve:task-keyed-atomic-palette-catalog:v1\x00"
|
|
44
|
+
_DECISION_DOMAIN = b"agent-evolve:task-keyed-atomic-palette-decision:v1\x00"
|
|
45
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
|
|
46
|
+
_MAX_TASK_KEY_BYTES = 1024
|
|
47
|
+
_MAX_EXPOSURE = (1 << 63) - 1
|
|
48
|
+
_MAX_SEED = (1 << 64) - 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _frame(value: bytes) -> bytes:
|
|
52
|
+
if type(value) is not bytes:
|
|
53
|
+
raise TypeError("framed values must be exact bytes")
|
|
54
|
+
return len(value).to_bytes(8, "big", signed=False) + value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_seed(seed: int) -> None:
|
|
58
|
+
if type(seed) is not int or not 0 <= seed <= _MAX_SEED:
|
|
59
|
+
raise ValueError("seed must be an exact uint64 integer")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _task_key_bytes(task_key: str) -> bytes:
|
|
63
|
+
if type(task_key) is not str or not task_key or "\x00" in task_key:
|
|
64
|
+
raise ValueError("task_key must be non-empty and NUL-free")
|
|
65
|
+
try:
|
|
66
|
+
encoded = task_key.encode("utf-8", errors="strict")
|
|
67
|
+
except UnicodeEncodeError as exc:
|
|
68
|
+
raise ValueError("task_key must be strict UTF-8") from exc
|
|
69
|
+
if len(encoded) > _MAX_TASK_KEY_BYTES:
|
|
70
|
+
raise ValueError("task_key exceeds its byte limit")
|
|
71
|
+
return encoded
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _task_order_sha256(
|
|
75
|
+
*,
|
|
76
|
+
seed: int,
|
|
77
|
+
task_key: str,
|
|
78
|
+
purpose: str,
|
|
79
|
+
payload: bytes,
|
|
80
|
+
) -> str:
|
|
81
|
+
_validate_seed(seed)
|
|
82
|
+
task_bytes = _task_key_bytes(task_key)
|
|
83
|
+
if type(purpose) is not str or not purpose.isascii() or not purpose:
|
|
84
|
+
raise ValueError("task-order purpose must be non-empty ASCII")
|
|
85
|
+
digest = hashlib.sha256()
|
|
86
|
+
digest.update(_TASK_ORDER_DOMAIN)
|
|
87
|
+
digest.update(seed.to_bytes(8, "big", signed=False))
|
|
88
|
+
digest.update(_frame(task_bytes))
|
|
89
|
+
digest.update(_frame(purpose.encode("ascii")))
|
|
90
|
+
digest.update(_frame(payload))
|
|
91
|
+
return digest.hexdigest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_family(value: str) -> None:
|
|
95
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
96
|
+
raise ValueError("family must use the closed lowercase token grammar")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_count(value: int, *, name: str) -> None:
|
|
100
|
+
if type(value) is not int or not 0 <= value <= _MAX_EXPOSURE:
|
|
101
|
+
raise ValueError(f"{name} must be an exact non-negative int63")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _path_record(path: JsonPath) -> dict[str, object]:
|
|
105
|
+
validate_json_path(path)
|
|
106
|
+
segments: list[dict[str, object]] = []
|
|
107
|
+
for segment in path.segments:
|
|
108
|
+
if type(segment) is ObjectKey:
|
|
109
|
+
segments.append({"kind": "object_key", "value": segment.value})
|
|
110
|
+
elif type(segment) is ArrayIndex:
|
|
111
|
+
segments.append({"kind": "array_index", "value": segment.value})
|
|
112
|
+
else: # pragma: no cover - JsonPath validation closes the union.
|
|
113
|
+
raise AssertionError("unsupported JSON-path segment")
|
|
114
|
+
return {"schema_identity": path.schema_identity, "segments": segments}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _option_record(option: AtomicEditOption) -> dict[str, object]:
|
|
118
|
+
validate_atomic_edit_option(option)
|
|
119
|
+
return {
|
|
120
|
+
"option_id": option.option_id,
|
|
121
|
+
"option_identity_sha256": option.identity_sha256,
|
|
122
|
+
"path": _path_record(option.path),
|
|
123
|
+
"replacement": option.replacement,
|
|
124
|
+
"family": option.family,
|
|
125
|
+
"metadata": [
|
|
126
|
+
{
|
|
127
|
+
"key": key,
|
|
128
|
+
# Metadata may bind source commands or other artifact content;
|
|
129
|
+
# its value belongs outside a durable inline event. The exact
|
|
130
|
+
# bytes remain transitively bound by option_identity_sha256.
|
|
131
|
+
"value_sha256": hashlib.sha256(
|
|
132
|
+
value.encode("utf-8", errors="strict")
|
|
133
|
+
).hexdigest(),
|
|
134
|
+
}
|
|
135
|
+
for key, value in option.metadata
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _canonical_catalog(
|
|
141
|
+
options: Sequence[AtomicEditOption],
|
|
142
|
+
) -> tuple[AtomicEditOption, ...]:
|
|
143
|
+
if isinstance(options, (str, bytes)):
|
|
144
|
+
raise TypeError("options must be a finite option sequence")
|
|
145
|
+
values = tuple(options)
|
|
146
|
+
if not values:
|
|
147
|
+
raise ValueError("options cannot be empty")
|
|
148
|
+
for option in values:
|
|
149
|
+
if type(option) is not AtomicEditOption:
|
|
150
|
+
raise TypeError("options must contain exact AtomicEditOption values")
|
|
151
|
+
validate_atomic_edit_option(option)
|
|
152
|
+
|
|
153
|
+
option_ids = [option.option_id for option in values]
|
|
154
|
+
if len(set(option_ids)) != len(option_ids):
|
|
155
|
+
raise ValueError("catalog option_id values must be globally unique")
|
|
156
|
+
identities = [option.identity_sha256 for option in values]
|
|
157
|
+
if len(set(identities)) != len(identities):
|
|
158
|
+
raise ValueError("catalog option identities must be globally unique")
|
|
159
|
+
|
|
160
|
+
replacements: set[tuple[bytes, bytes]] = set()
|
|
161
|
+
for option in values:
|
|
162
|
+
key = (
|
|
163
|
+
canonical_path_bytes(option.path),
|
|
164
|
+
canonical_typed_json_bytes(option.replacement),
|
|
165
|
+
)
|
|
166
|
+
if key in replacements:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"catalog cannot contain duplicate replacements at one path"
|
|
169
|
+
)
|
|
170
|
+
replacements.add(key)
|
|
171
|
+
return tuple(
|
|
172
|
+
sorted(
|
|
173
|
+
values,
|
|
174
|
+
key=lambda option: (
|
|
175
|
+
canonical_path_bytes(option.path),
|
|
176
|
+
option.option_id,
|
|
177
|
+
option.identity_sha256,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _catalog_sha256(options: tuple[AtomicEditOption, ...]) -> str:
|
|
184
|
+
canonical = _canonical_catalog(options)
|
|
185
|
+
digest = hashlib.sha256()
|
|
186
|
+
digest.update(_CATALOG_DOMAIN)
|
|
187
|
+
digest.update(len(canonical).to_bytes(8, "big", signed=False))
|
|
188
|
+
for option in canonical:
|
|
189
|
+
digest.update(bytes.fromhex(option.identity_sha256))
|
|
190
|
+
return digest.hexdigest()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
194
|
+
class PathFamilyExposure:
|
|
195
|
+
"""An append-only count for one exact path/family exposure cell."""
|
|
196
|
+
|
|
197
|
+
path: JsonPath
|
|
198
|
+
family: str
|
|
199
|
+
count: int
|
|
200
|
+
|
|
201
|
+
def __post_init__(self) -> None:
|
|
202
|
+
if type(self.path) is not JsonPath:
|
|
203
|
+
raise TypeError("path must be an exact JsonPath")
|
|
204
|
+
validate_json_path(self.path)
|
|
205
|
+
_validate_family(self.family)
|
|
206
|
+
_validate_count(self.count, name="exposure count")
|
|
207
|
+
|
|
208
|
+
def revalidate(self) -> None:
|
|
209
|
+
if type(self) is not PathFamilyExposure:
|
|
210
|
+
raise TypeError("exposure must be an exact PathFamilyExposure")
|
|
211
|
+
PathFamilyExposure.__post_init__(self)
|
|
212
|
+
|
|
213
|
+
def _key(self) -> tuple[bytes, str, int]:
|
|
214
|
+
self.revalidate()
|
|
215
|
+
return canonical_path_bytes(self.path), self.family, self.count
|
|
216
|
+
|
|
217
|
+
def __eq__(self, other: object) -> bool:
|
|
218
|
+
return (
|
|
219
|
+
type(self) is PathFamilyExposure
|
|
220
|
+
and type(other) is PathFamilyExposure
|
|
221
|
+
and self._key() == other._key()
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def __hash__(self) -> int:
|
|
225
|
+
return hash((PathFamilyExposure, self._key()))
|
|
226
|
+
|
|
227
|
+
def to_trace_record(self) -> dict[str, object]:
|
|
228
|
+
self.revalidate()
|
|
229
|
+
return {
|
|
230
|
+
"path": _path_record(self.path),
|
|
231
|
+
"family": self.family,
|
|
232
|
+
"count": self.count,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _canonical_exposures(
|
|
237
|
+
exposures: Sequence[PathFamilyExposure],
|
|
238
|
+
) -> tuple[PathFamilyExposure, ...]:
|
|
239
|
+
if isinstance(exposures, (str, bytes)):
|
|
240
|
+
raise TypeError("exposures must be a finite exposure sequence")
|
|
241
|
+
values = tuple(exposures)
|
|
242
|
+
for exposure in values:
|
|
243
|
+
if type(exposure) is not PathFamilyExposure:
|
|
244
|
+
raise TypeError("exposures must contain exact PathFamilyExposure values")
|
|
245
|
+
exposure.revalidate()
|
|
246
|
+
keys = [
|
|
247
|
+
(canonical_path_bytes(exposure.path), exposure.family) for exposure in values
|
|
248
|
+
]
|
|
249
|
+
if len(set(keys)) != len(keys):
|
|
250
|
+
raise ValueError("exposure cells must be unique")
|
|
251
|
+
return tuple(
|
|
252
|
+
sorted(
|
|
253
|
+
values,
|
|
254
|
+
key=lambda exposure: (
|
|
255
|
+
canonical_path_bytes(exposure.path),
|
|
256
|
+
exposure.family,
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class PathSelectionMode(str, Enum):
|
|
263
|
+
"""Why the exact atomic path was fixed for this palette."""
|
|
264
|
+
|
|
265
|
+
FORCED = "forced"
|
|
266
|
+
REQUIRED_OPTIONS = "required_options"
|
|
267
|
+
MINIMUM_EXPOSURE_TASK_KEYED = "minimum_exposure_task_keyed"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
271
|
+
class PalettePathRow:
|
|
272
|
+
"""Every fact used to admit and rank one catalog path."""
|
|
273
|
+
|
|
274
|
+
path: JsonPath
|
|
275
|
+
option_count: int
|
|
276
|
+
family_capacity: int
|
|
277
|
+
path_exposure: int
|
|
278
|
+
feasible_for_size: bool
|
|
279
|
+
eligible_for_choice: bool
|
|
280
|
+
task_order_sha256: str
|
|
281
|
+
|
|
282
|
+
def __post_init__(self) -> None:
|
|
283
|
+
if type(self.path) is not JsonPath:
|
|
284
|
+
raise TypeError("path must be an exact JsonPath")
|
|
285
|
+
validate_json_path(self.path)
|
|
286
|
+
if type(self.option_count) is not int or self.option_count <= 0:
|
|
287
|
+
raise ValueError("option_count must be a positive exact integer")
|
|
288
|
+
if (
|
|
289
|
+
type(self.family_capacity) is not int
|
|
290
|
+
or not 0 < self.family_capacity <= self.option_count
|
|
291
|
+
):
|
|
292
|
+
raise ValueError("family_capacity must lie within the path's option count")
|
|
293
|
+
_validate_count(self.path_exposure, name="path_exposure")
|
|
294
|
+
if type(self.feasible_for_size) is not bool:
|
|
295
|
+
raise TypeError("feasible_for_size must be an exact bool")
|
|
296
|
+
if type(self.eligible_for_choice) is not bool:
|
|
297
|
+
raise TypeError("eligible_for_choice must be an exact bool")
|
|
298
|
+
if self.eligible_for_choice and not self.feasible_for_size:
|
|
299
|
+
raise ValueError("an ineligible-size path cannot be chosen")
|
|
300
|
+
require_sha256(self.task_order_sha256, "task_order_sha256")
|
|
301
|
+
|
|
302
|
+
def revalidate(self) -> None:
|
|
303
|
+
if type(self) is not PalettePathRow:
|
|
304
|
+
raise TypeError("path row must be an exact PalettePathRow")
|
|
305
|
+
PalettePathRow.__post_init__(self)
|
|
306
|
+
|
|
307
|
+
def _key(self) -> tuple[bytes, int, int, int, bool, bool, str]:
|
|
308
|
+
self.revalidate()
|
|
309
|
+
return (
|
|
310
|
+
canonical_path_bytes(self.path),
|
|
311
|
+
self.option_count,
|
|
312
|
+
self.family_capacity,
|
|
313
|
+
self.path_exposure,
|
|
314
|
+
self.feasible_for_size,
|
|
315
|
+
self.eligible_for_choice,
|
|
316
|
+
self.task_order_sha256,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def __eq__(self, other: object) -> bool:
|
|
320
|
+
return (
|
|
321
|
+
type(self) is PalettePathRow
|
|
322
|
+
and type(other) is PalettePathRow
|
|
323
|
+
and self._key() == other._key()
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
__hash__ = None
|
|
327
|
+
|
|
328
|
+
def to_trace_record(self) -> dict[str, object]:
|
|
329
|
+
self.revalidate()
|
|
330
|
+
return {
|
|
331
|
+
"path": _path_record(self.path),
|
|
332
|
+
"option_count": self.option_count,
|
|
333
|
+
"family_capacity": self.family_capacity,
|
|
334
|
+
"path_exposure": self.path_exposure,
|
|
335
|
+
"feasible_for_size": self.feasible_for_size,
|
|
336
|
+
"eligible_for_choice": self.eligible_for_choice,
|
|
337
|
+
"task_order_sha256": self.task_order_sha256,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
342
|
+
class PaletteOptionRow:
|
|
343
|
+
"""Complete catalog row, including exposure, tie-break, and disposition."""
|
|
344
|
+
|
|
345
|
+
option: AtomicEditOption
|
|
346
|
+
cell_exposure: int
|
|
347
|
+
family_exposure: int
|
|
348
|
+
path_exposure: int
|
|
349
|
+
selection_order_sha256: str
|
|
350
|
+
presentation_order_sha256: str
|
|
351
|
+
on_chosen_path: bool
|
|
352
|
+
required: bool
|
|
353
|
+
palette_position: int | None
|
|
354
|
+
|
|
355
|
+
def __post_init__(self) -> None:
|
|
356
|
+
if type(self.option) is not AtomicEditOption:
|
|
357
|
+
raise TypeError("option must be an exact AtomicEditOption")
|
|
358
|
+
validate_atomic_edit_option(self.option)
|
|
359
|
+
_validate_count(self.cell_exposure, name="cell_exposure")
|
|
360
|
+
_validate_count(self.family_exposure, name="family_exposure")
|
|
361
|
+
_validate_count(self.path_exposure, name="path_exposure")
|
|
362
|
+
require_sha256(self.selection_order_sha256, "selection_order_sha256")
|
|
363
|
+
require_sha256(
|
|
364
|
+
self.presentation_order_sha256,
|
|
365
|
+
"presentation_order_sha256",
|
|
366
|
+
)
|
|
367
|
+
if type(self.on_chosen_path) is not bool:
|
|
368
|
+
raise TypeError("on_chosen_path must be an exact bool")
|
|
369
|
+
if type(self.required) is not bool:
|
|
370
|
+
raise TypeError("required must be an exact bool")
|
|
371
|
+
if self.required and not self.on_chosen_path:
|
|
372
|
+
raise ValueError("required options must be on the chosen path")
|
|
373
|
+
if self.palette_position is not None and (
|
|
374
|
+
type(self.palette_position) is not int or self.palette_position < 0
|
|
375
|
+
):
|
|
376
|
+
raise ValueError("palette_position must be None or non-negative")
|
|
377
|
+
if self.palette_position is not None and not self.on_chosen_path:
|
|
378
|
+
raise ValueError("selected options must be on the chosen path")
|
|
379
|
+
if self.required and self.palette_position is None:
|
|
380
|
+
raise ValueError("every required option must enter the palette")
|
|
381
|
+
|
|
382
|
+
def revalidate(self) -> None:
|
|
383
|
+
if type(self) is not PaletteOptionRow:
|
|
384
|
+
raise TypeError("option row must be an exact PaletteOptionRow")
|
|
385
|
+
PaletteOptionRow.__post_init__(self)
|
|
386
|
+
|
|
387
|
+
def _key(self) -> tuple[object, ...]:
|
|
388
|
+
self.revalidate()
|
|
389
|
+
return (
|
|
390
|
+
self.option.identity_sha256,
|
|
391
|
+
self.cell_exposure,
|
|
392
|
+
self.family_exposure,
|
|
393
|
+
self.path_exposure,
|
|
394
|
+
self.selection_order_sha256,
|
|
395
|
+
self.presentation_order_sha256,
|
|
396
|
+
self.on_chosen_path,
|
|
397
|
+
self.required,
|
|
398
|
+
self.palette_position,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def __eq__(self, other: object) -> bool:
|
|
402
|
+
return (
|
|
403
|
+
type(self) is PaletteOptionRow
|
|
404
|
+
and type(other) is PaletteOptionRow
|
|
405
|
+
and self._key() == other._key()
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
__hash__ = None
|
|
409
|
+
|
|
410
|
+
def to_trace_record(self) -> dict[str, object]:
|
|
411
|
+
self.revalidate()
|
|
412
|
+
return {
|
|
413
|
+
**_option_record(self.option),
|
|
414
|
+
"cell_exposure": self.cell_exposure,
|
|
415
|
+
"family_exposure": self.family_exposure,
|
|
416
|
+
"path_exposure": self.path_exposure,
|
|
417
|
+
"selection_order_sha256": self.selection_order_sha256,
|
|
418
|
+
"presentation_order_sha256": self.presentation_order_sha256,
|
|
419
|
+
"on_chosen_path": self.on_chosen_path,
|
|
420
|
+
"required": self.required,
|
|
421
|
+
"palette_position": self.palette_position,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@dataclass(frozen=True, slots=True)
|
|
426
|
+
class _PaletteParts:
|
|
427
|
+
chosen_path: JsonPath
|
|
428
|
+
path_selection_mode: PathSelectionMode
|
|
429
|
+
exposure_snapshot: tuple[PathFamilyExposure, ...]
|
|
430
|
+
path_rows: tuple[PalettePathRow, ...]
|
|
431
|
+
option_rows: tuple[PaletteOptionRow, ...]
|
|
432
|
+
palette: tuple[AtomicEditOption, ...]
|
|
433
|
+
catalog_sha256: str
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _build_parts(
|
|
437
|
+
*,
|
|
438
|
+
seed: int,
|
|
439
|
+
task_key: str,
|
|
440
|
+
options: Sequence[AtomicEditOption],
|
|
441
|
+
palette_size: int,
|
|
442
|
+
max_options_per_family: int | None,
|
|
443
|
+
exposures: Sequence[PathFamilyExposure],
|
|
444
|
+
forced_path: JsonPath | None,
|
|
445
|
+
required_option_ids: tuple[str, ...],
|
|
446
|
+
) -> _PaletteParts:
|
|
447
|
+
_validate_seed(seed)
|
|
448
|
+
_task_key_bytes(task_key)
|
|
449
|
+
catalog = _canonical_catalog(options)
|
|
450
|
+
exposure_snapshot = _canonical_exposures(exposures)
|
|
451
|
+
if type(palette_size) is not int or palette_size <= 0:
|
|
452
|
+
raise ValueError("palette_size must be a positive exact integer")
|
|
453
|
+
if max_options_per_family is not None and (
|
|
454
|
+
type(max_options_per_family) is not int or max_options_per_family <= 0
|
|
455
|
+
):
|
|
456
|
+
raise ValueError(
|
|
457
|
+
"max_options_per_family must be None or a positive exact integer"
|
|
458
|
+
)
|
|
459
|
+
if type(required_option_ids) is not tuple:
|
|
460
|
+
raise TypeError("required_option_ids must be an exact tuple")
|
|
461
|
+
if any(type(option_id) is not str for option_id in required_option_ids):
|
|
462
|
+
raise TypeError("required_option_ids must contain exact strings")
|
|
463
|
+
if tuple(sorted(required_option_ids)) != required_option_ids:
|
|
464
|
+
raise ValueError("required_option_ids must use canonical sorted order")
|
|
465
|
+
if len(set(required_option_ids)) != len(required_option_ids):
|
|
466
|
+
raise ValueError("required_option_ids cannot contain duplicates")
|
|
467
|
+
if len(required_option_ids) > palette_size:
|
|
468
|
+
raise ValueError("required option count cannot exceed palette_size")
|
|
469
|
+
if forced_path is not None:
|
|
470
|
+
if type(forced_path) is not JsonPath:
|
|
471
|
+
raise TypeError("forced_path must be an exact JsonPath or None")
|
|
472
|
+
validate_json_path(forced_path)
|
|
473
|
+
|
|
474
|
+
by_id = {option.option_id: option for option in catalog}
|
|
475
|
+
missing = tuple(
|
|
476
|
+
option_id for option_id in required_option_ids if option_id not in by_id
|
|
477
|
+
)
|
|
478
|
+
if missing:
|
|
479
|
+
raise ValueError(
|
|
480
|
+
f"required option IDs are absent from the catalog: {missing!r}"
|
|
481
|
+
)
|
|
482
|
+
required = tuple(by_id[option_id] for option_id in required_option_ids)
|
|
483
|
+
required_paths = {canonical_path_bytes(option.path) for option in required}
|
|
484
|
+
if len(required_paths) > 1:
|
|
485
|
+
raise ValueError("all required options must share one exact path")
|
|
486
|
+
required_path = required[0].path if required else None
|
|
487
|
+
if (
|
|
488
|
+
forced_path is not None
|
|
489
|
+
and required_path is not None
|
|
490
|
+
and forced_path != required_path
|
|
491
|
+
):
|
|
492
|
+
raise ValueError("required options do not belong to the forced path")
|
|
493
|
+
if max_options_per_family is not None:
|
|
494
|
+
required_family_counts: dict[str, int] = {}
|
|
495
|
+
for option in required:
|
|
496
|
+
required_family_counts[option.family] = (
|
|
497
|
+
required_family_counts.get(option.family, 0) + 1
|
|
498
|
+
)
|
|
499
|
+
if any(
|
|
500
|
+
count > max_options_per_family for count in required_family_counts.values()
|
|
501
|
+
):
|
|
502
|
+
raise ValueError("required options exceed max_options_per_family")
|
|
503
|
+
|
|
504
|
+
groups: dict[bytes, list[AtomicEditOption]] = {}
|
|
505
|
+
paths: dict[bytes, JsonPath] = {}
|
|
506
|
+
for option in catalog:
|
|
507
|
+
path_key = canonical_path_bytes(option.path)
|
|
508
|
+
groups.setdefault(path_key, []).append(option)
|
|
509
|
+
paths[path_key] = option.path
|
|
510
|
+
|
|
511
|
+
exposure_by_cell = {
|
|
512
|
+
(canonical_path_bytes(exposure.path), exposure.family): exposure.count
|
|
513
|
+
for exposure in exposure_snapshot
|
|
514
|
+
}
|
|
515
|
+
path_exposure: dict[bytes, int] = {}
|
|
516
|
+
family_exposure: dict[str, int] = {}
|
|
517
|
+
for exposure in exposure_snapshot:
|
|
518
|
+
path_key = canonical_path_bytes(exposure.path)
|
|
519
|
+
path_exposure[path_key] = path_exposure.get(path_key, 0) + exposure.count
|
|
520
|
+
family_exposure[exposure.family] = (
|
|
521
|
+
family_exposure.get(exposure.family, 0) + exposure.count
|
|
522
|
+
)
|
|
523
|
+
_validate_count(path_exposure[path_key], name="aggregated path exposure")
|
|
524
|
+
_validate_count(
|
|
525
|
+
family_exposure[exposure.family],
|
|
526
|
+
name="aggregated family exposure",
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if forced_path is not None:
|
|
530
|
+
forced_key = canonical_path_bytes(forced_path)
|
|
531
|
+
if forced_key not in groups:
|
|
532
|
+
raise ValueError("forced_path has no catalog options")
|
|
533
|
+
eligible_path_key = forced_key
|
|
534
|
+
mode = PathSelectionMode.FORCED
|
|
535
|
+
elif required_path is not None:
|
|
536
|
+
eligible_path_key = canonical_path_bytes(required_path)
|
|
537
|
+
mode = PathSelectionMode.REQUIRED_OPTIONS
|
|
538
|
+
else:
|
|
539
|
+
eligible_path_key = None
|
|
540
|
+
mode = PathSelectionMode.MINIMUM_EXPOSURE_TASK_KEYED
|
|
541
|
+
|
|
542
|
+
path_rows: list[PalettePathRow] = []
|
|
543
|
+
for path_key in sorted(groups):
|
|
544
|
+
family_counts: dict[str, int] = {}
|
|
545
|
+
for option in groups[path_key]:
|
|
546
|
+
family_counts[option.family] = family_counts.get(option.family, 0) + 1
|
|
547
|
+
family_capacity = (
|
|
548
|
+
len(groups[path_key])
|
|
549
|
+
if max_options_per_family is None
|
|
550
|
+
else sum(
|
|
551
|
+
min(count, max_options_per_family) for count in family_counts.values()
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
feasible = family_capacity >= palette_size
|
|
555
|
+
eligible = feasible and (
|
|
556
|
+
eligible_path_key is None or path_key == eligible_path_key
|
|
557
|
+
)
|
|
558
|
+
path_rows.append(
|
|
559
|
+
PalettePathRow(
|
|
560
|
+
path=paths[path_key],
|
|
561
|
+
option_count=len(groups[path_key]),
|
|
562
|
+
family_capacity=family_capacity,
|
|
563
|
+
path_exposure=path_exposure.get(path_key, 0),
|
|
564
|
+
feasible_for_size=feasible,
|
|
565
|
+
eligible_for_choice=eligible,
|
|
566
|
+
task_order_sha256=_task_order_sha256(
|
|
567
|
+
seed=seed,
|
|
568
|
+
task_key=task_key,
|
|
569
|
+
purpose="path",
|
|
570
|
+
payload=path_key,
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
eligible_rows = tuple(row for row in path_rows if row.eligible_for_choice)
|
|
575
|
+
if not eligible_rows:
|
|
576
|
+
if eligible_path_key is not None:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
"the constrained path cannot satisfy palette_size under the "
|
|
579
|
+
"family-cap constraint"
|
|
580
|
+
)
|
|
581
|
+
raise ValueError("no catalog path has enough options for the palette")
|
|
582
|
+
chosen_path_row = min(
|
|
583
|
+
eligible_rows,
|
|
584
|
+
key=lambda row: (
|
|
585
|
+
row.path_exposure,
|
|
586
|
+
row.task_order_sha256,
|
|
587
|
+
canonical_path_bytes(row.path),
|
|
588
|
+
),
|
|
589
|
+
)
|
|
590
|
+
chosen_path = chosen_path_row.path
|
|
591
|
+
chosen_key = canonical_path_bytes(chosen_path)
|
|
592
|
+
chosen_options = tuple(groups[chosen_key])
|
|
593
|
+
|
|
594
|
+
selection_hashes = {
|
|
595
|
+
option.option_id: _task_order_sha256(
|
|
596
|
+
seed=seed,
|
|
597
|
+
task_key=task_key,
|
|
598
|
+
purpose="option-selection",
|
|
599
|
+
payload=bytes.fromhex(option.identity_sha256),
|
|
600
|
+
)
|
|
601
|
+
for option in catalog
|
|
602
|
+
}
|
|
603
|
+
presentation_hashes = {
|
|
604
|
+
option.option_id: _task_order_sha256(
|
|
605
|
+
seed=seed,
|
|
606
|
+
task_key=task_key,
|
|
607
|
+
purpose="option-presentation",
|
|
608
|
+
payload=bytes.fromhex(option.identity_sha256),
|
|
609
|
+
)
|
|
610
|
+
for option in catalog
|
|
611
|
+
}
|
|
612
|
+
required_ids = set(required_option_ids)
|
|
613
|
+
remaining = tuple(
|
|
614
|
+
option for option in chosen_options if option.option_id not in required_ids
|
|
615
|
+
)
|
|
616
|
+
ranked_remaining = tuple(
|
|
617
|
+
sorted(
|
|
618
|
+
remaining,
|
|
619
|
+
key=lambda option: (
|
|
620
|
+
exposure_by_cell.get((chosen_key, option.family), 0),
|
|
621
|
+
family_exposure.get(option.family, 0),
|
|
622
|
+
selection_hashes[option.option_id],
|
|
623
|
+
option.option_id,
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
selected_set = set(required_option_ids)
|
|
628
|
+
selected_family_counts: dict[str, int] = {}
|
|
629
|
+
for option in required:
|
|
630
|
+
selected_family_counts[option.family] = (
|
|
631
|
+
selected_family_counts.get(option.family, 0) + 1
|
|
632
|
+
)
|
|
633
|
+
for option in ranked_remaining:
|
|
634
|
+
if len(selected_set) == palette_size:
|
|
635
|
+
break
|
|
636
|
+
if (
|
|
637
|
+
max_options_per_family is not None
|
|
638
|
+
and selected_family_counts.get(option.family, 0) >= max_options_per_family
|
|
639
|
+
):
|
|
640
|
+
continue
|
|
641
|
+
selected_set.add(option.option_id)
|
|
642
|
+
selected_family_counts[option.family] = (
|
|
643
|
+
selected_family_counts.get(option.family, 0) + 1
|
|
644
|
+
)
|
|
645
|
+
if len(selected_set) != palette_size: # pragma: no cover - size gate above.
|
|
646
|
+
raise RuntimeError("palette fill did not produce the requested size")
|
|
647
|
+
palette = tuple(
|
|
648
|
+
sorted(
|
|
649
|
+
(option for option in chosen_options if option.option_id in selected_set),
|
|
650
|
+
key=lambda option: (
|
|
651
|
+
presentation_hashes[option.option_id],
|
|
652
|
+
option.option_id,
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
positions = {option.option_id: index for index, option in enumerate(palette)}
|
|
657
|
+
|
|
658
|
+
option_rows = tuple(
|
|
659
|
+
PaletteOptionRow(
|
|
660
|
+
option=option,
|
|
661
|
+
cell_exposure=exposure_by_cell.get(
|
|
662
|
+
(canonical_path_bytes(option.path), option.family),
|
|
663
|
+
0,
|
|
664
|
+
),
|
|
665
|
+
family_exposure=family_exposure.get(option.family, 0),
|
|
666
|
+
path_exposure=path_exposure.get(canonical_path_bytes(option.path), 0),
|
|
667
|
+
selection_order_sha256=selection_hashes[option.option_id],
|
|
668
|
+
presentation_order_sha256=presentation_hashes[option.option_id],
|
|
669
|
+
on_chosen_path=canonical_path_bytes(option.path) == chosen_key,
|
|
670
|
+
required=option.option_id in required_ids,
|
|
671
|
+
palette_position=positions.get(option.option_id),
|
|
672
|
+
)
|
|
673
|
+
for option in catalog
|
|
674
|
+
)
|
|
675
|
+
return _PaletteParts(
|
|
676
|
+
chosen_path=chosen_path,
|
|
677
|
+
path_selection_mode=mode,
|
|
678
|
+
exposure_snapshot=exposure_snapshot,
|
|
679
|
+
path_rows=tuple(path_rows),
|
|
680
|
+
option_rows=option_rows,
|
|
681
|
+
palette=palette,
|
|
682
|
+
catalog_sha256=_catalog_sha256(catalog),
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
687
|
+
class AtomicPaletteDecision:
|
|
688
|
+
"""Frozen path, palette, full candidate table, and exact tie-break law."""
|
|
689
|
+
|
|
690
|
+
seed: int
|
|
691
|
+
task_key: str
|
|
692
|
+
palette_size: int
|
|
693
|
+
max_options_per_family: int | None
|
|
694
|
+
forced_path: JsonPath | None
|
|
695
|
+
chosen_path: JsonPath
|
|
696
|
+
path_selection_mode: PathSelectionMode
|
|
697
|
+
required_option_ids: tuple[str, ...]
|
|
698
|
+
exposure_snapshot: tuple[PathFamilyExposure, ...]
|
|
699
|
+
path_rows: tuple[PalettePathRow, ...]
|
|
700
|
+
option_rows: tuple[PaletteOptionRow, ...]
|
|
701
|
+
palette: tuple[AtomicEditOption, ...]
|
|
702
|
+
catalog_sha256: str
|
|
703
|
+
|
|
704
|
+
policy_id: ClassVar[str] = POLICY_ID
|
|
705
|
+
policy_version: ClassVar[int] = POLICY_VERSION
|
|
706
|
+
|
|
707
|
+
def __post_init__(self) -> None:
|
|
708
|
+
if type(self.path_selection_mode) is not PathSelectionMode:
|
|
709
|
+
raise TypeError("path_selection_mode must be a PathSelectionMode")
|
|
710
|
+
if type(self.exposure_snapshot) is not tuple:
|
|
711
|
+
raise TypeError("exposure_snapshot must be an exact tuple")
|
|
712
|
+
if type(self.path_rows) is not tuple or any(
|
|
713
|
+
type(row) is not PalettePathRow for row in self.path_rows
|
|
714
|
+
):
|
|
715
|
+
raise TypeError("path_rows must contain exact PalettePathRow values")
|
|
716
|
+
if type(self.option_rows) is not tuple or any(
|
|
717
|
+
type(row) is not PaletteOptionRow for row in self.option_rows
|
|
718
|
+
):
|
|
719
|
+
raise TypeError("option_rows must contain exact PaletteOptionRow values")
|
|
720
|
+
if type(self.palette) is not tuple or any(
|
|
721
|
+
type(option) is not AtomicEditOption for option in self.palette
|
|
722
|
+
):
|
|
723
|
+
raise TypeError("palette must contain exact AtomicEditOption values")
|
|
724
|
+
require_sha256(self.catalog_sha256, "catalog_sha256")
|
|
725
|
+
expected = _build_parts(
|
|
726
|
+
seed=self.seed,
|
|
727
|
+
task_key=self.task_key,
|
|
728
|
+
options=tuple(row.option for row in self.option_rows),
|
|
729
|
+
palette_size=self.palette_size,
|
|
730
|
+
max_options_per_family=self.max_options_per_family,
|
|
731
|
+
exposures=self.exposure_snapshot,
|
|
732
|
+
forced_path=self.forced_path,
|
|
733
|
+
required_option_ids=self.required_option_ids,
|
|
734
|
+
)
|
|
735
|
+
if self.chosen_path != expected.chosen_path:
|
|
736
|
+
raise ValueError("chosen_path does not match the policy law")
|
|
737
|
+
if self.path_selection_mode is not expected.path_selection_mode:
|
|
738
|
+
raise ValueError("path_selection_mode does not match the constraints")
|
|
739
|
+
if self.exposure_snapshot != expected.exposure_snapshot:
|
|
740
|
+
raise ValueError("exposure_snapshot must use canonical order")
|
|
741
|
+
if self.path_rows != expected.path_rows:
|
|
742
|
+
raise ValueError("path_rows do not match the complete selection facts")
|
|
743
|
+
if self.option_rows != expected.option_rows:
|
|
744
|
+
raise ValueError("option_rows do not match the complete selection facts")
|
|
745
|
+
if self.palette != expected.palette:
|
|
746
|
+
raise ValueError("palette does not match the task-keyed policy law")
|
|
747
|
+
if self.catalog_sha256 != expected.catalog_sha256:
|
|
748
|
+
raise ValueError("catalog_sha256 does not bind the complete catalog")
|
|
749
|
+
|
|
750
|
+
def revalidate(self) -> None:
|
|
751
|
+
if type(self) is not AtomicPaletteDecision:
|
|
752
|
+
raise TypeError("decision must be an exact AtomicPaletteDecision")
|
|
753
|
+
AtomicPaletteDecision.__post_init__(self)
|
|
754
|
+
|
|
755
|
+
@property
|
|
756
|
+
def task_key_sha256(self) -> str:
|
|
757
|
+
self.revalidate()
|
|
758
|
+
return hashlib.sha256(_frame(_task_key_bytes(self.task_key))).hexdigest()
|
|
759
|
+
|
|
760
|
+
def _trace_payload(self) -> dict[str, object]:
|
|
761
|
+
return {
|
|
762
|
+
"event_type": "atomic_palette_selected",
|
|
763
|
+
"policy_id": self.policy_id,
|
|
764
|
+
"policy_version": self.policy_version,
|
|
765
|
+
"seed": self.seed,
|
|
766
|
+
"task_key_sha256": hashlib.sha256(
|
|
767
|
+
_frame(_task_key_bytes(self.task_key))
|
|
768
|
+
).hexdigest(),
|
|
769
|
+
"palette_size": self.palette_size,
|
|
770
|
+
"max_options_per_family": self.max_options_per_family,
|
|
771
|
+
"forced_path": (
|
|
772
|
+
None if self.forced_path is None else _path_record(self.forced_path)
|
|
773
|
+
),
|
|
774
|
+
"chosen_path": _path_record(self.chosen_path),
|
|
775
|
+
"path_selection_mode": self.path_selection_mode.value,
|
|
776
|
+
"required_option_ids": list(self.required_option_ids),
|
|
777
|
+
"catalog_sha256": self.catalog_sha256,
|
|
778
|
+
"exposure_snapshot": [
|
|
779
|
+
exposure.to_trace_record() for exposure in self.exposure_snapshot
|
|
780
|
+
],
|
|
781
|
+
"path_rows": [row.to_trace_record() for row in self.path_rows],
|
|
782
|
+
"option_rows": [row.to_trace_record() for row in self.option_rows],
|
|
783
|
+
"palette_option_ids": [option.option_id for option in self.palette],
|
|
784
|
+
"palette_option_identity_sha256s": [
|
|
785
|
+
option.identity_sha256 for option in self.palette
|
|
786
|
+
],
|
|
787
|
+
"tie_break_law": {
|
|
788
|
+
"path": (
|
|
789
|
+
"minimum path exposure, then SHA-256(seed, task key, path), "
|
|
790
|
+
"then canonical typed path"
|
|
791
|
+
),
|
|
792
|
+
"fill": (
|
|
793
|
+
"minimum path/family-cell exposure, then family exposure, "
|
|
794
|
+
"then SHA-256(seed, task key, option), then option ID; "
|
|
795
|
+
"skip saturated families under max_options_per_family"
|
|
796
|
+
),
|
|
797
|
+
"presentation": ("SHA-256(seed, task key, option), then option ID"),
|
|
798
|
+
},
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
@property
|
|
802
|
+
def decision_sha256(self) -> str:
|
|
803
|
+
self.revalidate()
|
|
804
|
+
encoded = json.dumps(
|
|
805
|
+
self._trace_payload(),
|
|
806
|
+
allow_nan=False,
|
|
807
|
+
ensure_ascii=True,
|
|
808
|
+
separators=(",", ":"),
|
|
809
|
+
sort_keys=True,
|
|
810
|
+
).encode("ascii")
|
|
811
|
+
return hashlib.sha256(_DECISION_DOMAIN + encoded).hexdigest()
|
|
812
|
+
|
|
813
|
+
def to_trace_record(self) -> dict[str, object]:
|
|
814
|
+
"""Return a fresh JSON-safe record binding every candidate and tie-break."""
|
|
815
|
+
|
|
816
|
+
self.revalidate()
|
|
817
|
+
return {**self._trace_payload(), "decision_sha256": self.decision_sha256}
|
|
818
|
+
|
|
819
|
+
def __eq__(self, other: object) -> bool:
|
|
820
|
+
return (
|
|
821
|
+
type(self) is AtomicPaletteDecision
|
|
822
|
+
and type(other) is AtomicPaletteDecision
|
|
823
|
+
and self.decision_sha256 == other.decision_sha256
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
__hash__ = None
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@dataclass(frozen=True, slots=True)
|
|
830
|
+
class TaskKeyedPalettePolicy:
|
|
831
|
+
"""Select exact atomic path/palette choices without mutable RNG state."""
|
|
832
|
+
|
|
833
|
+
seed: int
|
|
834
|
+
|
|
835
|
+
policy_id: ClassVar[str] = POLICY_ID
|
|
836
|
+
policy_version: ClassVar[int] = POLICY_VERSION
|
|
837
|
+
|
|
838
|
+
def __post_init__(self) -> None:
|
|
839
|
+
_validate_seed(self.seed)
|
|
840
|
+
|
|
841
|
+
def select(
|
|
842
|
+
self,
|
|
843
|
+
*,
|
|
844
|
+
task_key: str,
|
|
845
|
+
options: Sequence[AtomicEditOption],
|
|
846
|
+
palette_size: int,
|
|
847
|
+
max_options_per_family: int | None = None,
|
|
848
|
+
exposures: Sequence[PathFamilyExposure] = (),
|
|
849
|
+
path: JsonPath | None = None,
|
|
850
|
+
required_option_ids: Sequence[str] = (),
|
|
851
|
+
) -> AtomicPaletteDecision:
|
|
852
|
+
"""Choose one path and exact provider order from immutable inputs.
|
|
853
|
+
|
|
854
|
+
``required_option_ids`` is a constraint, never an ordering hint. Its
|
|
855
|
+
members must be unique, share the selected path, and fit within the
|
|
856
|
+
requested palette. ``max_options_per_family`` is enforced during both
|
|
857
|
+
path admission and greedy fill. The returned tuple order remains
|
|
858
|
+
task-keyed.
|
|
859
|
+
"""
|
|
860
|
+
|
|
861
|
+
_validate_seed(self.seed)
|
|
862
|
+
if isinstance(required_option_ids, (str, bytes)):
|
|
863
|
+
raise TypeError("required_option_ids must be a finite string sequence")
|
|
864
|
+
supplied_required = tuple(required_option_ids)
|
|
865
|
+
if any(type(option_id) is not str for option_id in supplied_required):
|
|
866
|
+
raise TypeError("required_option_ids must contain exact strings")
|
|
867
|
+
if len(set(supplied_required)) != len(supplied_required):
|
|
868
|
+
raise ValueError("required_option_ids cannot contain duplicates")
|
|
869
|
+
canonical_required = tuple(sorted(supplied_required))
|
|
870
|
+
parts = _build_parts(
|
|
871
|
+
seed=self.seed,
|
|
872
|
+
task_key=task_key,
|
|
873
|
+
options=options,
|
|
874
|
+
palette_size=palette_size,
|
|
875
|
+
max_options_per_family=max_options_per_family,
|
|
876
|
+
exposures=exposures,
|
|
877
|
+
forced_path=path,
|
|
878
|
+
required_option_ids=canonical_required,
|
|
879
|
+
)
|
|
880
|
+
return AtomicPaletteDecision(
|
|
881
|
+
seed=self.seed,
|
|
882
|
+
task_key=task_key,
|
|
883
|
+
palette_size=palette_size,
|
|
884
|
+
max_options_per_family=max_options_per_family,
|
|
885
|
+
forced_path=path,
|
|
886
|
+
chosen_path=parts.chosen_path,
|
|
887
|
+
path_selection_mode=parts.path_selection_mode,
|
|
888
|
+
required_option_ids=canonical_required,
|
|
889
|
+
exposure_snapshot=parts.exposure_snapshot,
|
|
890
|
+
path_rows=parts.path_rows,
|
|
891
|
+
option_rows=parts.option_rows,
|
|
892
|
+
palette=parts.palette,
|
|
893
|
+
catalog_sha256=parts.catalog_sha256,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
__all__ = [
|
|
898
|
+
"AtomicPaletteDecision",
|
|
899
|
+
"POLICY_ID",
|
|
900
|
+
"POLICY_VERSION",
|
|
901
|
+
"PaletteOptionRow",
|
|
902
|
+
"PalettePathRow",
|
|
903
|
+
"PathFamilyExposure",
|
|
904
|
+
"PathSelectionMode",
|
|
905
|
+
"TaskKeyedPalettePolicy",
|
|
906
|
+
]
|