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,3169 @@
|
|
|
1
|
+
"""Framework-neutral contracts for one-call ranked action portfolios.
|
|
2
|
+
|
|
3
|
+
The selector sees immutable prompt views of versioned insight cards and one
|
|
4
|
+
parent-bound finite variation contract. It returns an ordered portfolio of
|
|
5
|
+
opaque option IDs; trusted application code resolves every ID back to the
|
|
6
|
+
sealed option before a decision can be published. The port deliberately does
|
|
7
|
+
not extend :class:`AgenticGenerator`: portfolio selection is an independently
|
|
8
|
+
replaceable policy, not another candidate-authoring mode.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from itertools import combinations, product
|
|
21
|
+
from threading import RLock
|
|
22
|
+
from typing import Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
from agent_evolve.domain.finite_variation import (
|
|
25
|
+
FiniteActionEvidenceBinding,
|
|
26
|
+
FiniteVariationContract,
|
|
27
|
+
ValidatedFiniteVariationIdentityIndex,
|
|
28
|
+
validated_finite_variation_identity_index,
|
|
29
|
+
)
|
|
30
|
+
from agent_evolve.domain.ids import CandidateId, LLMCallId
|
|
31
|
+
from agent_evolve.domain.insight import InsightRef
|
|
32
|
+
from agent_evolve.domain.patch import JsonPath, require_sha256
|
|
33
|
+
from agent_evolve.domain.typed_json import (
|
|
34
|
+
FrozenJsonArray,
|
|
35
|
+
FrozenJsonObject,
|
|
36
|
+
FrozenJsonValue,
|
|
37
|
+
freeze_json,
|
|
38
|
+
thaw_json,
|
|
39
|
+
typed_json_sha256,
|
|
40
|
+
)
|
|
41
|
+
from agent_evolve.ports.agentic_generator import (
|
|
42
|
+
AgenticCallTelemetry,
|
|
43
|
+
MetricEffectPrediction,
|
|
44
|
+
)
|
|
45
|
+
from agent_evolve.ports.portfolio_memory_dose import (
|
|
46
|
+
BoundedPortfolioMemoryDoseContract,
|
|
47
|
+
PortfolioMemoryDoseAssessment,
|
|
48
|
+
PortfolioMemoryDoseMember,
|
|
49
|
+
PortfolioMemoryDoseStage,
|
|
50
|
+
)
|
|
51
|
+
from agent_evolve.ports.structured_generator import MAX_OUTPUT_TOKENS
|
|
52
|
+
from agent_evolve.policies.variation.typed_patch import derive_patch
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
|
|
56
|
+
_OPTION_ID = re.compile(r"^[a-z][a-z0-9_.-]{0,255}$")
|
|
57
|
+
_METRIC_ID = re.compile(r"^[a-z][a-z0-9_.:-]{0,191}$")
|
|
58
|
+
_REQUEST_DOMAIN = b"agent-evolve:portfolio-selection-request:v1\x00"
|
|
59
|
+
_CARD_SNAPSHOT_DOMAIN = b"agent-evolve:portfolio-card-snapshot:v1\x00"
|
|
60
|
+
_DECISION_DOMAIN = b"agent-evolve:ranked-portfolio-decision:v1\x00"
|
|
61
|
+
_SUPPLEMENTAL_AUDIT_DOMAIN = (
|
|
62
|
+
b"agent-evolve:portfolio-selection-supplemental-audit:v1\x00"
|
|
63
|
+
)
|
|
64
|
+
_TRANSFER_REQUEST_DOMAIN = b"agent-evolve:card-transfer-request:v1\x00"
|
|
65
|
+
_TRANSFER_RECEIPT_DOMAIN = b"agent-evolve:card-transfer-score-receipt:v1\x00"
|
|
66
|
+
_CARD_SCORE_STATE_DOMAIN = b"agent-evolve:portfolio-card-score-state:v1\x00"
|
|
67
|
+
_CARD_ACTION_EVIDENCE_STATE_DOMAIN = (
|
|
68
|
+
b"agent-evolve:portfolio-card-action-evidence-state:v1\x00"
|
|
69
|
+
)
|
|
70
|
+
_CARD_SOURCE_BINDING_DOMAIN = b"agent-evolve:portfolio-card-source-binding:v1\x00"
|
|
71
|
+
_CARD_DERIVED_VIEW_DOMAIN = b"agent-evolve:portfolio-card-derived-view:v1\x00"
|
|
72
|
+
_CARD_SOURCE_REGISTRY_DOMAIN = b"agent-evolve:portfolio-card-source-registry:v1\x00"
|
|
73
|
+
_EXPERIMENTAL_VIEW_RECEIPT_DOMAIN = (
|
|
74
|
+
b"agent-evolve:portfolio-experimental-view-receipt:v1\x00"
|
|
75
|
+
)
|
|
76
|
+
_REDACTED_EVIDENCE_DOMAIN = b"agent-evolve:portfolio-card-redacted-evidence:v1\x00"
|
|
77
|
+
_PARENT_PATCH_CERTIFICATE_DOMAIN = (
|
|
78
|
+
b"agent-evolve:parent-patch-feasibility-certificate:v1\x00"
|
|
79
|
+
)
|
|
80
|
+
_PARENT_PATCH_WITNESS_ORDER_DOMAIN = (
|
|
81
|
+
b"agent-evolve:parent-patch-feasibility-witness-order:v1\x00"
|
|
82
|
+
)
|
|
83
|
+
_MAX_PARENT_PATCH_CERTIFICATES = 64
|
|
84
|
+
_MAX_PARENT_PATCH_CACHED_PATHS = 65_536
|
|
85
|
+
_MAX_PARENT_PATCH_FEASIBILITY_RESULTS = 512
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Scientific neutral controls use one public, immutable representation. An
|
|
89
|
+
# empty typed object contains no benchmark vocabulary and therefore cannot
|
|
90
|
+
# accidentally reveal an option through a nominally "neutral" sham card.
|
|
91
|
+
CANONICAL_NEUTRAL_PORTFOLIO_PROMPT_PAYLOAD = FrozenJsonObject(())
|
|
92
|
+
CANONICAL_REDACTED_PORTFOLIO_EVIDENCE_SHA256 = hashlib.sha256(
|
|
93
|
+
_REDACTED_EVIDENCE_DOMAIN
|
|
94
|
+
).hexdigest()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _canonical_json(value: object) -> bytes:
|
|
98
|
+
return json.dumps(
|
|
99
|
+
value,
|
|
100
|
+
allow_nan=False,
|
|
101
|
+
ensure_ascii=True,
|
|
102
|
+
separators=(",", ":"),
|
|
103
|
+
sort_keys=True,
|
|
104
|
+
).encode("ascii")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _hash(domain: bytes, value: object) -> str:
|
|
108
|
+
return hashlib.sha256(domain + _canonical_json(value)).hexdigest()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _reference_record(reference: InsightRef) -> dict[str, object]:
|
|
112
|
+
if type(reference) is not InsightRef:
|
|
113
|
+
raise TypeError("reference must be an exact InsightRef")
|
|
114
|
+
InsightRef.__post_init__(reference)
|
|
115
|
+
return {
|
|
116
|
+
"insight_id": reference.insight_id.value,
|
|
117
|
+
"version": reference.version,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _canonical_metric_ids(values: tuple[str, ...]) -> None:
|
|
122
|
+
if type(values) is not tuple or any(
|
|
123
|
+
type(value) is not str or _METRIC_ID.fullmatch(value) is None
|
|
124
|
+
for value in values
|
|
125
|
+
):
|
|
126
|
+
raise TypeError(
|
|
127
|
+
"required_metric_ids must be an exact tuple of metric identifiers"
|
|
128
|
+
)
|
|
129
|
+
if not values:
|
|
130
|
+
raise ValueError("required_metric_ids must be non-empty")
|
|
131
|
+
if values != tuple(sorted(set(values))):
|
|
132
|
+
raise ValueError("required_metric_ids must be unique and canonical")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class _ParentPatchFeasibilityCertificate:
|
|
137
|
+
"""One content-bound derivation of all parent-relative option paths."""
|
|
138
|
+
|
|
139
|
+
contract_identity_sha256: str
|
|
140
|
+
option_ids: tuple[str, ...]
|
|
141
|
+
option_identity_sha256s: tuple[str, ...]
|
|
142
|
+
option_families: tuple[str, ...]
|
|
143
|
+
paths_by_option: tuple[tuple[JsonPath, ...], ...]
|
|
144
|
+
certificate_sha256: str = field(init=False)
|
|
145
|
+
|
|
146
|
+
def __post_init__(self) -> None:
|
|
147
|
+
require_sha256(self.contract_identity_sha256, "contract_identity_sha256")
|
|
148
|
+
count = len(self.option_ids)
|
|
149
|
+
if (
|
|
150
|
+
count == 0
|
|
151
|
+
or type(self.option_ids) is not tuple
|
|
152
|
+
or any(
|
|
153
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
154
|
+
for value in self.option_ids
|
|
155
|
+
)
|
|
156
|
+
or len(set(self.option_ids)) != count
|
|
157
|
+
):
|
|
158
|
+
raise ValueError("certificate option IDs must be distinct finite IDs")
|
|
159
|
+
if (
|
|
160
|
+
type(self.option_identity_sha256s) is not tuple
|
|
161
|
+
or len(self.option_identity_sha256s) != count
|
|
162
|
+
):
|
|
163
|
+
raise ValueError("certificate identities must cover every option")
|
|
164
|
+
for value in self.option_identity_sha256s:
|
|
165
|
+
require_sha256(value, "certificate option identity")
|
|
166
|
+
if (
|
|
167
|
+
type(self.option_families) is not tuple
|
|
168
|
+
or len(self.option_families) != count
|
|
169
|
+
or any(
|
|
170
|
+
type(value) is not str or _TOKEN.fullmatch(value) is None
|
|
171
|
+
for value in self.option_families
|
|
172
|
+
)
|
|
173
|
+
):
|
|
174
|
+
raise ValueError("certificate families must cover every option")
|
|
175
|
+
if (
|
|
176
|
+
type(self.paths_by_option) is not tuple
|
|
177
|
+
or len(self.paths_by_option) != count
|
|
178
|
+
or any(
|
|
179
|
+
type(paths) is not tuple
|
|
180
|
+
or not paths
|
|
181
|
+
or any(type(path) is not JsonPath for path in paths)
|
|
182
|
+
for paths in self.paths_by_option
|
|
183
|
+
)
|
|
184
|
+
):
|
|
185
|
+
raise ValueError("certificate paths must cover every option")
|
|
186
|
+
object.__setattr__(
|
|
187
|
+
self,
|
|
188
|
+
"certificate_sha256",
|
|
189
|
+
_hash(
|
|
190
|
+
_PARENT_PATCH_CERTIFICATE_DOMAIN,
|
|
191
|
+
{
|
|
192
|
+
"contract_identity_sha256": self.contract_identity_sha256,
|
|
193
|
+
"options": [
|
|
194
|
+
{
|
|
195
|
+
"option_id": option_id,
|
|
196
|
+
"option_identity_sha256": option_identity_sha256,
|
|
197
|
+
"family": family,
|
|
198
|
+
"path_schema_sha256s": [
|
|
199
|
+
path.schema_identity for path in paths
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
for option_id, option_identity_sha256, family, paths in zip(
|
|
203
|
+
self.option_ids,
|
|
204
|
+
self.option_identity_sha256s,
|
|
205
|
+
self.option_families,
|
|
206
|
+
self.paths_by_option,
|
|
207
|
+
strict=True,
|
|
208
|
+
)
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_PARENT_PATCH_CACHE_LOCK = RLock()
|
|
216
|
+
_PARENT_PATCH_CERTIFICATES: OrderedDict[str, _ParentPatchFeasibilityCertificate] = (
|
|
217
|
+
OrderedDict()
|
|
218
|
+
)
|
|
219
|
+
_PARENT_PATCH_CACHED_PATH_COUNT = 0
|
|
220
|
+
_PARENT_PATCH_FEASIBILITY_RESULTS: OrderedDict[
|
|
221
|
+
tuple[str, tuple[str, ...], int, int | None], bool
|
|
222
|
+
] = OrderedDict()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _parent_patch_certificate(
|
|
226
|
+
contract: FiniteVariationContract,
|
|
227
|
+
) -> _ParentPatchFeasibilityCertificate:
|
|
228
|
+
"""Return a bounded-cache certificate after exact content validation."""
|
|
229
|
+
|
|
230
|
+
index = validated_finite_variation_identity_index(contract)
|
|
231
|
+
if type(index) is not ValidatedFiniteVariationIdentityIndex:
|
|
232
|
+
raise TypeError("identity_index must be exact or None")
|
|
233
|
+
index.__post_init__()
|
|
234
|
+
key = index.contract_identity_sha256
|
|
235
|
+
with _PARENT_PATCH_CACHE_LOCK:
|
|
236
|
+
cached = _PARENT_PATCH_CERTIFICATES.get(key)
|
|
237
|
+
if cached is not None:
|
|
238
|
+
if (
|
|
239
|
+
cached.option_ids != index.option_ids
|
|
240
|
+
or cached.option_identity_sha256s != index.option_identity_sha256s
|
|
241
|
+
):
|
|
242
|
+
raise RuntimeError("parent-patch certificate identity collision")
|
|
243
|
+
_PARENT_PATCH_CERTIFICATES.move_to_end(key)
|
|
244
|
+
return cached
|
|
245
|
+
|
|
246
|
+
base_id = CandidateId("candidate_portfolio_disjoint_parent")
|
|
247
|
+
target_id = CandidateId("candidate_portfolio_disjoint_child")
|
|
248
|
+
paths: list[tuple[JsonPath, ...]] = []
|
|
249
|
+
for option in contract.options:
|
|
250
|
+
patch = derive_patch(
|
|
251
|
+
contract.parent_configuration,
|
|
252
|
+
option.child_configuration,
|
|
253
|
+
base_candidate_id=base_id,
|
|
254
|
+
target_candidate_id=target_id,
|
|
255
|
+
)
|
|
256
|
+
if not patch.operations:
|
|
257
|
+
raise ValueError("finite option has no parent-relative patch")
|
|
258
|
+
paths.append(tuple(operation.path for operation in patch.operations))
|
|
259
|
+
certificate = _ParentPatchFeasibilityCertificate(
|
|
260
|
+
contract_identity_sha256=key,
|
|
261
|
+
option_ids=index.option_ids,
|
|
262
|
+
option_identity_sha256s=index.option_identity_sha256s,
|
|
263
|
+
option_families=tuple(option.family for option in contract.options),
|
|
264
|
+
paths_by_option=tuple(paths),
|
|
265
|
+
)
|
|
266
|
+
certificate_path_count = sum(len(value) for value in certificate.paths_by_option)
|
|
267
|
+
with _PARENT_PATCH_CACHE_LOCK:
|
|
268
|
+
global _PARENT_PATCH_CACHED_PATH_COUNT
|
|
269
|
+
prior = _PARENT_PATCH_CERTIFICATES.get(key)
|
|
270
|
+
if prior is not None:
|
|
271
|
+
if prior != certificate:
|
|
272
|
+
raise RuntimeError("parent-patch certificate identity collision")
|
|
273
|
+
_PARENT_PATCH_CERTIFICATES.move_to_end(key)
|
|
274
|
+
return prior
|
|
275
|
+
if certificate_path_count > _MAX_PARENT_PATCH_CACHED_PATHS:
|
|
276
|
+
return certificate
|
|
277
|
+
while _PARENT_PATCH_CERTIFICATES and (
|
|
278
|
+
len(_PARENT_PATCH_CERTIFICATES) >= _MAX_PARENT_PATCH_CERTIFICATES
|
|
279
|
+
or _PARENT_PATCH_CACHED_PATH_COUNT + certificate_path_count
|
|
280
|
+
> _MAX_PARENT_PATCH_CACHED_PATHS
|
|
281
|
+
):
|
|
282
|
+
_, evicted = _PARENT_PATCH_CERTIFICATES.popitem(last=False)
|
|
283
|
+
_PARENT_PATCH_CACHED_PATH_COUNT -= sum(
|
|
284
|
+
len(value) for value in evicted.paths_by_option
|
|
285
|
+
)
|
|
286
|
+
_PARENT_PATCH_CERTIFICATES[key] = certificate
|
|
287
|
+
_PARENT_PATCH_CACHED_PATH_COUNT += certificate_path_count
|
|
288
|
+
return certificate
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _parent_patch_paths_by_option(
|
|
292
|
+
contract: FiniteVariationContract,
|
|
293
|
+
option_ids: tuple[str, ...],
|
|
294
|
+
) -> dict[str, tuple[JsonPath, ...]]:
|
|
295
|
+
"""Resolve paths from one content-bound contract certificate."""
|
|
296
|
+
|
|
297
|
+
if type(option_ids) is not tuple or any(
|
|
298
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
299
|
+
for value in option_ids
|
|
300
|
+
):
|
|
301
|
+
raise TypeError("option_ids must be an exact tuple of finite option IDs")
|
|
302
|
+
if len(set(option_ids)) != len(option_ids):
|
|
303
|
+
raise ValueError("option_ids cannot repeat")
|
|
304
|
+
certificate = _parent_patch_certificate(contract)
|
|
305
|
+
all_paths = dict(
|
|
306
|
+
zip(
|
|
307
|
+
certificate.option_ids,
|
|
308
|
+
certificate.paths_by_option,
|
|
309
|
+
strict=True,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
if any(option_id not in all_paths for option_id in option_ids):
|
|
313
|
+
raise ValueError("option_id is outside the sealed finite contract")
|
|
314
|
+
return {option_id: all_paths[option_id] for option_id in option_ids}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _path_sets_are_disjoint(
|
|
318
|
+
left: tuple[JsonPath, ...],
|
|
319
|
+
right: tuple[JsonPath, ...],
|
|
320
|
+
) -> bool:
|
|
321
|
+
return not any(
|
|
322
|
+
left_path.is_prefix_of(right_path) or right_path.is_prefix_of(left_path)
|
|
323
|
+
for left_path in left
|
|
324
|
+
for right_path in right
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def pairwise_disjoint_parent_patch_pairs(
|
|
329
|
+
contract: FiniteVariationContract,
|
|
330
|
+
option_ids: tuple[str, ...],
|
|
331
|
+
) -> tuple[tuple[str, str], ...]:
|
|
332
|
+
"""Return every canonical option pair with non-overlapping parent patches."""
|
|
333
|
+
|
|
334
|
+
paths = _parent_patch_paths_by_option(contract, option_ids)
|
|
335
|
+
ordered = tuple(sorted(option_ids))
|
|
336
|
+
return tuple(
|
|
337
|
+
(left, right)
|
|
338
|
+
for left_index, left in enumerate(ordered)
|
|
339
|
+
for right in ordered[left_index + 1 :]
|
|
340
|
+
if _path_sets_are_disjoint(paths[left], paths[right])
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def single_path_parent_patch_option_ids(
|
|
345
|
+
contract: FiniteVariationContract,
|
|
346
|
+
option_ids: tuple[str, ...],
|
|
347
|
+
) -> tuple[str, ...]:
|
|
348
|
+
"""Return canonical options that change exactly one parent-relative path.
|
|
349
|
+
|
|
350
|
+
This is structural evidence derived from the sealed finite variation
|
|
351
|
+
contract. It carries no objective, workload, model, or provider
|
|
352
|
+
semantics and is therefore safe for generic trusted allocation code.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
paths = _parent_patch_paths_by_option(contract, option_ids)
|
|
356
|
+
return tuple(
|
|
357
|
+
option_id for option_id in sorted(option_ids) if len(paths[option_id]) == 1
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _validate_family_exposure_bounds(
|
|
362
|
+
values: tuple[tuple[str, int, int], ...],
|
|
363
|
+
*,
|
|
364
|
+
portfolio_size: int,
|
|
365
|
+
) -> None:
|
|
366
|
+
if type(values) is not tuple:
|
|
367
|
+
raise TypeError("family_exposure_bounds must be an exact tuple")
|
|
368
|
+
if len(values) > portfolio_size:
|
|
369
|
+
raise ValueError("family exposure bound count exceeds the portfolio size")
|
|
370
|
+
families: list[str] = []
|
|
371
|
+
minimum_total = 0
|
|
372
|
+
for value in values:
|
|
373
|
+
if type(value) is not tuple or len(value) != 3:
|
|
374
|
+
raise TypeError("family exposure bounds must be exact three-tuples")
|
|
375
|
+
family, minimum, maximum = value
|
|
376
|
+
if type(family) is not str or not family:
|
|
377
|
+
raise ValueError("family exposure family must be a non-empty string")
|
|
378
|
+
if type(minimum) is not int or not 0 <= minimum <= portfolio_size:
|
|
379
|
+
raise ValueError("family exposure minimum is outside the portfolio")
|
|
380
|
+
if type(maximum) is not int or not 0 <= maximum <= portfolio_size:
|
|
381
|
+
raise ValueError("family exposure maximum is outside the portfolio")
|
|
382
|
+
if minimum > maximum:
|
|
383
|
+
raise ValueError("family exposure minimum cannot exceed its maximum")
|
|
384
|
+
families.append(family)
|
|
385
|
+
minimum_total += minimum
|
|
386
|
+
if tuple(families) != tuple(sorted(set(families))):
|
|
387
|
+
raise ValueError("family exposure bounds must be unique and canonical")
|
|
388
|
+
if minimum_total > portfolio_size:
|
|
389
|
+
raise ValueError("family exposure minima exceed the portfolio size")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _distinct_family_minimum_remains_reachable(
|
|
393
|
+
*,
|
|
394
|
+
chosen: tuple[str, ...],
|
|
395
|
+
remaining: tuple[str, ...],
|
|
396
|
+
portfolio_size: int,
|
|
397
|
+
min_distinct_families: int | None,
|
|
398
|
+
family_exposure_bounds: tuple[tuple[str, int, int], ...],
|
|
399
|
+
family_by_id: dict[str, str],
|
|
400
|
+
) -> bool:
|
|
401
|
+
"""Return a cheap necessary condition for the distinct-family target.
|
|
402
|
+
|
|
403
|
+
A family minimum greater than one consumes duplicate-family slots. The
|
|
404
|
+
former feasibility search ignored that interaction and could enumerate a
|
|
405
|
+
large fraction of the K-subsets before proving, for example, that exact
|
|
406
|
+
two members from one family is incompatible with four distinct families
|
|
407
|
+
in a K=4 portfolio. This bound accounts for mandatory remaining slots and
|
|
408
|
+
then gives every free slot the optimistic benefit of introducing a new
|
|
409
|
+
family. It is therefore safe pruning: ``False`` proves infeasibility,
|
|
410
|
+
while ``True`` leaves the exact patch-disjoint search unchanged.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
if min_distinct_families is None:
|
|
414
|
+
return True
|
|
415
|
+
needed = portfolio_size - len(chosen)
|
|
416
|
+
chosen_counts: dict[str, int] = {}
|
|
417
|
+
for option_id in chosen:
|
|
418
|
+
family = family_by_id[option_id]
|
|
419
|
+
chosen_counts[family] = chosen_counts.get(family, 0) + 1
|
|
420
|
+
remaining_counts: dict[str, int] = {}
|
|
421
|
+
for option_id in remaining:
|
|
422
|
+
family = family_by_id[option_id]
|
|
423
|
+
remaining_counts[family] = remaining_counts.get(family, 0) + 1
|
|
424
|
+
|
|
425
|
+
maximum_by_family = {
|
|
426
|
+
family: maximum for family, _, maximum in family_exposure_bounds
|
|
427
|
+
}
|
|
428
|
+
mandatory_slots = 0
|
|
429
|
+
mandatory_new_families: set[str] = set()
|
|
430
|
+
for family, minimum, _ in family_exposure_bounds:
|
|
431
|
+
outstanding = max(minimum - chosen_counts.get(family, 0), 0)
|
|
432
|
+
if outstanding > remaining_counts.get(family, 0):
|
|
433
|
+
return False
|
|
434
|
+
mandatory_slots += outstanding
|
|
435
|
+
if outstanding and family not in chosen_counts:
|
|
436
|
+
mandatory_new_families.add(family)
|
|
437
|
+
if mandatory_slots > needed:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
optional_new_families = {
|
|
441
|
+
family
|
|
442
|
+
for family in remaining_counts
|
|
443
|
+
if family not in chosen_counts
|
|
444
|
+
and family not in mandatory_new_families
|
|
445
|
+
and maximum_by_family.get(family, portfolio_size) > 0
|
|
446
|
+
}
|
|
447
|
+
optimistic_distinct = (
|
|
448
|
+
len(chosen_counts)
|
|
449
|
+
+ len(mandatory_new_families)
|
|
450
|
+
+ min(needed - mandatory_slots, len(optional_new_families))
|
|
451
|
+
)
|
|
452
|
+
return optimistic_distinct >= min_distinct_families
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def finite_option_ids_have_pairwise_disjoint_parent_patch_subset(
|
|
456
|
+
contract: FiniteVariationContract,
|
|
457
|
+
option_ids: tuple[str, ...],
|
|
458
|
+
*,
|
|
459
|
+
portfolio_size: int,
|
|
460
|
+
min_distinct_families: int | None = None,
|
|
461
|
+
family_exposure_bounds: tuple[tuple[str, int, int], ...] = (),
|
|
462
|
+
) -> bool:
|
|
463
|
+
"""Decide exact K-subset feasibility with branch pruning and early exit.
|
|
464
|
+
|
|
465
|
+
Exact K-clique feasibility is exponential in the worst case. The public
|
|
466
|
+
finite contract is bounded to 1,024 options and portfolio sizes are small;
|
|
467
|
+
this search stops on its first witness and prunes every undersized suffix.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
if type(portfolio_size) is not int or portfolio_size <= 0:
|
|
471
|
+
raise ValueError("portfolio_size must be a positive exact integer")
|
|
472
|
+
if min_distinct_families is not None and (
|
|
473
|
+
type(min_distinct_families) is not int
|
|
474
|
+
or not 1 <= min_distinct_families <= portfolio_size
|
|
475
|
+
):
|
|
476
|
+
raise ValueError(
|
|
477
|
+
"min_distinct_families must lie within the requested portfolio"
|
|
478
|
+
)
|
|
479
|
+
_validate_family_exposure_bounds(
|
|
480
|
+
family_exposure_bounds,
|
|
481
|
+
portfolio_size=portfolio_size,
|
|
482
|
+
)
|
|
483
|
+
if type(option_ids) is not tuple or any(
|
|
484
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
485
|
+
for value in option_ids
|
|
486
|
+
):
|
|
487
|
+
raise TypeError("option_ids must be an exact tuple of finite option IDs")
|
|
488
|
+
if len(set(option_ids)) != len(option_ids):
|
|
489
|
+
raise ValueError("option_ids cannot repeat")
|
|
490
|
+
if portfolio_size > len(option_ids):
|
|
491
|
+
return False
|
|
492
|
+
certificate = _parent_patch_certificate(contract)
|
|
493
|
+
path_by_id = dict(
|
|
494
|
+
zip(certificate.option_ids, certificate.paths_by_option, strict=True)
|
|
495
|
+
)
|
|
496
|
+
family_by_id = dict(
|
|
497
|
+
zip(certificate.option_ids, certificate.option_families, strict=True)
|
|
498
|
+
)
|
|
499
|
+
if any(option_id not in path_by_id for option_id in option_ids):
|
|
500
|
+
raise ValueError("option_id is outside the sealed finite contract")
|
|
501
|
+
ordered_option_ids = tuple(sorted(option_ids))
|
|
502
|
+
cache_key = (
|
|
503
|
+
certificate.certificate_sha256,
|
|
504
|
+
ordered_option_ids,
|
|
505
|
+
portfolio_size,
|
|
506
|
+
min_distinct_families,
|
|
507
|
+
family_exposure_bounds,
|
|
508
|
+
)
|
|
509
|
+
with _PARENT_PATCH_CACHE_LOCK:
|
|
510
|
+
cached = _PARENT_PATCH_FEASIBILITY_RESULTS.get(cache_key)
|
|
511
|
+
if cached is not None:
|
|
512
|
+
_PARENT_PATCH_FEASIBILITY_RESULTS.move_to_end(cache_key)
|
|
513
|
+
return cached
|
|
514
|
+
|
|
515
|
+
def search(remaining: tuple[str, ...], chosen: tuple[str, ...]) -> bool:
|
|
516
|
+
needed = portfolio_size - len(chosen)
|
|
517
|
+
if needed == 0:
|
|
518
|
+
if (
|
|
519
|
+
min_distinct_families is not None
|
|
520
|
+
and len({family_by_id[value] for value in chosen})
|
|
521
|
+
< min_distinct_families
|
|
522
|
+
):
|
|
523
|
+
return False
|
|
524
|
+
return all(
|
|
525
|
+
minimum
|
|
526
|
+
<= sum(family_by_id[value] == family for value in chosen)
|
|
527
|
+
<= maximum
|
|
528
|
+
for family, minimum, maximum in family_exposure_bounds
|
|
529
|
+
)
|
|
530
|
+
if len(remaining) < needed:
|
|
531
|
+
return False
|
|
532
|
+
for family, minimum, maximum in family_exposure_bounds:
|
|
533
|
+
chosen_count = sum(family_by_id[value] == family for value in chosen)
|
|
534
|
+
if chosen_count > maximum:
|
|
535
|
+
return False
|
|
536
|
+
remaining_count = sum(
|
|
537
|
+
family_by_id[value] == family for value in remaining
|
|
538
|
+
)
|
|
539
|
+
if chosen_count + min(needed, remaining_count) < minimum:
|
|
540
|
+
return False
|
|
541
|
+
if not _distinct_family_minimum_remains_reachable(
|
|
542
|
+
chosen=chosen,
|
|
543
|
+
remaining=remaining,
|
|
544
|
+
portfolio_size=portfolio_size,
|
|
545
|
+
min_distinct_families=min_distinct_families,
|
|
546
|
+
family_exposure_bounds=family_exposure_bounds,
|
|
547
|
+
family_by_id=family_by_id,
|
|
548
|
+
):
|
|
549
|
+
return False
|
|
550
|
+
for index, option_id in enumerate(remaining):
|
|
551
|
+
if any(
|
|
552
|
+
family_by_id[option_id] == family
|
|
553
|
+
and sum(family_by_id[value] == family for value in chosen)
|
|
554
|
+
>= maximum
|
|
555
|
+
for family, _, maximum in family_exposure_bounds
|
|
556
|
+
):
|
|
557
|
+
continue
|
|
558
|
+
if all(
|
|
559
|
+
_path_sets_are_disjoint(path_by_id[option_id], path_by_id[prior])
|
|
560
|
+
for prior in chosen
|
|
561
|
+
) and search(remaining[index + 1 :], (*chosen, option_id)):
|
|
562
|
+
return True
|
|
563
|
+
return False
|
|
564
|
+
|
|
565
|
+
result = search(ordered_option_ids, ())
|
|
566
|
+
with _PARENT_PATCH_CACHE_LOCK:
|
|
567
|
+
_PARENT_PATCH_FEASIBILITY_RESULTS[cache_key] = result
|
|
568
|
+
_PARENT_PATCH_FEASIBILITY_RESULTS.move_to_end(cache_key)
|
|
569
|
+
while (
|
|
570
|
+
len(_PARENT_PATCH_FEASIBILITY_RESULTS)
|
|
571
|
+
> _MAX_PARENT_PATCH_FEASIBILITY_RESULTS
|
|
572
|
+
):
|
|
573
|
+
_PARENT_PATCH_FEASIBILITY_RESULTS.popitem(last=False)
|
|
574
|
+
return result
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def pairwise_disjoint_parent_patch_witness(
|
|
578
|
+
contract: FiniteVariationContract,
|
|
579
|
+
option_ids: tuple[str, ...],
|
|
580
|
+
*,
|
|
581
|
+
portfolio_size: int,
|
|
582
|
+
min_distinct_families: int | None = None,
|
|
583
|
+
family_exposure_bounds: tuple[tuple[str, int, int], ...] = (),
|
|
584
|
+
ordering_key_sha256: str | None = None,
|
|
585
|
+
preferred_option_ids: tuple[str, ...] = (),
|
|
586
|
+
required_option_ids: tuple[str, ...] = (),
|
|
587
|
+
) -> tuple[str, ...] | None:
|
|
588
|
+
"""Return one deterministic feasibility witness, or ``None`` if absent.
|
|
589
|
+
|
|
590
|
+
The witness is derived only from the sealed parent-relative patches and
|
|
591
|
+
option families. It is therefore safe to expose before evaluation as a
|
|
592
|
+
structural decoding aid: it proves how to satisfy the hard combinatorial
|
|
593
|
+
contract without ranking an option by objective quality. The default
|
|
594
|
+
preserves canonical option-ID order. An optional authenticated SHA-256
|
|
595
|
+
key gives independent requests deterministic, domain-separated orderings
|
|
596
|
+
without consulting outcomes, option prose, or workload semantics. When
|
|
597
|
+
``required_option_ids`` is a hard membership constraint with no ranking
|
|
598
|
+
semantics. When ``preferred_option_ids`` is non-empty, the witness first
|
|
599
|
+
includes every required option, then maximizes the number of retained
|
|
600
|
+
preferred options, and finally prefers the lexicographically earliest
|
|
601
|
+
preferred-rank combination. Canonical/keyed order breaks ties only among
|
|
602
|
+
nonpreferred completions. This supports an outcome-blind protected-source
|
|
603
|
+
floor alongside minimum-intervention semantic projection.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
if type(portfolio_size) is not int or portfolio_size <= 0:
|
|
607
|
+
raise ValueError("portfolio_size must be a positive exact integer")
|
|
608
|
+
if min_distinct_families is not None and (
|
|
609
|
+
type(min_distinct_families) is not int
|
|
610
|
+
or not 1 <= min_distinct_families <= portfolio_size
|
|
611
|
+
):
|
|
612
|
+
raise ValueError(
|
|
613
|
+
"min_distinct_families must lie within the requested portfolio"
|
|
614
|
+
)
|
|
615
|
+
_validate_family_exposure_bounds(
|
|
616
|
+
family_exposure_bounds,
|
|
617
|
+
portfolio_size=portfolio_size,
|
|
618
|
+
)
|
|
619
|
+
if type(option_ids) is not tuple or any(
|
|
620
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
621
|
+
for value in option_ids
|
|
622
|
+
):
|
|
623
|
+
raise TypeError("option_ids must be an exact tuple of finite option IDs")
|
|
624
|
+
if len(set(option_ids)) != len(option_ids):
|
|
625
|
+
raise ValueError("option_ids cannot repeat")
|
|
626
|
+
if type(preferred_option_ids) is not tuple or any(
|
|
627
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
628
|
+
for value in preferred_option_ids
|
|
629
|
+
):
|
|
630
|
+
raise TypeError(
|
|
631
|
+
"preferred_option_ids must be an exact tuple of finite option IDs"
|
|
632
|
+
)
|
|
633
|
+
if len(set(preferred_option_ids)) != len(preferred_option_ids):
|
|
634
|
+
raise ValueError("preferred_option_ids cannot repeat")
|
|
635
|
+
if not set(preferred_option_ids).issubset(option_ids):
|
|
636
|
+
raise ValueError("preferred_option_ids must be drawn from option_ids")
|
|
637
|
+
if type(required_option_ids) is not tuple or any(
|
|
638
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
639
|
+
for value in required_option_ids
|
|
640
|
+
):
|
|
641
|
+
raise TypeError(
|
|
642
|
+
"required_option_ids must be an exact tuple of finite option IDs"
|
|
643
|
+
)
|
|
644
|
+
if required_option_ids != tuple(sorted(set(required_option_ids))):
|
|
645
|
+
raise ValueError("required_option_ids must be unique and canonical")
|
|
646
|
+
if not set(required_option_ids).issubset(option_ids):
|
|
647
|
+
raise ValueError("required_option_ids must be drawn from option_ids")
|
|
648
|
+
if len(required_option_ids) > portfolio_size:
|
|
649
|
+
return None
|
|
650
|
+
if portfolio_size > len(option_ids):
|
|
651
|
+
return None
|
|
652
|
+
if ordering_key_sha256 is not None:
|
|
653
|
+
require_sha256(ordering_key_sha256, "ordering_key_sha256")
|
|
654
|
+
|
|
655
|
+
certificate = _parent_patch_certificate(contract)
|
|
656
|
+
path_by_id = dict(
|
|
657
|
+
zip(certificate.option_ids, certificate.paths_by_option, strict=True)
|
|
658
|
+
)
|
|
659
|
+
family_by_id = dict(
|
|
660
|
+
zip(certificate.option_ids, certificate.option_families, strict=True)
|
|
661
|
+
)
|
|
662
|
+
if any(option_id not in path_by_id for option_id in option_ids):
|
|
663
|
+
raise ValueError("option_id is outside the sealed finite contract")
|
|
664
|
+
if ordering_key_sha256 is None:
|
|
665
|
+
ordered_option_ids = tuple(sorted(option_ids))
|
|
666
|
+
else:
|
|
667
|
+
ordering_key = bytes.fromhex(ordering_key_sha256)
|
|
668
|
+
ordered_option_ids = tuple(
|
|
669
|
+
sorted(
|
|
670
|
+
option_ids,
|
|
671
|
+
key=lambda option_id: (
|
|
672
|
+
hashlib.sha256(
|
|
673
|
+
_PARENT_PATCH_WITNESS_ORDER_DOMAIN
|
|
674
|
+
+ ordering_key
|
|
675
|
+
+ option_id.encode("ascii", errors="strict")
|
|
676
|
+
).digest(),
|
|
677
|
+
option_id,
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def search(
|
|
683
|
+
remaining: tuple[str, ...],
|
|
684
|
+
chosen: tuple[str, ...],
|
|
685
|
+
) -> tuple[str, ...] | None:
|
|
686
|
+
needed = portfolio_size - len(chosen)
|
|
687
|
+
if needed == 0:
|
|
688
|
+
if (
|
|
689
|
+
min_distinct_families is not None
|
|
690
|
+
and len({family_by_id[value] for value in chosen})
|
|
691
|
+
< min_distinct_families
|
|
692
|
+
):
|
|
693
|
+
return None
|
|
694
|
+
if any(
|
|
695
|
+
not minimum
|
|
696
|
+
<= sum(family_by_id[value] == family for value in chosen)
|
|
697
|
+
<= maximum
|
|
698
|
+
for family, minimum, maximum in family_exposure_bounds
|
|
699
|
+
):
|
|
700
|
+
return None
|
|
701
|
+
return chosen
|
|
702
|
+
if len(remaining) < needed:
|
|
703
|
+
return None
|
|
704
|
+
for family, minimum, maximum in family_exposure_bounds:
|
|
705
|
+
chosen_count = sum(family_by_id[value] == family for value in chosen)
|
|
706
|
+
if chosen_count > maximum:
|
|
707
|
+
return None
|
|
708
|
+
remaining_count = sum(
|
|
709
|
+
family_by_id[value] == family for value in remaining
|
|
710
|
+
)
|
|
711
|
+
if chosen_count + min(needed, remaining_count) < minimum:
|
|
712
|
+
return None
|
|
713
|
+
if not _distinct_family_minimum_remains_reachable(
|
|
714
|
+
chosen=chosen,
|
|
715
|
+
remaining=remaining,
|
|
716
|
+
portfolio_size=portfolio_size,
|
|
717
|
+
min_distinct_families=min_distinct_families,
|
|
718
|
+
family_exposure_bounds=family_exposure_bounds,
|
|
719
|
+
family_by_id=family_by_id,
|
|
720
|
+
):
|
|
721
|
+
return None
|
|
722
|
+
for index, option_id in enumerate(remaining):
|
|
723
|
+
if any(
|
|
724
|
+
family_by_id[option_id] == family
|
|
725
|
+
and sum(family_by_id[value] == family for value in chosen)
|
|
726
|
+
>= maximum
|
|
727
|
+
for family, _, maximum in family_exposure_bounds
|
|
728
|
+
):
|
|
729
|
+
continue
|
|
730
|
+
if not all(
|
|
731
|
+
_path_sets_are_disjoint(path_by_id[option_id], path_by_id[prior])
|
|
732
|
+
for prior in chosen
|
|
733
|
+
):
|
|
734
|
+
continue
|
|
735
|
+
witness = search(remaining[index + 1 :], (*chosen, option_id))
|
|
736
|
+
if witness is not None:
|
|
737
|
+
return witness
|
|
738
|
+
return None
|
|
739
|
+
|
|
740
|
+
required_set = set(required_option_ids)
|
|
741
|
+
if any(
|
|
742
|
+
not _path_sets_are_disjoint(path_by_id[left], path_by_id[right])
|
|
743
|
+
for index, left in enumerate(required_option_ids)
|
|
744
|
+
for right in required_option_ids[index + 1 :]
|
|
745
|
+
):
|
|
746
|
+
return None
|
|
747
|
+
if any(
|
|
748
|
+
sum(family_by_id[value] == family for value in required_option_ids)
|
|
749
|
+
> maximum
|
|
750
|
+
for family, _, maximum in family_exposure_bounds
|
|
751
|
+
):
|
|
752
|
+
return None
|
|
753
|
+
completion_option_ids = tuple(
|
|
754
|
+
value for value in ordered_option_ids if value not in required_set
|
|
755
|
+
)
|
|
756
|
+
remaining_preferred = tuple(
|
|
757
|
+
value for value in preferred_option_ids if value not in required_set
|
|
758
|
+
)
|
|
759
|
+
if not remaining_preferred:
|
|
760
|
+
return search(completion_option_ids, required_option_ids)
|
|
761
|
+
|
|
762
|
+
preferred_set = set(remaining_preferred)
|
|
763
|
+
nonpreferred_option_ids = tuple(
|
|
764
|
+
value for value in completion_option_ids if value not in preferred_set
|
|
765
|
+
)
|
|
766
|
+
maximum_retained = min(
|
|
767
|
+
portfolio_size - len(required_option_ids),
|
|
768
|
+
len(remaining_preferred),
|
|
769
|
+
)
|
|
770
|
+
for retained_count in range(maximum_retained, -1, -1):
|
|
771
|
+
for preferred_subset in combinations(
|
|
772
|
+
remaining_preferred,
|
|
773
|
+
retained_count,
|
|
774
|
+
):
|
|
775
|
+
if any(
|
|
776
|
+
not _path_sets_are_disjoint(
|
|
777
|
+
path_by_id[left],
|
|
778
|
+
path_by_id[right],
|
|
779
|
+
)
|
|
780
|
+
for index, left in enumerate(preferred_subset)
|
|
781
|
+
for right in preferred_subset[index + 1 :]
|
|
782
|
+
):
|
|
783
|
+
continue
|
|
784
|
+
if any(
|
|
785
|
+
not _path_sets_are_disjoint(
|
|
786
|
+
path_by_id[required],
|
|
787
|
+
path_by_id[preferred],
|
|
788
|
+
)
|
|
789
|
+
for required in required_option_ids
|
|
790
|
+
for preferred in preferred_subset
|
|
791
|
+
):
|
|
792
|
+
continue
|
|
793
|
+
if any(
|
|
794
|
+
sum(
|
|
795
|
+
family_by_id[value] == family
|
|
796
|
+
for value in (*required_option_ids, *preferred_subset)
|
|
797
|
+
)
|
|
798
|
+
> maximum
|
|
799
|
+
for family, _, maximum in family_exposure_bounds
|
|
800
|
+
):
|
|
801
|
+
continue
|
|
802
|
+
witness = search(
|
|
803
|
+
nonpreferred_option_ids,
|
|
804
|
+
(*required_option_ids, *preferred_subset),
|
|
805
|
+
)
|
|
806
|
+
if witness is not None:
|
|
807
|
+
return witness
|
|
808
|
+
return None
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def project_family_exposure_bounds_to_pairwise_disjoint_feasibility(
|
|
812
|
+
contract: FiniteVariationContract,
|
|
813
|
+
option_ids: tuple[str, ...],
|
|
814
|
+
*,
|
|
815
|
+
portfolio_size: int,
|
|
816
|
+
min_distinct_families: int | None = None,
|
|
817
|
+
requested_bounds: tuple[tuple[str, int, int], ...],
|
|
818
|
+
) -> tuple[tuple[str, int, int], ...]:
|
|
819
|
+
"""Project requested family bounds to the closest structural K-feasibility.
|
|
820
|
+
|
|
821
|
+
The projection is outcome-blind. It first preserves the requested intervals
|
|
822
|
+
exactly when any pairwise-disjoint K-subset satisfies them. Otherwise it
|
|
823
|
+
chooses the feasible exact count vector with minimum L1 interval violation,
|
|
824
|
+
using canonical count order only to break ties.
|
|
825
|
+
"""
|
|
826
|
+
|
|
827
|
+
_validate_family_exposure_bounds(
|
|
828
|
+
requested_bounds,
|
|
829
|
+
portfolio_size=portfolio_size,
|
|
830
|
+
)
|
|
831
|
+
if not requested_bounds:
|
|
832
|
+
return ()
|
|
833
|
+
if finite_option_ids_have_pairwise_disjoint_parent_patch_subset(
|
|
834
|
+
contract,
|
|
835
|
+
option_ids,
|
|
836
|
+
portfolio_size=portfolio_size,
|
|
837
|
+
min_distinct_families=min_distinct_families,
|
|
838
|
+
family_exposure_bounds=requested_bounds,
|
|
839
|
+
):
|
|
840
|
+
return requested_bounds
|
|
841
|
+
count_vectors = tuple(
|
|
842
|
+
sorted(
|
|
843
|
+
product(range(portfolio_size + 1), repeat=len(requested_bounds)),
|
|
844
|
+
key=lambda counts: (
|
|
845
|
+
sum(
|
|
846
|
+
max(minimum - count, 0, count - maximum)
|
|
847
|
+
for (_, minimum, maximum), count in zip(
|
|
848
|
+
requested_bounds,
|
|
849
|
+
counts,
|
|
850
|
+
strict=True,
|
|
851
|
+
)
|
|
852
|
+
),
|
|
853
|
+
counts,
|
|
854
|
+
),
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
for counts in count_vectors:
|
|
858
|
+
# Action families are mutually exclusive labels on each selected
|
|
859
|
+
# option. Exact counts whose sum exceeds K cannot describe any
|
|
860
|
+
# portfolio and are outside the validated bound domain.
|
|
861
|
+
if sum(counts) > portfolio_size:
|
|
862
|
+
continue
|
|
863
|
+
exact = tuple(
|
|
864
|
+
(family, count, count)
|
|
865
|
+
for (family, _, _), count in zip(
|
|
866
|
+
requested_bounds,
|
|
867
|
+
counts,
|
|
868
|
+
strict=True,
|
|
869
|
+
)
|
|
870
|
+
)
|
|
871
|
+
if not finite_option_ids_have_pairwise_disjoint_parent_patch_subset(
|
|
872
|
+
contract,
|
|
873
|
+
option_ids,
|
|
874
|
+
portfolio_size=portfolio_size,
|
|
875
|
+
min_distinct_families=min_distinct_families,
|
|
876
|
+
family_exposure_bounds=exact,
|
|
877
|
+
):
|
|
878
|
+
continue
|
|
879
|
+
return exact
|
|
880
|
+
raise ValueError("finite option universe has no feasible evaluation portfolio")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def finite_portfolio_has_pairwise_disjoint_parent_patches(
|
|
884
|
+
contract: FiniteVariationContract,
|
|
885
|
+
*,
|
|
886
|
+
portfolio_size: int,
|
|
887
|
+
min_distinct_families: int | None = None,
|
|
888
|
+
) -> bool:
|
|
889
|
+
"""Decide whether a finite contract contains one disjoint K-portfolio."""
|
|
890
|
+
|
|
891
|
+
return finite_option_ids_have_pairwise_disjoint_parent_patch_subset(
|
|
892
|
+
contract,
|
|
893
|
+
tuple(option.option_id for option in contract.options),
|
|
894
|
+
portfolio_size=portfolio_size,
|
|
895
|
+
min_distinct_families=min_distinct_families,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def validate_pairwise_disjoint_parent_patch_selection(
|
|
900
|
+
contract: FiniteVariationContract,
|
|
901
|
+
option_ids: tuple[str, ...],
|
|
902
|
+
) -> None:
|
|
903
|
+
"""Fail closed unless every selected sealed option has a disjoint patch."""
|
|
904
|
+
|
|
905
|
+
paths = _parent_patch_paths_by_option(contract, option_ids)
|
|
906
|
+
for left_index, left in enumerate(option_ids):
|
|
907
|
+
for right in option_ids[left_index + 1 :]:
|
|
908
|
+
if not _path_sets_are_disjoint(paths[left], paths[right]):
|
|
909
|
+
raise ValueError(
|
|
910
|
+
"selected finite options have overlapping parent-relative patches"
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
@dataclass(frozen=True, slots=True)
|
|
915
|
+
class CardScoreComponent:
|
|
916
|
+
"""One interpretable scalar in a card's selector-visible score bundle.
|
|
917
|
+
|
|
918
|
+
Components retain their scoring definition and evidence lineage so a
|
|
919
|
+
benchmark can rotate, redact, or independently adjudicate complete score
|
|
920
|
+
bundles without collapsing distinct semantics into an opaque global
|
|
921
|
+
score. ``evidence_count`` may exceed the number of receipts because one
|
|
922
|
+
sealed receipt can summarize multiple evidence rows.
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
score_id: str
|
|
926
|
+
value: float
|
|
927
|
+
definition_sha256: str
|
|
928
|
+
evidence_count: int
|
|
929
|
+
receipt_sha256s: tuple[str, ...]
|
|
930
|
+
|
|
931
|
+
def __post_init__(self) -> None:
|
|
932
|
+
if (
|
|
933
|
+
type(self.score_id) is not str
|
|
934
|
+
or _METRIC_ID.fullmatch(self.score_id) is None
|
|
935
|
+
):
|
|
936
|
+
raise ValueError("score_id must use the closed metric-token grammar")
|
|
937
|
+
if type(self.value) is not float or not math.isfinite(self.value):
|
|
938
|
+
raise TypeError("value must be a finite canonical float")
|
|
939
|
+
require_sha256(self.definition_sha256, "definition_sha256")
|
|
940
|
+
if type(self.evidence_count) is not int or self.evidence_count < 0:
|
|
941
|
+
raise ValueError("evidence_count must be a non-negative exact integer")
|
|
942
|
+
if type(self.receipt_sha256s) is not tuple or any(
|
|
943
|
+
type(value) is not str for value in self.receipt_sha256s
|
|
944
|
+
):
|
|
945
|
+
raise TypeError("receipt_sha256s must be an exact tuple of strings")
|
|
946
|
+
for value in self.receipt_sha256s:
|
|
947
|
+
require_sha256(value, "receipt_sha256")
|
|
948
|
+
if self.receipt_sha256s != tuple(sorted(set(self.receipt_sha256s))):
|
|
949
|
+
raise ValueError("receipt_sha256s must be unique and canonical")
|
|
950
|
+
|
|
951
|
+
def to_record(self) -> dict[str, object]:
|
|
952
|
+
self.__post_init__()
|
|
953
|
+
return {
|
|
954
|
+
"score_id": self.score_id,
|
|
955
|
+
"value_hex": self.value.hex(),
|
|
956
|
+
"definition_sha256": self.definition_sha256,
|
|
957
|
+
"evidence_count": self.evidence_count,
|
|
958
|
+
"receipt_sha256s": list(self.receipt_sha256s),
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
def prompt_record(self) -> dict[str, object]:
|
|
962
|
+
self.__post_init__()
|
|
963
|
+
return {
|
|
964
|
+
"score_id": self.score_id,
|
|
965
|
+
"value": self.value,
|
|
966
|
+
"definition_sha256": self.definition_sha256,
|
|
967
|
+
"evidence_count": self.evidence_count,
|
|
968
|
+
"receipt_sha256s": list(self.receipt_sha256s),
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _validate_finite_action_evidence(
|
|
973
|
+
values: tuple[FiniteActionEvidenceBinding, ...],
|
|
974
|
+
*,
|
|
975
|
+
name: str,
|
|
976
|
+
) -> None:
|
|
977
|
+
if type(values) is not tuple or any(
|
|
978
|
+
type(binding) is not FiniteActionEvidenceBinding for binding in values
|
|
979
|
+
):
|
|
980
|
+
raise TypeError(
|
|
981
|
+
f"{name} must be an exact tuple of FiniteActionEvidenceBinding values"
|
|
982
|
+
)
|
|
983
|
+
for binding in values:
|
|
984
|
+
FiniteActionEvidenceBinding.__post_init__(binding)
|
|
985
|
+
contrast_ids = tuple(binding.contrast_id for binding in values)
|
|
986
|
+
if contrast_ids != tuple(sorted(set(contrast_ids))):
|
|
987
|
+
raise ValueError(f"{name} must have unique canonical contrast order")
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
@dataclass(frozen=True, slots=True)
|
|
991
|
+
class PortfolioCardPromptPayload:
|
|
992
|
+
"""Typed scientific prompt boundary for one portfolio card.
|
|
993
|
+
|
|
994
|
+
Free-form benchmark prose is confined to ``action_neutral_payload``.
|
|
995
|
+
Exact option attribution is carried only by the structured finite-action
|
|
996
|
+
bindings. This type does not itself know the request's complete option
|
|
997
|
+
catalog; :func:`validate_portfolio_experimental_view` performs that
|
|
998
|
+
catalog-relative leak check before a scientific request is admitted.
|
|
999
|
+
"""
|
|
1000
|
+
|
|
1001
|
+
action_neutral_payload: FrozenJsonObject
|
|
1002
|
+
finite_action_evidence: tuple[FiniteActionEvidenceBinding, ...]
|
|
1003
|
+
|
|
1004
|
+
def __post_init__(self) -> None:
|
|
1005
|
+
if type(self.action_neutral_payload) is not FrozenJsonObject:
|
|
1006
|
+
raise TypeError("action_neutral_payload must be an exact FrozenJsonObject")
|
|
1007
|
+
if freeze_json(self.action_neutral_payload) is not self.action_neutral_payload:
|
|
1008
|
+
raise TypeError("action_neutral_payload must already be frozen typed JSON")
|
|
1009
|
+
_validate_finite_action_evidence(
|
|
1010
|
+
self.finite_action_evidence,
|
|
1011
|
+
name="finite_action_evidence",
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
@property
|
|
1015
|
+
def action_neutral_payload_sha256(self) -> str:
|
|
1016
|
+
self.__post_init__()
|
|
1017
|
+
return typed_json_sha256(self.action_neutral_payload)
|
|
1018
|
+
|
|
1019
|
+
def prompt_record(self) -> dict[str, object]:
|
|
1020
|
+
"""Render action-neutral prose and exact attribution separately."""
|
|
1021
|
+
|
|
1022
|
+
self.__post_init__()
|
|
1023
|
+
return {
|
|
1024
|
+
"action_neutral_payload": thaw_json(self.action_neutral_payload),
|
|
1025
|
+
"finite_action_evidence": [
|
|
1026
|
+
binding.to_record() for binding in self.finite_action_evidence
|
|
1027
|
+
],
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
def to_record(self) -> dict[str, object]:
|
|
1031
|
+
"""Return a canonical evidence record without duplicating neutral prose."""
|
|
1032
|
+
|
|
1033
|
+
self.__post_init__()
|
|
1034
|
+
return {
|
|
1035
|
+
"schema_version": 1,
|
|
1036
|
+
"action_neutral_payload_sha256": self.action_neutral_payload_sha256,
|
|
1037
|
+
"finite_action_evidence": [
|
|
1038
|
+
binding.to_record() for binding in self.finite_action_evidence
|
|
1039
|
+
],
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _typed_json_text_values(
|
|
1044
|
+
value: FrozenJsonValue,
|
|
1045
|
+
) -> tuple[tuple[str, str], ...]:
|
|
1046
|
+
"""Return paths and every key/string in an already-frozen JSON tree."""
|
|
1047
|
+
|
|
1048
|
+
found: list[tuple[str, str]] = []
|
|
1049
|
+
|
|
1050
|
+
def visit(node: FrozenJsonValue, path: str) -> None:
|
|
1051
|
+
if type(node) is str:
|
|
1052
|
+
found.append((path, node))
|
|
1053
|
+
return
|
|
1054
|
+
if type(node) is FrozenJsonArray:
|
|
1055
|
+
for index, item in enumerate(node.items):
|
|
1056
|
+
visit(item, f"{path}[{index}]")
|
|
1057
|
+
return
|
|
1058
|
+
if type(node) is FrozenJsonObject:
|
|
1059
|
+
for key, item in node.items:
|
|
1060
|
+
found.append((f"{path}.<key>", key))
|
|
1061
|
+
visit(item, f"{path}.{key}")
|
|
1062
|
+
|
|
1063
|
+
visit(value, "$")
|
|
1064
|
+
return tuple(found)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _contains_explicit_option_id(text: str, option_id: str) -> bool:
|
|
1068
|
+
"""Detect catalog option identity anywhere in supposedly neutral text.
|
|
1069
|
+
|
|
1070
|
+
A token-boundary check is unsafe because ``.`` is both legal inside an
|
|
1071
|
+
option ID and ordinary sentence punctuation: ``"choose alpha.x1."`` would
|
|
1072
|
+
otherwise evade the guard. Scientific neutral payloads have no legitimate
|
|
1073
|
+
reason to contain a catalog option ID even as a substring, so the stricter
|
|
1074
|
+
containment rule is intentional.
|
|
1075
|
+
"""
|
|
1076
|
+
|
|
1077
|
+
return option_id in text.casefold()
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _validate_action_neutral_payload(
|
|
1081
|
+
payload: PortfolioCardPromptPayload,
|
|
1082
|
+
*,
|
|
1083
|
+
identity_index: ValidatedFiniteVariationIdentityIndex,
|
|
1084
|
+
) -> None:
|
|
1085
|
+
"""Fail closed when neutral prose carries exact finite-option identity."""
|
|
1086
|
+
|
|
1087
|
+
if type(payload) is not PortfolioCardPromptPayload:
|
|
1088
|
+
raise TypeError("payload must be an exact PortfolioCardPromptPayload")
|
|
1089
|
+
payload.__post_init__()
|
|
1090
|
+
if type(identity_index) is not ValidatedFiniteVariationIdentityIndex:
|
|
1091
|
+
raise TypeError("identity_index must be an exact validated identity index")
|
|
1092
|
+
identity_index.__post_init__()
|
|
1093
|
+
|
|
1094
|
+
source_option_ids = {
|
|
1095
|
+
binding.option_id for binding in payload.finite_action_evidence
|
|
1096
|
+
}
|
|
1097
|
+
forbidden_hashes = {
|
|
1098
|
+
identity_index.contract_identity_sha256,
|
|
1099
|
+
*identity_index.option_identity_sha256s,
|
|
1100
|
+
*identity_index.child_configuration_sha256s,
|
|
1101
|
+
*(binding.contrast_id for binding in payload.finite_action_evidence),
|
|
1102
|
+
*(
|
|
1103
|
+
binding.option_identity_sha256
|
|
1104
|
+
for binding in payload.finite_action_evidence
|
|
1105
|
+
),
|
|
1106
|
+
*(
|
|
1107
|
+
binding.contract_identity_sha256
|
|
1108
|
+
for binding in payload.finite_action_evidence
|
|
1109
|
+
),
|
|
1110
|
+
*(binding.identity_sha256 for binding in payload.finite_action_evidence),
|
|
1111
|
+
}
|
|
1112
|
+
for path, text in _typed_json_text_values(payload.action_neutral_payload):
|
|
1113
|
+
folded = text.casefold()
|
|
1114
|
+
for option_id in identity_index.option_ids:
|
|
1115
|
+
if _contains_explicit_option_id(folded, option_id):
|
|
1116
|
+
raise ValueError(
|
|
1117
|
+
"scientific action-neutral payload contains explicit "
|
|
1118
|
+
f"option_id at {path}"
|
|
1119
|
+
)
|
|
1120
|
+
for option_id in source_option_ids:
|
|
1121
|
+
if _contains_explicit_option_id(folded, option_id):
|
|
1122
|
+
raise ValueError(
|
|
1123
|
+
"scientific action-neutral payload contains source "
|
|
1124
|
+
f"option_id at {path}"
|
|
1125
|
+
)
|
|
1126
|
+
if any(value in folded for value in forbidden_hashes):
|
|
1127
|
+
raise ValueError(
|
|
1128
|
+
"scientific action-neutral payload contains exact option or "
|
|
1129
|
+
f"contract attribution at {path}"
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def portfolio_card_action_evidence_sha256(
|
|
1134
|
+
values: tuple[FiniteActionEvidenceBinding, ...],
|
|
1135
|
+
) -> str:
|
|
1136
|
+
"""Hash-bind the exact prompt-visible action-attribution view."""
|
|
1137
|
+
|
|
1138
|
+
_validate_finite_action_evidence(values, name="finite_action_evidence")
|
|
1139
|
+
return _hash(
|
|
1140
|
+
_CARD_ACTION_EVIDENCE_STATE_DOMAIN,
|
|
1141
|
+
{
|
|
1142
|
+
"schema_version": 1,
|
|
1143
|
+
"finite_action_evidence": [value.to_record() for value in values],
|
|
1144
|
+
},
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _card_score_state_record(
|
|
1149
|
+
score_components: tuple[CardScoreComponent, ...],
|
|
1150
|
+
assigned_score: float | None,
|
|
1151
|
+
) -> dict[str, object]:
|
|
1152
|
+
if type(score_components) is not tuple or any(
|
|
1153
|
+
type(component) is not CardScoreComponent for component in score_components
|
|
1154
|
+
):
|
|
1155
|
+
raise TypeError(
|
|
1156
|
+
"score_components must be an exact tuple of CardScoreComponent values"
|
|
1157
|
+
)
|
|
1158
|
+
for component in score_components:
|
|
1159
|
+
component.__post_init__()
|
|
1160
|
+
score_ids = tuple(component.score_id for component in score_components)
|
|
1161
|
+
if score_ids != tuple(sorted(set(score_ids))):
|
|
1162
|
+
raise ValueError("score_components must use unique canonical score_id order")
|
|
1163
|
+
if assigned_score is not None and (
|
|
1164
|
+
type(assigned_score) is not float or not math.isfinite(assigned_score)
|
|
1165
|
+
):
|
|
1166
|
+
raise TypeError("assigned_score must be a finite canonical float or None")
|
|
1167
|
+
return {
|
|
1168
|
+
"score_components": [component.to_record() for component in score_components],
|
|
1169
|
+
"assigned_score_hex": (
|
|
1170
|
+
None if assigned_score is None else assigned_score.hex()
|
|
1171
|
+
),
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def portfolio_card_score_state_sha256(
|
|
1176
|
+
score_components: tuple[CardScoreComponent, ...],
|
|
1177
|
+
assigned_score: float | None,
|
|
1178
|
+
) -> str:
|
|
1179
|
+
"""Hash-bind the exact score projection carried by one card view."""
|
|
1180
|
+
|
|
1181
|
+
return _hash(
|
|
1182
|
+
_CARD_SCORE_STATE_DOMAIN,
|
|
1183
|
+
_card_score_state_record(score_components, assigned_score),
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
@dataclass(frozen=True, slots=True)
|
|
1188
|
+
class PortfolioCardSourceBinding:
|
|
1189
|
+
"""Integrity-bound source provenance for one insight-to-card projection.
|
|
1190
|
+
|
|
1191
|
+
This receipt binds the immutable insight version and evidence lineage to the
|
|
1192
|
+
exact source view constructed by trusted application code. A later
|
|
1193
|
+
experimental view may redact or permute that view, but it must retain this
|
|
1194
|
+
receipt unchanged and publish a separate :class:`PortfolioCardViewReceipt`.
|
|
1195
|
+
"""
|
|
1196
|
+
|
|
1197
|
+
reference: InsightRef
|
|
1198
|
+
content_sha256: str
|
|
1199
|
+
evidence_lineage_identity_sha256: str
|
|
1200
|
+
finite_action_evidence: tuple[FiniteActionEvidenceBinding, ...]
|
|
1201
|
+
source_prompt_view_sha256: str
|
|
1202
|
+
source_evidence_sha256: str
|
|
1203
|
+
source_score_state_sha256: str
|
|
1204
|
+
source_receipt_sha256: str
|
|
1205
|
+
|
|
1206
|
+
def __post_init__(self) -> None:
|
|
1207
|
+
_reference_record(self.reference)
|
|
1208
|
+
for name in (
|
|
1209
|
+
"content_sha256",
|
|
1210
|
+
"evidence_lineage_identity_sha256",
|
|
1211
|
+
"source_prompt_view_sha256",
|
|
1212
|
+
"source_evidence_sha256",
|
|
1213
|
+
"source_score_state_sha256",
|
|
1214
|
+
"source_receipt_sha256",
|
|
1215
|
+
):
|
|
1216
|
+
require_sha256(getattr(self, name), name)
|
|
1217
|
+
_validate_finite_action_evidence(
|
|
1218
|
+
self.finite_action_evidence,
|
|
1219
|
+
name="finite_action_evidence",
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1223
|
+
self.__post_init__()
|
|
1224
|
+
return {
|
|
1225
|
+
"schema_version": 1,
|
|
1226
|
+
"reference": _reference_record(self.reference),
|
|
1227
|
+
"content_sha256": self.content_sha256,
|
|
1228
|
+
"evidence_lineage_identity_sha256": (self.evidence_lineage_identity_sha256),
|
|
1229
|
+
"finite_action_evidence": [
|
|
1230
|
+
binding.to_record() for binding in self.finite_action_evidence
|
|
1231
|
+
],
|
|
1232
|
+
"source_prompt_view_sha256": self.source_prompt_view_sha256,
|
|
1233
|
+
"source_evidence_sha256": self.source_evidence_sha256,
|
|
1234
|
+
"source_score_state_sha256": self.source_score_state_sha256,
|
|
1235
|
+
"source_receipt_sha256": self.source_receipt_sha256,
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
@property
|
|
1239
|
+
def binding_sha256(self) -> str:
|
|
1240
|
+
return _hash(_CARD_SOURCE_BINDING_DOMAIN, self._unsigned_record())
|
|
1241
|
+
|
|
1242
|
+
def to_record(self) -> dict[str, object]:
|
|
1243
|
+
return {**self._unsigned_record(), "binding_sha256": self.binding_sha256}
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _bind_portfolio_card_source(
|
|
1247
|
+
*,
|
|
1248
|
+
reference: InsightRef,
|
|
1249
|
+
content_sha256: str,
|
|
1250
|
+
evidence_lineage_identity_sha256: str,
|
|
1251
|
+
finite_action_evidence: tuple[FiniteActionEvidenceBinding, ...],
|
|
1252
|
+
prompt_payload: FrozenJsonObject,
|
|
1253
|
+
evidence_sha256: str,
|
|
1254
|
+
score_components: tuple[CardScoreComponent, ...],
|
|
1255
|
+
assigned_score: float | None,
|
|
1256
|
+
source_receipt_sha256: str,
|
|
1257
|
+
) -> PortfolioCardSourceBinding:
|
|
1258
|
+
"""Issue a source binding from exact framework-neutral projection values."""
|
|
1259
|
+
|
|
1260
|
+
if type(prompt_payload) is not FrozenJsonObject:
|
|
1261
|
+
raise TypeError("prompt_payload must be an exact FrozenJsonObject")
|
|
1262
|
+
if freeze_json(prompt_payload) is not prompt_payload:
|
|
1263
|
+
raise TypeError("prompt_payload must already be frozen typed JSON")
|
|
1264
|
+
return PortfolioCardSourceBinding(
|
|
1265
|
+
reference=reference,
|
|
1266
|
+
content_sha256=content_sha256,
|
|
1267
|
+
evidence_lineage_identity_sha256=evidence_lineage_identity_sha256,
|
|
1268
|
+
finite_action_evidence=finite_action_evidence,
|
|
1269
|
+
source_prompt_view_sha256=typed_json_sha256(prompt_payload),
|
|
1270
|
+
source_evidence_sha256=evidence_sha256,
|
|
1271
|
+
source_score_state_sha256=portfolio_card_score_state_sha256(
|
|
1272
|
+
score_components,
|
|
1273
|
+
assigned_score,
|
|
1274
|
+
),
|
|
1275
|
+
source_receipt_sha256=source_receipt_sha256,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
@dataclass(frozen=True, slots=True, init=False)
|
|
1280
|
+
class PortfolioCardSourceRegistry:
|
|
1281
|
+
"""Application-admitted source bindings for one scientific request.
|
|
1282
|
+
|
|
1283
|
+
The registry is a hash-bound integrity and admission receipt, not a
|
|
1284
|
+
cryptographic signature. Its public constructor is closed: trusted
|
|
1285
|
+
application code issues it only after joining every binding to an exact
|
|
1286
|
+
supplied memory entry. Request validation then prevents a source binding
|
|
1287
|
+
synthesized directly by an adapter from entering without that admission.
|
|
1288
|
+
"""
|
|
1289
|
+
|
|
1290
|
+
source_bindings: tuple[PortfolioCardSourceBinding, ...]
|
|
1291
|
+
|
|
1292
|
+
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
1293
|
+
del args, kwargs
|
|
1294
|
+
raise TypeError(
|
|
1295
|
+
"PortfolioCardSourceRegistry is issued by trusted application code"
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
def __post_init__(self) -> None:
|
|
1299
|
+
if type(self.source_bindings) is not tuple or not self.source_bindings:
|
|
1300
|
+
raise ValueError("source_bindings must be a non-empty exact tuple")
|
|
1301
|
+
if any(
|
|
1302
|
+
type(binding) is not PortfolioCardSourceBinding
|
|
1303
|
+
for binding in self.source_bindings
|
|
1304
|
+
):
|
|
1305
|
+
raise TypeError(
|
|
1306
|
+
"source_bindings must contain exact PortfolioCardSourceBinding values"
|
|
1307
|
+
)
|
|
1308
|
+
for binding in self.source_bindings:
|
|
1309
|
+
binding.__post_init__()
|
|
1310
|
+
identities = tuple(binding.binding_sha256 for binding in self.source_bindings)
|
|
1311
|
+
if identities != tuple(sorted(set(identities))):
|
|
1312
|
+
raise ValueError("source_bindings must use unique canonical binding order")
|
|
1313
|
+
references = tuple(binding.reference for binding in self.source_bindings)
|
|
1314
|
+
if len(set(references)) != len(references):
|
|
1315
|
+
raise ValueError("source_bindings cannot repeat an insight reference")
|
|
1316
|
+
|
|
1317
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1318
|
+
self.__post_init__()
|
|
1319
|
+
return {
|
|
1320
|
+
"schema_version": 1,
|
|
1321
|
+
"source_bindings": [
|
|
1322
|
+
binding.to_record() for binding in self.source_bindings
|
|
1323
|
+
],
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
@property
|
|
1327
|
+
def registry_sha256(self) -> str:
|
|
1328
|
+
return _hash(_CARD_SOURCE_REGISTRY_DOMAIN, self._unsigned_record())
|
|
1329
|
+
|
|
1330
|
+
def to_record(self) -> dict[str, object]:
|
|
1331
|
+
return {
|
|
1332
|
+
**self._unsigned_record(),
|
|
1333
|
+
"registry_sha256": self.registry_sha256,
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _issue_portfolio_card_source_registry(
|
|
1338
|
+
source_bindings: tuple[PortfolioCardSourceBinding, ...],
|
|
1339
|
+
) -> PortfolioCardSourceRegistry:
|
|
1340
|
+
"""Internal issuer used only after the application validates source joins."""
|
|
1341
|
+
|
|
1342
|
+
registry = object.__new__(PortfolioCardSourceRegistry)
|
|
1343
|
+
object.__setattr__(registry, "source_bindings", source_bindings)
|
|
1344
|
+
registry.__post_init__()
|
|
1345
|
+
return registry
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
class PortfolioCardViewTransform(str, Enum):
|
|
1349
|
+
"""Closed declarations for an experiment-controlled derived card view."""
|
|
1350
|
+
|
|
1351
|
+
EVIDENCE_PERMUTATION = "evidence_permutation"
|
|
1352
|
+
EVIDENCE_REDACTION = "evidence_redaction"
|
|
1353
|
+
PROMPT_PERMUTATION = "prompt_permutation"
|
|
1354
|
+
PROMPT_PROJECTION = "prompt_projection"
|
|
1355
|
+
PROMPT_REDACTION = "prompt_redaction"
|
|
1356
|
+
SCORE_PERMUTATION = "score_permutation"
|
|
1357
|
+
SCORE_REDACTION = "score_redaction"
|
|
1358
|
+
ACTION_EVIDENCE_PERMUTATION = "action_evidence_permutation"
|
|
1359
|
+
ACTION_EVIDENCE_REDACTION = "action_evidence_redaction"
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
@dataclass(frozen=True, slots=True)
|
|
1363
|
+
class PortfolioCardViewReceipt:
|
|
1364
|
+
"""Receipt for a derived view that never replaces its source provenance."""
|
|
1365
|
+
|
|
1366
|
+
source_binding_sha256: str
|
|
1367
|
+
transforms: tuple[PortfolioCardViewTransform, ...]
|
|
1368
|
+
derived_prompt_view_sha256: str
|
|
1369
|
+
derived_evidence_sha256: str
|
|
1370
|
+
derived_score_state_sha256: str
|
|
1371
|
+
derived_action_evidence_sha256: str
|
|
1372
|
+
prompt_source_binding_sha256: str | None
|
|
1373
|
+
evidence_source_binding_sha256: str | None
|
|
1374
|
+
score_source_binding_sha256: str | None
|
|
1375
|
+
action_evidence_source_binding_sha256: str | None
|
|
1376
|
+
policy_id: str
|
|
1377
|
+
policy_version: int
|
|
1378
|
+
policy_definition_sha256: str
|
|
1379
|
+
|
|
1380
|
+
def __post_init__(self) -> None:
|
|
1381
|
+
require_sha256(self.source_binding_sha256, "source_binding_sha256")
|
|
1382
|
+
if (
|
|
1383
|
+
type(self.transforms) is not tuple
|
|
1384
|
+
or not self.transforms
|
|
1385
|
+
or any(
|
|
1386
|
+
type(value) is not PortfolioCardViewTransform
|
|
1387
|
+
for value in self.transforms
|
|
1388
|
+
)
|
|
1389
|
+
):
|
|
1390
|
+
raise TypeError(
|
|
1391
|
+
"transforms must be a non-empty exact tuple of "
|
|
1392
|
+
"PortfolioCardViewTransform values"
|
|
1393
|
+
)
|
|
1394
|
+
canonical = tuple(sorted(set(self.transforms), key=lambda item: item.value))
|
|
1395
|
+
if self.transforms != canonical:
|
|
1396
|
+
raise ValueError("transforms must be unique and canonically ordered")
|
|
1397
|
+
for name in (
|
|
1398
|
+
"derived_prompt_view_sha256",
|
|
1399
|
+
"derived_evidence_sha256",
|
|
1400
|
+
"derived_score_state_sha256",
|
|
1401
|
+
"derived_action_evidence_sha256",
|
|
1402
|
+
"policy_definition_sha256",
|
|
1403
|
+
):
|
|
1404
|
+
require_sha256(getattr(self, name), name)
|
|
1405
|
+
for name in (
|
|
1406
|
+
"prompt_source_binding_sha256",
|
|
1407
|
+
"evidence_source_binding_sha256",
|
|
1408
|
+
"score_source_binding_sha256",
|
|
1409
|
+
"action_evidence_source_binding_sha256",
|
|
1410
|
+
):
|
|
1411
|
+
value = getattr(self, name)
|
|
1412
|
+
if value is not None:
|
|
1413
|
+
require_sha256(value, name)
|
|
1414
|
+
if type(self.policy_id) is not str or _TOKEN.fullmatch(self.policy_id) is None:
|
|
1415
|
+
raise ValueError("policy_id must use the closed lowercase token grammar")
|
|
1416
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
1417
|
+
raise ValueError("policy_version must be a positive exact integer")
|
|
1418
|
+
transform_set = set(self.transforms)
|
|
1419
|
+
prompt_permutation = (
|
|
1420
|
+
PortfolioCardViewTransform.PROMPT_PERMUTATION in transform_set
|
|
1421
|
+
)
|
|
1422
|
+
evidence_permutation = (
|
|
1423
|
+
PortfolioCardViewTransform.EVIDENCE_PERMUTATION in transform_set
|
|
1424
|
+
)
|
|
1425
|
+
score_permutation = (
|
|
1426
|
+
PortfolioCardViewTransform.SCORE_PERMUTATION in transform_set
|
|
1427
|
+
)
|
|
1428
|
+
action_evidence_permutation = (
|
|
1429
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_PERMUTATION in transform_set
|
|
1430
|
+
)
|
|
1431
|
+
if prompt_permutation != (self.prompt_source_binding_sha256 is not None):
|
|
1432
|
+
raise ValueError(
|
|
1433
|
+
"prompt permutation requires exactly one prompt source binding"
|
|
1434
|
+
)
|
|
1435
|
+
if evidence_permutation != (self.evidence_source_binding_sha256 is not None):
|
|
1436
|
+
raise ValueError(
|
|
1437
|
+
"evidence permutation requires exactly one evidence source binding"
|
|
1438
|
+
)
|
|
1439
|
+
if score_permutation != (self.score_source_binding_sha256 is not None):
|
|
1440
|
+
raise ValueError(
|
|
1441
|
+
"score permutation requires exactly one score source binding"
|
|
1442
|
+
)
|
|
1443
|
+
if action_evidence_permutation != (
|
|
1444
|
+
self.action_evidence_source_binding_sha256 is not None
|
|
1445
|
+
):
|
|
1446
|
+
raise ValueError(
|
|
1447
|
+
"action-evidence permutation requires exactly one source binding"
|
|
1448
|
+
)
|
|
1449
|
+
for value in (
|
|
1450
|
+
self.prompt_source_binding_sha256,
|
|
1451
|
+
self.evidence_source_binding_sha256,
|
|
1452
|
+
self.score_source_binding_sha256,
|
|
1453
|
+
self.action_evidence_source_binding_sha256,
|
|
1454
|
+
):
|
|
1455
|
+
if value is not None and value == self.source_binding_sha256:
|
|
1456
|
+
raise ValueError("a permuted view must name a different source binding")
|
|
1457
|
+
|
|
1458
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1459
|
+
self.__post_init__()
|
|
1460
|
+
return {
|
|
1461
|
+
"schema_version": 1,
|
|
1462
|
+
"source_binding_sha256": self.source_binding_sha256,
|
|
1463
|
+
"transforms": [value.value for value in self.transforms],
|
|
1464
|
+
"derived_prompt_view_sha256": self.derived_prompt_view_sha256,
|
|
1465
|
+
"derived_evidence_sha256": self.derived_evidence_sha256,
|
|
1466
|
+
"derived_score_state_sha256": self.derived_score_state_sha256,
|
|
1467
|
+
"derived_action_evidence_sha256": (self.derived_action_evidence_sha256),
|
|
1468
|
+
"prompt_source_binding_sha256": (self.prompt_source_binding_sha256),
|
|
1469
|
+
"evidence_source_binding_sha256": (self.evidence_source_binding_sha256),
|
|
1470
|
+
"score_source_binding_sha256": self.score_source_binding_sha256,
|
|
1471
|
+
"action_evidence_source_binding_sha256": (
|
|
1472
|
+
self.action_evidence_source_binding_sha256
|
|
1473
|
+
),
|
|
1474
|
+
"policy_id": self.policy_id,
|
|
1475
|
+
"policy_version": self.policy_version,
|
|
1476
|
+
"policy_definition_sha256": self.policy_definition_sha256,
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
@property
|
|
1480
|
+
def receipt_sha256(self) -> str:
|
|
1481
|
+
return _hash(_CARD_DERIVED_VIEW_DOMAIN, self._unsigned_record())
|
|
1482
|
+
|
|
1483
|
+
def to_record(self) -> dict[str, object]:
|
|
1484
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
@dataclass(frozen=True, slots=True)
|
|
1488
|
+
class CardTransferAdjudicationRequest:
|
|
1489
|
+
"""Benchmark-owned prediction/outcome projection presented for scoring."""
|
|
1490
|
+
|
|
1491
|
+
card_key: str
|
|
1492
|
+
reference: InsightRef
|
|
1493
|
+
prediction: FrozenJsonObject
|
|
1494
|
+
outcome: FrozenJsonObject
|
|
1495
|
+
source_receipt_sha256s: tuple[str, ...]
|
|
1496
|
+
|
|
1497
|
+
def __post_init__(self) -> None:
|
|
1498
|
+
if type(self.card_key) is not str or _TOKEN.fullmatch(self.card_key) is None:
|
|
1499
|
+
raise ValueError("card_key must use the closed lowercase token grammar")
|
|
1500
|
+
_reference_record(self.reference)
|
|
1501
|
+
for name in ("prediction", "outcome"):
|
|
1502
|
+
value = getattr(self, name)
|
|
1503
|
+
if type(value) is not FrozenJsonObject:
|
|
1504
|
+
raise TypeError(f"{name} must be an exact FrozenJsonObject")
|
|
1505
|
+
if freeze_json(value) is not value:
|
|
1506
|
+
raise TypeError(f"{name} must already be frozen typed JSON")
|
|
1507
|
+
if type(self.source_receipt_sha256s) is not tuple or any(
|
|
1508
|
+
type(value) is not str for value in self.source_receipt_sha256s
|
|
1509
|
+
):
|
|
1510
|
+
raise TypeError("source_receipt_sha256s must be an exact tuple of strings")
|
|
1511
|
+
for value in self.source_receipt_sha256s:
|
|
1512
|
+
require_sha256(value, "source_receipt_sha256")
|
|
1513
|
+
if self.source_receipt_sha256s != tuple(
|
|
1514
|
+
sorted(set(self.source_receipt_sha256s))
|
|
1515
|
+
):
|
|
1516
|
+
raise ValueError("source_receipt_sha256s must be unique and canonical")
|
|
1517
|
+
|
|
1518
|
+
def to_record(self) -> dict[str, object]:
|
|
1519
|
+
self.__post_init__()
|
|
1520
|
+
return {
|
|
1521
|
+
"schema_version": 1,
|
|
1522
|
+
"card_key": self.card_key,
|
|
1523
|
+
"reference": _reference_record(self.reference),
|
|
1524
|
+
"prediction_sha256": typed_json_sha256(self.prediction),
|
|
1525
|
+
"outcome_sha256": typed_json_sha256(self.outcome),
|
|
1526
|
+
"source_receipt_sha256s": list(self.source_receipt_sha256s),
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
@property
|
|
1530
|
+
def request_sha256(self) -> str:
|
|
1531
|
+
return _hash(_TRANSFER_REQUEST_DOMAIN, self.to_record())
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
1535
|
+
class CardTransferScoreReceipt:
|
|
1536
|
+
"""Sealed adjudicator output for one exact transfer observation."""
|
|
1537
|
+
|
|
1538
|
+
request_sha256: str
|
|
1539
|
+
score_component: CardScoreComponent
|
|
1540
|
+
adjudicator_id: str
|
|
1541
|
+
adjudicator_version: int
|
|
1542
|
+
adjudicator_definition_sha256: str
|
|
1543
|
+
|
|
1544
|
+
def __post_init__(self) -> None:
|
|
1545
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
1546
|
+
if type(self.score_component) is not CardScoreComponent:
|
|
1547
|
+
raise TypeError("score_component must be an exact CardScoreComponent")
|
|
1548
|
+
self.score_component.__post_init__()
|
|
1549
|
+
if (
|
|
1550
|
+
type(self.adjudicator_id) is not str
|
|
1551
|
+
or _TOKEN.fullmatch(self.adjudicator_id) is None
|
|
1552
|
+
):
|
|
1553
|
+
raise ValueError(
|
|
1554
|
+
"adjudicator_id must use the closed lowercase token grammar"
|
|
1555
|
+
)
|
|
1556
|
+
if type(self.adjudicator_version) is not int or self.adjudicator_version <= 0:
|
|
1557
|
+
raise ValueError("adjudicator_version must be a positive exact integer")
|
|
1558
|
+
require_sha256(
|
|
1559
|
+
self.adjudicator_definition_sha256,
|
|
1560
|
+
"adjudicator_definition_sha256",
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1564
|
+
self.__post_init__()
|
|
1565
|
+
return {
|
|
1566
|
+
"schema_version": 1,
|
|
1567
|
+
"request_sha256": self.request_sha256,
|
|
1568
|
+
"score_component": self.score_component.to_record(),
|
|
1569
|
+
"adjudicator_id": self.adjudicator_id,
|
|
1570
|
+
"adjudicator_version": self.adjudicator_version,
|
|
1571
|
+
"adjudicator_definition_sha256": self.adjudicator_definition_sha256,
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
@property
|
|
1575
|
+
def receipt_sha256(self) -> str:
|
|
1576
|
+
return _hash(_TRANSFER_RECEIPT_DOMAIN, self._unsigned_record())
|
|
1577
|
+
|
|
1578
|
+
def to_record(self) -> dict[str, object]:
|
|
1579
|
+
record = self._unsigned_record()
|
|
1580
|
+
return {**record, "receipt_sha256": self.receipt_sha256}
|
|
1581
|
+
|
|
1582
|
+
def __eq__(self, other: object) -> bool:
|
|
1583
|
+
return (
|
|
1584
|
+
type(self) is CardTransferScoreReceipt
|
|
1585
|
+
and type(other) is CardTransferScoreReceipt
|
|
1586
|
+
and self.receipt_sha256 == other.receipt_sha256
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
__hash__ = None
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
def validate_card_transfer_score_receipt(
|
|
1593
|
+
request: CardTransferAdjudicationRequest,
|
|
1594
|
+
receipt: CardTransferScoreReceipt,
|
|
1595
|
+
) -> None:
|
|
1596
|
+
"""Reject a score receipt detached from its projection or lineage."""
|
|
1597
|
+
|
|
1598
|
+
if type(request) is not CardTransferAdjudicationRequest:
|
|
1599
|
+
raise TypeError("request must be an exact CardTransferAdjudicationRequest")
|
|
1600
|
+
request.__post_init__()
|
|
1601
|
+
if type(receipt) is not CardTransferScoreReceipt:
|
|
1602
|
+
raise TypeError("receipt must be an exact CardTransferScoreReceipt")
|
|
1603
|
+
receipt.__post_init__()
|
|
1604
|
+
if receipt.request_sha256 != request.request_sha256:
|
|
1605
|
+
raise ValueError("score receipt is bound to a different transfer request")
|
|
1606
|
+
if receipt.score_component.receipt_sha256s != request.source_receipt_sha256s:
|
|
1607
|
+
raise ValueError("score component lineage differs from the transfer request")
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
@runtime_checkable
|
|
1611
|
+
class CardTransferAdjudicator(Protocol):
|
|
1612
|
+
"""Benchmark-injected policy for scoring one card transfer observation."""
|
|
1613
|
+
|
|
1614
|
+
def adjudicate(
|
|
1615
|
+
self,
|
|
1616
|
+
request: CardTransferAdjudicationRequest,
|
|
1617
|
+
) -> CardTransferScoreReceipt: ...
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
@dataclass(frozen=True, slots=True)
|
|
1621
|
+
class PortfolioCard:
|
|
1622
|
+
"""One prompt-visible card view bound to immutable source evidence.
|
|
1623
|
+
|
|
1624
|
+
``prompt_payload`` is the action-neutral, experiment-controlled view.
|
|
1625
|
+
Adaptive, score-permuted, names-only, and redacted arms may project views
|
|
1626
|
+
while retaining the same source content/evidence hashes. The selector
|
|
1627
|
+
addresses cards only by the opaque task-local ``card_key``.
|
|
1628
|
+
``finite_action_evidence`` is the prompt-visible action-attribution view.
|
|
1629
|
+
On a source card it equals the immutable action lineage in
|
|
1630
|
+
``source_binding``. A derived experimental view may permute or redact it
|
|
1631
|
+
only through an explicit donor-bound view receipt; source provenance stays
|
|
1632
|
+
unchanged. Empty evidence preserves legacy card records and prompt bytes.
|
|
1633
|
+
"""
|
|
1634
|
+
|
|
1635
|
+
card_key: str
|
|
1636
|
+
reference: InsightRef
|
|
1637
|
+
content_sha256: str
|
|
1638
|
+
evidence_sha256: str
|
|
1639
|
+
prompt_payload: FrozenJsonObject
|
|
1640
|
+
score_components: tuple[CardScoreComponent, ...] = ()
|
|
1641
|
+
assigned_score: float | None = None
|
|
1642
|
+
finite_action_evidence: tuple[FiniteActionEvidenceBinding, ...] = ()
|
|
1643
|
+
source_binding: PortfolioCardSourceBinding | None = None
|
|
1644
|
+
derived_view_receipt: PortfolioCardViewReceipt | None = None
|
|
1645
|
+
|
|
1646
|
+
def __post_init__(self) -> None:
|
|
1647
|
+
if type(self.card_key) is not str or _TOKEN.fullmatch(self.card_key) is None:
|
|
1648
|
+
raise ValueError("card_key must use the closed lowercase token grammar")
|
|
1649
|
+
_reference_record(self.reference)
|
|
1650
|
+
require_sha256(self.content_sha256, "content_sha256")
|
|
1651
|
+
require_sha256(self.evidence_sha256, "evidence_sha256")
|
|
1652
|
+
if type(self.prompt_payload) is not FrozenJsonObject:
|
|
1653
|
+
raise TypeError("prompt_payload must be an exact FrozenJsonObject")
|
|
1654
|
+
if freeze_json(self.prompt_payload) is not self.prompt_payload:
|
|
1655
|
+
raise TypeError("prompt_payload must already be frozen typed JSON")
|
|
1656
|
+
prompt_view_sha256 = typed_json_sha256(self.prompt_payload)
|
|
1657
|
+
score_state_sha256 = portfolio_card_score_state_sha256(
|
|
1658
|
+
self.score_components,
|
|
1659
|
+
self.assigned_score,
|
|
1660
|
+
)
|
|
1661
|
+
action_evidence_sha256 = portfolio_card_action_evidence_sha256(
|
|
1662
|
+
self.finite_action_evidence
|
|
1663
|
+
)
|
|
1664
|
+
_validate_finite_action_evidence(
|
|
1665
|
+
self.finite_action_evidence,
|
|
1666
|
+
name="finite_action_evidence",
|
|
1667
|
+
)
|
|
1668
|
+
if self.source_binding is None:
|
|
1669
|
+
if self.finite_action_evidence:
|
|
1670
|
+
raise ValueError(
|
|
1671
|
+
"non-empty finite_action_evidence requires a source binding"
|
|
1672
|
+
)
|
|
1673
|
+
if self.derived_view_receipt is not None:
|
|
1674
|
+
raise ValueError("a derived view receipt requires a source binding")
|
|
1675
|
+
return
|
|
1676
|
+
if type(self.source_binding) is not PortfolioCardSourceBinding:
|
|
1677
|
+
raise TypeError(
|
|
1678
|
+
"source_binding must be an exact PortfolioCardSourceBinding or None"
|
|
1679
|
+
)
|
|
1680
|
+
self.source_binding.__post_init__()
|
|
1681
|
+
if (
|
|
1682
|
+
self.reference != self.source_binding.reference
|
|
1683
|
+
or self.content_sha256 != self.source_binding.content_sha256
|
|
1684
|
+
):
|
|
1685
|
+
raise ValueError("card identity differs from its source binding")
|
|
1686
|
+
if self.derived_view_receipt is None:
|
|
1687
|
+
if (
|
|
1688
|
+
prompt_view_sha256 != self.source_binding.source_prompt_view_sha256
|
|
1689
|
+
or self.evidence_sha256 != self.source_binding.source_evidence_sha256
|
|
1690
|
+
or score_state_sha256 != self.source_binding.source_score_state_sha256
|
|
1691
|
+
or self.finite_action_evidence
|
|
1692
|
+
!= self.source_binding.finite_action_evidence
|
|
1693
|
+
):
|
|
1694
|
+
raise ValueError(
|
|
1695
|
+
"card view differs from its source without a derived view receipt"
|
|
1696
|
+
)
|
|
1697
|
+
return
|
|
1698
|
+
if type(self.derived_view_receipt) is not PortfolioCardViewReceipt:
|
|
1699
|
+
raise TypeError(
|
|
1700
|
+
"derived_view_receipt must be an exact PortfolioCardViewReceipt or None"
|
|
1701
|
+
)
|
|
1702
|
+
receipt = self.derived_view_receipt
|
|
1703
|
+
receipt.__post_init__()
|
|
1704
|
+
if receipt.source_binding_sha256 != self.source_binding.binding_sha256:
|
|
1705
|
+
raise ValueError("derived view receipt names a different source binding")
|
|
1706
|
+
if (
|
|
1707
|
+
receipt.derived_prompt_view_sha256 != prompt_view_sha256
|
|
1708
|
+
or receipt.derived_evidence_sha256 != self.evidence_sha256
|
|
1709
|
+
or receipt.derived_score_state_sha256 != score_state_sha256
|
|
1710
|
+
or receipt.derived_action_evidence_sha256 != action_evidence_sha256
|
|
1711
|
+
):
|
|
1712
|
+
raise ValueError("derived view receipt differs from the exact card view")
|
|
1713
|
+
transforms = set(receipt.transforms)
|
|
1714
|
+
prompt_changed = (
|
|
1715
|
+
prompt_view_sha256 != self.source_binding.source_prompt_view_sha256
|
|
1716
|
+
)
|
|
1717
|
+
evidence_changed = (
|
|
1718
|
+
self.evidence_sha256 != self.source_binding.source_evidence_sha256
|
|
1719
|
+
)
|
|
1720
|
+
score_changed = (
|
|
1721
|
+
score_state_sha256 != self.source_binding.source_score_state_sha256
|
|
1722
|
+
)
|
|
1723
|
+
action_evidence_changed = (
|
|
1724
|
+
self.finite_action_evidence != self.source_binding.finite_action_evidence
|
|
1725
|
+
)
|
|
1726
|
+
prompt_transforms = transforms & {
|
|
1727
|
+
PortfolioCardViewTransform.PROMPT_PERMUTATION,
|
|
1728
|
+
PortfolioCardViewTransform.PROMPT_PROJECTION,
|
|
1729
|
+
PortfolioCardViewTransform.PROMPT_REDACTION,
|
|
1730
|
+
}
|
|
1731
|
+
if len(prompt_transforms) > 1 or (prompt_changed and not prompt_transforms):
|
|
1732
|
+
raise ValueError("prompt transform declaration does not match the view")
|
|
1733
|
+
if (
|
|
1734
|
+
not prompt_changed
|
|
1735
|
+
and PortfolioCardViewTransform.PROMPT_PROJECTION in prompt_transforms
|
|
1736
|
+
):
|
|
1737
|
+
raise ValueError("prompt projection must change the exact prompt view")
|
|
1738
|
+
evidence_transforms = transforms & {
|
|
1739
|
+
PortfolioCardViewTransform.EVIDENCE_PERMUTATION,
|
|
1740
|
+
PortfolioCardViewTransform.EVIDENCE_REDACTION,
|
|
1741
|
+
}
|
|
1742
|
+
if len(evidence_transforms) > 1 or (
|
|
1743
|
+
evidence_changed and not evidence_transforms
|
|
1744
|
+
):
|
|
1745
|
+
raise ValueError("evidence transform declaration does not match the view")
|
|
1746
|
+
if (
|
|
1747
|
+
PortfolioCardViewTransform.PROMPT_REDACTION in prompt_transforms
|
|
1748
|
+
and self.prompt_payload != CANONICAL_NEUTRAL_PORTFOLIO_PROMPT_PAYLOAD
|
|
1749
|
+
):
|
|
1750
|
+
raise ValueError(
|
|
1751
|
+
"redacted prompt payload must use the canonical neutral value"
|
|
1752
|
+
)
|
|
1753
|
+
if (
|
|
1754
|
+
PortfolioCardViewTransform.EVIDENCE_REDACTION in evidence_transforms
|
|
1755
|
+
and self.evidence_sha256 != CANONICAL_REDACTED_PORTFOLIO_EVIDENCE_SHA256
|
|
1756
|
+
):
|
|
1757
|
+
raise ValueError("redacted evidence must use the canonical sentinel")
|
|
1758
|
+
score_transforms = transforms & {
|
|
1759
|
+
PortfolioCardViewTransform.SCORE_PERMUTATION,
|
|
1760
|
+
PortfolioCardViewTransform.SCORE_REDACTION,
|
|
1761
|
+
}
|
|
1762
|
+
if len(score_transforms) > 1 or (score_changed and not score_transforms):
|
|
1763
|
+
raise ValueError("score transform declaration does not match the view")
|
|
1764
|
+
if PortfolioCardViewTransform.SCORE_REDACTION in score_transforms and (
|
|
1765
|
+
self.score_components or self.assigned_score is not None
|
|
1766
|
+
):
|
|
1767
|
+
raise ValueError("redacted score state must be empty")
|
|
1768
|
+
action_evidence_transforms = transforms & {
|
|
1769
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_PERMUTATION,
|
|
1770
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_REDACTION,
|
|
1771
|
+
}
|
|
1772
|
+
if len(action_evidence_transforms) > 1 or (
|
|
1773
|
+
action_evidence_changed and not action_evidence_transforms
|
|
1774
|
+
):
|
|
1775
|
+
raise ValueError(
|
|
1776
|
+
"action-evidence transform declaration does not match the view"
|
|
1777
|
+
)
|
|
1778
|
+
if (
|
|
1779
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_REDACTION
|
|
1780
|
+
in action_evidence_transforms
|
|
1781
|
+
and self.finite_action_evidence
|
|
1782
|
+
):
|
|
1783
|
+
raise ValueError("redacted action evidence must be empty")
|
|
1784
|
+
|
|
1785
|
+
@property
|
|
1786
|
+
def prompt_view_sha256(self) -> str:
|
|
1787
|
+
self.__post_init__()
|
|
1788
|
+
return typed_json_sha256(self.prompt_payload)
|
|
1789
|
+
|
|
1790
|
+
@property
|
|
1791
|
+
def typed_prompt_payload(self) -> PortfolioCardPromptPayload:
|
|
1792
|
+
"""Return the scientific split between neutral prose and attribution."""
|
|
1793
|
+
|
|
1794
|
+
self.__post_init__()
|
|
1795
|
+
return PortfolioCardPromptPayload(
|
|
1796
|
+
action_neutral_payload=self.prompt_payload,
|
|
1797
|
+
finite_action_evidence=self.finite_action_evidence,
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
def to_record(self) -> dict[str, object]:
|
|
1801
|
+
self.__post_init__()
|
|
1802
|
+
return {
|
|
1803
|
+
"card_key": self.card_key,
|
|
1804
|
+
"reference": _reference_record(self.reference),
|
|
1805
|
+
"content_sha256": self.content_sha256,
|
|
1806
|
+
"evidence_sha256": self.evidence_sha256,
|
|
1807
|
+
"prompt_view_sha256": self.prompt_view_sha256,
|
|
1808
|
+
"score_components": [
|
|
1809
|
+
component.to_record() for component in self.score_components
|
|
1810
|
+
],
|
|
1811
|
+
"assigned_score_hex": (
|
|
1812
|
+
None if self.assigned_score is None else self.assigned_score.hex()
|
|
1813
|
+
),
|
|
1814
|
+
**(
|
|
1815
|
+
{}
|
|
1816
|
+
if not self.finite_action_evidence
|
|
1817
|
+
else {
|
|
1818
|
+
"finite_action_evidence": [
|
|
1819
|
+
binding.to_record() for binding in self.finite_action_evidence
|
|
1820
|
+
]
|
|
1821
|
+
}
|
|
1822
|
+
),
|
|
1823
|
+
**(
|
|
1824
|
+
{}
|
|
1825
|
+
if self.source_binding is None
|
|
1826
|
+
else {"source_binding": self.source_binding.to_record()}
|
|
1827
|
+
),
|
|
1828
|
+
**(
|
|
1829
|
+
{}
|
|
1830
|
+
if self.derived_view_receipt is None
|
|
1831
|
+
else {"derived_view_receipt": self.derived_view_receipt.to_record()}
|
|
1832
|
+
),
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
def prompt_record(self) -> dict[str, object]:
|
|
1836
|
+
"""Return the exact detached view rendered for the selector."""
|
|
1837
|
+
|
|
1838
|
+
self.__post_init__()
|
|
1839
|
+
return {
|
|
1840
|
+
"card_key": self.card_key,
|
|
1841
|
+
"insight_id": self.reference.insight_id.value,
|
|
1842
|
+
"insight_version": self.reference.version,
|
|
1843
|
+
"content_sha256": self.content_sha256,
|
|
1844
|
+
"evidence_sha256": self.evidence_sha256,
|
|
1845
|
+
"score_components": [
|
|
1846
|
+
component.prompt_record() for component in self.score_components
|
|
1847
|
+
],
|
|
1848
|
+
"assigned_score": self.assigned_score,
|
|
1849
|
+
"payload": thaw_json(self.prompt_payload),
|
|
1850
|
+
**(
|
|
1851
|
+
{}
|
|
1852
|
+
if not self.finite_action_evidence
|
|
1853
|
+
else {
|
|
1854
|
+
"finite_action_evidence": [
|
|
1855
|
+
binding.to_record() for binding in self.finite_action_evidence
|
|
1856
|
+
]
|
|
1857
|
+
}
|
|
1858
|
+
),
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
def portfolio_card_snapshot_sha256(cards: tuple[PortfolioCard, ...]) -> str:
|
|
1863
|
+
"""Bind the complete source/view/score state supplied to one selector."""
|
|
1864
|
+
|
|
1865
|
+
if type(cards) is not tuple or any(
|
|
1866
|
+
type(card) is not PortfolioCard for card in cards
|
|
1867
|
+
):
|
|
1868
|
+
raise TypeError("cards must be an exact tuple of PortfolioCard values")
|
|
1869
|
+
for card in cards:
|
|
1870
|
+
card.__post_init__()
|
|
1871
|
+
return _hash(
|
|
1872
|
+
_CARD_SNAPSHOT_DOMAIN,
|
|
1873
|
+
{"cards": [card.to_record() for card in cards]},
|
|
1874
|
+
)
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
def derive_portfolio_card_view(
|
|
1878
|
+
source_card: PortfolioCard,
|
|
1879
|
+
*,
|
|
1880
|
+
prompt_payload: FrozenJsonObject,
|
|
1881
|
+
evidence_sha256: str,
|
|
1882
|
+
score_components: tuple[CardScoreComponent, ...],
|
|
1883
|
+
assigned_score: float | None,
|
|
1884
|
+
transforms: tuple[PortfolioCardViewTransform, ...],
|
|
1885
|
+
policy_id: str,
|
|
1886
|
+
policy_version: int,
|
|
1887
|
+
policy_definition_sha256: str,
|
|
1888
|
+
prompt_source_card: PortfolioCard | None = None,
|
|
1889
|
+
evidence_source_card: PortfolioCard | None = None,
|
|
1890
|
+
score_source_card: PortfolioCard | None = None,
|
|
1891
|
+
finite_action_evidence: tuple[FiniteActionEvidenceBinding, ...] | None = None,
|
|
1892
|
+
action_evidence_source_card: PortfolioCard | None = None,
|
|
1893
|
+
) -> PortfolioCard:
|
|
1894
|
+
"""Derive an explicit experimental view without changing source lineage."""
|
|
1895
|
+
|
|
1896
|
+
if type(source_card) is not PortfolioCard:
|
|
1897
|
+
raise TypeError("source_card must be an exact PortfolioCard")
|
|
1898
|
+
source_card.__post_init__()
|
|
1899
|
+
if source_card.source_binding is None:
|
|
1900
|
+
raise ValueError("derived views require a source-bound card")
|
|
1901
|
+
if source_card.derived_view_receipt is not None:
|
|
1902
|
+
raise ValueError(
|
|
1903
|
+
"derived views must start from the integrity-bound source view"
|
|
1904
|
+
)
|
|
1905
|
+
if type(prompt_payload) is not FrozenJsonObject:
|
|
1906
|
+
raise TypeError("prompt_payload must be an exact FrozenJsonObject")
|
|
1907
|
+
if freeze_json(prompt_payload) is not prompt_payload:
|
|
1908
|
+
raise TypeError("prompt_payload must already be frozen typed JSON")
|
|
1909
|
+
prompt_source_binding_sha256: str | None = None
|
|
1910
|
+
if prompt_source_card is not None:
|
|
1911
|
+
if type(prompt_source_card) is not PortfolioCard:
|
|
1912
|
+
raise TypeError("prompt_source_card must be an exact PortfolioCard")
|
|
1913
|
+
prompt_source_card.__post_init__()
|
|
1914
|
+
if prompt_source_card.source_binding is None:
|
|
1915
|
+
raise ValueError("prompt source card lacks integrity-bound provenance")
|
|
1916
|
+
prompt_source_binding_sha256 = prompt_source_card.source_binding.binding_sha256
|
|
1917
|
+
if (
|
|
1918
|
+
PortfolioCardViewTransform.PROMPT_PERMUTATION in transforms
|
|
1919
|
+
and typed_json_sha256(prompt_payload)
|
|
1920
|
+
!= prompt_source_card.source_binding.source_prompt_view_sha256
|
|
1921
|
+
):
|
|
1922
|
+
raise ValueError("permuted prompt must equal the named source prompt view")
|
|
1923
|
+
evidence_source_binding_sha256: str | None = None
|
|
1924
|
+
if evidence_source_card is not None:
|
|
1925
|
+
if type(evidence_source_card) is not PortfolioCard:
|
|
1926
|
+
raise TypeError("evidence_source_card must be an exact PortfolioCard")
|
|
1927
|
+
evidence_source_card.__post_init__()
|
|
1928
|
+
if evidence_source_card.source_binding is None:
|
|
1929
|
+
raise ValueError("evidence source card lacks integrity-bound provenance")
|
|
1930
|
+
evidence_source_binding_sha256 = (
|
|
1931
|
+
evidence_source_card.source_binding.binding_sha256
|
|
1932
|
+
)
|
|
1933
|
+
if (
|
|
1934
|
+
PortfolioCardViewTransform.EVIDENCE_PERMUTATION in transforms
|
|
1935
|
+
and evidence_sha256
|
|
1936
|
+
!= evidence_source_card.source_binding.source_evidence_sha256
|
|
1937
|
+
):
|
|
1938
|
+
raise ValueError("permuted evidence must equal the named source evidence")
|
|
1939
|
+
score_source_binding_sha256: str | None = None
|
|
1940
|
+
if score_source_card is not None:
|
|
1941
|
+
if type(score_source_card) is not PortfolioCard:
|
|
1942
|
+
raise TypeError("score_source_card must be an exact PortfolioCard")
|
|
1943
|
+
score_source_card.__post_init__()
|
|
1944
|
+
if score_source_card.source_binding is None:
|
|
1945
|
+
raise ValueError("score source card lacks integrity-bound provenance")
|
|
1946
|
+
score_source_binding_sha256 = score_source_card.source_binding.binding_sha256
|
|
1947
|
+
if (
|
|
1948
|
+
PortfolioCardViewTransform.SCORE_PERMUTATION in transforms
|
|
1949
|
+
and portfolio_card_score_state_sha256(
|
|
1950
|
+
score_components,
|
|
1951
|
+
assigned_score,
|
|
1952
|
+
)
|
|
1953
|
+
!= score_source_card.source_binding.source_score_state_sha256
|
|
1954
|
+
):
|
|
1955
|
+
raise ValueError(
|
|
1956
|
+
"permuted score state must equal the named source score state"
|
|
1957
|
+
)
|
|
1958
|
+
projected_action_evidence = (
|
|
1959
|
+
source_card.source_binding.finite_action_evidence
|
|
1960
|
+
if finite_action_evidence is None
|
|
1961
|
+
else finite_action_evidence
|
|
1962
|
+
)
|
|
1963
|
+
_validate_finite_action_evidence(
|
|
1964
|
+
projected_action_evidence,
|
|
1965
|
+
name="finite_action_evidence",
|
|
1966
|
+
)
|
|
1967
|
+
action_evidence_source_binding_sha256: str | None = None
|
|
1968
|
+
if action_evidence_source_card is not None:
|
|
1969
|
+
if type(action_evidence_source_card) is not PortfolioCard:
|
|
1970
|
+
raise TypeError(
|
|
1971
|
+
"action_evidence_source_card must be an exact PortfolioCard"
|
|
1972
|
+
)
|
|
1973
|
+
action_evidence_source_card.__post_init__()
|
|
1974
|
+
if action_evidence_source_card.source_binding is None:
|
|
1975
|
+
raise ValueError(
|
|
1976
|
+
"action-evidence source card lacks integrity-bound provenance"
|
|
1977
|
+
)
|
|
1978
|
+
action_evidence_source_binding_sha256 = (
|
|
1979
|
+
action_evidence_source_card.source_binding.binding_sha256
|
|
1980
|
+
)
|
|
1981
|
+
if (
|
|
1982
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_PERMUTATION in transforms
|
|
1983
|
+
and projected_action_evidence
|
|
1984
|
+
!= action_evidence_source_card.source_binding.finite_action_evidence
|
|
1985
|
+
):
|
|
1986
|
+
raise ValueError(
|
|
1987
|
+
"permuted action evidence must equal the named source action evidence"
|
|
1988
|
+
)
|
|
1989
|
+
if (
|
|
1990
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_REDACTION in transforms
|
|
1991
|
+
and projected_action_evidence
|
|
1992
|
+
):
|
|
1993
|
+
raise ValueError("redacted action evidence must be empty")
|
|
1994
|
+
receipt = PortfolioCardViewReceipt(
|
|
1995
|
+
source_binding_sha256=source_card.source_binding.binding_sha256,
|
|
1996
|
+
transforms=transforms,
|
|
1997
|
+
derived_prompt_view_sha256=typed_json_sha256(prompt_payload),
|
|
1998
|
+
derived_evidence_sha256=evidence_sha256,
|
|
1999
|
+
derived_score_state_sha256=portfolio_card_score_state_sha256(
|
|
2000
|
+
score_components,
|
|
2001
|
+
assigned_score,
|
|
2002
|
+
),
|
|
2003
|
+
derived_action_evidence_sha256=(
|
|
2004
|
+
portfolio_card_action_evidence_sha256(projected_action_evidence)
|
|
2005
|
+
),
|
|
2006
|
+
prompt_source_binding_sha256=prompt_source_binding_sha256,
|
|
2007
|
+
evidence_source_binding_sha256=evidence_source_binding_sha256,
|
|
2008
|
+
score_source_binding_sha256=score_source_binding_sha256,
|
|
2009
|
+
action_evidence_source_binding_sha256=(action_evidence_source_binding_sha256),
|
|
2010
|
+
policy_id=policy_id,
|
|
2011
|
+
policy_version=policy_version,
|
|
2012
|
+
policy_definition_sha256=policy_definition_sha256,
|
|
2013
|
+
)
|
|
2014
|
+
return PortfolioCard(
|
|
2015
|
+
card_key=source_card.card_key,
|
|
2016
|
+
reference=source_card.reference,
|
|
2017
|
+
content_sha256=source_card.content_sha256,
|
|
2018
|
+
evidence_sha256=evidence_sha256,
|
|
2019
|
+
prompt_payload=prompt_payload,
|
|
2020
|
+
score_components=score_components,
|
|
2021
|
+
assigned_score=assigned_score,
|
|
2022
|
+
finite_action_evidence=projected_action_evidence,
|
|
2023
|
+
source_binding=source_card.source_binding,
|
|
2024
|
+
derived_view_receipt=receipt,
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
class PortfolioExperimentalArm(str, Enum):
|
|
2029
|
+
"""Closed scientific M/P/N portfolio-view arms."""
|
|
2030
|
+
|
|
2031
|
+
MEMORY = "m"
|
|
2032
|
+
PERMUTED_PLACEBO = "p"
|
|
2033
|
+
NEUTRAL = "n"
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
@dataclass(frozen=True, slots=True)
|
|
2037
|
+
class PortfolioExperimentalViewReceipt:
|
|
2038
|
+
"""Request-level commitment to one complete scientific card view.
|
|
2039
|
+
|
|
2040
|
+
Per-card receipts bind compartment bytes. This receipt additionally binds
|
|
2041
|
+
the arm and the population-level permutation invariant, which cannot be
|
|
2042
|
+
established by validating cards independently.
|
|
2043
|
+
"""
|
|
2044
|
+
|
|
2045
|
+
arm: PortfolioExperimentalArm
|
|
2046
|
+
source_registry_sha256: str
|
|
2047
|
+
card_snapshot_sha256: str
|
|
2048
|
+
source_donor_binding_pairs: tuple[tuple[str, str], ...]
|
|
2049
|
+
policy_id: str
|
|
2050
|
+
policy_version: int
|
|
2051
|
+
policy_definition_sha256: str
|
|
2052
|
+
|
|
2053
|
+
def __post_init__(self) -> None:
|
|
2054
|
+
if type(self.arm) is not PortfolioExperimentalArm:
|
|
2055
|
+
raise TypeError("arm must be an exact PortfolioExperimentalArm")
|
|
2056
|
+
require_sha256(self.source_registry_sha256, "source_registry_sha256")
|
|
2057
|
+
require_sha256(self.card_snapshot_sha256, "card_snapshot_sha256")
|
|
2058
|
+
if type(self.source_donor_binding_pairs) is not tuple or any(
|
|
2059
|
+
type(pair) is not tuple
|
|
2060
|
+
or len(pair) != 2
|
|
2061
|
+
or any(type(value) is not str for value in pair)
|
|
2062
|
+
for pair in self.source_donor_binding_pairs
|
|
2063
|
+
):
|
|
2064
|
+
raise TypeError(
|
|
2065
|
+
"source_donor_binding_pairs must be an exact tuple of SHA pairs"
|
|
2066
|
+
)
|
|
2067
|
+
for source_sha256, donor_sha256 in self.source_donor_binding_pairs:
|
|
2068
|
+
require_sha256(source_sha256, "source_binding_sha256")
|
|
2069
|
+
require_sha256(donor_sha256, "donor_binding_sha256")
|
|
2070
|
+
if self.source_donor_binding_pairs != tuple(
|
|
2071
|
+
sorted(self.source_donor_binding_pairs)
|
|
2072
|
+
):
|
|
2073
|
+
raise ValueError("source-donor pairs must use canonical source order")
|
|
2074
|
+
sources = tuple(pair[0] for pair in self.source_donor_binding_pairs)
|
|
2075
|
+
donors = tuple(pair[1] for pair in self.source_donor_binding_pairs)
|
|
2076
|
+
if len(set(sources)) != len(sources):
|
|
2077
|
+
raise ValueError("source-donor pairs cannot repeat a source")
|
|
2078
|
+
if self.arm is PortfolioExperimentalArm.PERMUTED_PLACEBO:
|
|
2079
|
+
if not sources:
|
|
2080
|
+
raise ValueError("P requires a non-empty donor permutation")
|
|
2081
|
+
if set(sources) != set(donors) or len(set(donors)) != len(donors):
|
|
2082
|
+
raise ValueError("P donors must be a bijection over source cards")
|
|
2083
|
+
if any(
|
|
2084
|
+
source == donor for source, donor in self.source_donor_binding_pairs
|
|
2085
|
+
):
|
|
2086
|
+
raise ValueError("P donor permutation must be a derangement")
|
|
2087
|
+
elif self.source_donor_binding_pairs:
|
|
2088
|
+
raise ValueError("only P may carry source-donor pairs")
|
|
2089
|
+
if type(self.policy_id) is not str or _TOKEN.fullmatch(self.policy_id) is None:
|
|
2090
|
+
raise ValueError("policy_id must use the closed lowercase token grammar")
|
|
2091
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
2092
|
+
raise ValueError("policy_version must be a positive exact integer")
|
|
2093
|
+
require_sha256(self.policy_definition_sha256, "policy_definition_sha256")
|
|
2094
|
+
|
|
2095
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
2096
|
+
self.__post_init__()
|
|
2097
|
+
return {
|
|
2098
|
+
"schema_version": 1,
|
|
2099
|
+
"arm": self.arm.value,
|
|
2100
|
+
"source_registry_sha256": self.source_registry_sha256,
|
|
2101
|
+
"card_snapshot_sha256": self.card_snapshot_sha256,
|
|
2102
|
+
"source_donor_binding_pairs": [
|
|
2103
|
+
{"source_binding_sha256": source, "donor_binding_sha256": donor}
|
|
2104
|
+
for source, donor in self.source_donor_binding_pairs
|
|
2105
|
+
],
|
|
2106
|
+
"policy_id": self.policy_id,
|
|
2107
|
+
"policy_version": self.policy_version,
|
|
2108
|
+
"policy_definition_sha256": self.policy_definition_sha256,
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
@property
|
|
2112
|
+
def receipt_sha256(self) -> str:
|
|
2113
|
+
return _hash(_EXPERIMENTAL_VIEW_RECEIPT_DOMAIN, self._unsigned_record())
|
|
2114
|
+
|
|
2115
|
+
def to_record(self) -> dict[str, object]:
|
|
2116
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def validate_portfolio_experimental_view(
|
|
2120
|
+
*,
|
|
2121
|
+
cards: tuple[PortfolioCard, ...],
|
|
2122
|
+
finite_variation_contract: FiniteVariationContract,
|
|
2123
|
+
source_registry: PortfolioCardSourceRegistry,
|
|
2124
|
+
receipt: PortfolioExperimentalViewReceipt,
|
|
2125
|
+
) -> None:
|
|
2126
|
+
"""Validate a complete provider-neutral M/P/N scientific prompt view."""
|
|
2127
|
+
|
|
2128
|
+
if (
|
|
2129
|
+
type(cards) is not tuple
|
|
2130
|
+
or not cards
|
|
2131
|
+
or any(type(card) is not PortfolioCard for card in cards)
|
|
2132
|
+
):
|
|
2133
|
+
raise TypeError("cards must be a non-empty exact tuple of PortfolioCard values")
|
|
2134
|
+
identity_index = validated_finite_variation_identity_index(
|
|
2135
|
+
finite_variation_contract
|
|
2136
|
+
)
|
|
2137
|
+
if type(source_registry) is not PortfolioCardSourceRegistry:
|
|
2138
|
+
raise TypeError("source_registry must be an exact PortfolioCardSourceRegistry")
|
|
2139
|
+
source_registry.__post_init__()
|
|
2140
|
+
if type(receipt) is not PortfolioExperimentalViewReceipt:
|
|
2141
|
+
raise TypeError("receipt must be an exact PortfolioExperimentalViewReceipt")
|
|
2142
|
+
receipt.__post_init__()
|
|
2143
|
+
if receipt.source_registry_sha256 != source_registry.registry_sha256:
|
|
2144
|
+
raise ValueError("experimental receipt names a different source registry")
|
|
2145
|
+
if receipt.card_snapshot_sha256 != portfolio_card_snapshot_sha256(cards):
|
|
2146
|
+
raise ValueError("experimental receipt names a different card snapshot")
|
|
2147
|
+
|
|
2148
|
+
sources = {
|
|
2149
|
+
binding.binding_sha256: binding for binding in source_registry.source_bindings
|
|
2150
|
+
}
|
|
2151
|
+
cards_by_source: dict[str, PortfolioCard] = {}
|
|
2152
|
+
for card in cards:
|
|
2153
|
+
card.__post_init__()
|
|
2154
|
+
if card.source_binding is None:
|
|
2155
|
+
raise ValueError("scientific views reject unbound legacy cards")
|
|
2156
|
+
source_sha256 = card.source_binding.binding_sha256
|
|
2157
|
+
if source_sha256 in cards_by_source:
|
|
2158
|
+
raise ValueError("scientific views cannot repeat a source card")
|
|
2159
|
+
cards_by_source[source_sha256] = card
|
|
2160
|
+
admitted = sources.get(source_sha256)
|
|
2161
|
+
if admitted is None or admitted != card.source_binding:
|
|
2162
|
+
raise ValueError(
|
|
2163
|
+
"scientific card source differs from the admitted registry"
|
|
2164
|
+
)
|
|
2165
|
+
_validate_action_neutral_payload(
|
|
2166
|
+
card.typed_prompt_payload,
|
|
2167
|
+
identity_index=identity_index,
|
|
2168
|
+
)
|
|
2169
|
+
if cards_by_source.keys() != sources.keys():
|
|
2170
|
+
raise ValueError("scientific cards differ from the complete source registry")
|
|
2171
|
+
|
|
2172
|
+
if receipt.arm is PortfolioExperimentalArm.MEMORY:
|
|
2173
|
+
if any(card.derived_view_receipt is not None for card in cards):
|
|
2174
|
+
raise ValueError("M requires pristine coherent source cards")
|
|
2175
|
+
return
|
|
2176
|
+
|
|
2177
|
+
if receipt.arm is PortfolioExperimentalArm.PERMUTED_PLACEBO:
|
|
2178
|
+
required_transforms = tuple(
|
|
2179
|
+
sorted(
|
|
2180
|
+
(
|
|
2181
|
+
PortfolioCardViewTransform.EVIDENCE_PERMUTATION,
|
|
2182
|
+
PortfolioCardViewTransform.PROMPT_PERMUTATION,
|
|
2183
|
+
PortfolioCardViewTransform.SCORE_PERMUTATION,
|
|
2184
|
+
),
|
|
2185
|
+
key=lambda value: value.value,
|
|
2186
|
+
)
|
|
2187
|
+
)
|
|
2188
|
+
inferred_pairs: list[tuple[str, str]] = []
|
|
2189
|
+
for source_sha256, card in cards_by_source.items():
|
|
2190
|
+
view = card.derived_view_receipt
|
|
2191
|
+
if view is None or view.transforms != required_transforms:
|
|
2192
|
+
raise ValueError(
|
|
2193
|
+
"P requires exact prompt/evidence/score permutation while "
|
|
2194
|
+
"retaining source action evidence"
|
|
2195
|
+
)
|
|
2196
|
+
donors = (
|
|
2197
|
+
view.prompt_source_binding_sha256,
|
|
2198
|
+
view.evidence_source_binding_sha256,
|
|
2199
|
+
view.score_source_binding_sha256,
|
|
2200
|
+
)
|
|
2201
|
+
if any(donor is None for donor in donors) or len(set(donors)) != 1:
|
|
2202
|
+
raise ValueError(
|
|
2203
|
+
"P requires one donor across prompt/evidence/score compartments"
|
|
2204
|
+
)
|
|
2205
|
+
donor_sha256 = donors[0]
|
|
2206
|
+
assert donor_sha256 is not None
|
|
2207
|
+
donor = sources.get(donor_sha256)
|
|
2208
|
+
if donor is None:
|
|
2209
|
+
raise ValueError("P donor is outside the admitted source registry")
|
|
2210
|
+
if (
|
|
2211
|
+
view.derived_prompt_view_sha256 != donor.source_prompt_view_sha256
|
|
2212
|
+
or view.derived_evidence_sha256 != donor.source_evidence_sha256
|
|
2213
|
+
or view.derived_score_state_sha256 != donor.source_score_state_sha256
|
|
2214
|
+
):
|
|
2215
|
+
raise ValueError(
|
|
2216
|
+
"P prompt/evidence/score compartment differs from its donor"
|
|
2217
|
+
)
|
|
2218
|
+
if (
|
|
2219
|
+
view.action_evidence_source_binding_sha256 is not None
|
|
2220
|
+
or card.finite_action_evidence
|
|
2221
|
+
!= card.source_binding.finite_action_evidence
|
|
2222
|
+
or view.derived_action_evidence_sha256
|
|
2223
|
+
!= portfolio_card_action_evidence_sha256(
|
|
2224
|
+
card.source_binding.finite_action_evidence
|
|
2225
|
+
)
|
|
2226
|
+
):
|
|
2227
|
+
raise ValueError(
|
|
2228
|
+
"P must retain the source card's finite-action evidence"
|
|
2229
|
+
)
|
|
2230
|
+
inferred_pairs.append((source_sha256, donor_sha256))
|
|
2231
|
+
if tuple(sorted(inferred_pairs)) != receipt.source_donor_binding_pairs:
|
|
2232
|
+
raise ValueError("P receipt differs from the exact donor permutation")
|
|
2233
|
+
return
|
|
2234
|
+
|
|
2235
|
+
required_transforms = tuple(
|
|
2236
|
+
sorted(
|
|
2237
|
+
(
|
|
2238
|
+
PortfolioCardViewTransform.ACTION_EVIDENCE_REDACTION,
|
|
2239
|
+
PortfolioCardViewTransform.EVIDENCE_REDACTION,
|
|
2240
|
+
PortfolioCardViewTransform.PROMPT_REDACTION,
|
|
2241
|
+
PortfolioCardViewTransform.SCORE_REDACTION,
|
|
2242
|
+
),
|
|
2243
|
+
key=lambda value: value.value,
|
|
2244
|
+
)
|
|
2245
|
+
)
|
|
2246
|
+
for card in cards:
|
|
2247
|
+
view = card.derived_view_receipt
|
|
2248
|
+
if view is None or view.transforms != required_transforms:
|
|
2249
|
+
raise ValueError("N requires exact canonical compartment redaction")
|
|
2250
|
+
if (
|
|
2251
|
+
card.prompt_payload != CANONICAL_NEUTRAL_PORTFOLIO_PROMPT_PAYLOAD
|
|
2252
|
+
or card.evidence_sha256 != CANONICAL_REDACTED_PORTFOLIO_EVIDENCE_SHA256
|
|
2253
|
+
or card.score_components
|
|
2254
|
+
or card.assigned_score is not None
|
|
2255
|
+
or card.finite_action_evidence
|
|
2256
|
+
):
|
|
2257
|
+
raise ValueError("N card compartments are not canonically neutral")
|
|
2258
|
+
|
|
2259
|
+
|
|
2260
|
+
@dataclass(frozen=True, slots=True)
|
|
2261
|
+
class _PortfolioSelectionRequestValidationReceipt:
|
|
2262
|
+
finite_identity_index: ValidatedFiniteVariationIdentityIndex
|
|
2263
|
+
context_sha256: str
|
|
2264
|
+
card_snapshot_sha256: str
|
|
2265
|
+
|
|
2266
|
+
def __post_init__(self) -> None:
|
|
2267
|
+
if (
|
|
2268
|
+
type(self.finite_identity_index)
|
|
2269
|
+
is not ValidatedFiniteVariationIdentityIndex
|
|
2270
|
+
):
|
|
2271
|
+
raise TypeError("finite_identity_index must be exact")
|
|
2272
|
+
self.finite_identity_index.__post_init__()
|
|
2273
|
+
require_sha256(self.context_sha256, "context_sha256")
|
|
2274
|
+
require_sha256(self.card_snapshot_sha256, "card_snapshot_sha256")
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
@dataclass(frozen=True, slots=True)
|
|
2278
|
+
class PortfolioSelectionRequest:
|
|
2279
|
+
"""One exact logical call over a sealed action and card snapshot."""
|
|
2280
|
+
|
|
2281
|
+
call_id: LLMCallId
|
|
2282
|
+
operation: str
|
|
2283
|
+
instruction: str
|
|
2284
|
+
context: FrozenJsonObject
|
|
2285
|
+
finite_variation_contract: FiniteVariationContract
|
|
2286
|
+
cards: tuple[PortfolioCard, ...]
|
|
2287
|
+
portfolio_size: int
|
|
2288
|
+
required_metric_ids: tuple[str, ...]
|
|
2289
|
+
min_distinct_families: int | None = None
|
|
2290
|
+
require_supporting_cards: bool = True
|
|
2291
|
+
require_pairwise_disjoint_parent_patches: bool = False
|
|
2292
|
+
max_output_tokens: int = 2_048
|
|
2293
|
+
temperature: float | None = None
|
|
2294
|
+
source_registry: PortfolioCardSourceRegistry | None = None
|
|
2295
|
+
experimental_view_receipt: PortfolioExperimentalViewReceipt | None = None
|
|
2296
|
+
memory_dose_contract: BoundedPortfolioMemoryDoseContract | None = None
|
|
2297
|
+
candidate_pool_required_option_ids: tuple[str, ...] = ()
|
|
2298
|
+
_validation_receipt: _PortfolioSelectionRequestValidationReceipt = field(
|
|
2299
|
+
init=False,
|
|
2300
|
+
repr=False,
|
|
2301
|
+
compare=False,
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
def __post_init__(self) -> None:
|
|
2305
|
+
if type(self.call_id) is not LLMCallId:
|
|
2306
|
+
raise TypeError("call_id must be an exact LLMCallId")
|
|
2307
|
+
LLMCallId.__post_init__(self.call_id)
|
|
2308
|
+
if type(self.operation) is not str or _TOKEN.fullmatch(self.operation) is None:
|
|
2309
|
+
raise ValueError("operation must use the closed lowercase token grammar")
|
|
2310
|
+
if (
|
|
2311
|
+
type(self.instruction) is not str
|
|
2312
|
+
or not self.instruction.strip()
|
|
2313
|
+
or self.instruction != self.instruction.strip()
|
|
2314
|
+
):
|
|
2315
|
+
raise ValueError("instruction must be canonical non-empty text")
|
|
2316
|
+
if type(self.context) is not FrozenJsonObject:
|
|
2317
|
+
raise TypeError("context must be an exact FrozenJsonObject")
|
|
2318
|
+
if freeze_json(self.context) is not self.context:
|
|
2319
|
+
raise TypeError("context must already be frozen typed JSON")
|
|
2320
|
+
finite_identity_index = validated_finite_variation_identity_index(
|
|
2321
|
+
self.finite_variation_contract
|
|
2322
|
+
)
|
|
2323
|
+
if type(self.cards) is not tuple or not self.cards:
|
|
2324
|
+
raise ValueError("cards must be a non-empty exact tuple")
|
|
2325
|
+
if any(type(card) is not PortfolioCard for card in self.cards):
|
|
2326
|
+
raise TypeError("cards must contain exact PortfolioCard values")
|
|
2327
|
+
for card in self.cards:
|
|
2328
|
+
card.__post_init__()
|
|
2329
|
+
card_keys = tuple(card.card_key for card in self.cards)
|
|
2330
|
+
if card_keys != tuple(sorted(set(card_keys))):
|
|
2331
|
+
raise ValueError("cards must use unique canonical card_key order")
|
|
2332
|
+
references = tuple(card.reference for card in self.cards)
|
|
2333
|
+
if len(set(references)) != len(references):
|
|
2334
|
+
raise ValueError("cards cannot repeat an exact insight reference")
|
|
2335
|
+
source_bindings = {
|
|
2336
|
+
card.source_binding.binding_sha256: card.source_binding
|
|
2337
|
+
for card in self.cards
|
|
2338
|
+
if card.source_binding is not None
|
|
2339
|
+
}
|
|
2340
|
+
if source_bindings:
|
|
2341
|
+
if len(source_bindings) != len(self.cards):
|
|
2342
|
+
raise ValueError(
|
|
2343
|
+
"a source-admitted request cannot mix bound and legacy cards"
|
|
2344
|
+
)
|
|
2345
|
+
if type(self.source_registry) is not PortfolioCardSourceRegistry:
|
|
2346
|
+
raise ValueError(
|
|
2347
|
+
"source-bound cards require trusted application admission"
|
|
2348
|
+
)
|
|
2349
|
+
self.source_registry.__post_init__()
|
|
2350
|
+
admitted = {
|
|
2351
|
+
binding.binding_sha256: binding
|
|
2352
|
+
for binding in self.source_registry.source_bindings
|
|
2353
|
+
}
|
|
2354
|
+
if admitted.keys() != source_bindings.keys():
|
|
2355
|
+
raise ValueError(
|
|
2356
|
+
"source registry differs from the request card source set"
|
|
2357
|
+
)
|
|
2358
|
+
for binding_sha256, binding in source_bindings.items():
|
|
2359
|
+
if admitted[binding_sha256] != binding:
|
|
2360
|
+
raise ValueError(
|
|
2361
|
+
"source registry binding differs from the request card"
|
|
2362
|
+
)
|
|
2363
|
+
elif self.source_registry is not None:
|
|
2364
|
+
raise ValueError("legacy unbound cards cannot carry a source registry")
|
|
2365
|
+
for card in self.cards:
|
|
2366
|
+
receipt = card.derived_view_receipt
|
|
2367
|
+
if receipt is None:
|
|
2368
|
+
continue
|
|
2369
|
+
prompt_source_sha256 = receipt.prompt_source_binding_sha256
|
|
2370
|
+
if prompt_source_sha256 is not None:
|
|
2371
|
+
prompt_source = source_bindings.get(prompt_source_sha256)
|
|
2372
|
+
if prompt_source is None:
|
|
2373
|
+
raise ValueError(
|
|
2374
|
+
"derived card view names a prompt source outside the request"
|
|
2375
|
+
)
|
|
2376
|
+
if (
|
|
2377
|
+
receipt.derived_prompt_view_sha256
|
|
2378
|
+
!= prompt_source.source_prompt_view_sha256
|
|
2379
|
+
):
|
|
2380
|
+
raise ValueError(
|
|
2381
|
+
"permuted prompt differs from the named source view"
|
|
2382
|
+
)
|
|
2383
|
+
evidence_source_sha256 = receipt.evidence_source_binding_sha256
|
|
2384
|
+
if evidence_source_sha256 is not None:
|
|
2385
|
+
evidence_source = source_bindings.get(evidence_source_sha256)
|
|
2386
|
+
if evidence_source is None:
|
|
2387
|
+
raise ValueError(
|
|
2388
|
+
"derived card view names an evidence source outside the request"
|
|
2389
|
+
)
|
|
2390
|
+
if (
|
|
2391
|
+
receipt.derived_evidence_sha256
|
|
2392
|
+
!= evidence_source.source_evidence_sha256
|
|
2393
|
+
):
|
|
2394
|
+
raise ValueError(
|
|
2395
|
+
"permuted evidence differs from the named source view"
|
|
2396
|
+
)
|
|
2397
|
+
score_source_sha256 = receipt.score_source_binding_sha256
|
|
2398
|
+
if score_source_sha256 is not None:
|
|
2399
|
+
score_source = source_bindings.get(score_source_sha256)
|
|
2400
|
+
if score_source is None:
|
|
2401
|
+
raise ValueError(
|
|
2402
|
+
"derived card view names a score source outside the request"
|
|
2403
|
+
)
|
|
2404
|
+
if (
|
|
2405
|
+
receipt.derived_score_state_sha256
|
|
2406
|
+
!= score_source.source_score_state_sha256
|
|
2407
|
+
):
|
|
2408
|
+
raise ValueError(
|
|
2409
|
+
"permuted score state differs from the named source view"
|
|
2410
|
+
)
|
|
2411
|
+
action_source_sha256 = receipt.action_evidence_source_binding_sha256
|
|
2412
|
+
if action_source_sha256 is not None:
|
|
2413
|
+
action_source = source_bindings.get(action_source_sha256)
|
|
2414
|
+
if action_source is None:
|
|
2415
|
+
raise ValueError(
|
|
2416
|
+
"derived card view names an action-evidence source "
|
|
2417
|
+
"outside the request"
|
|
2418
|
+
)
|
|
2419
|
+
if (
|
|
2420
|
+
receipt.derived_action_evidence_sha256
|
|
2421
|
+
!= portfolio_card_action_evidence_sha256(
|
|
2422
|
+
action_source.finite_action_evidence
|
|
2423
|
+
)
|
|
2424
|
+
):
|
|
2425
|
+
raise ValueError(
|
|
2426
|
+
"permuted action evidence differs from the named source view"
|
|
2427
|
+
)
|
|
2428
|
+
if self.experimental_view_receipt is not None:
|
|
2429
|
+
if (
|
|
2430
|
+
type(self.experimental_view_receipt)
|
|
2431
|
+
is not PortfolioExperimentalViewReceipt
|
|
2432
|
+
):
|
|
2433
|
+
raise TypeError(
|
|
2434
|
+
"experimental_view_receipt must be an exact "
|
|
2435
|
+
"PortfolioExperimentalViewReceipt or None"
|
|
2436
|
+
)
|
|
2437
|
+
if type(self.source_registry) is not PortfolioCardSourceRegistry:
|
|
2438
|
+
raise ValueError(
|
|
2439
|
+
"a scientific experimental view requires source admission"
|
|
2440
|
+
)
|
|
2441
|
+
validate_portfolio_experimental_view(
|
|
2442
|
+
cards=self.cards,
|
|
2443
|
+
finite_variation_contract=self.finite_variation_contract,
|
|
2444
|
+
source_registry=self.source_registry,
|
|
2445
|
+
receipt=self.experimental_view_receipt,
|
|
2446
|
+
)
|
|
2447
|
+
if type(self.portfolio_size) is not int or self.portfolio_size <= 0:
|
|
2448
|
+
raise ValueError("portfolio_size must be a positive exact integer")
|
|
2449
|
+
if self.portfolio_size > len(self.finite_variation_contract.options):
|
|
2450
|
+
raise ValueError("portfolio_size exceeds the finite option count")
|
|
2451
|
+
_canonical_metric_ids(self.required_metric_ids)
|
|
2452
|
+
available_families = {
|
|
2453
|
+
option.family for option in self.finite_variation_contract.options
|
|
2454
|
+
}
|
|
2455
|
+
if self.min_distinct_families is not None:
|
|
2456
|
+
if (
|
|
2457
|
+
type(self.min_distinct_families) is not int
|
|
2458
|
+
or self.min_distinct_families <= 0
|
|
2459
|
+
):
|
|
2460
|
+
raise ValueError(
|
|
2461
|
+
"min_distinct_families must be a positive exact integer or None"
|
|
2462
|
+
)
|
|
2463
|
+
if self.min_distinct_families > self.portfolio_size:
|
|
2464
|
+
raise ValueError("min_distinct_families cannot exceed portfolio_size")
|
|
2465
|
+
if self.min_distinct_families > len(available_families):
|
|
2466
|
+
raise ValueError("finite contract cannot satisfy min_distinct_families")
|
|
2467
|
+
if type(self.require_supporting_cards) is not bool:
|
|
2468
|
+
raise TypeError("require_supporting_cards must be an exact bool")
|
|
2469
|
+
if self.memory_dose_contract is not None:
|
|
2470
|
+
if type(self.memory_dose_contract) is not (
|
|
2471
|
+
BoundedPortfolioMemoryDoseContract
|
|
2472
|
+
):
|
|
2473
|
+
raise TypeError(
|
|
2474
|
+
"memory_dose_contract must be an exact bounded contract or None"
|
|
2475
|
+
)
|
|
2476
|
+
self.memory_dose_contract.__post_init__()
|
|
2477
|
+
if self.require_supporting_cards:
|
|
2478
|
+
raise ValueError(
|
|
2479
|
+
"bounded memory dose requires unattributed exploration slots"
|
|
2480
|
+
)
|
|
2481
|
+
if (
|
|
2482
|
+
self.memory_dose_contract.finite_contract_identity_sha256
|
|
2483
|
+
!= finite_identity_index.contract_identity_sha256
|
|
2484
|
+
):
|
|
2485
|
+
raise ValueError("memory-dose contract names a foreign finite contract")
|
|
2486
|
+
cards_by_key = {value.card_key: value for value in self.cards}
|
|
2487
|
+
if not set(self.memory_dose_contract.assigned_card_keys).issubset(
|
|
2488
|
+
cards_by_key
|
|
2489
|
+
):
|
|
2490
|
+
raise ValueError(
|
|
2491
|
+
"memory-dose contract names a card outside the request"
|
|
2492
|
+
)
|
|
2493
|
+
if any(
|
|
2494
|
+
cards_by_key[support.card_key].content_sha256
|
|
2495
|
+
!= support.card_content_sha256
|
|
2496
|
+
for support in self.memory_dose_contract.card_supports
|
|
2497
|
+
):
|
|
2498
|
+
raise ValueError(
|
|
2499
|
+
"memory-dose support differs from the request card content"
|
|
2500
|
+
)
|
|
2501
|
+
if (
|
|
2502
|
+
type(self.candidate_pool_required_option_ids) is not tuple
|
|
2503
|
+
or any(
|
|
2504
|
+
type(value) is not str or _OPTION_ID.fullmatch(value) is None
|
|
2505
|
+
for value in self.candidate_pool_required_option_ids
|
|
2506
|
+
)
|
|
2507
|
+
):
|
|
2508
|
+
raise TypeError(
|
|
2509
|
+
"candidate_pool_required_option_ids must be an exact option-ID tuple"
|
|
2510
|
+
)
|
|
2511
|
+
if self.candidate_pool_required_option_ids != tuple(
|
|
2512
|
+
sorted(set(self.candidate_pool_required_option_ids))
|
|
2513
|
+
):
|
|
2514
|
+
raise ValueError(
|
|
2515
|
+
"candidate_pool_required_option_ids must be unique and canonical"
|
|
2516
|
+
)
|
|
2517
|
+
if not set(self.candidate_pool_required_option_ids).issubset(
|
|
2518
|
+
finite_identity_index.option_ids
|
|
2519
|
+
):
|
|
2520
|
+
raise ValueError(
|
|
2521
|
+
"candidate_pool_required_option_ids escapes the finite contract"
|
|
2522
|
+
)
|
|
2523
|
+
if type(self.require_pairwise_disjoint_parent_patches) is not bool:
|
|
2524
|
+
raise TypeError(
|
|
2525
|
+
"require_pairwise_disjoint_parent_patches must be an exact bool"
|
|
2526
|
+
)
|
|
2527
|
+
if self.require_pairwise_disjoint_parent_patches and not (
|
|
2528
|
+
finite_portfolio_has_pairwise_disjoint_parent_patches(
|
|
2529
|
+
self.finite_variation_contract,
|
|
2530
|
+
portfolio_size=self.portfolio_size,
|
|
2531
|
+
min_distinct_families=self.min_distinct_families,
|
|
2532
|
+
)
|
|
2533
|
+
):
|
|
2534
|
+
raise ValueError(
|
|
2535
|
+
"finite contract has no feasible pairwise-disjoint portfolio"
|
|
2536
|
+
)
|
|
2537
|
+
if (
|
|
2538
|
+
type(self.max_output_tokens) is not int
|
|
2539
|
+
or not 1 <= self.max_output_tokens <= MAX_OUTPUT_TOKENS
|
|
2540
|
+
):
|
|
2541
|
+
raise ValueError(f"max_output_tokens must lie in [1, {MAX_OUTPUT_TOKENS}]")
|
|
2542
|
+
if self.temperature is not None and (
|
|
2543
|
+
isinstance(self.temperature, bool)
|
|
2544
|
+
or not isinstance(self.temperature, (int, float))
|
|
2545
|
+
or not math.isfinite(float(self.temperature))
|
|
2546
|
+
or not 0 <= float(self.temperature) <= 2
|
|
2547
|
+
):
|
|
2548
|
+
raise ValueError("temperature must be finite in [0,2] or None")
|
|
2549
|
+
object.__setattr__(
|
|
2550
|
+
self,
|
|
2551
|
+
"_validation_receipt",
|
|
2552
|
+
_PortfolioSelectionRequestValidationReceipt(
|
|
2553
|
+
finite_identity_index=finite_identity_index,
|
|
2554
|
+
context_sha256=typed_json_sha256(self.context),
|
|
2555
|
+
card_snapshot_sha256=portfolio_card_snapshot_sha256(self.cards),
|
|
2556
|
+
),
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2559
|
+
@property
|
|
2560
|
+
def context_sha256(self) -> str:
|
|
2561
|
+
if type(self.context) is not FrozenJsonObject:
|
|
2562
|
+
raise TypeError("context must be an exact FrozenJsonObject")
|
|
2563
|
+
if freeze_json(self.context) is not self.context:
|
|
2564
|
+
raise TypeError("context must already be frozen typed JSON")
|
|
2565
|
+
return typed_json_sha256(self.context)
|
|
2566
|
+
|
|
2567
|
+
@property
|
|
2568
|
+
def card_snapshot_sha256(self) -> str:
|
|
2569
|
+
return portfolio_card_snapshot_sha256(self.cards)
|
|
2570
|
+
|
|
2571
|
+
def to_record(self) -> dict[str, object]:
|
|
2572
|
+
self.__post_init__()
|
|
2573
|
+
receipt = self._validation_receipt
|
|
2574
|
+
return {
|
|
2575
|
+
"schema_version": 1,
|
|
2576
|
+
"call_id": self.call_id.value,
|
|
2577
|
+
"operation": self.operation,
|
|
2578
|
+
"instruction_sha256": hashlib.sha256(
|
|
2579
|
+
self.instruction.encode("utf-8", errors="strict")
|
|
2580
|
+
).hexdigest(),
|
|
2581
|
+
"context_sha256": receipt.context_sha256,
|
|
2582
|
+
"finite_contract_identity_sha256": (
|
|
2583
|
+
receipt.finite_identity_index.contract_identity_sha256
|
|
2584
|
+
),
|
|
2585
|
+
"card_snapshot_sha256": receipt.card_snapshot_sha256,
|
|
2586
|
+
"portfolio_size": self.portfolio_size,
|
|
2587
|
+
"required_metric_ids": list(self.required_metric_ids),
|
|
2588
|
+
**(
|
|
2589
|
+
{}
|
|
2590
|
+
if self.source_registry is None
|
|
2591
|
+
else {"source_registry_sha256": self.source_registry.registry_sha256}
|
|
2592
|
+
),
|
|
2593
|
+
**(
|
|
2594
|
+
{}
|
|
2595
|
+
if self.experimental_view_receipt is None
|
|
2596
|
+
else {
|
|
2597
|
+
"experimental_view_receipt_sha256": (
|
|
2598
|
+
self.experimental_view_receipt.receipt_sha256
|
|
2599
|
+
)
|
|
2600
|
+
}
|
|
2601
|
+
),
|
|
2602
|
+
"min_distinct_families": self.min_distinct_families,
|
|
2603
|
+
"require_supporting_cards": self.require_supporting_cards,
|
|
2604
|
+
**(
|
|
2605
|
+
{}
|
|
2606
|
+
if self.memory_dose_contract is None
|
|
2607
|
+
else {"memory_dose_contract": (self.memory_dose_contract.to_record())}
|
|
2608
|
+
),
|
|
2609
|
+
**(
|
|
2610
|
+
{}
|
|
2611
|
+
if not self.candidate_pool_required_option_ids
|
|
2612
|
+
else {
|
|
2613
|
+
"candidate_pool_required_option_ids": list(
|
|
2614
|
+
self.candidate_pool_required_option_ids
|
|
2615
|
+
)
|
|
2616
|
+
}
|
|
2617
|
+
),
|
|
2618
|
+
**(
|
|
2619
|
+
{
|
|
2620
|
+
"require_pairwise_disjoint_parent_patches": True,
|
|
2621
|
+
}
|
|
2622
|
+
if self.require_pairwise_disjoint_parent_patches
|
|
2623
|
+
else {}
|
|
2624
|
+
),
|
|
2625
|
+
"max_output_tokens": self.max_output_tokens,
|
|
2626
|
+
"temperature_hex": (
|
|
2627
|
+
None if self.temperature is None else float(self.temperature).hex()
|
|
2628
|
+
),
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
@property
|
|
2632
|
+
def request_sha256(self) -> str:
|
|
2633
|
+
return _hash(_REQUEST_DOMAIN, self.to_record())
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
@dataclass(frozen=True, slots=True)
|
|
2637
|
+
class PortfolioMemberDraft:
|
|
2638
|
+
"""Framework-neutral structured output before trusted option resolution."""
|
|
2639
|
+
|
|
2640
|
+
option_id: str
|
|
2641
|
+
supporting_card_keys: tuple[str, ...]
|
|
2642
|
+
effect_predictions: tuple[MetricEffectPrediction, ...]
|
|
2643
|
+
design_rationale: str
|
|
2644
|
+
|
|
2645
|
+
def __post_init__(self) -> None:
|
|
2646
|
+
if (
|
|
2647
|
+
type(self.option_id) is not str
|
|
2648
|
+
or _OPTION_ID.fullmatch(self.option_id) is None
|
|
2649
|
+
):
|
|
2650
|
+
raise ValueError("option_id must use the closed option grammar")
|
|
2651
|
+
if type(self.supporting_card_keys) is not tuple or any(
|
|
2652
|
+
type(value) is not str or _TOKEN.fullmatch(value) is None
|
|
2653
|
+
for value in self.supporting_card_keys
|
|
2654
|
+
):
|
|
2655
|
+
raise TypeError(
|
|
2656
|
+
"supporting_card_keys must be an exact tuple of card tokens"
|
|
2657
|
+
)
|
|
2658
|
+
if self.supporting_card_keys != tuple(sorted(set(self.supporting_card_keys))):
|
|
2659
|
+
raise ValueError("supporting_card_keys must be unique and canonical")
|
|
2660
|
+
if type(self.effect_predictions) is not tuple or any(
|
|
2661
|
+
type(value) is not MetricEffectPrediction
|
|
2662
|
+
for value in self.effect_predictions
|
|
2663
|
+
):
|
|
2664
|
+
raise TypeError(
|
|
2665
|
+
"effect_predictions must contain exact MetricEffectPrediction values"
|
|
2666
|
+
)
|
|
2667
|
+
for prediction in self.effect_predictions:
|
|
2668
|
+
MetricEffectPrediction.__post_init__(prediction)
|
|
2669
|
+
metric_ids = tuple(value.metric_id for value in self.effect_predictions)
|
|
2670
|
+
if metric_ids != tuple(sorted(set(metric_ids))):
|
|
2671
|
+
raise ValueError("effect_predictions must be unique and metric-ordered")
|
|
2672
|
+
if (
|
|
2673
|
+
type(self.design_rationale) is not str
|
|
2674
|
+
or not self.design_rationale.strip()
|
|
2675
|
+
or self.design_rationale != self.design_rationale.strip()
|
|
2676
|
+
):
|
|
2677
|
+
raise ValueError("design_rationale must be canonical non-empty text")
|
|
2678
|
+
|
|
2679
|
+
|
|
2680
|
+
@dataclass(frozen=True, slots=True)
|
|
2681
|
+
class RankedPortfolioMember:
|
|
2682
|
+
"""One trusted ranked action resolved against the sealed finite contract."""
|
|
2683
|
+
|
|
2684
|
+
rank: int
|
|
2685
|
+
option_id: str
|
|
2686
|
+
option_identity_sha256: str
|
|
2687
|
+
child_configuration_sha256: str
|
|
2688
|
+
family: str
|
|
2689
|
+
supporting_card_keys: tuple[str, ...]
|
|
2690
|
+
effect_predictions: tuple[MetricEffectPrediction, ...]
|
|
2691
|
+
design_rationale: str
|
|
2692
|
+
|
|
2693
|
+
def __post_init__(self) -> None:
|
|
2694
|
+
if type(self.rank) is not int or self.rank <= 0:
|
|
2695
|
+
raise ValueError("rank must be a positive exact integer")
|
|
2696
|
+
PortfolioMemberDraft(
|
|
2697
|
+
option_id=self.option_id,
|
|
2698
|
+
supporting_card_keys=self.supporting_card_keys,
|
|
2699
|
+
effect_predictions=self.effect_predictions,
|
|
2700
|
+
design_rationale=self.design_rationale,
|
|
2701
|
+
)
|
|
2702
|
+
require_sha256(self.option_identity_sha256, "option_identity_sha256")
|
|
2703
|
+
require_sha256(
|
|
2704
|
+
self.child_configuration_sha256,
|
|
2705
|
+
"child_configuration_sha256",
|
|
2706
|
+
)
|
|
2707
|
+
if type(self.family) is not str or _TOKEN.fullmatch(self.family) is None:
|
|
2708
|
+
raise ValueError("family must use the closed lowercase token grammar")
|
|
2709
|
+
|
|
2710
|
+
def to_record(self) -> dict[str, object]:
|
|
2711
|
+
self.__post_init__()
|
|
2712
|
+
return {
|
|
2713
|
+
"rank": self.rank,
|
|
2714
|
+
"option_id": self.option_id,
|
|
2715
|
+
"option_identity_sha256": self.option_identity_sha256,
|
|
2716
|
+
"child_configuration_sha256": self.child_configuration_sha256,
|
|
2717
|
+
"family": self.family,
|
|
2718
|
+
"supporting_card_keys": list(self.supporting_card_keys),
|
|
2719
|
+
"effect_predictions": [
|
|
2720
|
+
{
|
|
2721
|
+
"metric_id": prediction.metric_id,
|
|
2722
|
+
"direction": prediction.direction.value,
|
|
2723
|
+
}
|
|
2724
|
+
for prediction in self.effect_predictions
|
|
2725
|
+
],
|
|
2726
|
+
"design_rationale_sha256": hashlib.sha256(
|
|
2727
|
+
self.design_rationale.encode("utf-8", errors="strict")
|
|
2728
|
+
).hexdigest(),
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
def to_audit_record(self) -> dict[str, object]:
|
|
2732
|
+
"""Return the committed member record with its rationale plaintext.
|
|
2733
|
+
|
|
2734
|
+
``to_record`` remains the stable hash boundary: the rationale is bound
|
|
2735
|
+
there by digest only. This projection makes model-authored reasoning
|
|
2736
|
+
available to trace analysis while retaining that committed digest for
|
|
2737
|
+
direct verification.
|
|
2738
|
+
"""
|
|
2739
|
+
|
|
2740
|
+
return {**self.to_record(), "design_rationale": self.design_rationale}
|
|
2741
|
+
|
|
2742
|
+
|
|
2743
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
2744
|
+
class RankedPortfolioDecision:
|
|
2745
|
+
"""All-or-nothing ranked selection bound to one exact request snapshot."""
|
|
2746
|
+
|
|
2747
|
+
request_sha256: str
|
|
2748
|
+
context_sha256: str
|
|
2749
|
+
finite_contract_identity_sha256: str
|
|
2750
|
+
card_snapshot_sha256: str
|
|
2751
|
+
members: tuple[RankedPortfolioMember, ...]
|
|
2752
|
+
policy_id: str
|
|
2753
|
+
policy_version: int
|
|
2754
|
+
policy_definition_sha256: str
|
|
2755
|
+
memory_dose_assessment: PortfolioMemoryDoseAssessment | None = None
|
|
2756
|
+
|
|
2757
|
+
def __post_init__(self) -> None:
|
|
2758
|
+
for name in (
|
|
2759
|
+
"request_sha256",
|
|
2760
|
+
"context_sha256",
|
|
2761
|
+
"finite_contract_identity_sha256",
|
|
2762
|
+
"card_snapshot_sha256",
|
|
2763
|
+
"policy_definition_sha256",
|
|
2764
|
+
):
|
|
2765
|
+
require_sha256(getattr(self, name), name)
|
|
2766
|
+
if type(self.members) is not tuple or not self.members:
|
|
2767
|
+
raise ValueError("members must be a non-empty exact tuple")
|
|
2768
|
+
if any(type(member) is not RankedPortfolioMember for member in self.members):
|
|
2769
|
+
raise TypeError("members must contain exact RankedPortfolioMember values")
|
|
2770
|
+
for member in self.members:
|
|
2771
|
+
member.__post_init__()
|
|
2772
|
+
if tuple(member.rank for member in self.members) != tuple(
|
|
2773
|
+
range(1, len(self.members) + 1)
|
|
2774
|
+
):
|
|
2775
|
+
raise ValueError("member ranks must be contiguous and tuple-ordered")
|
|
2776
|
+
option_ids = tuple(member.option_id for member in self.members)
|
|
2777
|
+
if len(set(option_ids)) != len(option_ids):
|
|
2778
|
+
raise ValueError("a ranked portfolio cannot repeat an option")
|
|
2779
|
+
if type(self.policy_id) is not str or _TOKEN.fullmatch(self.policy_id) is None:
|
|
2780
|
+
raise ValueError("policy_id must use the closed lowercase token grammar")
|
|
2781
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
2782
|
+
raise ValueError("policy_version must be a positive exact integer")
|
|
2783
|
+
if self.memory_dose_assessment is not None:
|
|
2784
|
+
if type(self.memory_dose_assessment) is not (PortfolioMemoryDoseAssessment):
|
|
2785
|
+
raise TypeError("memory_dose_assessment must be exact or None")
|
|
2786
|
+
self.memory_dose_assessment.__post_init__()
|
|
2787
|
+
if (
|
|
2788
|
+
self.memory_dose_assessment.stage
|
|
2789
|
+
is not PortfolioMemoryDoseStage.EVALUATED_PORTFOLIO
|
|
2790
|
+
or not self.memory_dose_assessment.passed
|
|
2791
|
+
):
|
|
2792
|
+
raise ValueError(
|
|
2793
|
+
"ranked decision memory dose must be a passing evaluation"
|
|
2794
|
+
)
|
|
2795
|
+
|
|
2796
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
2797
|
+
self.__post_init__()
|
|
2798
|
+
return {
|
|
2799
|
+
"schema_version": 1,
|
|
2800
|
+
"request_sha256": self.request_sha256,
|
|
2801
|
+
"context_sha256": self.context_sha256,
|
|
2802
|
+
"finite_contract_identity_sha256": (self.finite_contract_identity_sha256),
|
|
2803
|
+
"card_snapshot_sha256": self.card_snapshot_sha256,
|
|
2804
|
+
"members": [member.to_record() for member in self.members],
|
|
2805
|
+
"policy_id": self.policy_id,
|
|
2806
|
+
"policy_version": self.policy_version,
|
|
2807
|
+
"policy_definition_sha256": self.policy_definition_sha256,
|
|
2808
|
+
**(
|
|
2809
|
+
{}
|
|
2810
|
+
if self.memory_dose_assessment is None
|
|
2811
|
+
else {
|
|
2812
|
+
"memory_dose_assessment": (self.memory_dose_assessment.to_record())
|
|
2813
|
+
}
|
|
2814
|
+
),
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
@property
|
|
2818
|
+
def decision_sha256(self) -> str:
|
|
2819
|
+
return _hash(_DECISION_DOMAIN, self._unsigned_record())
|
|
2820
|
+
|
|
2821
|
+
def to_record(self) -> dict[str, object]:
|
|
2822
|
+
record = self._unsigned_record()
|
|
2823
|
+
return {**record, "decision_sha256": self.decision_sha256}
|
|
2824
|
+
|
|
2825
|
+
def to_audit_record(self) -> dict[str, object]:
|
|
2826
|
+
"""Return the decision commitment plus auditable member plaintext."""
|
|
2827
|
+
|
|
2828
|
+
record = self.to_record()
|
|
2829
|
+
return {
|
|
2830
|
+
**record,
|
|
2831
|
+
"members": [member.to_audit_record() for member in self.members],
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
def __eq__(self, other: object) -> bool:
|
|
2835
|
+
return (
|
|
2836
|
+
type(self) is RankedPortfolioDecision
|
|
2837
|
+
and type(other) is RankedPortfolioDecision
|
|
2838
|
+
and self.decision_sha256 == other.decision_sha256
|
|
2839
|
+
)
|
|
2840
|
+
|
|
2841
|
+
__hash__ = None
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
def resolve_ranked_portfolio_decision(
|
|
2845
|
+
request: PortfolioSelectionRequest,
|
|
2846
|
+
drafts: tuple[PortfolioMemberDraft, ...],
|
|
2847
|
+
*,
|
|
2848
|
+
policy_id: str,
|
|
2849
|
+
policy_version: int,
|
|
2850
|
+
policy_definition_sha256: str,
|
|
2851
|
+
memory_dose_assessment: PortfolioMemoryDoseAssessment | None = None,
|
|
2852
|
+
) -> RankedPortfolioDecision:
|
|
2853
|
+
"""Resolve the complete structured output or publish no decision."""
|
|
2854
|
+
|
|
2855
|
+
if type(request) is not PortfolioSelectionRequest:
|
|
2856
|
+
raise TypeError("request must be an exact PortfolioSelectionRequest")
|
|
2857
|
+
request.__post_init__()
|
|
2858
|
+
if type(drafts) is not tuple or any(
|
|
2859
|
+
type(draft) is not PortfolioMemberDraft for draft in drafts
|
|
2860
|
+
):
|
|
2861
|
+
raise TypeError("drafts must be an exact tuple of PortfolioMemberDraft values")
|
|
2862
|
+
if len(drafts) != request.portfolio_size:
|
|
2863
|
+
raise ValueError(
|
|
2864
|
+
"selector output does not contain exactly portfolio_size members"
|
|
2865
|
+
)
|
|
2866
|
+
for draft in drafts:
|
|
2867
|
+
draft.__post_init__()
|
|
2868
|
+
option_ids = tuple(draft.option_id for draft in drafts)
|
|
2869
|
+
if len(set(option_ids)) != len(option_ids):
|
|
2870
|
+
raise ValueError("selector output repeats a finite option")
|
|
2871
|
+
card_keys = {card.card_key for card in request.cards}
|
|
2872
|
+
for draft in drafts:
|
|
2873
|
+
if not set(draft.supporting_card_keys).issubset(card_keys):
|
|
2874
|
+
raise ValueError(
|
|
2875
|
+
"selector output cites a card outside the request snapshot"
|
|
2876
|
+
)
|
|
2877
|
+
if request.require_supporting_cards and not draft.supporting_card_keys:
|
|
2878
|
+
raise ValueError("every portfolio member must cite a supporting card")
|
|
2879
|
+
if tuple(value.metric_id for value in draft.effect_predictions) != (
|
|
2880
|
+
request.required_metric_ids
|
|
2881
|
+
):
|
|
2882
|
+
raise ValueError(
|
|
2883
|
+
"every portfolio member must predict the exact required metrics"
|
|
2884
|
+
)
|
|
2885
|
+
|
|
2886
|
+
contract = request.finite_variation_contract
|
|
2887
|
+
options = tuple(contract.resolve(draft.option_id) for draft in drafts)
|
|
2888
|
+
if (
|
|
2889
|
+
request.min_distinct_families is not None
|
|
2890
|
+
and len({option.family for option in options}) < request.min_distinct_families
|
|
2891
|
+
):
|
|
2892
|
+
raise ValueError("selector output violates min_distinct_families")
|
|
2893
|
+
if request.require_pairwise_disjoint_parent_patches:
|
|
2894
|
+
validate_pairwise_disjoint_parent_patch_selection(
|
|
2895
|
+
contract,
|
|
2896
|
+
option_ids,
|
|
2897
|
+
)
|
|
2898
|
+
decision = RankedPortfolioDecision(
|
|
2899
|
+
request_sha256=request.request_sha256,
|
|
2900
|
+
context_sha256=request.context_sha256,
|
|
2901
|
+
finite_contract_identity_sha256=contract.identity_sha256,
|
|
2902
|
+
card_snapshot_sha256=request.card_snapshot_sha256,
|
|
2903
|
+
members=tuple(
|
|
2904
|
+
RankedPortfolioMember(
|
|
2905
|
+
rank=index,
|
|
2906
|
+
option_id=option.option_id,
|
|
2907
|
+
option_identity_sha256=option.identity_sha256,
|
|
2908
|
+
child_configuration_sha256=option.child_configuration_sha256,
|
|
2909
|
+
family=option.family,
|
|
2910
|
+
supporting_card_keys=draft.supporting_card_keys,
|
|
2911
|
+
effect_predictions=draft.effect_predictions,
|
|
2912
|
+
design_rationale=draft.design_rationale,
|
|
2913
|
+
)
|
|
2914
|
+
for index, (draft, option) in enumerate(
|
|
2915
|
+
zip(drafts, options, strict=True),
|
|
2916
|
+
start=1,
|
|
2917
|
+
)
|
|
2918
|
+
),
|
|
2919
|
+
policy_id=policy_id,
|
|
2920
|
+
policy_version=policy_version,
|
|
2921
|
+
policy_definition_sha256=policy_definition_sha256,
|
|
2922
|
+
memory_dose_assessment=memory_dose_assessment,
|
|
2923
|
+
)
|
|
2924
|
+
validate_ranked_portfolio_decision(request, decision)
|
|
2925
|
+
return decision
|
|
2926
|
+
|
|
2927
|
+
|
|
2928
|
+
def validate_ranked_portfolio_decision(
|
|
2929
|
+
request: PortfolioSelectionRequest,
|
|
2930
|
+
decision: RankedPortfolioDecision,
|
|
2931
|
+
) -> None:
|
|
2932
|
+
"""Revalidate a decision against the exact request at any trust boundary."""
|
|
2933
|
+
|
|
2934
|
+
if type(request) is not PortfolioSelectionRequest:
|
|
2935
|
+
raise TypeError("request must be an exact PortfolioSelectionRequest")
|
|
2936
|
+
request.__post_init__()
|
|
2937
|
+
if type(decision) is not RankedPortfolioDecision:
|
|
2938
|
+
raise TypeError("decision must be an exact RankedPortfolioDecision")
|
|
2939
|
+
decision.__post_init__()
|
|
2940
|
+
if (
|
|
2941
|
+
decision.request_sha256 != request.request_sha256
|
|
2942
|
+
or decision.context_sha256 != request.context_sha256
|
|
2943
|
+
or decision.finite_contract_identity_sha256
|
|
2944
|
+
!= request.finite_variation_contract.identity_sha256
|
|
2945
|
+
or decision.card_snapshot_sha256 != request.card_snapshot_sha256
|
|
2946
|
+
):
|
|
2947
|
+
raise ValueError("decision is bound to a different request snapshot")
|
|
2948
|
+
if len(decision.members) != request.portfolio_size:
|
|
2949
|
+
raise ValueError("decision member count differs from portfolio_size")
|
|
2950
|
+
cards = {card.card_key for card in request.cards}
|
|
2951
|
+
option_families: set[str] = set()
|
|
2952
|
+
for member in decision.members:
|
|
2953
|
+
option = request.finite_variation_contract.resolve(member.option_id)
|
|
2954
|
+
if (
|
|
2955
|
+
member.option_identity_sha256 != option.identity_sha256
|
|
2956
|
+
or member.child_configuration_sha256 != option.child_configuration_sha256
|
|
2957
|
+
or member.family != option.family
|
|
2958
|
+
):
|
|
2959
|
+
raise ValueError("decision member differs from its sealed finite option")
|
|
2960
|
+
if not set(member.supporting_card_keys).issubset(cards):
|
|
2961
|
+
raise ValueError("decision member cites a foreign card")
|
|
2962
|
+
if request.require_supporting_cards and not member.supporting_card_keys:
|
|
2963
|
+
raise ValueError("decision member omitted required card attribution")
|
|
2964
|
+
if (
|
|
2965
|
+
tuple(prediction.metric_id for prediction in member.effect_predictions)
|
|
2966
|
+
!= request.required_metric_ids
|
|
2967
|
+
):
|
|
2968
|
+
raise ValueError("decision member metric predictions differ from request")
|
|
2969
|
+
option_families.add(option.family)
|
|
2970
|
+
dose = request.memory_dose_contract
|
|
2971
|
+
assessment = decision.memory_dose_assessment
|
|
2972
|
+
if dose is None:
|
|
2973
|
+
if assessment is not None:
|
|
2974
|
+
raise ValueError("decision supplied memory dose for an unbounded request")
|
|
2975
|
+
else:
|
|
2976
|
+
if assessment is None:
|
|
2977
|
+
raise ValueError(
|
|
2978
|
+
"bounded request requires an evaluated memory-dose receipt"
|
|
2979
|
+
)
|
|
2980
|
+
assessment.__post_init__()
|
|
2981
|
+
if (
|
|
2982
|
+
assessment.contract_sha256 != dose.contract_sha256
|
|
2983
|
+
or assessment.stage is not PortfolioMemoryDoseStage.EVALUATED_PORTFOLIO
|
|
2984
|
+
or not assessment.passed
|
|
2985
|
+
):
|
|
2986
|
+
raise ValueError(
|
|
2987
|
+
"decision memory-dose receipt differs from the request contract"
|
|
2988
|
+
)
|
|
2989
|
+
expected_members = tuple(
|
|
2990
|
+
PortfolioMemoryDoseMember(
|
|
2991
|
+
rank=value.rank,
|
|
2992
|
+
option_id=value.option_id,
|
|
2993
|
+
option_identity_sha256=value.option_identity_sha256,
|
|
2994
|
+
supporting_card_keys=value.supporting_card_keys,
|
|
2995
|
+
)
|
|
2996
|
+
for value in decision.members
|
|
2997
|
+
)
|
|
2998
|
+
if assessment.member_content_binding_sha256s != tuple(
|
|
2999
|
+
value.content_binding_sha256 for value in expected_members
|
|
3000
|
+
):
|
|
3001
|
+
raise ValueError(
|
|
3002
|
+
"decision memory-dose receipt differs from selected members"
|
|
3003
|
+
)
|
|
3004
|
+
if request.min_distinct_families is not None and len(option_families) < (
|
|
3005
|
+
request.min_distinct_families
|
|
3006
|
+
):
|
|
3007
|
+
raise ValueError("decision violates min_distinct_families")
|
|
3008
|
+
if request.require_pairwise_disjoint_parent_patches:
|
|
3009
|
+
validate_pairwise_disjoint_parent_patch_selection(
|
|
3010
|
+
request.finite_variation_contract,
|
|
3011
|
+
tuple(member.option_id for member in decision.members),
|
|
3012
|
+
)
|
|
3013
|
+
|
|
3014
|
+
|
|
3015
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
3016
|
+
class PortfolioSelectionSupplementalAudit:
|
|
3017
|
+
"""Opaque, integrity-bound selector evidence carried beside a v1 decision.
|
|
3018
|
+
|
|
3019
|
+
The ranked decision and materialization receipt remain the stable evaluator
|
|
3020
|
+
boundary. Optional selectors may retain richer policy-specific evidence in
|
|
3021
|
+
``payload`` without making the application layer depend on that policy.
|
|
3022
|
+
"""
|
|
3023
|
+
|
|
3024
|
+
audit_kind: str
|
|
3025
|
+
request_sha256: str
|
|
3026
|
+
decision_sha256: str
|
|
3027
|
+
payload: FrozenJsonObject
|
|
3028
|
+
|
|
3029
|
+
def __post_init__(self) -> None:
|
|
3030
|
+
if (
|
|
3031
|
+
type(self.audit_kind) is not str
|
|
3032
|
+
or _TOKEN.fullmatch(self.audit_kind) is None
|
|
3033
|
+
):
|
|
3034
|
+
raise ValueError("audit_kind must use the closed lowercase token grammar")
|
|
3035
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
3036
|
+
require_sha256(self.decision_sha256, "decision_sha256")
|
|
3037
|
+
if type(self.payload) is not FrozenJsonObject:
|
|
3038
|
+
raise TypeError("payload must be an exact FrozenJsonObject")
|
|
3039
|
+
if freeze_json(self.payload) is not self.payload:
|
|
3040
|
+
raise TypeError("payload must already be frozen typed JSON")
|
|
3041
|
+
|
|
3042
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
3043
|
+
self.__post_init__()
|
|
3044
|
+
return {
|
|
3045
|
+
"schema_version": 1,
|
|
3046
|
+
"audit_kind": self.audit_kind,
|
|
3047
|
+
"request_sha256": self.request_sha256,
|
|
3048
|
+
"decision_sha256": self.decision_sha256,
|
|
3049
|
+
"payload_sha256": typed_json_sha256(self.payload),
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
@property
|
|
3053
|
+
def audit_sha256(self) -> str:
|
|
3054
|
+
return _hash(_SUPPLEMENTAL_AUDIT_DOMAIN, self._unsigned_record())
|
|
3055
|
+
|
|
3056
|
+
def to_record(self) -> dict[str, object]:
|
|
3057
|
+
return {
|
|
3058
|
+
**self._unsigned_record(),
|
|
3059
|
+
"payload": thaw_json(self.payload),
|
|
3060
|
+
"audit_sha256": self.audit_sha256,
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
def __eq__(self, other: object) -> bool:
|
|
3064
|
+
return (
|
|
3065
|
+
type(other) is PortfolioSelectionSupplementalAudit
|
|
3066
|
+
and self.audit_sha256 == other.audit_sha256
|
|
3067
|
+
)
|
|
3068
|
+
|
|
3069
|
+
__hash__ = None
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
@dataclass(frozen=True, slots=True)
|
|
3073
|
+
class PortfolioSelectionResult:
|
|
3074
|
+
"""One selection outcome and the provider evidence that produced it.
|
|
3075
|
+
|
|
3076
|
+
``telemetry`` and ``provider_free`` are mutually exclusive and exactly one
|
|
3077
|
+
must be present. A bare ``None`` telemetry with no assertion is rejected:
|
|
3078
|
+
absence must be asserted and then measured, never inferred from a missing
|
|
3079
|
+
field. A policy that reaches the provider supplies telemetry as before; a
|
|
3080
|
+
policy that does not must say so, and the runtime confirms it against the
|
|
3081
|
+
outbound journals rather than taking the claim on trust.
|
|
3082
|
+
"""
|
|
3083
|
+
|
|
3084
|
+
decision: RankedPortfolioDecision
|
|
3085
|
+
telemetry: AgenticCallTelemetry | None
|
|
3086
|
+
supplemental_audit: PortfolioSelectionSupplementalAudit | None = None
|
|
3087
|
+
provider_free: bool = False
|
|
3088
|
+
|
|
3089
|
+
def __post_init__(self) -> None:
|
|
3090
|
+
if type(self.decision) is not RankedPortfolioDecision:
|
|
3091
|
+
raise TypeError("decision must be an exact RankedPortfolioDecision")
|
|
3092
|
+
self.decision.__post_init__()
|
|
3093
|
+
if type(self.provider_free) is not bool:
|
|
3094
|
+
raise TypeError("provider_free must be an exact bool")
|
|
3095
|
+
if self.provider_free and self.telemetry is not None:
|
|
3096
|
+
raise ValueError(
|
|
3097
|
+
"a provider-free selection cannot also carry call telemetry"
|
|
3098
|
+
)
|
|
3099
|
+
if not self.provider_free and self.telemetry is None:
|
|
3100
|
+
raise ValueError(
|
|
3101
|
+
"selection telemetry is absent and provider_free was not "
|
|
3102
|
+
"asserted; absence must be asserted, not inferred"
|
|
3103
|
+
)
|
|
3104
|
+
if self.telemetry is not None:
|
|
3105
|
+
if type(self.telemetry) is not AgenticCallTelemetry:
|
|
3106
|
+
raise TypeError("telemetry must be exact or None")
|
|
3107
|
+
AgenticCallTelemetry.__post_init__(self.telemetry)
|
|
3108
|
+
if self.supplemental_audit is not None:
|
|
3109
|
+
if type(self.supplemental_audit) is not PortfolioSelectionSupplementalAudit:
|
|
3110
|
+
raise TypeError("supplemental_audit must be exact or None")
|
|
3111
|
+
self.supplemental_audit.__post_init__()
|
|
3112
|
+
if (
|
|
3113
|
+
self.supplemental_audit.request_sha256 != self.decision.request_sha256
|
|
3114
|
+
or self.supplemental_audit.decision_sha256
|
|
3115
|
+
!= self.decision.decision_sha256
|
|
3116
|
+
):
|
|
3117
|
+
raise ValueError(
|
|
3118
|
+
"supplemental audit differs from the ranked decision identity"
|
|
3119
|
+
)
|
|
3120
|
+
|
|
3121
|
+
|
|
3122
|
+
@runtime_checkable
|
|
3123
|
+
class PortfolioSelectionPolicy(Protocol):
|
|
3124
|
+
"""Select one complete ranked portfolio; partial results are not representable."""
|
|
3125
|
+
|
|
3126
|
+
async def select(
|
|
3127
|
+
self,
|
|
3128
|
+
request: PortfolioSelectionRequest,
|
|
3129
|
+
) -> PortfolioSelectionResult: ...
|
|
3130
|
+
|
|
3131
|
+
|
|
3132
|
+
__all__ = [
|
|
3133
|
+
"CANONICAL_NEUTRAL_PORTFOLIO_PROMPT_PAYLOAD",
|
|
3134
|
+
"CANONICAL_REDACTED_PORTFOLIO_EVIDENCE_SHA256",
|
|
3135
|
+
"CardScoreComponent",
|
|
3136
|
+
"CardTransferAdjudicationRequest",
|
|
3137
|
+
"CardTransferAdjudicator",
|
|
3138
|
+
"CardTransferScoreReceipt",
|
|
3139
|
+
"PortfolioCard",
|
|
3140
|
+
"PortfolioCardPromptPayload",
|
|
3141
|
+
"PortfolioCardSourceBinding",
|
|
3142
|
+
"PortfolioCardSourceRegistry",
|
|
3143
|
+
"PortfolioCardViewReceipt",
|
|
3144
|
+
"PortfolioCardViewTransform",
|
|
3145
|
+
"PortfolioExperimentalArm",
|
|
3146
|
+
"PortfolioExperimentalViewReceipt",
|
|
3147
|
+
"PortfolioMemberDraft",
|
|
3148
|
+
"PortfolioSelectionPolicy",
|
|
3149
|
+
"PortfolioSelectionRequest",
|
|
3150
|
+
"PortfolioSelectionResult",
|
|
3151
|
+
"PortfolioSelectionSupplementalAudit",
|
|
3152
|
+
"RankedPortfolioDecision",
|
|
3153
|
+
"RankedPortfolioMember",
|
|
3154
|
+
"derive_portfolio_card_view",
|
|
3155
|
+
"portfolio_card_action_evidence_sha256",
|
|
3156
|
+
"portfolio_card_score_state_sha256",
|
|
3157
|
+
"portfolio_card_snapshot_sha256",
|
|
3158
|
+
"finite_portfolio_has_pairwise_disjoint_parent_patches",
|
|
3159
|
+
"finite_option_ids_have_pairwise_disjoint_parent_patch_subset",
|
|
3160
|
+
"pairwise_disjoint_parent_patch_witness",
|
|
3161
|
+
"pairwise_disjoint_parent_patch_pairs",
|
|
3162
|
+
"single_path_parent_patch_option_ids",
|
|
3163
|
+
"project_family_exposure_bounds_to_pairwise_disjoint_feasibility",
|
|
3164
|
+
"resolve_ranked_portfolio_decision",
|
|
3165
|
+
"validate_card_transfer_score_receipt",
|
|
3166
|
+
"validate_portfolio_experimental_view",
|
|
3167
|
+
"validate_pairwise_disjoint_parent_patch_selection",
|
|
3168
|
+
"validate_ranked_portfolio_decision",
|
|
3169
|
+
]
|