agentevolve-optimizer 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. agent_evolve/__init__.py +722 -0
  2. agent_evolve/agentic.py +2800 -0
  3. agent_evolve/api.py +767 -0
  4. agent_evolve/application/__init__.py +1580 -0
  5. agent_evolve/application/action_allocation.py +744 -0
  6. agent_evolve/application/action_allocation_frame.py +347 -0
  7. agent_evolve/application/action_allocation_frame_commit.py +185 -0
  8. agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
  9. agent_evolve/application/action_allocation_frame_v3.py +338 -0
  10. agent_evolve/application/action_archive_value.py +497 -0
  11. agent_evolve/application/action_evidence_consistency.py +455 -0
  12. agent_evolve/application/action_forecast_partitioning.py +1471 -0
  13. agent_evolve/application/action_metric_projection.py +211 -0
  14. agent_evolve/application/action_role_value.py +680 -0
  15. agent_evolve/application/action_score_authorities.py +363 -0
  16. agent_evolve/application/action_structural_signature.py +116 -0
  17. agent_evolve/application/action_target_realization.py +402 -0
  18. agent_evolve/application/agentic_evolution.py +7734 -0
  19. agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
  20. agent_evolve/application/anchor_residual_identification.py +463 -0
  21. agent_evolve/application/archive_conditioned_action_target.py +208 -0
  22. agent_evolve/application/artifact_journal.py +246 -0
  23. agent_evolve/application/artifact_replay.py +347 -0
  24. agent_evolve/application/budgeted_optimizer.py +1828 -0
  25. agent_evolve/application/calibrated_campaign.py +485 -0
  26. agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
  27. agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
  28. agent_evolve/application/campaign_capacity_recourse.py +254 -0
  29. agent_evolve/application/campaign_contextual_outcomes.py +119 -0
  30. agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
  31. agent_evolve/application/campaign_evidence_registry.py +262 -0
  32. agent_evolve/application/campaign_execution.py +2537 -0
  33. agent_evolve/application/campaign_generation_audit.py +942 -0
  34. agent_evolve/application/campaign_learning.py +1812 -0
  35. agent_evolve/application/campaign_learning_runtime.py +1977 -0
  36. agent_evolve/application/campaign_search_phase.py +227 -0
  37. agent_evolve/application/campaign_selector_context_extension.py +220 -0
  38. agent_evolve/application/campaign_variation_envelope.py +649 -0
  39. agent_evolve/application/campaign_variation_trace.py +451 -0
  40. agent_evolve/application/candidate_archive_consequence.py +128 -0
  41. agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
  42. agent_evolve/application/composite_outcome_updater.py +145 -0
  43. agent_evolve/application/composition_portfolio_selection.py +363 -0
  44. agent_evolve/application/concurrent_stage.py +144 -0
  45. agent_evolve/application/contextual_action_allocation.py +181 -0
  46. agent_evolve/application/contextual_campaign_outcomes.py +267 -0
  47. agent_evolve/application/contextual_campaign_planning.py +1366 -0
  48. agent_evolve/application/contextual_delayed_credit.py +651 -0
  49. agent_evolve/application/contextual_search_controller.py +2374 -0
  50. agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
  51. agent_evolve/application/decision_metric_projection.py +112 -0
  52. agent_evolve/application/derived_action_semantics.py +129 -0
  53. agent_evolve/application/detailed_evaluation.py +449 -0
  54. agent_evolve/application/earned_lineage.py +1011 -0
  55. agent_evolve/application/effective_choice_audit.py +484 -0
  56. agent_evolve/application/empirical_consequence_calibration.py +908 -0
  57. agent_evolve/application/evaluation_accounting.py +325 -0
  58. agent_evolve/application/evaluation_cache.py +199 -0
  59. agent_evolve/application/evaluation_escrow.py +547 -0
  60. agent_evolve/application/evaluation_recourse.py +253 -0
  61. agent_evolve/application/event_recorder.py +151 -0
  62. agent_evolve/application/evolution_campaign.py +1840 -0
  63. agent_evolve/application/executable_hypothesis.py +323 -0
  64. agent_evolve/application/factorial_branch_pilot.py +772 -0
  65. agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
  66. agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
  67. agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
  68. agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
  69. agent_evolve/application/finite_action_selection.py +188 -0
  70. agent_evolve/application/finite_action_set.py +306 -0
  71. agent_evolve/application/finite_action_transition.py +537 -0
  72. agent_evolve/application/finite_variation_eligibility.py +296 -0
  73. agent_evolve/application/forecast_geometry_portfolio.py +799 -0
  74. agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
  75. agent_evolve/application/front_proximity_admission.py +311 -0
  76. agent_evolve/application/front_proximity_parent_basis.py +458 -0
  77. agent_evolve/application/frozen_hurdle_score.py +659 -0
  78. agent_evolve/application/g3_causal_screen.py +2257 -0
  79. agent_evolve/application/g3_causal_validation.py +1046 -0
  80. agent_evolve/application/g3_postseal_curation.py +818 -0
  81. agent_evolve/application/gated_agentic_generator.py +205 -0
  82. agent_evolve/application/generation_feedback.py +293 -0
  83. agent_evolve/application/generative_proposal_journal.py +185 -0
  84. agent_evolve/application/geometry_conditional_elasticity.py +453 -0
  85. agent_evolve/application/global_wave_action_allocation.py +1151 -0
  86. agent_evolve/application/head_mass_conditional_seat.py +268 -0
  87. agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
  88. agent_evolve/application/identifiable_reflection_learning.py +395 -0
  89. agent_evolve/application/identifiable_reflection_request.py +364 -0
  90. agent_evolve/application/in_memory_residual_archive.py +341 -0
  91. agent_evolve/application/insight_memory.py +1804 -0
  92. agent_evolve/application/live_runtime_manifest.py +758 -0
  93. agent_evolve/application/llm_task_queue.py +769 -0
  94. agent_evolve/application/matched_finite_action_block.py +409 -0
  95. agent_evolve/application/materialized_action_broker.py +2328 -0
  96. agent_evolve/application/materialized_action_constraints.py +83 -0
  97. agent_evolve/application/materialized_variation.py +211 -0
  98. agent_evolve/application/multi_option_evolution.py +1536 -0
  99. agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
  100. agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
  101. agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
  102. agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
  103. agent_evolve/application/outcome_relation.py +193 -0
  104. agent_evolve/application/paired_allocation_comparison.py +241 -0
  105. agent_evolve/application/paired_block_schedule.py +127 -0
  106. agent_evolve/application/parent_measurement.py +226 -0
  107. agent_evolve/application/pareto_archive.py +811 -0
  108. agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
  109. agent_evolve/application/portfolio_evolution.py +2950 -0
  110. agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
  111. agent_evolve/application/portfolio_memory_attribution.py +581 -0
  112. agent_evolve/application/portfolio_memory_dose.py +788 -0
  113. agent_evolve/application/portfolio_memory_matched_control.py +938 -0
  114. agent_evolve/application/portfolio_memory_transfer.py +297 -0
  115. agent_evolve/application/portfolio_optimization_memory.py +363 -0
  116. agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
  117. agent_evolve/application/portfolio_projection.py +335 -0
  118. agent_evolve/application/portfolio_recombination.py +2032 -0
  119. agent_evolve/application/post_evolution_reflection.py +834 -0
  120. agent_evolve/application/postcommit_rank_authority.py +245 -0
  121. agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
  122. agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
  123. agent_evolve/application/prequential_residual_exploration.py +343 -0
  124. agent_evolve/application/prequential_score_portfolio.py +954 -0
  125. agent_evolve/application/projections.py +292 -0
  126. agent_evolve/application/protected_action_committee.py +1027 -0
  127. agent_evolve/application/protected_branch_pilot.py +376 -0
  128. agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
  129. agent_evolve/application/provider_replay.py +910 -0
  130. agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
  131. agent_evolve/application/recombination_residual_expert.py +403 -0
  132. agent_evolve/application/reflection_workflow.py +571 -0
  133. agent_evolve/application/region_conditional_credit.py +911 -0
  134. agent_evolve/application/residual_campaign_runtime.py +531 -0
  135. agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
  136. agent_evolve/application/residual_headroom_ledger.py +1544 -0
  137. agent_evolve/application/residual_learning_transaction.py +396 -0
  138. agent_evolve/application/residual_portfolio_evolution.py +1228 -0
  139. agent_evolve/application/residual_reachability.py +749 -0
  140. agent_evolve/application/residual_stage_credit.py +499 -0
  141. agent_evolve/application/same_prefix_paired_audit.py +1580 -0
  142. agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
  143. agent_evolve/application/sequential_lineage_allocation.py +1017 -0
  144. agent_evolve/application/sequential_market_replay.py +1395 -0
  145. agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
  146. agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
  147. agent_evolve/application/single_score_action_allocation.py +299 -0
  148. agent_evolve/application/source_exposure_allocation.py +906 -0
  149. agent_evolve/application/staged_memory.py +210 -0
  150. agent_evolve/application/stratified_cold_start_allocation.py +732 -0
  151. agent_evolve/application/support_guarded_hurdle_score.py +549 -0
  152. agent_evolve/application/target_conditioned_action_forecast.py +595 -0
  153. agent_evolve/application/target_conditioned_campaign.py +566 -0
  154. agent_evolve/application/treatment_assignment.py +201 -0
  155. agent_evolve/application/trusted_objective_evidence.py +217 -0
  156. agent_evolve/application/two_stage_action_evolution.py +1131 -0
  157. agent_evolve/application/v8lite_allocation_policy.py +1083 -0
  158. agent_evolve/application/v9_candidate_policy.py +1303 -0
  159. agent_evolve/bootstrap.py +108 -0
  160. agent_evolve/campaign_presets.py +517 -0
  161. agent_evolve/campaign_profiles.py +452 -0
  162. agent_evolve/campaign_variation_topology.py +288 -0
  163. agent_evolve/campaign_workload.py +950 -0
  164. agent_evolve/cli.py +797 -0
  165. agent_evolve/contract.py +241 -0
  166. agent_evolve/core/__init__.py +91 -0
  167. agent_evolve/core/action_semantics.py +411 -0
  168. agent_evolve/core/authored.py +105 -0
  169. agent_evolve/core/formatting.py +286 -0
  170. agent_evolve/core/optimization_semantics.py +324 -0
  171. agent_evolve/core/problem.py +167 -0
  172. agent_evolve/core/results.py +323 -0
  173. agent_evolve/core/stats.py +70 -0
  174. agent_evolve/core/telemetry.py +100 -0
  175. agent_evolve/domain/__init__.py +89 -0
  176. agent_evolve/domain/artifact.py +162 -0
  177. agent_evolve/domain/durable_text.py +68 -0
  178. agent_evolve/domain/event.py +1454 -0
  179. agent_evolve/domain/finite_action_set.py +426 -0
  180. agent_evolve/domain/finite_variation.py +526 -0
  181. agent_evolve/domain/generative_emission.py +559 -0
  182. agent_evolve/domain/ids.py +163 -0
  183. agent_evolve/domain/inline_text.py +106 -0
  184. agent_evolve/domain/insight.py +27 -0
  185. agent_evolve/domain/lineage.py +737 -0
  186. agent_evolve/domain/llm_task_queue.py +960 -0
  187. agent_evolve/domain/outcome.py +96 -0
  188. agent_evolve/domain/patch.py +854 -0
  189. agent_evolve/domain/typed_json.py +542 -0
  190. agent_evolve/domain/variation_space.py +158 -0
  191. agent_evolve/driver.py +1014 -0
  192. agent_evolve/harness/__init__.py +29 -0
  193. agent_evolve/harness/base.py +242 -0
  194. agent_evolve/harness/directives.py +163 -0
  195. agent_evolve/harness/generative_seal.py +479 -0
  196. agent_evolve/harness/registry.py +41 -0
  197. agent_evolve/infrastructure/__init__.py +39 -0
  198. agent_evolve/infrastructure/artifacts/__init__.py +6 -0
  199. agent_evolve/infrastructure/artifacts/_verification.py +67 -0
  200. agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
  201. agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
  202. agent_evolve/infrastructure/asyncio_runtime.py +109 -0
  203. agent_evolve/infrastructure/authored_runtime.py +188 -0
  204. agent_evolve/infrastructure/authored_worker.py +171 -0
  205. agent_evolve/infrastructure/clock.py +53 -0
  206. agent_evolve/infrastructure/events/__init__.py +6 -0
  207. agent_evolve/infrastructure/events/_validation.py +89 -0
  208. agent_evolve/infrastructure/events/in_memory.py +56 -0
  209. agent_evolve/infrastructure/events/jsonl.py +193 -0
  210. agent_evolve/infrastructure/exception_provenance.py +215 -0
  211. agent_evolve/infrastructure/ids.py +118 -0
  212. agent_evolve/infrastructure/lineage_codec.py +1836 -0
  213. agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
  214. agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
  215. agent_evolve/infrastructure/resource_lease.py +370 -0
  216. agent_evolve/infrastructure/sanitization/__init__.py +8 -0
  217. agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
  218. agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
  219. agent_evolve/infrastructure/stream_liveness.py +383 -0
  220. agent_evolve/infrastructure/subprocess_boundary.py +136 -0
  221. agent_evolve/integrations/__init__.py +1 -0
  222. agent_evolve/integrations/botorch/__init__.py +28 -0
  223. agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
  224. agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
  225. agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
  226. agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
  227. agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
  228. agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
  229. agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
  230. agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
  231. agent_evolve/integrations/completion.py +242 -0
  232. agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
  233. agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
  234. agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
  235. agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
  236. agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
  237. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
  238. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
  239. agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
  240. agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
  241. agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
  242. agent_evolve/integrations/pydantic_ai/harness.py +159 -0
  243. agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
  244. agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
  245. agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
  246. agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
  247. agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
  248. agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
  249. agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
  250. agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
  251. agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
  252. agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
  253. agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
  254. agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
  255. agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
  256. agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
  257. agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
  258. agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
  259. agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
  260. agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
  261. agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
  262. agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
  263. agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
  264. agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
  265. agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
  266. agent_evolve/integrations/pymoo_adapter.py +242 -0
  267. agent_evolve/policies/__init__.py +17 -0
  268. agent_evolve/policies/check.py +469 -0
  269. agent_evolve/policies/emit_scaffold.py +451 -0
  270. agent_evolve/policies/feedback/__init__.py +37 -0
  271. agent_evolve/policies/feedback/held_out_asn.py +1325 -0
  272. agent_evolve/policies/genetic.py +607 -0
  273. agent_evolve/policies/llm_backoff.py +183 -0
  274. agent_evolve/policies/llm_chooser.py +226 -0
  275. agent_evolve/policies/llm_generator.py +1760 -0
  276. agent_evolve/policies/llm_init.py +267 -0
  277. agent_evolve/policies/llm_operator.py +109 -0
  278. agent_evolve/policies/llm_prior.py +194 -0
  279. agent_evolve/policies/llm_surrogate.py +334 -0
  280. agent_evolve/policies/measurement_evidence.py +704 -0
  281. agent_evolve/policies/memory/__init__.py +223 -0
  282. agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
  283. agent_evolve/policies/memory/compatibility_matching.py +593 -0
  284. agent_evolve/policies/memory/global_falsification.py +1841 -0
  285. agent_evolve/policies/memory/prompt_shape.py +503 -0
  286. agent_evolve/policies/memory/randomized_subset.py +714 -0
  287. agent_evolve/policies/memory/staged_causal.py +1270 -0
  288. agent_evolve/policies/memory/treatment_compliance.py +759 -0
  289. agent_evolve/policies/objective_resolution/__init__.py +17 -0
  290. agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
  291. agent_evolve/policies/operator_portfolio.py +407 -0
  292. agent_evolve/policies/reguidance.py +1133 -0
  293. agent_evolve/policies/reward/__init__.py +83 -0
  294. agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
  295. agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
  296. agent_evolve/policies/reward/affine_hypervolume.py +490 -0
  297. agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
  298. agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
  299. agent_evolve/policies/reward/frozen_archive.py +360 -0
  300. agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
  301. agent_evolve/policies/search_state.py +208 -0
  302. agent_evolve/policies/selection/__init__.py +345 -0
  303. agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
  304. agent_evolve/policies/selection/affine_frontier_context.py +330 -0
  305. agent_evolve/policies/selection/affine_frontier_target.py +473 -0
  306. agent_evolve/policies/selection/archive_elite.py +1346 -0
  307. agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
  308. agent_evolve/policies/selection/calibrated_slate.py +1394 -0
  309. agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
  310. agent_evolve/policies/selection/common_candidate_pool.py +685 -0
  311. agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
  312. agent_evolve/policies/selection/disjoint_pairs.py +479 -0
  313. agent_evolve/policies/selection/elite_explorer.py +719 -0
  314. agent_evolve/policies/selection/finite_action.py +187 -0
  315. agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
  316. agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
  317. agent_evolve/policies/selection/forecast_calibration.py +922 -0
  318. agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
  319. agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
  320. agent_evolve/policies/selection/full_support_slate.py +91 -0
  321. agent_evolve/policies/selection/meaningful_direction.py +240 -0
  322. agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
  323. agent_evolve/policies/selection/model_anchored_slate.py +826 -0
  324. agent_evolve/policies/selection/phenotype_recourse.py +979 -0
  325. agent_evolve/policies/selection/proposal_support.py +368 -0
  326. agent_evolve/policies/selection/random_portfolio.py +254 -0
  327. agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
  328. agent_evolve/policies/selection/residual_frontier.py +463 -0
  329. agent_evolve/policies/selection/residual_frontier_target.py +605 -0
  330. agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
  331. agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
  332. agent_evolve/policies/selection/target_conditioned_features.py +812 -0
  333. agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
  334. agent_evolve/policies/selection/task_keyed_palette.py +906 -0
  335. agent_evolve/policies/semantics.py +147 -0
  336. agent_evolve/policies/structure.py +362 -0
  337. agent_evolve/policies/structured_output_budget.py +62 -0
  338. agent_evolve/policies/surrogate.py +696 -0
  339. agent_evolve/policies/variation/__init__.py +1 -0
  340. agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
  341. agent_evolve/policies/variation/crossover_inheritance.py +575 -0
  342. agent_evolve/policies/variation/disjoint_recombination.py +611 -0
  343. agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
  344. agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
  345. agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
  346. agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
  347. agent_evolve/policies/variation/typed_patch.py +1981 -0
  348. agent_evolve/policies/weighted_prior.py +394 -0
  349. agent_evolve/ports/__init__.py +383 -0
  350. agent_evolve/ports/action_allocation.py +733 -0
  351. agent_evolve/ports/action_allocation_frame.py +1153 -0
  352. agent_evolve/ports/action_allocation_frame_commit.py +294 -0
  353. agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
  354. agent_evolve/ports/action_allocation_frame_v3.py +995 -0
  355. agent_evolve/ports/action_forecast.py +1568 -0
  356. agent_evolve/ports/action_metric_projection.py +165 -0
  357. agent_evolve/ports/agentic_generator.py +1561 -0
  358. agent_evolve/ports/archive_context.py +136 -0
  359. agent_evolve/ports/artifact_sanitizer.py +44 -0
  360. agent_evolve/ports/artifact_store.py +225 -0
  361. agent_evolve/ports/clock.py +13 -0
  362. agent_evolve/ports/contextual_search_allocation.py +827 -0
  363. agent_evolve/ports/decision_metric_projection.py +258 -0
  364. agent_evolve/ports/event_store.py +55 -0
  365. agent_evolve/ports/executable_hypothesis.py +557 -0
  366. agent_evolve/ports/finite_acquisition.py +377 -0
  367. agent_evolve/ports/finite_acquisition_batch.py +296 -0
  368. agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
  369. agent_evolve/ports/finite_acquisition_json.py +247 -0
  370. agent_evolve/ports/finite_acquisition_space.py +168 -0
  371. agent_evolve/ports/finite_action_selection.py +348 -0
  372. agent_evolve/ports/finite_action_set.py +256 -0
  373. agent_evolve/ports/frontier_target.py +396 -0
  374. agent_evolve/ports/generation_failure.py +43 -0
  375. agent_evolve/ports/hard_feasibility.py +233 -0
  376. agent_evolve/ports/id_factory.py +34 -0
  377. agent_evolve/ports/llm_task_queue.py +93 -0
  378. agent_evolve/ports/objective_resolution.py +419 -0
  379. agent_evolve/ports/paired_allocation_comparison.py +401 -0
  380. agent_evolve/ports/paired_block_schedule.py +475 -0
  381. agent_evolve/ports/parent_measurement.py +336 -0
  382. agent_evolve/ports/portfolio_memory_dose.py +643 -0
  383. agent_evolve/ports/portfolio_selection.py +3169 -0
  384. agent_evolve/ports/postcommit_rank_authority.py +467 -0
  385. agent_evolve/ports/presented_action_evidence.py +794 -0
  386. agent_evolve/ports/resource_lease.py +162 -0
  387. agent_evolve/ports/structured_generator.py +734 -0
  388. agent_evolve/ports/structured_output_budget.py +120 -0
  389. agent_evolve/ports/subprocess_boundary.py +138 -0
  390. agent_evolve/ports/treatment_assignment.py +466 -0
  391. agent_evolve/ports/variation_catalog.py +76 -0
  392. agent_evolve/ports/variation_source.py +226 -0
  393. agent_evolve/proposal_mode.py +157 -0
  394. agent_evolve/proposers/__init__.py +10 -0
  395. agent_evolve/proposers/random_proposer.py +188 -0
  396. agent_evolve/provider_accounting.py +163 -0
  397. agent_evolve/py.typed +0 -0
  398. agent_evolve/reference_method.py +1570 -0
  399. agent_evolve/session/__init__.py +11 -0
  400. agent_evolve/session/authorship.py +864 -0
  401. agent_evolve/session/evaluate.py +236 -0
  402. agent_evolve/session/fidelity.py +237 -0
  403. agent_evolve/session/genetic_loop.py +742 -0
  404. agent_evolve/session/loop.py +803 -0
  405. agent_evolve/session/screening.py +671 -0
  406. agent_evolve/settings.py +376 -0
  407. agent_evolve/workload_kit.py +368 -0
  408. agent_evolve/workload_prompt.py +398 -0
  409. agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
  410. agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
  411. agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
  412. agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
  413. agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
  414. agentevolve_optimizer-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2950 @@
1
+ """One-call ranked portfolio selection with concurrent exact evaluation.
2
+
3
+ The model selects only opaque IDs from a parent-bound finite contract. This
4
+ application service resolves every ranked ID, derives an exact parent-relative
5
+ patch, materializes every sealed child under engine authority, and submits the
6
+ whole wave through ``AgenticEvolutionEngine.run_materialized_invocations``.
7
+ Optional insight credit is recorded once for the complete wave, never once per
8
+ child.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import math
16
+ import re
17
+ from collections.abc import Callable
18
+ from dataclasses import dataclass, field, replace
19
+ from decimal import Decimal
20
+ from enum import Enum
21
+ from typing import Protocol, runtime_checkable
22
+
23
+ from agent_evolve.application.agentic_evolution import (
24
+ EvolutionCandidate,
25
+ InvocationOutcome,
26
+ InvocationPlan,
27
+ MaterializedInvocation,
28
+ OperatorKind,
29
+ ProposalAuthority,
30
+ RewardPolicyBinding,
31
+ )
32
+ from agent_evolve.application.insight_memory import (
33
+ InsightLifecycleState,
34
+ InsightMemoryBank,
35
+ InsightOrigin,
36
+ QuarantineTestAdmissionReceipt,
37
+ )
38
+ from agent_evolve.application.outcome_relation import OutcomeRelation
39
+ from agent_evolve.application.portfolio_memory_matched_control import (
40
+ PortfolioMemoryMatchedArmAssignment,
41
+ PortfolioMemoryMatchedArmView,
42
+ PortfolioMemoryMatchedControlPlan,
43
+ )
44
+ from agent_evolve.domain.ids import (
45
+ ArtifactId,
46
+ CandidateId,
47
+ LLMCallId,
48
+ OperatorInvocationId,
49
+ )
50
+ from agent_evolve.domain.insight import InsightRef
51
+ from agent_evolve.domain.outcome import (
52
+ FailureCategory,
53
+ FailureCode,
54
+ FailureRecord,
55
+ validate_failure_pair,
56
+ )
57
+ from agent_evolve.domain.patch import ArrayIndex, JsonPath, ObjectKey, require_sha256
58
+ from agent_evolve.domain.typed_json import (
59
+ FrozenJsonObject,
60
+ freeze_json,
61
+ thaw_json,
62
+ typed_json_equal,
63
+ typed_json_sha256,
64
+ )
65
+ from agent_evolve.policies.memory.randomized_subset import (
66
+ InsightSelectionDecision,
67
+ InsightTrial,
68
+ )
69
+ from agent_evolve.policies.memory.staged_causal import (
70
+ MemoryScoreSnapshot,
71
+ ResolvedInsightAssignment,
72
+ insight_selection_decision_sha256,
73
+ )
74
+ from agent_evolve.policies.variation.typed_patch import derive_patch
75
+ from agent_evolve.ports.agentic_generator import (
76
+ AgenticCallTelemetry,
77
+ CandidateDraft,
78
+ MetricEffectPrediction,
79
+ SourceAttribution,
80
+ )
81
+ from agent_evolve.ports.id_factory import IdFactory
82
+ from agent_evolve.ports.portfolio_selection import (
83
+ PortfolioSelectionPolicy,
84
+ PortfolioSelectionRequest,
85
+ PortfolioSelectionResult,
86
+ PortfolioSelectionSupplementalAudit,
87
+ RankedPortfolioDecision,
88
+ RankedPortfolioMember,
89
+ validate_ranked_portfolio_decision,
90
+ )
91
+
92
+
93
+ PORTFOLIO_MATERIALIZATION_POLICY_ID = "ranked_portfolio_exact_finite_option"
94
+ PORTFOLIO_MATERIALIZATION_POLICY_VERSION = 1
95
+
96
+ _TOKEN = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
97
+ _TELEMETRY_DOMAIN = b"agent-evolve:portfolio-evolution-telemetry:v1\x00"
98
+ _MATERIALIZATION_DOMAIN = b"agent-evolve:portfolio-evolution-materialization:v1\x00"
99
+ _MEMBER_DOMAIN = b"agent-evolve:portfolio-evolution-member:v1\x00"
100
+ _CANDIDATE_FAILURE_DOMAIN = (
101
+ b"agent-evolve:portfolio-candidate-failure-evidence:v1\x00"
102
+ )
103
+ _ACTION_CARD_ATTRIBUTION_DOMAIN = (
104
+ b"agent-evolve:portfolio-action-card-attribution:v1\x00"
105
+ )
106
+ _ACTION_ATTRIBUTION_DOMAIN = b"agent-evolve:portfolio-action-attribution:v1\x00"
107
+ _MEMORY_CREDIT_DOMAIN = b"agent-evolve:portfolio-evolution-memory-credit:v1\x00"
108
+ _PENDING_MEMORY_CREDIT_DOMAIN = (
109
+ b"agent-evolve:portfolio-evolution-pending-memory-credit:v1\x00"
110
+ )
111
+ _MEMORY_CREDIT_BATCH_DOMAIN = (
112
+ b"agent-evolve:portfolio-evolution-memory-credit-batch:v1\x00"
113
+ )
114
+ _MEMORY_CREDIT_BATCH_PREPARATION_DOMAIN = (
115
+ b"agent-evolve:portfolio-evolution-memory-credit-batch-preparation:v1\x00"
116
+ )
117
+ _MEMORY_TREATMENT_BINDING_DOMAIN = (
118
+ b"agent-evolve:portfolio-memory-treatment-binding:v1\x00"
119
+ )
120
+ _MEMORY_CONTEXT_PROJECTION_DOMAIN = (
121
+ b"agent-evolve:portfolio-memory-context-projection:v1\x00"
122
+ )
123
+ _WAVE_DOMAIN = b"agent-evolve:portfolio-evolution-wave:v1\x00"
124
+ _PREPARED_WAVE_DOMAIN = b"agent-evolve:portfolio-evolution-prepared-wave:v1\x00"
125
+ _AGGREGATION_BINDING_DOMAIN = (
126
+ b"agent-evolve:portfolio-reward-aggregation-binding:v1\x00"
127
+ )
128
+
129
+
130
+ def _canonical_json(record: dict[str, object]) -> bytes:
131
+ return json.dumps(
132
+ record,
133
+ ensure_ascii=True,
134
+ allow_nan=False,
135
+ separators=(",", ":"),
136
+ sort_keys=True,
137
+ ).encode("ascii", errors="strict")
138
+
139
+
140
+ def _hash_record(domain: bytes, record: dict[str, object]) -> str:
141
+ return hashlib.sha256(domain + _canonical_json(record)).hexdigest()
142
+
143
+
144
+ def _require_token(value: str, name: str) -> None:
145
+ if type(value) is not str or _TOKEN.fullmatch(value) is None:
146
+ raise ValueError(f"{name} must use the closed lowercase token grammar")
147
+
148
+
149
+ def _path_text(path: JsonPath) -> str:
150
+ """Project a typed patch path into the engine's candidate-path vocabulary."""
151
+
152
+ parts = ["$"]
153
+ for segment in path.segments:
154
+ if type(segment) is ObjectKey:
155
+ parts.append(f".{segment.value}")
156
+ elif type(segment) is ArrayIndex:
157
+ parts.append(f"[{segment.value}]")
158
+ else: # pragma: no cover - JsonPath closes the segment union.
159
+ raise AssertionError("unsupported JSON-path segment")
160
+ return "".join(parts)
161
+
162
+
163
+ def _telemetry_record(telemetry: AgenticCallTelemetry) -> dict[str, object]:
164
+ if type(telemetry) is not AgenticCallTelemetry:
165
+ raise TypeError("selection telemetry must be exact")
166
+ AgenticCallTelemetry.__post_init__(telemetry)
167
+ for name in ("provider_response_id", "finish_reason"):
168
+ value = getattr(telemetry, name)
169
+ if value is not None and type(value) is not str:
170
+ raise TypeError(f"telemetry {name} must be an exact string or None")
171
+ cost = telemetry.cost_usd
172
+ if cost is not None:
173
+ if type(cost) is not Decimal:
174
+ raise TypeError("telemetry cost_usd must be an exact Decimal or None")
175
+ if not cost.is_finite() or cost < 0:
176
+ raise ValueError("telemetry cost_usd must be finite and non-negative")
177
+ return {
178
+ "requested_model": telemetry.requested_model,
179
+ "resolved_model": telemetry.resolved_model,
180
+ "resolved_provider": telemetry.resolved_provider,
181
+ "provider_response_id": telemetry.provider_response_id,
182
+ "finish_reason": telemetry.finish_reason,
183
+ "input_tokens": telemetry.input_tokens,
184
+ "output_tokens": telemetry.output_tokens,
185
+ "reasoning_tokens": telemetry.reasoning_tokens,
186
+ "cache_read_tokens": telemetry.cache_read_tokens,
187
+ "cache_write_tokens": telemetry.cache_write_tokens,
188
+ "cost_usd": None if cost is None else str(cost),
189
+ "latency_ns": telemetry.latency_ns,
190
+ "attempt_count": telemetry.attempt_count,
191
+ }
192
+
193
+
194
+ def portfolio_selection_telemetry_sha256(
195
+ telemetry: AgenticCallTelemetry,
196
+ ) -> str:
197
+ """Return the exact identity of one ranked-selector call's telemetry."""
198
+
199
+ return _hash_record(_TELEMETRY_DOMAIN, _telemetry_record(telemetry))
200
+
201
+
202
+ PortfolioRewardAggregator = Callable[[tuple[InvocationOutcome, ...]], float]
203
+
204
+
205
+ EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256 = hashlib.sha256(
206
+ b"agent-evolve:portfolio-memory-context-projection:exact-identity:v1"
207
+ ).hexdigest()
208
+ MEMORY_ESTIMAND_CONTEXT_KEY = "memory_estimand_context"
209
+ MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256 = hashlib.sha256(
210
+ b"agent-evolve:portfolio-memory-context-projection:reserved-root-object-subtree:v1"
211
+ ).hexdigest()
212
+
213
+
214
+ @dataclass(frozen=True, slots=True)
215
+ class PortfolioRewardAggregationBinding:
216
+ """Identified aggregate endpoint for one multi-candidate credit unit."""
217
+
218
+ aggregate: PortfolioRewardAggregator
219
+ aggregation_id: str
220
+ aggregation_version: int
221
+ definition_sha256: str
222
+
223
+ def __post_init__(self) -> None:
224
+ if not callable(self.aggregate):
225
+ raise TypeError("aggregate must be callable")
226
+ _require_token(self.aggregation_id, "aggregation_id")
227
+ if type(self.aggregation_version) is not int or self.aggregation_version <= 0:
228
+ raise ValueError("aggregation_version must be a positive exact integer")
229
+ require_sha256(self.definition_sha256, "definition_sha256")
230
+
231
+ def to_record(self) -> dict[str, object]:
232
+ self.__post_init__()
233
+ return {
234
+ "aggregation_id": self.aggregation_id,
235
+ "aggregation_version": self.aggregation_version,
236
+ "definition_sha256": self.definition_sha256,
237
+ }
238
+
239
+ @property
240
+ def binding_sha256(self) -> str:
241
+ return _hash_record(_AGGREGATION_BINDING_DOMAIN, self.to_record())
242
+
243
+
244
+ @dataclass(frozen=True, slots=True)
245
+ class PortfolioMemoryContextProjectionBinding:
246
+ """Replay one core-owned selector-context to estimand-context projection."""
247
+
248
+ estimand_context_sha256: str
249
+ selector_context_sha256: str
250
+ projection_key: str | None
251
+
252
+ def __post_init__(self) -> None:
253
+ require_sha256(self.estimand_context_sha256, "estimand_context_sha256")
254
+ require_sha256(self.selector_context_sha256, "selector_context_sha256")
255
+ if self.projection_key not in (None, MEMORY_ESTIMAND_CONTEXT_KEY):
256
+ raise ValueError("projection_key must use a core-owned projection")
257
+ if (
258
+ self.projection_key is None
259
+ and self.estimand_context_sha256 != self.selector_context_sha256
260
+ ):
261
+ raise ValueError("exact context projection requires identical hashes")
262
+
263
+ @property
264
+ def projection_id(self) -> str:
265
+ return (
266
+ "exact_context_identity"
267
+ if self.projection_key is None
268
+ else "reserved_memory_estimand_subtree"
269
+ )
270
+
271
+ @property
272
+ def projection_version(self) -> int:
273
+ return 1
274
+
275
+ @property
276
+ def definition_sha256(self) -> str:
277
+ return (
278
+ EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256
279
+ if self.projection_key is None
280
+ else MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256
281
+ )
282
+
283
+ @classmethod
284
+ def exact_identity(cls, context_sha256: str):
285
+ require_sha256(context_sha256, "context_sha256")
286
+ return cls(
287
+ estimand_context_sha256=context_sha256,
288
+ selector_context_sha256=context_sha256,
289
+ projection_key=None,
290
+ )
291
+
292
+ @classmethod
293
+ def from_selector_context(
294
+ cls,
295
+ selector_context: FrozenJsonObject,
296
+ ) -> "PortfolioMemoryContextProjectionBinding":
297
+ projected = cls._project_reserved_subtree(selector_context)
298
+ return cls(
299
+ estimand_context_sha256=typed_json_sha256(projected),
300
+ selector_context_sha256=typed_json_sha256(selector_context),
301
+ projection_key=MEMORY_ESTIMAND_CONTEXT_KEY,
302
+ )
303
+
304
+ @staticmethod
305
+ def _project_reserved_subtree(
306
+ selector_context: FrozenJsonObject,
307
+ ) -> FrozenJsonObject:
308
+ if type(selector_context) is not FrozenJsonObject:
309
+ raise TypeError("selector_context must be an exact FrozenJsonObject")
310
+ values = dict(selector_context.items)
311
+ projected = values.get(MEMORY_ESTIMAND_CONTEXT_KEY)
312
+ if type(projected) is not FrozenJsonObject:
313
+ raise ValueError(
314
+ "selector context must contain the reserved memory estimand object"
315
+ )
316
+ return projected
317
+
318
+ def replay(self, selector_context: FrozenJsonObject) -> FrozenJsonObject:
319
+ self.__post_init__()
320
+ observed_selector_sha256 = typed_json_sha256(selector_context)
321
+ if observed_selector_sha256 != self.selector_context_sha256:
322
+ raise ValueError(
323
+ "context projection selector hash differs from selector context"
324
+ )
325
+ if self.projection_key is None:
326
+ projected = selector_context
327
+ else:
328
+ projected = self._project_reserved_subtree(selector_context)
329
+ if typed_json_sha256(projected) != self.estimand_context_sha256:
330
+ raise ValueError("context projection subtree differs from estimand context")
331
+ return projected
332
+
333
+ def to_record(self) -> dict[str, object]:
334
+ self.__post_init__()
335
+ return {
336
+ "schema_version": 1,
337
+ "estimand_context_sha256": self.estimand_context_sha256,
338
+ "selector_context_sha256": self.selector_context_sha256,
339
+ "projection_path": (
340
+ [] if self.projection_key is None else [self.projection_key]
341
+ ),
342
+ "projection_id": self.projection_id,
343
+ "projection_version": self.projection_version,
344
+ "definition_sha256": self.definition_sha256,
345
+ }
346
+
347
+ @property
348
+ def binding_sha256(self) -> str:
349
+ return _hash_record(_MEMORY_CONTEXT_PROJECTION_DOMAIN, self.to_record())
350
+
351
+
352
+ @dataclass(frozen=True, slots=True)
353
+ class PortfolioMemoryCreditPlan:
354
+ """One randomized card assignment credited once across the complete wave."""
355
+
356
+ decision: InsightSelectionDecision
357
+ credit_unit_id: OperatorInvocationId
358
+ aggregation: PortfolioRewardAggregationBinding
359
+ card_snapshot_sha256: str
360
+ score_snapshot: MemoryScoreSnapshot
361
+ assignment: ResolvedInsightAssignment
362
+ card_source_registry_sha256: str | None = None
363
+ quarantine_admission: QuarantineTestAdmissionReceipt | None = None
364
+ quarantine_admission_subset_authorization_sha256: str | None = None
365
+ context_projection: PortfolioMemoryContextProjectionBinding | None = None
366
+
367
+ def __post_init__(self) -> None:
368
+ if type(self.decision) is not InsightSelectionDecision:
369
+ raise TypeError("decision must be an exact InsightSelectionDecision")
370
+ InsightSelectionDecision.__post_init__(self.decision)
371
+ if not self.decision.credit_identifiable:
372
+ raise ValueError("portfolio memory credit requires identifiable overlap")
373
+ if type(self.credit_unit_id) is not OperatorInvocationId:
374
+ raise TypeError("credit_unit_id must be an exact OperatorInvocationId")
375
+ OperatorInvocationId.__post_init__(self.credit_unit_id)
376
+ if type(self.aggregation) is not PortfolioRewardAggregationBinding:
377
+ raise TypeError("aggregation must be an exact binding")
378
+ PortfolioRewardAggregationBinding.__post_init__(self.aggregation)
379
+ require_sha256(self.card_snapshot_sha256, "card_snapshot_sha256")
380
+ if type(self.score_snapshot) is not MemoryScoreSnapshot:
381
+ raise TypeError("score_snapshot must be an exact MemoryScoreSnapshot")
382
+ MemoryScoreSnapshot.__post_init__(self.score_snapshot)
383
+ if type(self.assignment) is not ResolvedInsightAssignment:
384
+ raise TypeError("assignment must be an exact ResolvedInsightAssignment")
385
+ ResolvedInsightAssignment.__post_init__(self.assignment)
386
+ self.assignment.validate_against_snapshot(self.score_snapshot)
387
+ if (
388
+ self.assignment.credit_unit_id != self.credit_unit_id
389
+ or self.assignment.selection_decision != self.decision
390
+ or self.assignment.prompt_shape_sha256 != self.card_snapshot_sha256
391
+ ):
392
+ raise ValueError(
393
+ "resolved assignment differs from the credit unit, decision, or cards"
394
+ )
395
+ if self.card_source_registry_sha256 is not None:
396
+ require_sha256(
397
+ self.card_source_registry_sha256,
398
+ "card_source_registry_sha256",
399
+ )
400
+ if self.quarantine_admission is not None:
401
+ if type(self.quarantine_admission) is not QuarantineTestAdmissionReceipt:
402
+ raise TypeError("quarantine_admission must be an exact receipt or None")
403
+ QuarantineTestAdmissionReceipt.__post_init__(
404
+ self.quarantine_admission
405
+ )
406
+ subset_authorization = (
407
+ self.quarantine_admission_subset_authorization_sha256
408
+ )
409
+ if subset_authorization is not None:
410
+ if self.quarantine_admission is None:
411
+ raise ValueError(
412
+ "quarantine subset authorization requires an admission"
413
+ )
414
+ require_sha256(
415
+ subset_authorization,
416
+ "quarantine_admission_subset_authorization_sha256",
417
+ )
418
+ projection = self.context_projection
419
+ if projection is not None:
420
+ if type(projection) is not PortfolioMemoryContextProjectionBinding:
421
+ raise TypeError("context_projection must be an exact binding or None")
422
+ PortfolioMemoryContextProjectionBinding.__post_init__(projection)
423
+ if projection.estimand_context_sha256 != self.decision.context_hash:
424
+ raise ValueError(
425
+ "context projection estimand differs from memory decision context"
426
+ )
427
+
428
+ @property
429
+ def treatment_binding_sha256(self) -> str:
430
+ self.__post_init__()
431
+ return _hash_record(
432
+ _MEMORY_TREATMENT_BINDING_DOMAIN,
433
+ {
434
+ "schema_version": 2,
435
+ "selection_decision_sha256": (
436
+ insight_selection_decision_sha256(self.decision)
437
+ ),
438
+ "card_snapshot_sha256": self.card_snapshot_sha256,
439
+ "card_source_registry_sha256": (
440
+ self.card_source_registry_sha256
441
+ ),
442
+ "assignment_receipt_sha256": self.assignment.assignment_sha256,
443
+ "score_snapshot_sha256": self.score_snapshot.snapshot_sha256,
444
+ "quarantine_admission_receipt_sha256": (
445
+ None
446
+ if self.quarantine_admission is None
447
+ else self.quarantine_admission.receipt_sha256
448
+ ),
449
+ "quarantine_admission_subset_authorization_sha256": (
450
+ self.quarantine_admission_subset_authorization_sha256
451
+ ),
452
+ },
453
+ )
454
+
455
+ def resolve_context_projection(
456
+ self,
457
+ selector_context: FrozenJsonObject,
458
+ ) -> PortfolioMemoryContextProjectionBinding:
459
+ self.__post_init__()
460
+ if type(selector_context) is not FrozenJsonObject:
461
+ raise TypeError("selector_context must be an exact FrozenJsonObject")
462
+ selector_context_sha256 = typed_json_sha256(selector_context)
463
+ projection = self.context_projection
464
+ if projection is None:
465
+ if self.decision.context_hash != selector_context_sha256:
466
+ raise ValueError(
467
+ "memory decision context differs from selector context; an "
468
+ "explicit authenticated context projection is required"
469
+ )
470
+ return PortfolioMemoryContextProjectionBinding.exact_identity(
471
+ selector_context_sha256
472
+ )
473
+ projection.replay(selector_context)
474
+ if projection.estimand_context_sha256 != self.decision.context_hash:
475
+ raise ValueError(
476
+ "context projection subtree differs from memory decision context"
477
+ )
478
+ return projection
479
+
480
+
481
+ @dataclass(frozen=True, slots=True)
482
+ class PortfolioMemoryMatchedControlWavePlan:
483
+ """One arm of a precommitted active-versus-neutral diagnostic pair."""
484
+
485
+ plan: PortfolioMemoryMatchedControlPlan
486
+ assignment: PortfolioMemoryMatchedArmAssignment
487
+ arm_view: PortfolioMemoryMatchedArmView
488
+ aggregation: PortfolioRewardAggregationBinding
489
+ context_projection: PortfolioMemoryContextProjectionBinding
490
+
491
+ def __post_init__(self) -> None:
492
+ if type(self.plan) is not PortfolioMemoryMatchedControlPlan:
493
+ raise TypeError("plan must be an exact matched-control plan")
494
+ self.plan.__post_init__()
495
+ if type(self.assignment) is not PortfolioMemoryMatchedArmAssignment:
496
+ raise TypeError("assignment must be an exact matched arm assignment")
497
+ self.assignment.__post_init__()
498
+ if self.assignment not in self.plan.assignments:
499
+ raise ValueError("matched arm assignment is outside its plan")
500
+ if type(self.arm_view) is not PortfolioMemoryMatchedArmView:
501
+ raise TypeError("arm_view must be an exact matched arm view")
502
+ self.arm_view.__post_init__()
503
+ if (
504
+ self.arm_view.plan_sha256 != self.plan.plan_sha256
505
+ or self.arm_view.assignment != self.assignment
506
+ ):
507
+ raise ValueError("matched arm view differs from its plan assignment")
508
+ if type(self.aggregation) is not PortfolioRewardAggregationBinding:
509
+ raise TypeError("aggregation must be an exact reward binding")
510
+ self.aggregation.__post_init__()
511
+ if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
512
+ raise TypeError("context_projection must be an exact binding")
513
+ self.context_projection.__post_init__()
514
+ if (
515
+ self.context_projection.estimand_context_sha256
516
+ != self.plan.exact_context_sha256
517
+ ):
518
+ raise ValueError("matched context projection differs from its plan")
519
+
520
+ def to_record(self) -> dict[str, object]:
521
+ self.__post_init__()
522
+ return {
523
+ "schema_version": 1,
524
+ "plan_sha256": self.plan.plan_sha256,
525
+ "assignment": self.assignment.to_record(),
526
+ "arm_view": self.arm_view.to_record(),
527
+ "aggregation": self.aggregation.to_record(),
528
+ "context_projection": self.context_projection.to_record(),
529
+ "card_vs_neutral_effect_identified": False,
530
+ "online_score_update_allowed": False,
531
+ }
532
+
533
+
534
+ @dataclass(frozen=True, slots=True)
535
+ class PortfolioVariationWaveRequest:
536
+ """Parent, generation, selector request, and optional causal credit unit."""
537
+
538
+ selection_request: PortfolioSelectionRequest
539
+ parent: EvolutionCandidate
540
+ generation: int
541
+ label_prefix: str
542
+ phase: str = "portfolio_evolution"
543
+ memory_credit: PortfolioMemoryCreditPlan | None = None
544
+ matched_memory_control: PortfolioMemoryMatchedControlWavePlan | None = None
545
+
546
+ def __post_init__(self) -> None:
547
+ if type(self.selection_request) is not PortfolioSelectionRequest:
548
+ raise TypeError("selection_request must be exact")
549
+ PortfolioSelectionRequest.__post_init__(self.selection_request)
550
+ if type(self.parent) is not EvolutionCandidate:
551
+ raise TypeError("parent must be an exact EvolutionCandidate")
552
+ EvolutionCandidate.__post_init__(self.parent)
553
+ if not self.parent.valid:
554
+ raise ValueError("portfolio evolution requires a valid parent")
555
+ contract = self.selection_request.finite_variation_contract
556
+ if (
557
+ contract.parent_configuration_sha256
558
+ != self.parent.occurrence.configuration_hash
559
+ or not typed_json_equal(
560
+ contract.parent_configuration,
561
+ self.parent.configuration,
562
+ )
563
+ ):
564
+ raise ValueError("portfolio finite contract is bound to a different parent")
565
+ if (
566
+ type(self.generation) is not int
567
+ or self.generation <= self.parent.generation
568
+ ):
569
+ raise ValueError("portfolio generation must follow the parent generation")
570
+ _require_token(self.label_prefix, "label_prefix")
571
+ _require_token(self.phase, "phase")
572
+ credit = self.memory_credit
573
+ matched = self.matched_memory_control
574
+ if credit is not None and matched is not None:
575
+ raise ValueError("one wave cannot carry legacy and matched memory credit")
576
+ if credit is not None:
577
+ if type(credit) is not PortfolioMemoryCreditPlan:
578
+ raise TypeError("memory_credit must be an exact plan or None")
579
+ PortfolioMemoryCreditPlan.__post_init__(credit)
580
+ credit.resolve_context_projection(self.selection_request.context)
581
+ if (
582
+ credit.card_snapshot_sha256
583
+ != self.selection_request.card_snapshot_sha256
584
+ ):
585
+ raise ValueError("memory credit is bound to a different card snapshot")
586
+ source_registry = self.selection_request.source_registry
587
+ observed_registry_sha256 = (
588
+ None if source_registry is None else source_registry.registry_sha256
589
+ )
590
+ if credit.card_source_registry_sha256 != observed_registry_sha256:
591
+ raise ValueError(
592
+ "memory credit is bound to a different card source registry"
593
+ )
594
+ card_references = tuple(
595
+ sorted(card.reference for card in self.selection_request.cards)
596
+ )
597
+ if card_references != credit.decision.selected:
598
+ raise ValueError(
599
+ "selector request card references must equal selected memory "
600
+ "references"
601
+ )
602
+ if matched is None:
603
+ return
604
+ if type(matched) is not PortfolioMemoryMatchedControlWavePlan:
605
+ raise TypeError("matched_memory_control must be an exact plan or None")
606
+ matched.__post_init__()
607
+ # Generation is a hard identity. The campaign runtime separately
608
+ # joins the stable lane through its decision slot; label text is not an
609
+ # authority and therefore cannot establish that join here.
610
+ if matched.assignment.unit.generation != self.generation:
611
+ raise ValueError("matched assignment generation differs from wave")
612
+ matched.context_projection.replay(self.selection_request.context)
613
+ view = matched.arm_view
614
+ if self.selection_request.cards != view.cards:
615
+ raise ValueError("matched selector cards differ from the arm view")
616
+ if self.selection_request.source_registry != view.source_registry:
617
+ raise ValueError("matched selector registry differs from the arm view")
618
+ if (
619
+ self.selection_request.experimental_view_receipt
620
+ != view.experimental_view_receipt
621
+ ):
622
+ raise ValueError("matched selector receipt differs from the arm view")
623
+ if (
624
+ self.selection_request.candidate_pool_required_option_ids
625
+ != view.required_common_pool_option_ids
626
+ ):
627
+ raise ValueError("matched selector required actions differ from arm view")
628
+ dose = self.selection_request.memory_dose_contract
629
+ if view.memory_dose_allowed:
630
+ if dose is None:
631
+ raise ValueError("matched M arm requires an administered memory dose")
632
+ elif dose is not None:
633
+ raise ValueError("matched N arm cannot carry a memory dose")
634
+
635
+
636
+ @dataclass(frozen=True, slots=True)
637
+ class PortfolioMemberMaterializationReceipt:
638
+ """Replay identity for one exact finite option resolved by rank."""
639
+
640
+ request_sha256: str
641
+ decision_sha256: str
642
+ selection_telemetry_sha256: str
643
+ rank: int
644
+ option_id: str
645
+ option_identity_sha256: str
646
+ child_configuration_sha256: str
647
+ parent_candidate_id: CandidateId
648
+ parent_configuration_sha256: str
649
+ generation: int
650
+ candidate_id: CandidateId
651
+ patch_sha256: str
652
+ changed_paths: tuple[str, ...]
653
+
654
+ def __post_init__(self) -> None:
655
+ for name in (
656
+ "request_sha256",
657
+ "decision_sha256",
658
+ "selection_telemetry_sha256",
659
+ "option_identity_sha256",
660
+ "child_configuration_sha256",
661
+ "parent_configuration_sha256",
662
+ "patch_sha256",
663
+ ):
664
+ require_sha256(getattr(self, name), name)
665
+ if type(self.rank) is not int or self.rank <= 0:
666
+ raise ValueError("rank must be a positive exact integer")
667
+ if type(self.option_id) is not str or not self.option_id:
668
+ raise ValueError("option_id must be non-empty")
669
+ if type(self.parent_candidate_id) is not CandidateId:
670
+ raise TypeError("parent_candidate_id must be exact")
671
+ if type(self.candidate_id) is not CandidateId:
672
+ raise TypeError("candidate_id must be exact")
673
+ CandidateId.__post_init__(self.parent_candidate_id)
674
+ CandidateId.__post_init__(self.candidate_id)
675
+ if self.parent_candidate_id == self.candidate_id:
676
+ raise ValueError("materialized child cannot reuse the parent occurrence")
677
+ if type(self.generation) is not int or self.generation <= 0:
678
+ raise ValueError("generation must be positive")
679
+ if type(self.changed_paths) is not tuple or any(
680
+ type(path) is not str or not path.startswith("$.")
681
+ for path in self.changed_paths
682
+ ):
683
+ raise TypeError("changed_paths must be exact candidate paths")
684
+ if not self.changed_paths:
685
+ raise ValueError("finite option materialization must change a path")
686
+ if self.changed_paths != tuple(sorted(set(self.changed_paths))):
687
+ raise ValueError("changed_paths must be unique and canonical")
688
+
689
+ def _unsigned_record(self) -> dict[str, object]:
690
+ self.__post_init__()
691
+ return {
692
+ "schema_version": 1,
693
+ "selection_authority": "model_ranked_finite_option_ids",
694
+ "materialization_authority": "engine_exact_sealed_children",
695
+ "materialization_policy_id": PORTFOLIO_MATERIALIZATION_POLICY_ID,
696
+ "materialization_policy_version": (
697
+ PORTFOLIO_MATERIALIZATION_POLICY_VERSION
698
+ ),
699
+ "request_sha256": self.request_sha256,
700
+ "decision_sha256": self.decision_sha256,
701
+ "selection_telemetry_sha256": self.selection_telemetry_sha256,
702
+ "rank": self.rank,
703
+ "option_id": self.option_id,
704
+ "option_identity_sha256": self.option_identity_sha256,
705
+ "child_configuration_sha256": self.child_configuration_sha256,
706
+ "parent_candidate_id": self.parent_candidate_id.value,
707
+ "parent_configuration_sha256": self.parent_configuration_sha256,
708
+ "generation": self.generation,
709
+ "candidate_id": self.candidate_id.value,
710
+ "patch_sha256": self.patch_sha256,
711
+ "changed_paths": list(self.changed_paths),
712
+ "source_attribution": [
713
+ {"path": path, "source": "mutation"} for path in self.changed_paths
714
+ ],
715
+ "model_authored_configuration_fields": 0,
716
+ }
717
+
718
+ @property
719
+ def receipt_sha256(self) -> str:
720
+ return _hash_record(_MATERIALIZATION_DOMAIN, self._unsigned_record())
721
+
722
+ def to_record(self) -> dict[str, object]:
723
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
724
+
725
+
726
+ class PortfolioMemberDisposition(str, Enum):
727
+ """Closed terminal meanings for a completely evaluated portfolio member."""
728
+
729
+ SCORED = "scored"
730
+ CANDIDATE_INFEASIBLE = "candidate_infeasible"
731
+
732
+
733
+ @dataclass(frozen=True, slots=True)
734
+ class PortfolioCandidateFailureEvidence:
735
+ """Content-minimized proof of candidate-attributable infeasibility.
736
+
737
+ The detailed-evaluation digest authenticates the full evaluator record. The
738
+ compact projection makes the terminal category usable without copying a
739
+ potentially sensitive free-text diagnostic into every portfolio receipt.
740
+ """
741
+
742
+ detailed_evaluation_sha256: str
743
+ failure_code: FailureCode
744
+ failure_message_sha256: str
745
+ retryable: bool
746
+ exception_type: str | None
747
+ diagnostics_artifact_id: ArtifactId | None
748
+
749
+ def __post_init__(self) -> None:
750
+ require_sha256(
751
+ self.detailed_evaluation_sha256,
752
+ "detailed_evaluation_sha256",
753
+ )
754
+ if type(self.failure_code) is not FailureCode:
755
+ raise TypeError("failure_code must be an exact FailureCode")
756
+ # Reuse the closed domain taxonomy rather than maintaining a second
757
+ # hand-written list of candidate-attributable codes here.
758
+ validate_failure_pair(FailureCategory.CANDIDATE, self.failure_code)
759
+ require_sha256(self.failure_message_sha256, "failure_message_sha256")
760
+ if type(self.retryable) is not bool:
761
+ raise TypeError("retryable must be an exact boolean")
762
+ if self.exception_type is not None and (
763
+ type(self.exception_type) is not str
764
+ or not self.exception_type.strip()
765
+ or self.exception_type != self.exception_type.strip()
766
+ ):
767
+ raise ValueError(
768
+ "exception_type must be canonical non-empty text or None"
769
+ )
770
+ if self.diagnostics_artifact_id is not None:
771
+ if type(self.diagnostics_artifact_id) is not ArtifactId:
772
+ raise TypeError(
773
+ "diagnostics_artifact_id must be an exact ArtifactId or None"
774
+ )
775
+ ArtifactId.__post_init__(self.diagnostics_artifact_id)
776
+
777
+ @classmethod
778
+ def from_failure_record(
779
+ cls,
780
+ failure: FailureRecord,
781
+ *,
782
+ detailed_evaluation_sha256: str,
783
+ ) -> PortfolioCandidateFailureEvidence:
784
+ """Project one exact candidate-category evaluator failure."""
785
+
786
+ if type(failure) is not FailureRecord:
787
+ raise TypeError("failure must be an exact FailureRecord")
788
+ FailureRecord.__post_init__(failure)
789
+ if failure.category is not FailureCategory.CANDIDATE:
790
+ raise ValueError(
791
+ "portfolio infeasibility requires candidate-category evidence"
792
+ )
793
+ return cls(
794
+ detailed_evaluation_sha256=detailed_evaluation_sha256,
795
+ failure_code=failure.code,
796
+ failure_message_sha256=hashlib.sha256(
797
+ failure.message.encode("utf-8", errors="strict")
798
+ ).hexdigest(),
799
+ retryable=failure.retryable,
800
+ exception_type=failure.exception_type,
801
+ diagnostics_artifact_id=failure.diagnostics_artifact_id,
802
+ )
803
+
804
+ def _unsigned_record(self) -> dict[str, object]:
805
+ self.__post_init__()
806
+ return {
807
+ "schema_version": 1,
808
+ "failure_category": FailureCategory.CANDIDATE.value,
809
+ "failure_code": self.failure_code.value,
810
+ "failure_message_sha256": self.failure_message_sha256,
811
+ "retryable": self.retryable,
812
+ "exception_type": self.exception_type,
813
+ "diagnostics_artifact_id": (
814
+ None
815
+ if self.diagnostics_artifact_id is None
816
+ else self.diagnostics_artifact_id.value
817
+ ),
818
+ "detailed_evaluation_sha256": self.detailed_evaluation_sha256,
819
+ }
820
+
821
+ @property
822
+ def evidence_sha256(self) -> str:
823
+ return _hash_record(_CANDIDATE_FAILURE_DOMAIN, self._unsigned_record())
824
+
825
+ def to_record(self) -> dict[str, object]:
826
+ return {**self._unsigned_record(), "evidence_sha256": self.evidence_sha256}
827
+
828
+
829
+ @dataclass(frozen=True, slots=True)
830
+ class PortfolioVariationMemberReceipt:
831
+ """Closed materialization-to-outcome join for one ranked member."""
832
+
833
+ materialization: PortfolioMemberMaterializationReceipt
834
+ operator_invocation_id: OperatorInvocationId
835
+ reward_definition_sha256: str
836
+ reward: float
837
+ parent_relations: tuple[OutcomeRelation, ...]
838
+ detailed_evaluation_sha256: str | None
839
+ dominates_any_parent: bool
840
+ better_than_any_parent: bool
841
+ disposition: PortfolioMemberDisposition = PortfolioMemberDisposition.SCORED
842
+ candidate_failure: PortfolioCandidateFailureEvidence | None = None
843
+
844
+ def __post_init__(self) -> None:
845
+ if type(self.materialization) is not PortfolioMemberMaterializationReceipt:
846
+ raise TypeError("materialization must be an exact receipt")
847
+ PortfolioMemberMaterializationReceipt.__post_init__(self.materialization)
848
+ if type(self.operator_invocation_id) is not OperatorInvocationId:
849
+ raise TypeError("operator_invocation_id must be exact")
850
+ OperatorInvocationId.__post_init__(self.operator_invocation_id)
851
+ require_sha256(self.reward_definition_sha256, "reward_definition_sha256")
852
+ if type(self.reward) is not float or not math.isfinite(self.reward):
853
+ raise TypeError("reward must be a finite canonical float")
854
+ if type(self.parent_relations) is not tuple or any(
855
+ type(relation) is not OutcomeRelation for relation in self.parent_relations
856
+ ):
857
+ raise TypeError("parent_relations must contain exact values")
858
+ if self.detailed_evaluation_sha256 is not None:
859
+ require_sha256(
860
+ self.detailed_evaluation_sha256,
861
+ "detailed_evaluation_sha256",
862
+ )
863
+ if (
864
+ type(self.dominates_any_parent) is not bool
865
+ or type(self.better_than_any_parent) is not bool
866
+ ):
867
+ raise TypeError("outcome comparison projections must be bool")
868
+ if type(self.disposition) is not PortfolioMemberDisposition:
869
+ raise TypeError("disposition must be a PortfolioMemberDisposition")
870
+ failure = self.candidate_failure
871
+ if self.disposition is PortfolioMemberDisposition.SCORED:
872
+ if len(self.parent_relations) != 1:
873
+ raise ValueError("scored portfolio member must compare to its parent")
874
+ if failure is not None:
875
+ raise ValueError("scored portfolio member cannot carry failure evidence")
876
+ else:
877
+ if type(failure) is not PortfolioCandidateFailureEvidence:
878
+ raise TypeError(
879
+ "candidate-infeasible member requires exact failure evidence"
880
+ )
881
+ PortfolioCandidateFailureEvidence.__post_init__(failure)
882
+ if self.detailed_evaluation_sha256 != failure.detailed_evaluation_sha256:
883
+ raise ValueError(
884
+ "candidate failure identifies another detailed evaluation"
885
+ )
886
+ if self.parent_relations:
887
+ raise ValueError(
888
+ "candidate-infeasible member cannot publish parent relations"
889
+ )
890
+ if self.dominates_any_parent or self.better_than_any_parent:
891
+ raise ValueError(
892
+ "candidate-infeasible member cannot publish improvement flags"
893
+ )
894
+
895
+ @property
896
+ def engine_reward(self) -> float:
897
+ """Return the exact terminal reward published by the engine."""
898
+
899
+ return self.reward
900
+
901
+ def _unsigned_record(self) -> dict[str, object]:
902
+ self.__post_init__()
903
+ return {
904
+ "schema_version": 2,
905
+ "materialization": self.materialization.to_record(),
906
+ "operator_invocation_id": self.operator_invocation_id.value,
907
+ "candidate_id": self.materialization.candidate_id.value,
908
+ "candidate_configuration_sha256": (
909
+ self.materialization.child_configuration_sha256
910
+ ),
911
+ "disposition": self.disposition.value,
912
+ "candidate_valid": (
913
+ self.disposition is PortfolioMemberDisposition.SCORED
914
+ ),
915
+ "operator_compliant": True,
916
+ "evidence_compliant": True,
917
+ "reward_definition_sha256": self.reward_definition_sha256,
918
+ "engine_reward_hex": self.engine_reward.hex(),
919
+ "reward_hex": self.reward.hex(),
920
+ "parent_relations": [value.value for value in self.parent_relations],
921
+ "detailed_evaluation_sha256": self.detailed_evaluation_sha256,
922
+ "candidate_failure": (
923
+ None
924
+ if self.candidate_failure is None
925
+ else self.candidate_failure.to_record()
926
+ ),
927
+ "dominates_any_parent": self.dominates_any_parent,
928
+ "better_than_any_parent": self.better_than_any_parent,
929
+ }
930
+
931
+ @property
932
+ def outcome_sha256(self) -> str:
933
+ return _hash_record(_MEMBER_DOMAIN, self._unsigned_record())
934
+
935
+ def to_record(self) -> dict[str, object]:
936
+ return {**self._unsigned_record(), "outcome_sha256": self.outcome_sha256}
937
+
938
+
939
+ @dataclass(frozen=True, slots=True)
940
+ class PortfolioActionCardAttribution:
941
+ """Exact request-card identity cited for one selected finite action."""
942
+
943
+ card_key: str
944
+ reference: InsightRef
945
+ content_sha256: str
946
+ evidence_sha256: str
947
+
948
+ def __post_init__(self) -> None:
949
+ _require_token(self.card_key, "card_key")
950
+ if type(self.reference) is not InsightRef:
951
+ raise TypeError("reference must be an exact InsightRef")
952
+ InsightRef.__post_init__(self.reference)
953
+ require_sha256(self.content_sha256, "content_sha256")
954
+ require_sha256(self.evidence_sha256, "evidence_sha256")
955
+
956
+ def _unsigned_record(self) -> dict[str, object]:
957
+ self.__post_init__()
958
+ return {
959
+ "schema_version": 1,
960
+ "card_key": self.card_key,
961
+ "reference": {
962
+ "insight_id": self.reference.insight_id.value,
963
+ "version": self.reference.version,
964
+ },
965
+ "content_sha256": self.content_sha256,
966
+ "evidence_sha256": self.evidence_sha256,
967
+ }
968
+
969
+ @property
970
+ def binding_sha256(self) -> str:
971
+ return _hash_record(
972
+ _ACTION_CARD_ATTRIBUTION_DOMAIN,
973
+ self._unsigned_record(),
974
+ )
975
+
976
+ def to_record(self) -> dict[str, object]:
977
+ return {**self._unsigned_record(), "binding_sha256": self.binding_sha256}
978
+
979
+
980
+ @dataclass(frozen=True, slots=True)
981
+ class PortfolioActionAttributionReceipt:
982
+ """Authenticated selector-card to materialized-outcome join for one action.
983
+
984
+ This is diagnostic attribution, not a causal credit unit. The randomized
985
+ memory treatment remains one whole-wave ITT trial in
986
+ :class:`PortfolioMemoryCreditReceipt`.
987
+ """
988
+
989
+ request_sha256: str
990
+ decision_sha256: str
991
+ card_snapshot_sha256: str
992
+ rank: int
993
+ option_id: str
994
+ option_identity_sha256: str
995
+ child_configuration_sha256: str
996
+ family: str
997
+ supporting_cards: tuple[PortfolioActionCardAttribution, ...]
998
+ effect_predictions: tuple[MetricEffectPrediction, ...]
999
+ design_rationale_sha256: str
1000
+ materialization_receipt_sha256: str
1001
+ outcome_sha256: str
1002
+ operator_invocation_id: OperatorInvocationId
1003
+ candidate_id: CandidateId
1004
+
1005
+ def __post_init__(self) -> None:
1006
+ for name in (
1007
+ "request_sha256",
1008
+ "decision_sha256",
1009
+ "card_snapshot_sha256",
1010
+ "option_identity_sha256",
1011
+ "child_configuration_sha256",
1012
+ "design_rationale_sha256",
1013
+ "materialization_receipt_sha256",
1014
+ "outcome_sha256",
1015
+ ):
1016
+ require_sha256(getattr(self, name), name)
1017
+ if type(self.rank) is not int or self.rank <= 0:
1018
+ raise ValueError("rank must be a positive exact integer")
1019
+ if type(self.option_id) is not str or not self.option_id:
1020
+ raise ValueError("option_id must be non-empty")
1021
+ _require_token(self.family, "family")
1022
+ if type(self.supporting_cards) is not tuple or any(
1023
+ type(value) is not PortfolioActionCardAttribution
1024
+ for value in self.supporting_cards
1025
+ ):
1026
+ raise TypeError(
1027
+ "supporting_cards must contain exact card-attribution values"
1028
+ )
1029
+ for value in self.supporting_cards:
1030
+ PortfolioActionCardAttribution.__post_init__(value)
1031
+ card_keys = tuple(value.card_key for value in self.supporting_cards)
1032
+ if card_keys != tuple(sorted(set(card_keys))):
1033
+ raise ValueError("supporting cards must use canonical unique card keys")
1034
+ references = tuple(value.reference for value in self.supporting_cards)
1035
+ if len(set(references)) != len(references):
1036
+ raise ValueError("supporting cards cannot repeat an insight reference")
1037
+ if type(self.effect_predictions) is not tuple or any(
1038
+ type(value) is not MetricEffectPrediction
1039
+ for value in self.effect_predictions
1040
+ ):
1041
+ raise TypeError(
1042
+ "effect_predictions must contain exact MetricEffectPrediction values"
1043
+ )
1044
+ for value in self.effect_predictions:
1045
+ MetricEffectPrediction.__post_init__(value)
1046
+ metric_ids = tuple(value.metric_id for value in self.effect_predictions)
1047
+ if metric_ids != tuple(sorted(set(metric_ids))):
1048
+ raise ValueError("effect predictions must use canonical metric order")
1049
+ if type(self.operator_invocation_id) is not OperatorInvocationId:
1050
+ raise TypeError("operator_invocation_id must be exact")
1051
+ OperatorInvocationId.__post_init__(self.operator_invocation_id)
1052
+ if type(self.candidate_id) is not CandidateId:
1053
+ raise TypeError("candidate_id must be exact")
1054
+ CandidateId.__post_init__(self.candidate_id)
1055
+
1056
+ @property
1057
+ def supporting_card_keys(self) -> tuple[str, ...]:
1058
+ self.__post_init__()
1059
+ return tuple(value.card_key for value in self.supporting_cards)
1060
+
1061
+ def _unsigned_record(self) -> dict[str, object]:
1062
+ self.__post_init__()
1063
+ return {
1064
+ "schema_version": 1,
1065
+ "attribution_scope": "post_treatment_diagnostic_not_causal_credit",
1066
+ "request_sha256": self.request_sha256,
1067
+ "decision_sha256": self.decision_sha256,
1068
+ "card_snapshot_sha256": self.card_snapshot_sha256,
1069
+ "selected_member": {
1070
+ "rank": self.rank,
1071
+ "option_id": self.option_id,
1072
+ "option_identity_sha256": self.option_identity_sha256,
1073
+ "child_configuration_sha256": self.child_configuration_sha256,
1074
+ "family": self.family,
1075
+ "supporting_card_keys": list(self.supporting_card_keys),
1076
+ "effect_predictions": [
1077
+ {
1078
+ "metric_id": value.metric_id,
1079
+ "direction": value.direction.value,
1080
+ }
1081
+ for value in self.effect_predictions
1082
+ ],
1083
+ "design_rationale_sha256": self.design_rationale_sha256,
1084
+ },
1085
+ "supporting_cards": [value.to_record() for value in self.supporting_cards],
1086
+ "materialization_receipt_sha256": (self.materialization_receipt_sha256),
1087
+ "outcome_sha256": self.outcome_sha256,
1088
+ "operator_invocation_id": self.operator_invocation_id.value,
1089
+ "candidate_id": self.candidate_id.value,
1090
+ }
1091
+
1092
+ @property
1093
+ def receipt_sha256(self) -> str:
1094
+ return _hash_record(_ACTION_ATTRIBUTION_DOMAIN, self._unsigned_record())
1095
+
1096
+ def to_record(self) -> dict[str, object]:
1097
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
1098
+
1099
+
1100
+ @dataclass(frozen=True, slots=True)
1101
+ class PortfolioPendingMemoryCredit:
1102
+ """Complete credit evidence awaiting a generation publication barrier."""
1103
+
1104
+ credit_unit_id: OperatorInvocationId
1105
+ decision: InsightSelectionDecision
1106
+ candidate_ids: tuple[CandidateId, ...]
1107
+ aggregation: PortfolioRewardAggregationBinding
1108
+ context_projection: PortfolioMemoryContextProjectionBinding
1109
+ reward: float
1110
+ treatment_binding_sha256: str
1111
+ generation: int
1112
+
1113
+ def __post_init__(self) -> None:
1114
+ if type(self.credit_unit_id) is not OperatorInvocationId:
1115
+ raise TypeError("credit_unit_id must be exact")
1116
+ OperatorInvocationId.__post_init__(self.credit_unit_id)
1117
+ if type(self.decision) is not InsightSelectionDecision:
1118
+ raise TypeError("decision must be an exact InsightSelectionDecision")
1119
+ InsightSelectionDecision.__post_init__(self.decision)
1120
+ if type(self.candidate_ids) is not tuple or any(
1121
+ type(value) is not CandidateId for value in self.candidate_ids
1122
+ ):
1123
+ raise TypeError("candidate_ids must contain exact CandidateId values")
1124
+ if not self.candidate_ids or len(set(self.candidate_ids)) != len(
1125
+ self.candidate_ids
1126
+ ):
1127
+ raise ValueError("candidate_ids must be non-empty and unique")
1128
+ if type(self.aggregation) is not PortfolioRewardAggregationBinding:
1129
+ raise TypeError("aggregation must be an exact binding")
1130
+ PortfolioRewardAggregationBinding.__post_init__(self.aggregation)
1131
+ if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
1132
+ raise TypeError("context_projection must be an exact binding")
1133
+ PortfolioMemoryContextProjectionBinding.__post_init__(self.context_projection)
1134
+ if (
1135
+ self.context_projection.estimand_context_sha256
1136
+ != self.decision.context_hash
1137
+ ):
1138
+ raise ValueError("context projection differs from decision context")
1139
+ if type(self.reward) is not float or not math.isfinite(self.reward):
1140
+ raise TypeError("reward must be a finite canonical float")
1141
+ require_sha256(
1142
+ self.treatment_binding_sha256,
1143
+ "treatment_binding_sha256",
1144
+ )
1145
+ if type(self.generation) is not int or self.generation <= 0:
1146
+ raise ValueError("generation must be a positive exact integer")
1147
+
1148
+ @property
1149
+ def selection_decision_sha256(self) -> str:
1150
+ return insight_selection_decision_sha256(self.decision)
1151
+
1152
+ def to_trial(self) -> InsightTrial:
1153
+ self.__post_init__()
1154
+ return InsightTrial(
1155
+ credit_unit_id=self.credit_unit_id,
1156
+ candidate_ids=self.candidate_ids,
1157
+ reward_definition_hash=self.aggregation.definition_sha256,
1158
+ decision=self.decision,
1159
+ reward=self.reward,
1160
+ treatment_binding_sha256=self.treatment_binding_sha256,
1161
+ generation=self.generation,
1162
+ )
1163
+
1164
+ def to_committed_receipt(self) -> "PortfolioMemoryCreditReceipt":
1165
+ self.__post_init__()
1166
+ return PortfolioMemoryCreditReceipt(
1167
+ credit_unit_id=self.credit_unit_id,
1168
+ selection_decision_sha256=self.selection_decision_sha256,
1169
+ selection_decision_context_sha256=self.decision.context_hash,
1170
+ candidate_ids=self.candidate_ids,
1171
+ aggregation_id=self.aggregation.aggregation_id,
1172
+ aggregation_version=self.aggregation.aggregation_version,
1173
+ aggregation_definition_sha256=self.aggregation.definition_sha256,
1174
+ aggregation_binding_sha256=self.aggregation.binding_sha256,
1175
+ context_projection=self.context_projection,
1176
+ reward=self.reward,
1177
+ treatment_binding_sha256=self.treatment_binding_sha256,
1178
+ generation=self.generation,
1179
+ )
1180
+
1181
+ def _unsigned_record(self) -> dict[str, object]:
1182
+ self.__post_init__()
1183
+ return {
1184
+ "schema_version": 1,
1185
+ "status": "pending_generation_barrier",
1186
+ "credit_unit_id": self.credit_unit_id.value,
1187
+ "selection_decision_sha256": self.selection_decision_sha256,
1188
+ "selection_decision_context_sha256": self.decision.context_hash,
1189
+ "candidate_ids": [value.value for value in self.candidate_ids],
1190
+ "aggregation": {
1191
+ **self.aggregation.to_record(),
1192
+ "binding_sha256": self.aggregation.binding_sha256,
1193
+ },
1194
+ "context_projection": {
1195
+ **self.context_projection.to_record(),
1196
+ "binding_sha256": self.context_projection.binding_sha256,
1197
+ },
1198
+ "reward_hex": self.reward.hex(),
1199
+ "treatment_binding_sha256": self.treatment_binding_sha256,
1200
+ "generation": self.generation,
1201
+ }
1202
+
1203
+ @property
1204
+ def pending_sha256(self) -> str:
1205
+ return _hash_record(_PENDING_MEMORY_CREDIT_DOMAIN, self._unsigned_record())
1206
+
1207
+ def to_record(self) -> dict[str, object]:
1208
+ return {**self._unsigned_record(), "pending_sha256": self.pending_sha256}
1209
+
1210
+
1211
+ @dataclass(frozen=True, slots=True)
1212
+ class PortfolioMemoryCreditReceipt:
1213
+ """Evidence that a complete portfolio became one memory trial."""
1214
+
1215
+ credit_unit_id: OperatorInvocationId
1216
+ selection_decision_sha256: str
1217
+ selection_decision_context_sha256: str
1218
+ candidate_ids: tuple[CandidateId, ...]
1219
+ aggregation_id: str
1220
+ aggregation_version: int
1221
+ aggregation_definition_sha256: str
1222
+ aggregation_binding_sha256: str
1223
+ context_projection: PortfolioMemoryContextProjectionBinding
1224
+ reward: float
1225
+ treatment_binding_sha256: str
1226
+ generation: int
1227
+
1228
+ def __post_init__(self) -> None:
1229
+ if type(self.credit_unit_id) is not OperatorInvocationId:
1230
+ raise TypeError("credit_unit_id must be exact")
1231
+ OperatorInvocationId.__post_init__(self.credit_unit_id)
1232
+ for name in (
1233
+ "selection_decision_sha256",
1234
+ "selection_decision_context_sha256",
1235
+ "aggregation_definition_sha256",
1236
+ "aggregation_binding_sha256",
1237
+ ):
1238
+ require_sha256(getattr(self, name), name)
1239
+ if type(self.candidate_ids) is not tuple or any(
1240
+ type(value) is not CandidateId for value in self.candidate_ids
1241
+ ):
1242
+ raise TypeError("candidate_ids must contain exact CandidateId values")
1243
+ if not self.candidate_ids or len(set(self.candidate_ids)) != len(
1244
+ self.candidate_ids
1245
+ ):
1246
+ raise ValueError("candidate_ids must be non-empty and unique")
1247
+ _require_token(self.aggregation_id, "aggregation_id")
1248
+ if type(self.aggregation_version) is not int or self.aggregation_version <= 0:
1249
+ raise ValueError("aggregation_version must be positive")
1250
+ if type(self.reward) is not float or not math.isfinite(self.reward):
1251
+ raise TypeError("reward must be a finite canonical float")
1252
+ require_sha256(
1253
+ self.treatment_binding_sha256,
1254
+ "treatment_binding_sha256",
1255
+ )
1256
+ if type(self.generation) is not int or self.generation <= 0:
1257
+ raise ValueError("generation must be a positive exact integer")
1258
+ if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
1259
+ raise TypeError("context_projection must be an exact binding")
1260
+ PortfolioMemoryContextProjectionBinding.__post_init__(self.context_projection)
1261
+ if (
1262
+ self.context_projection.estimand_context_sha256
1263
+ != self.selection_decision_context_sha256
1264
+ ):
1265
+ raise ValueError(
1266
+ "context projection estimand differs from decision context receipt"
1267
+ )
1268
+
1269
+ def _unsigned_record(self) -> dict[str, object]:
1270
+ self.__post_init__()
1271
+ return {
1272
+ "schema_version": 1,
1273
+ "credit_unit_id": self.credit_unit_id.value,
1274
+ "selection_decision_sha256": self.selection_decision_sha256,
1275
+ "selection_decision_context_sha256": (
1276
+ self.selection_decision_context_sha256
1277
+ ),
1278
+ "candidate_ids": [value.value for value in self.candidate_ids],
1279
+ "aggregation": {
1280
+ "aggregation_id": self.aggregation_id,
1281
+ "aggregation_version": self.aggregation_version,
1282
+ "definition_sha256": self.aggregation_definition_sha256,
1283
+ "binding_sha256": self.aggregation_binding_sha256,
1284
+ },
1285
+ "context_projection": {
1286
+ **self.context_projection.to_record(),
1287
+ "binding_sha256": self.context_projection.binding_sha256,
1288
+ },
1289
+ "reward_hex": self.reward.hex(),
1290
+ "treatment_binding_sha256": self.treatment_binding_sha256,
1291
+ "generation": self.generation,
1292
+ "memory_trial_count": 1,
1293
+ }
1294
+
1295
+ @property
1296
+ def receipt_sha256(self) -> str:
1297
+ return _hash_record(_MEMORY_CREDIT_DOMAIN, self._unsigned_record())
1298
+
1299
+ def to_record(self) -> dict[str, object]:
1300
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
1301
+
1302
+
1303
+ @dataclass(frozen=True, slots=True)
1304
+ class PortfolioMemoryCreditBatchReceipt:
1305
+ """Canonical publication receipt for one generation's pending credits."""
1306
+
1307
+ generation: int
1308
+ credits: tuple[PortfolioMemoryCreditReceipt, ...]
1309
+ memory_trial_count_before: int
1310
+ memory_trial_count_after: int
1311
+
1312
+ def __post_init__(self) -> None:
1313
+ if type(self.generation) is not int or self.generation <= 0:
1314
+ raise ValueError("generation must be a positive exact integer")
1315
+ if type(self.credits) is not tuple or not self.credits:
1316
+ raise ValueError("credits must be a non-empty exact tuple")
1317
+ if any(type(value) is not PortfolioMemoryCreditReceipt for value in self.credits):
1318
+ raise TypeError("credits must contain exact credit receipts")
1319
+ for value in self.credits:
1320
+ PortfolioMemoryCreditReceipt.__post_init__(value)
1321
+ if any(value.generation != self.generation for value in self.credits):
1322
+ raise ValueError("memory credits differ from the batch generation")
1323
+ credit_ids = tuple(value.credit_unit_id.value for value in self.credits)
1324
+ if credit_ids != tuple(sorted(set(credit_ids))):
1325
+ raise ValueError("credits must use canonical unique credit-unit order")
1326
+ for name in ("memory_trial_count_before", "memory_trial_count_after"):
1327
+ value = getattr(self, name)
1328
+ if type(value) is not int or value < 0:
1329
+ raise ValueError(f"{name} must be a non-negative exact integer")
1330
+ if self.memory_trial_count_after - self.memory_trial_count_before != len(
1331
+ self.credits
1332
+ ):
1333
+ raise ValueError("memory trial counts differ from the committed batch")
1334
+
1335
+ def _unsigned_record(self) -> dict[str, object]:
1336
+ self.__post_init__()
1337
+ return {
1338
+ "schema_version": 1,
1339
+ "publication_scope": "post_concurrent_generation_barrier",
1340
+ "generation": self.generation,
1341
+ "memory_trial_count_before": self.memory_trial_count_before,
1342
+ "memory_trial_count_after": self.memory_trial_count_after,
1343
+ "credit_count": len(self.credits),
1344
+ "credits": [value.to_record() for value in self.credits],
1345
+ }
1346
+
1347
+ @property
1348
+ def receipt_sha256(self) -> str:
1349
+ return _hash_record(_MEMORY_CREDIT_BATCH_DOMAIN, self._unsigned_record())
1350
+
1351
+ def to_record(self) -> dict[str, object]:
1352
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
1353
+
1354
+
1355
+ @dataclass(frozen=True, slots=True)
1356
+ class PortfolioVariationWaveReceipt:
1357
+ """Closed one-selector-call to N-evaluation portfolio receipt."""
1358
+
1359
+ selection_call_id: LLMCallId
1360
+ request_sha256: str
1361
+ decision_sha256: str
1362
+ selection_policy_id: str
1363
+ selection_policy_version: int
1364
+ selection_policy_definition_sha256: str
1365
+ selection_telemetry: AgenticCallTelemetry
1366
+ selection_telemetry_sha256: str
1367
+ parent_candidate_id: CandidateId
1368
+ parent_configuration_sha256: str
1369
+ generation: int
1370
+ members: tuple[PortfolioVariationMemberReceipt, ...]
1371
+ memory_credit: PortfolioMemoryCreditReceipt | None = None
1372
+ action_attributions: tuple[PortfolioActionAttributionReceipt, ...] = ()
1373
+
1374
+ def __post_init__(self) -> None:
1375
+ if type(self.selection_call_id) is not LLMCallId:
1376
+ raise TypeError("selection_call_id must be exact")
1377
+ LLMCallId.__post_init__(self.selection_call_id)
1378
+ for name in (
1379
+ "request_sha256",
1380
+ "decision_sha256",
1381
+ "selection_policy_definition_sha256",
1382
+ "selection_telemetry_sha256",
1383
+ "parent_configuration_sha256",
1384
+ ):
1385
+ require_sha256(getattr(self, name), name)
1386
+ _require_token(self.selection_policy_id, "selection_policy_id")
1387
+ if (
1388
+ type(self.selection_policy_version) is not int
1389
+ or self.selection_policy_version <= 0
1390
+ ):
1391
+ raise ValueError("selection_policy_version must be positive")
1392
+ if type(self.selection_telemetry) is not AgenticCallTelemetry:
1393
+ raise TypeError("selection_telemetry must be exact")
1394
+ if self.selection_telemetry_sha256 != portfolio_selection_telemetry_sha256(
1395
+ self.selection_telemetry
1396
+ ):
1397
+ raise ValueError("selection telemetry digest does not verify")
1398
+ if type(self.parent_candidate_id) is not CandidateId:
1399
+ raise TypeError("parent_candidate_id must be exact")
1400
+ CandidateId.__post_init__(self.parent_candidate_id)
1401
+ if type(self.generation) is not int or self.generation <= 0:
1402
+ raise ValueError("generation must be positive")
1403
+ if type(self.members) is not tuple or not self.members:
1404
+ raise ValueError("members must be a non-empty exact tuple")
1405
+ for member in self.members:
1406
+ if type(member) is not PortfolioVariationMemberReceipt:
1407
+ raise TypeError("members must contain exact receipts")
1408
+ PortfolioVariationMemberReceipt.__post_init__(member)
1409
+ if tuple(member.materialization.rank for member in self.members) != tuple(
1410
+ range(1, len(self.members) + 1)
1411
+ ):
1412
+ raise ValueError("member receipts must use contiguous ranked order")
1413
+ for member in self.members:
1414
+ materialization = member.materialization
1415
+ if (
1416
+ materialization.request_sha256 != self.request_sha256
1417
+ or materialization.decision_sha256 != self.decision_sha256
1418
+ or materialization.selection_telemetry_sha256
1419
+ != self.selection_telemetry_sha256
1420
+ or materialization.parent_candidate_id != self.parent_candidate_id
1421
+ or materialization.parent_configuration_sha256
1422
+ != self.parent_configuration_sha256
1423
+ or materialization.generation != self.generation
1424
+ ):
1425
+ raise ValueError("member receipt differs from its wave identity")
1426
+ for values, name in (
1427
+ (
1428
+ tuple(member.materialization.option_id for member in self.members),
1429
+ "option IDs",
1430
+ ),
1431
+ (
1432
+ tuple(member.materialization.candidate_id for member in self.members),
1433
+ "candidate IDs",
1434
+ ),
1435
+ (
1436
+ tuple(
1437
+ member.materialization.child_configuration_sha256
1438
+ for member in self.members
1439
+ ),
1440
+ "child configurations",
1441
+ ),
1442
+ (
1443
+ tuple(member.operator_invocation_id for member in self.members),
1444
+ "operator invocations",
1445
+ ),
1446
+ (
1447
+ tuple(member.materialization.receipt_sha256 for member in self.members),
1448
+ "materialization receipts",
1449
+ ),
1450
+ ):
1451
+ if len(set(values)) != len(values):
1452
+ raise ValueError(f"portfolio wave contains colliding {name}")
1453
+ if type(self.action_attributions) is not tuple or any(
1454
+ type(value) is not PortfolioActionAttributionReceipt
1455
+ for value in self.action_attributions
1456
+ ):
1457
+ raise TypeError(
1458
+ "action_attributions must contain exact attribution receipts"
1459
+ )
1460
+ for value in self.action_attributions:
1461
+ PortfolioActionAttributionReceipt.__post_init__(value)
1462
+ if self.action_attributions:
1463
+ if len(self.action_attributions) != len(self.members):
1464
+ raise ValueError(
1465
+ "action attributions must exactly cover the wave members"
1466
+ )
1467
+ if (
1468
+ len({value.card_snapshot_sha256 for value in self.action_attributions})
1469
+ != 1
1470
+ ):
1471
+ raise ValueError(
1472
+ "action attributions must share one request-card snapshot"
1473
+ )
1474
+ if tuple(value.rank for value in self.action_attributions) != tuple(
1475
+ range(1, len(self.members) + 1)
1476
+ ):
1477
+ raise ValueError("action attributions must use contiguous ranked order")
1478
+ for attribution, member in zip(
1479
+ self.action_attributions,
1480
+ self.members,
1481
+ strict=True,
1482
+ ):
1483
+ materialization = member.materialization
1484
+ if (
1485
+ attribution.request_sha256 != self.request_sha256
1486
+ or attribution.decision_sha256 != self.decision_sha256
1487
+ or attribution.rank != materialization.rank
1488
+ or attribution.option_id != materialization.option_id
1489
+ or attribution.option_identity_sha256
1490
+ != materialization.option_identity_sha256
1491
+ or attribution.child_configuration_sha256
1492
+ != materialization.child_configuration_sha256
1493
+ or attribution.materialization_receipt_sha256
1494
+ != materialization.receipt_sha256
1495
+ or attribution.outcome_sha256 != member.outcome_sha256
1496
+ or attribution.operator_invocation_id
1497
+ != member.operator_invocation_id
1498
+ or attribution.candidate_id != materialization.candidate_id
1499
+ ):
1500
+ raise ValueError(
1501
+ "action attribution differs from its evaluated wave member"
1502
+ )
1503
+ if len({value.receipt_sha256 for value in self.action_attributions}) != len(
1504
+ self.action_attributions
1505
+ ):
1506
+ raise ValueError("portfolio wave repeats an action attribution")
1507
+ if self.memory_credit is not None:
1508
+ if type(self.memory_credit) is not PortfolioMemoryCreditReceipt:
1509
+ raise TypeError("memory_credit must be an exact receipt or None")
1510
+ PortfolioMemoryCreditReceipt.__post_init__(self.memory_credit)
1511
+ if self.memory_credit.candidate_ids != tuple(
1512
+ member.materialization.candidate_id for member in self.members
1513
+ ):
1514
+ raise ValueError("memory credit candidate IDs differ from the wave")
1515
+ if self.memory_credit.credit_unit_id in {
1516
+ member.operator_invocation_id for member in self.members
1517
+ }:
1518
+ raise ValueError("memory credit unit collides with a child invocation")
1519
+
1520
+ def _unsigned_record(self) -> dict[str, object]:
1521
+ self.__post_init__()
1522
+ return {
1523
+ "schema_version": 1,
1524
+ "selection_call_count": 1,
1525
+ "selection_call_id": self.selection_call_id.value,
1526
+ "request_sha256": self.request_sha256,
1527
+ "decision_sha256": self.decision_sha256,
1528
+ "selection_policy": {
1529
+ "policy_id": self.selection_policy_id,
1530
+ "policy_version": self.selection_policy_version,
1531
+ "definition_sha256": self.selection_policy_definition_sha256,
1532
+ },
1533
+ "selection_telemetry": _telemetry_record(self.selection_telemetry),
1534
+ "selection_telemetry_sha256": self.selection_telemetry_sha256,
1535
+ "parent_candidate_id": self.parent_candidate_id.value,
1536
+ "parent_configuration_sha256": self.parent_configuration_sha256,
1537
+ "generation": self.generation,
1538
+ "member_count": len(self.members),
1539
+ "concurrent_materialized_evaluation_wave": True,
1540
+ "members": [member.to_record() for member in self.members],
1541
+ **(
1542
+ {}
1543
+ if not self.action_attributions
1544
+ else {
1545
+ "action_attributions": [
1546
+ value.to_record() for value in self.action_attributions
1547
+ ]
1548
+ }
1549
+ ),
1550
+ "memory_credit": (
1551
+ None if self.memory_credit is None else self.memory_credit.to_record()
1552
+ ),
1553
+ }
1554
+
1555
+ @property
1556
+ def receipt_sha256(self) -> str:
1557
+ return _hash_record(_WAVE_DOMAIN, self._unsigned_record())
1558
+
1559
+ def to_record(self) -> dict[str, object]:
1560
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
1561
+
1562
+
1563
+ @dataclass(frozen=True, slots=True)
1564
+ class PortfolioVariationWaveResult:
1565
+ """Planner-facing wave result with an exact receipt-to-outcome join.
1566
+
1567
+ The receipt remains the evidence-only serialization boundary. Outcomes
1568
+ remain typed runtime values so a generic planner can promote candidates to
1569
+ later generations or pass the complete wave to reflection without parsing
1570
+ evidence records back into domain objects.
1571
+ """
1572
+
1573
+ receipt: PortfolioVariationWaveReceipt
1574
+ outcomes: tuple[InvocationOutcome, ...]
1575
+ selection_decision: RankedPortfolioDecision | None = None
1576
+ supplemental_selection_audit: PortfolioSelectionSupplementalAudit | None = None
1577
+ pending_memory_credit: PortfolioPendingMemoryCredit | None = None
1578
+
1579
+ def __post_init__(self) -> None:
1580
+ if type(self.receipt) is not PortfolioVariationWaveReceipt:
1581
+ raise TypeError("receipt must be an exact PortfolioVariationWaveReceipt")
1582
+ PortfolioVariationWaveReceipt.__post_init__(self.receipt)
1583
+ if type(self.outcomes) is not tuple or any(
1584
+ type(outcome) is not InvocationOutcome for outcome in self.outcomes
1585
+ ):
1586
+ raise TypeError("outcomes must contain exact InvocationOutcome values")
1587
+ if len(self.outcomes) != len(self.receipt.members):
1588
+ raise ValueError("result outcomes differ from the receipt member count")
1589
+ pending = self.pending_memory_credit
1590
+ if pending is not None:
1591
+ if type(pending) is not PortfolioPendingMemoryCredit:
1592
+ raise TypeError(
1593
+ "pending_memory_credit must be an exact pending credit or None"
1594
+ )
1595
+ PortfolioPendingMemoryCredit.__post_init__(pending)
1596
+ if self.receipt.memory_credit is not None:
1597
+ raise ValueError(
1598
+ "a wave result cannot be both pending and memory-committed"
1599
+ )
1600
+ if pending.candidate_ids != tuple(
1601
+ member.materialization.candidate_id for member in self.receipt.members
1602
+ ):
1603
+ raise ValueError("pending memory credit candidate IDs differ from wave")
1604
+ decision = self.selection_decision
1605
+ if decision is not None:
1606
+ if type(decision) is not RankedPortfolioDecision:
1607
+ raise TypeError(
1608
+ "selection_decision must be an exact RankedPortfolioDecision "
1609
+ "or None"
1610
+ )
1611
+ RankedPortfolioDecision.__post_init__(decision)
1612
+ if (
1613
+ decision.decision_sha256 != self.receipt.decision_sha256
1614
+ or decision.request_sha256 != self.receipt.request_sha256
1615
+ or decision.policy_id != self.receipt.selection_policy_id
1616
+ or decision.policy_version != self.receipt.selection_policy_version
1617
+ or decision.policy_definition_sha256
1618
+ != self.receipt.selection_policy_definition_sha256
1619
+ ):
1620
+ raise ValueError(
1621
+ "selection decision differs from the wave receipt identity"
1622
+ )
1623
+ if len(decision.members) != len(self.receipt.members):
1624
+ raise ValueError(
1625
+ "selection decision differs from the wave materialization count"
1626
+ )
1627
+ for selected, received in zip(
1628
+ decision.members,
1629
+ self.receipt.members,
1630
+ strict=True,
1631
+ ):
1632
+ materialization = received.materialization
1633
+ if (
1634
+ selected.rank != materialization.rank
1635
+ or selected.option_id != materialization.option_id
1636
+ or selected.option_identity_sha256
1637
+ != materialization.option_identity_sha256
1638
+ or selected.child_configuration_sha256
1639
+ != materialization.child_configuration_sha256
1640
+ ):
1641
+ raise ValueError(
1642
+ "selection decision differs from a wave materialization"
1643
+ )
1644
+ if self.receipt.action_attributions:
1645
+ for selected, attribution in zip(
1646
+ decision.members,
1647
+ self.receipt.action_attributions,
1648
+ strict=True,
1649
+ ):
1650
+ rationale_sha256 = hashlib.sha256(
1651
+ selected.design_rationale.encode(
1652
+ "utf-8",
1653
+ errors="strict",
1654
+ )
1655
+ ).hexdigest()
1656
+ if (
1657
+ attribution.decision_sha256 != decision.decision_sha256
1658
+ or attribution.card_snapshot_sha256
1659
+ != decision.card_snapshot_sha256
1660
+ or attribution.rank != selected.rank
1661
+ or attribution.option_id != selected.option_id
1662
+ or attribution.option_identity_sha256
1663
+ != selected.option_identity_sha256
1664
+ or attribution.child_configuration_sha256
1665
+ != selected.child_configuration_sha256
1666
+ or attribution.family != selected.family
1667
+ or attribution.supporting_card_keys
1668
+ != selected.supporting_card_keys
1669
+ or attribution.effect_predictions != selected.effect_predictions
1670
+ or attribution.design_rationale_sha256 != rationale_sha256
1671
+ ):
1672
+ raise ValueError(
1673
+ "action attribution differs from the selection decision"
1674
+ )
1675
+ audit = self.supplemental_selection_audit
1676
+ if audit is not None:
1677
+ if type(audit) is not PortfolioSelectionSupplementalAudit:
1678
+ raise TypeError("supplemental_selection_audit must be exact or None")
1679
+ audit.__post_init__()
1680
+ if decision is None:
1681
+ raise ValueError("supplemental selection audit requires a decision")
1682
+ if (
1683
+ audit.request_sha256 != decision.request_sha256
1684
+ or audit.decision_sha256 != decision.decision_sha256
1685
+ ):
1686
+ raise ValueError(
1687
+ "supplemental selection audit differs from the decision"
1688
+ )
1689
+ for member, outcome in zip(
1690
+ self.receipt.members,
1691
+ self.outcomes,
1692
+ strict=True,
1693
+ ):
1694
+ InvocationOutcome.__post_init__(outcome)
1695
+ materialization = member.materialization
1696
+ prepared = outcome.prepared
1697
+ candidate = outcome.candidate
1698
+ if (
1699
+ outcome.failure_stage is not None
1700
+ or candidate is None
1701
+ or not candidate.operator_compliant
1702
+ or not candidate.evidence_compliant
1703
+ ):
1704
+ raise ValueError("result contains a failed or partial outcome")
1705
+ detailed = candidate.detailed_evaluation
1706
+ detailed_sha256 = None if detailed is None else detailed.evidence_sha256
1707
+ observed_failure: PortfolioCandidateFailureEvidence | None = None
1708
+ if member.disposition is PortfolioMemberDisposition.SCORED:
1709
+ if not candidate.valid:
1710
+ raise ValueError(
1711
+ "scored result member contains an invalid candidate"
1712
+ )
1713
+ else:
1714
+ if candidate.valid or detailed is None or detailed.failure is None:
1715
+ raise ValueError(
1716
+ "candidate-infeasible result lacks detailed failure evidence"
1717
+ )
1718
+ observed_failure = (
1719
+ PortfolioCandidateFailureEvidence.from_failure_record(
1720
+ detailed.failure,
1721
+ detailed_evaluation_sha256=detailed.evidence_sha256,
1722
+ )
1723
+ )
1724
+ if (
1725
+ prepared.operator_invocation_id != member.operator_invocation_id
1726
+ or prepared.proposal_authority is not ProposalAuthority.ENGINE
1727
+ or prepared.call_id is not None
1728
+ or prepared.candidate_id != materialization.candidate_id
1729
+ or prepared.materialization_policy_id
1730
+ != PORTFOLIO_MATERIALIZATION_POLICY_ID
1731
+ or prepared.materialization_policy_version
1732
+ != PORTFOLIO_MATERIALIZATION_POLICY_VERSION
1733
+ or prepared.materialization_receipt_hash
1734
+ != materialization.receipt_sha256
1735
+ or prepared.variation_case.reward_definition_hash
1736
+ != member.reward_definition_sha256
1737
+ or candidate.candidate_id != materialization.candidate_id
1738
+ or candidate.occurrence.configuration_hash
1739
+ != materialization.child_configuration_sha256
1740
+ or candidate.generation != materialization.generation
1741
+ or candidate.operator_kind is not OperatorKind.TYPED_MUTATION
1742
+ or candidate.parent_ids != (materialization.parent_candidate_id,)
1743
+ or candidate.parent_patch_hashes != (materialization.patch_sha256,)
1744
+ or candidate.call_telemetry is not None
1745
+ or tuple(
1746
+ (item.path, item.source) for item in candidate.source_attribution
1747
+ )
1748
+ != tuple((path, "mutation") for path in materialization.changed_paths)
1749
+ or outcome.reward != member.reward
1750
+ or outcome.parent_relations != member.parent_relations
1751
+ or outcome.dominates_any_parent != member.dominates_any_parent
1752
+ or outcome.better_than_any_parent != member.better_than_any_parent
1753
+ or detailed_sha256 != member.detailed_evaluation_sha256
1754
+ or observed_failure != member.candidate_failure
1755
+ ):
1756
+ raise ValueError("result outcome differs from its receipt member")
1757
+
1758
+ @property
1759
+ def action_attributions(self) -> tuple[PortfolioActionAttributionReceipt, ...]:
1760
+ """Return exact per-action attribution receipts in ranked order."""
1761
+
1762
+ self.__post_init__()
1763
+ return self.receipt.action_attributions
1764
+
1765
+ @property
1766
+ def candidates(self) -> tuple[EvolutionCandidate, ...]:
1767
+ """Return the full ranked intention-to-treat candidate population."""
1768
+
1769
+ candidates: list[EvolutionCandidate] = []
1770
+ for outcome in self.outcomes:
1771
+ candidate = outcome.candidate
1772
+ if candidate is None: # pragma: no cover - closed by __post_init__.
1773
+ raise AssertionError("validated portfolio outcome lost its candidate")
1774
+ candidates.append(candidate)
1775
+ return tuple(candidates)
1776
+
1777
+ @property
1778
+ def scored_candidates(self) -> tuple[EvolutionCandidate, ...]:
1779
+ """Return only candidates with complete decision-objective vectors."""
1780
+
1781
+ return tuple(
1782
+ candidate
1783
+ for member, candidate in zip(
1784
+ self.receipt.members,
1785
+ self.candidates,
1786
+ strict=True,
1787
+ )
1788
+ if member.disposition is PortfolioMemberDisposition.SCORED
1789
+ )
1790
+
1791
+ @property
1792
+ def infeasible_candidates(self) -> tuple[EvolutionCandidate, ...]:
1793
+ """Return evaluated candidate-attributable infeasibilities in rank order."""
1794
+
1795
+ return tuple(
1796
+ candidate
1797
+ for member, candidate in zip(
1798
+ self.receipt.members,
1799
+ self.candidates,
1800
+ strict=True,
1801
+ )
1802
+ if member.disposition is PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
1803
+ )
1804
+
1805
+ @property
1806
+ def selection_decision_audit_record(self) -> dict[str, object] | None:
1807
+ """Return model reasoning for trace audit, or ``None`` for legacy results."""
1808
+
1809
+ if self.selection_decision is None:
1810
+ return None
1811
+ ranked = self.selection_decision.to_audit_record()
1812
+ if self.supplemental_selection_audit is None:
1813
+ return ranked
1814
+ return {
1815
+ "ranked_decision": ranked,
1816
+ "supplemental_selector_audit": (
1817
+ self.supplemental_selection_audit.to_record()
1818
+ ),
1819
+ }
1820
+
1821
+
1822
+ @dataclass(frozen=True, slots=True)
1823
+ class PortfolioMemoryCreditBatchPreparation:
1824
+ """Sealed, non-mutating preview of one generation memory publication.
1825
+
1826
+ ``prepared_results`` contain the exact committed-receipt projection that
1827
+ downstream prepare hooks may inspect, while ``expected_trials`` remain
1828
+ absent from the live memory bank until :class:`PortfolioEvolution` commits
1829
+ this exact value. Retaining the prior immutable trial snapshot turns a
1830
+ concurrent or out-of-band bank change into a stale-preparation failure.
1831
+ """
1832
+
1833
+ generation: int
1834
+ source_result_receipt_sha256s: tuple[str, ...]
1835
+ source_pending_credit_sha256s: tuple[str | None, ...]
1836
+ prepared_results: tuple[PortfolioVariationWaveResult, ...]
1837
+ expected_trials: tuple[InsightTrial, ...]
1838
+ prior_memory_trials: tuple[InsightTrial, ...]
1839
+ batch_receipt: PortfolioMemoryCreditBatchReceipt | None
1840
+
1841
+ def __post_init__(self) -> None:
1842
+ if type(self.generation) is not int or self.generation <= 0:
1843
+ raise ValueError("generation must be a positive exact integer")
1844
+ if (
1845
+ type(self.source_result_receipt_sha256s) is not tuple
1846
+ or not self.source_result_receipt_sha256s
1847
+ ):
1848
+ raise ValueError("source result receipts must be a non-empty exact tuple")
1849
+ for value in self.source_result_receipt_sha256s:
1850
+ require_sha256(value, "source_result_receipt_sha256")
1851
+ if (
1852
+ type(self.source_pending_credit_sha256s) is not tuple
1853
+ or len(self.source_pending_credit_sha256s)
1854
+ != len(self.source_result_receipt_sha256s)
1855
+ ):
1856
+ raise ValueError("pending credit identities differ from source results")
1857
+ for value in self.source_pending_credit_sha256s:
1858
+ if value is not None:
1859
+ require_sha256(value, "source_pending_credit_sha256")
1860
+ if (
1861
+ type(self.prepared_results) is not tuple
1862
+ or len(self.prepared_results) != len(self.source_result_receipt_sha256s)
1863
+ or any(
1864
+ type(value) is not PortfolioVariationWaveResult
1865
+ for value in self.prepared_results
1866
+ )
1867
+ ):
1868
+ raise ValueError("prepared_results must exactly cover source results")
1869
+ for result in self.prepared_results:
1870
+ PortfolioVariationWaveResult.__post_init__(result)
1871
+ if result.receipt.generation != self.generation:
1872
+ raise ValueError("prepared results differ from the generation")
1873
+ if result.pending_memory_credit is not None:
1874
+ raise ValueError("prepared results cannot retain pending memory credit")
1875
+ for name in ("expected_trials", "prior_memory_trials"):
1876
+ values = getattr(self, name)
1877
+ if type(values) is not tuple or any(
1878
+ type(value) is not InsightTrial for value in values
1879
+ ):
1880
+ raise TypeError(f"{name} must contain exact InsightTrial values")
1881
+ for value in values:
1882
+ InsightTrial.__post_init__(value)
1883
+ if self.batch_receipt is None:
1884
+ if self.expected_trials or any(self.source_pending_credit_sha256s):
1885
+ raise ValueError("a no-credit preparation cannot contain credit state")
1886
+ if any(
1887
+ result.receipt.memory_credit is not None
1888
+ for result in self.prepared_results
1889
+ ):
1890
+ raise ValueError("a no-credit preparation contains committed receipts")
1891
+ else:
1892
+ if type(self.batch_receipt) is not PortfolioMemoryCreditBatchReceipt:
1893
+ raise TypeError("batch_receipt must be exact or None")
1894
+ PortfolioMemoryCreditBatchReceipt.__post_init__(self.batch_receipt)
1895
+ if self.batch_receipt.generation != self.generation:
1896
+ raise ValueError("memory batch differs from the generation")
1897
+ if len(self.expected_trials) != len(self.batch_receipt.credits):
1898
+ raise ValueError("prospective trials differ from batch credits")
1899
+ if tuple(
1900
+ value.credit_unit_id for value in self.expected_trials
1901
+ ) != tuple(
1902
+ value.credit_unit_id for value in self.batch_receipt.credits
1903
+ ):
1904
+ raise ValueError("prospective trials differ from canonical credits")
1905
+ for trial, credit in zip(
1906
+ self.expected_trials,
1907
+ self.batch_receipt.credits,
1908
+ strict=True,
1909
+ ):
1910
+ if (
1911
+ insight_selection_decision_sha256(trial.decision)
1912
+ != credit.selection_decision_sha256
1913
+ or trial.decision.context_hash
1914
+ != credit.selection_decision_context_sha256
1915
+ or trial.candidate_ids != credit.candidate_ids
1916
+ or trial.reward_definition_hash
1917
+ != credit.aggregation_definition_sha256
1918
+ or trial.reward != credit.reward
1919
+ or trial.treatment_binding_sha256
1920
+ != credit.treatment_binding_sha256
1921
+ or trial.generation != credit.generation
1922
+ ):
1923
+ raise ValueError(
1924
+ "prospective trial differs from its committed credit receipt"
1925
+ )
1926
+ if self.batch_receipt.memory_trial_count_before != len(
1927
+ self.prior_memory_trials
1928
+ ):
1929
+ raise ValueError("batch receipt differs from prior memory snapshot")
1930
+
1931
+ @staticmethod
1932
+ def _trial_record(value: InsightTrial) -> dict[str, object]:
1933
+ return {
1934
+ "credit_unit_id": value.credit_unit_id.value,
1935
+ "candidate_ids": [item.value for item in value.candidate_ids],
1936
+ "reward_definition_sha256": value.reward_definition_hash,
1937
+ "selection_decision_sha256": insight_selection_decision_sha256(
1938
+ value.decision
1939
+ ),
1940
+ "reward_hex": value.reward.hex(),
1941
+ "treatment_binding_sha256": value.treatment_binding_sha256,
1942
+ "generation": value.generation,
1943
+ }
1944
+
1945
+ def _unsigned_record(self) -> dict[str, object]:
1946
+ self.__post_init__()
1947
+ return {
1948
+ "schema_version": 1,
1949
+ "generation": self.generation,
1950
+ "source_result_receipt_sha256s": list(
1951
+ self.source_result_receipt_sha256s
1952
+ ),
1953
+ "source_pending_credit_sha256s": list(
1954
+ self.source_pending_credit_sha256s
1955
+ ),
1956
+ "prepared_result_receipt_sha256s": [
1957
+ value.receipt.receipt_sha256 for value in self.prepared_results
1958
+ ],
1959
+ "prior_memory_trials": [
1960
+ self._trial_record(value) for value in self.prior_memory_trials
1961
+ ],
1962
+ "expected_trials": [
1963
+ self._trial_record(value) for value in self.expected_trials
1964
+ ],
1965
+ "memory_credit_batch_receipt_sha256": (
1966
+ None
1967
+ if self.batch_receipt is None
1968
+ else self.batch_receipt.receipt_sha256
1969
+ ),
1970
+ }
1971
+
1972
+ @property
1973
+ def preparation_sha256(self) -> str:
1974
+ return _hash_record(
1975
+ _MEMORY_CREDIT_BATCH_PREPARATION_DOMAIN,
1976
+ self._unsigned_record(),
1977
+ )
1978
+
1979
+ def to_record(self) -> dict[str, object]:
1980
+ return {**self._unsigned_record(), "preparation_sha256": self.preparation_sha256}
1981
+
1982
+
1983
+ @runtime_checkable
1984
+ class MaterializedPortfolioEngine(Protocol):
1985
+ async def run_materialized_invocations(
1986
+ self,
1987
+ items: tuple[MaterializedInvocation, ...],
1988
+ *,
1989
+ reward_binding: RewardPolicyBinding | None = None,
1990
+ ) -> tuple[InvocationOutcome, ...]: ...
1991
+
1992
+
1993
+ # A distinct, non-secret constant standing where a telemetry digest would go
1994
+ # for a selection that provably made no provider call. It is not a hash of any
1995
+ # telemetry record: there is no record, and inventing one would be the
1996
+ # fabrication this design exists to prevent.
1997
+ PROVIDER_FREE_SELECTION_TELEMETRY_SHA256 = hashlib.sha256(
1998
+ b"agent-evolve:provider-free-portfolio-selection:v1\x00"
1999
+ b"no-provider-call-observed-over-the-selection-window"
2000
+ ).hexdigest()
2001
+
2002
+
2003
+ @runtime_checkable
2004
+ class ProviderTrafficWitness(Protocol):
2005
+ """Counts provider calls actually observed, for verifying a claimed zero.
2006
+
2007
+ The runtime reads this immediately before and after a selection. A policy
2008
+ that asserts ``provider_free`` must leave the count unchanged; if it moved,
2009
+ the claim was false and the wave fails loudly rather than sealing a receipt
2010
+ that says no call happened while one did.
2011
+
2012
+ Implementations should count from the journals the run declared -- see
2013
+ ``agent_evolve.provider_accounting.measure_provider_usage`` -- so that a
2014
+ missing sink is an instrumentation fault rather than a silent zero.
2015
+ """
2016
+
2017
+ def observed_provider_calls(self) -> int: ...
2018
+
2019
+ @dataclass(frozen=True, slots=True)
2020
+ class PreparedPortfolioVariationWave:
2021
+ """One selected and materialized portfolio before any real evaluation.
2022
+
2023
+ This is the public hand-off used by residual proposal markets. The LLM
2024
+ decision has already been resolved against the sealed finite contract and
2025
+ every target occurrence has been allocated, but the expensive evaluator
2026
+ has not run. A downstream broker can therefore compare these actions with
2027
+ acquisition, restart, or recombination proposals before spending budget.
2028
+ """
2029
+
2030
+ wave: PortfolioVariationWaveRequest
2031
+ selection: PortfolioSelectionResult
2032
+ selection_telemetry_sha256: str
2033
+ invocations: tuple[MaterializedInvocation, ...]
2034
+ materializations: tuple[PortfolioMemberMaterializationReceipt, ...]
2035
+ preparation_sha256: str = field(init=False)
2036
+
2037
+ def __post_init__(self) -> None:
2038
+ if type(self.wave) is not PortfolioVariationWaveRequest:
2039
+ raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
2040
+ PortfolioVariationWaveRequest.__post_init__(self.wave)
2041
+ if type(self.selection) is not PortfolioSelectionResult:
2042
+ raise TypeError("selection must be an exact PortfolioSelectionResult")
2043
+ PortfolioSelectionResult.__post_init__(self.selection)
2044
+ telemetry = self.selection.telemetry
2045
+ require_sha256(
2046
+ self.selection_telemetry_sha256,
2047
+ "selection_telemetry_sha256",
2048
+ )
2049
+ # A provider-free selection has no telemetry to digest, by construction:
2050
+ # there was no call. It carries the distinct sentinel instead, and the
2051
+ # zero it asserts was verified against a traffic witness at selection
2052
+ # time. Absence is therefore checked here, never inferred.
2053
+ if telemetry is None:
2054
+ if not self.selection.provider_free:
2055
+ raise ValueError("prepared portfolio selection requires telemetry")
2056
+ if self.selection_telemetry_sha256 != (
2057
+ PROVIDER_FREE_SELECTION_TELEMETRY_SHA256
2058
+ ):
2059
+ raise ValueError(
2060
+ "a provider-free prepared selection must carry the "
2061
+ "provider-free telemetry sentinel"
2062
+ )
2063
+ elif (
2064
+ portfolio_selection_telemetry_sha256(telemetry)
2065
+ != self.selection_telemetry_sha256
2066
+ ):
2067
+ raise ValueError("prepared telemetry identity differs from selection")
2068
+ validate_ranked_portfolio_decision(
2069
+ self.wave.selection_request,
2070
+ self.selection.decision,
2071
+ )
2072
+ count = len(self.selection.decision.members)
2073
+ if (
2074
+ type(self.invocations) is not tuple
2075
+ or type(self.materializations) is not tuple
2076
+ or len(self.invocations) != count
2077
+ or len(self.materializations) != count
2078
+ ):
2079
+ raise ValueError("prepared members must exactly cover the selection")
2080
+ for invocation in self.invocations:
2081
+ if type(invocation) is not MaterializedInvocation:
2082
+ raise TypeError("invocations must contain exact materialized values")
2083
+ MaterializedInvocation.__post_init__(invocation)
2084
+ for receipt in self.materializations:
2085
+ if type(receipt) is not PortfolioMemberMaterializationReceipt:
2086
+ raise TypeError("materializations must contain exact receipts")
2087
+ PortfolioMemberMaterializationReceipt.__post_init__(receipt)
2088
+ for selected, invocation, receipt in zip(
2089
+ self.selection.decision.members,
2090
+ self.invocations,
2091
+ self.materializations,
2092
+ strict=True,
2093
+ ):
2094
+ if (
2095
+ selected.rank != receipt.rank
2096
+ or selected.option_id != receipt.option_id
2097
+ or selected.option_identity_sha256
2098
+ != receipt.option_identity_sha256
2099
+ or selected.child_configuration_sha256
2100
+ != receipt.child_configuration_sha256
2101
+ or invocation.candidate_id != receipt.candidate_id
2102
+ or invocation.materialization_receipt_hash
2103
+ != receipt.receipt_sha256
2104
+ or invocation.plan.parents != (self.wave.parent,)
2105
+ or invocation.plan.generation != self.wave.generation
2106
+ or typed_json_sha256(freeze_json(invocation.draft.configuration))
2107
+ != receipt.child_configuration_sha256
2108
+ ):
2109
+ raise ValueError(
2110
+ "prepared invocation differs from its selected finite option"
2111
+ )
2112
+ candidate_ids = tuple(value.candidate_id for value in self.invocations)
2113
+ child_sha256s = tuple(
2114
+ value.child_configuration_sha256 for value in self.materializations
2115
+ )
2116
+ if (
2117
+ len(set(candidate_ids)) != count
2118
+ or len(set(child_sha256s)) != count
2119
+ ):
2120
+ raise ValueError("prepared portfolio contains colliding members")
2121
+ object.__setattr__(
2122
+ self,
2123
+ "preparation_sha256",
2124
+ _hash_record(
2125
+ _PREPARED_WAVE_DOMAIN,
2126
+ {
2127
+ "schema_version": 1,
2128
+ "request_sha256": self.wave.selection_request.request_sha256,
2129
+ "decision_sha256": self.selection.decision.decision_sha256,
2130
+ "selection_telemetry_sha256": (
2131
+ self.selection_telemetry_sha256
2132
+ ),
2133
+ "parent_candidate_id": self.wave.parent.candidate_id.value,
2134
+ "parent_configuration_sha256": (
2135
+ self.wave.parent.occurrence.configuration_hash
2136
+ ),
2137
+ "generation": self.wave.generation,
2138
+ "materialization_receipt_sha256s": [
2139
+ value.receipt_sha256 for value in self.materializations
2140
+ ],
2141
+ "evaluation_performed": False,
2142
+ },
2143
+ ),
2144
+ )
2145
+
2146
+ def to_record(self) -> dict[str, object]:
2147
+ self.__post_init__()
2148
+ return {
2149
+ "schema_version": 1,
2150
+ "request_sha256": self.wave.selection_request.request_sha256,
2151
+ "decision_sha256": self.selection.decision.decision_sha256,
2152
+ "selection_telemetry_sha256": self.selection_telemetry_sha256,
2153
+ "parent_candidate_id": self.wave.parent.candidate_id.value,
2154
+ "parent_configuration_sha256": (
2155
+ self.wave.parent.occurrence.configuration_hash
2156
+ ),
2157
+ "generation": self.wave.generation,
2158
+ "materializations": [
2159
+ value.to_record() for value in self.materializations
2160
+ ],
2161
+ "preparation_sha256": self.preparation_sha256,
2162
+ "evaluation_performed": False,
2163
+ }
2164
+
2165
+
2166
+ @dataclass(frozen=True, slots=True)
2167
+ class EvaluatedPreparedPortfolioMember:
2168
+ """A broker-selected prepared member joined to one exact engine outcome."""
2169
+
2170
+ selected: RankedPortfolioMember
2171
+ invocation: MaterializedInvocation
2172
+ materialization: PortfolioMemberMaterializationReceipt
2173
+ outcome: InvocationOutcome
2174
+ receipt: PortfolioVariationMemberReceipt
2175
+
2176
+ def __post_init__(self) -> None:
2177
+ if type(self.selected) is not RankedPortfolioMember:
2178
+ raise TypeError("selected must be an exact ranked member")
2179
+ RankedPortfolioMember.__post_init__(self.selected)
2180
+ if type(self.invocation) is not MaterializedInvocation:
2181
+ raise TypeError("invocation must be exact")
2182
+ MaterializedInvocation.__post_init__(self.invocation)
2183
+ if type(self.materialization) is not PortfolioMemberMaterializationReceipt:
2184
+ raise TypeError("materialization must be exact")
2185
+ PortfolioMemberMaterializationReceipt.__post_init__(self.materialization)
2186
+ if type(self.outcome) is not InvocationOutcome:
2187
+ raise TypeError("outcome must be exact")
2188
+ InvocationOutcome.__post_init__(self.outcome)
2189
+ if type(self.receipt) is not PortfolioVariationMemberReceipt:
2190
+ raise TypeError("receipt must be an exact variation-member receipt")
2191
+ PortfolioVariationMemberReceipt.__post_init__(self.receipt)
2192
+ if (
2193
+ self.selected.rank != self.materialization.rank
2194
+ or self.invocation.candidate_id != self.materialization.candidate_id
2195
+ or self.receipt.materialization != self.materialization
2196
+ or self.outcome.candidate is None
2197
+ or self.outcome.candidate.candidate_id
2198
+ != self.materialization.candidate_id
2199
+ ):
2200
+ raise ValueError("evaluated prepared member has a broken exact join")
2201
+
2202
+
2203
+ @dataclass(slots=True)
2204
+ class PortfolioEvolution:
2205
+ """Execute one ranked finite portfolio as a concurrent exact wave."""
2206
+
2207
+ engine: MaterializedPortfolioEngine
2208
+ selector: PortfolioSelectionPolicy
2209
+ ids: IdFactory
2210
+ memory: InsightMemoryBank | None = None
2211
+ provider_traffic_witness: ProviderTrafficWitness | None = None
2212
+
2213
+ def __post_init__(self) -> None:
2214
+ if not isinstance(self.engine, MaterializedPortfolioEngine):
2215
+ raise TypeError("engine must implement run_materialized_invocations")
2216
+ if not isinstance(self.selector, PortfolioSelectionPolicy):
2217
+ raise TypeError("selector must implement PortfolioSelectionPolicy")
2218
+ if not isinstance(self.ids, IdFactory):
2219
+ raise TypeError("ids must implement IdFactory")
2220
+ if self.memory is not None and type(self.memory) is not InsightMemoryBank:
2221
+ raise TypeError("memory must be an exact InsightMemoryBank or None")
2222
+ if self.provider_traffic_witness is not None and not isinstance(
2223
+ self.provider_traffic_witness, ProviderTrafficWitness
2224
+ ):
2225
+ raise TypeError(
2226
+ "provider_traffic_witness must implement observed_provider_calls"
2227
+ )
2228
+
2229
+ def _materialize_member(
2230
+ self,
2231
+ wave: PortfolioVariationWaveRequest,
2232
+ decision: RankedPortfolioDecision,
2233
+ telemetry_sha256: str,
2234
+ member: RankedPortfolioMember,
2235
+ ) -> tuple[MaterializedInvocation, PortfolioMemberMaterializationReceipt]:
2236
+ contract = wave.selection_request.finite_variation_contract
2237
+ option = contract.resolve(member.option_id)
2238
+ if (
2239
+ member.option_identity_sha256 != option.identity_sha256
2240
+ or member.child_configuration_sha256 != option.child_configuration_sha256
2241
+ ):
2242
+ raise ValueError("ranked member drifted from its sealed finite option")
2243
+ candidate_id = self.ids.new_candidate_id()
2244
+ patch = derive_patch(
2245
+ wave.parent.configuration,
2246
+ option.child_configuration,
2247
+ base_candidate_id=wave.parent.candidate_id,
2248
+ target_candidate_id=candidate_id,
2249
+ )
2250
+ if not patch.operations:
2251
+ raise ValueError(
2252
+ "ranked finite option materialized no parent-relative patch"
2253
+ )
2254
+ top_level: set[str] = set()
2255
+ changed_paths: set[str] = set()
2256
+ for operation in patch.operations:
2257
+ if (
2258
+ not operation.path.segments
2259
+ or type(operation.path.segments[0]) is not ObjectKey
2260
+ ):
2261
+ raise ValueError("ranked finite option changed the candidate root")
2262
+ top_level.add(operation.path.segments[0].value)
2263
+ changed_paths.add(_path_text(operation.path))
2264
+ paths = tuple(sorted(changed_paths))
2265
+ receipt = PortfolioMemberMaterializationReceipt(
2266
+ request_sha256=wave.selection_request.request_sha256,
2267
+ decision_sha256=decision.decision_sha256,
2268
+ selection_telemetry_sha256=telemetry_sha256,
2269
+ rank=member.rank,
2270
+ option_id=member.option_id,
2271
+ option_identity_sha256=member.option_identity_sha256,
2272
+ child_configuration_sha256=member.child_configuration_sha256,
2273
+ parent_candidate_id=wave.parent.candidate_id,
2274
+ parent_configuration_sha256=(wave.parent.occurrence.configuration_hash),
2275
+ generation=wave.generation,
2276
+ candidate_id=candidate_id,
2277
+ patch_sha256=patch.patch_hash,
2278
+ changed_paths=paths,
2279
+ )
2280
+ configuration = thaw_json(option.child_configuration)
2281
+ if type(configuration) is not dict:
2282
+ raise TypeError("finite option child must be an object")
2283
+ plan = InvocationPlan(
2284
+ operator_kind=OperatorKind.TYPED_MUTATION,
2285
+ parents=(wave.parent,),
2286
+ generation=wave.generation,
2287
+ label=f"{wave.label_prefix}.rank_{member.rank:04d}",
2288
+ allowed_top_level=tuple(sorted(top_level)),
2289
+ phase=wave.phase,
2290
+ )
2291
+ invocation = MaterializedInvocation(
2292
+ plan=plan,
2293
+ draft=CandidateDraft(
2294
+ configuration=configuration,
2295
+ design_rationale=(
2296
+ "Engine materialized one exact sealed finite option selected "
2297
+ "by ranked opaque identifier."
2298
+ ),
2299
+ intended_changes=paths,
2300
+ source_attribution=tuple(
2301
+ SourceAttribution(path, "mutation") for path in paths
2302
+ ),
2303
+ ),
2304
+ candidate_id=candidate_id,
2305
+ materialization_policy_id=PORTFOLIO_MATERIALIZATION_POLICY_ID,
2306
+ materialization_policy_version=(PORTFOLIO_MATERIALIZATION_POLICY_VERSION),
2307
+ materialization_receipt_hash=receipt.receipt_sha256,
2308
+ )
2309
+ return invocation, receipt
2310
+
2311
+ async def prepare(
2312
+ self,
2313
+ wave: PortfolioVariationWaveRequest,
2314
+ ) -> PreparedPortfolioVariationWave:
2315
+ """Select once and materialize every member without evaluating it."""
2316
+
2317
+ if type(wave) is not PortfolioVariationWaveRequest:
2318
+ raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
2319
+ PortfolioVariationWaveRequest.__post_init__(wave)
2320
+ request_sha256 = wave.selection_request.request_sha256
2321
+ contract_sha256 = (
2322
+ wave.selection_request.finite_variation_contract.identity_sha256
2323
+ )
2324
+ parent_sha256 = wave.parent.occurrence.configuration_hash
2325
+ observed_before = (
2326
+ self.provider_traffic_witness.observed_provider_calls()
2327
+ if self.provider_traffic_witness is not None
2328
+ else None
2329
+ )
2330
+ result = await self.selector.select(wave.selection_request)
2331
+ if type(result) is not PortfolioSelectionResult:
2332
+ raise TypeError("selector must return an exact PortfolioSelectionResult")
2333
+ PortfolioSelectionResult.__post_init__(result)
2334
+ if result.provider_free:
2335
+ # The assertion is not taken on trust. A provider-free selection is
2336
+ # a measured zero over the selection window, and a selector that
2337
+ # claims one while reaching the provider fails here rather than
2338
+ # sealing a receipt that contradicts the journals.
2339
+ if self.provider_traffic_witness is None:
2340
+ raise ValueError(
2341
+ "a provider-free selection requires a provider traffic "
2342
+ "witness; an unverified claim is not evidence"
2343
+ )
2344
+ observed_after = self.provider_traffic_witness.observed_provider_calls()
2345
+ if observed_after != observed_before:
2346
+ raise ValueError(
2347
+ "selector asserted provider_free but "
2348
+ f"{observed_after - observed_before} provider call(s) were "
2349
+ "observed during the selection window"
2350
+ )
2351
+ telemetry_sha256 = PROVIDER_FREE_SELECTION_TELEMETRY_SHA256
2352
+ else:
2353
+ if result.telemetry is None:
2354
+ raise ValueError("portfolio selection requires exact call telemetry")
2355
+ telemetry_sha256 = portfolio_selection_telemetry_sha256(result.telemetry)
2356
+ validate_ranked_portfolio_decision(wave.selection_request, result.decision)
2357
+ if (
2358
+ wave.selection_request.request_sha256 != request_sha256
2359
+ or wave.selection_request.finite_variation_contract.identity_sha256
2360
+ != contract_sha256
2361
+ or wave.parent.occurrence.configuration_hash != parent_sha256
2362
+ or not typed_json_equal(
2363
+ wave.parent.configuration,
2364
+ wave.selection_request.finite_variation_contract.parent_configuration,
2365
+ )
2366
+ ):
2367
+ raise ValueError(
2368
+ "portfolio parent or request contract drifted during selection"
2369
+ )
2370
+ materialized_pairs = tuple(
2371
+ self._materialize_member(
2372
+ wave,
2373
+ result.decision,
2374
+ telemetry_sha256,
2375
+ member,
2376
+ )
2377
+ for member in result.decision.members
2378
+ )
2379
+ return PreparedPortfolioVariationWave(
2380
+ wave=wave,
2381
+ selection=result,
2382
+ selection_telemetry_sha256=telemetry_sha256,
2383
+ invocations=tuple(value[0] for value in materialized_pairs),
2384
+ materializations=tuple(value[1] for value in materialized_pairs),
2385
+ )
2386
+
2387
+ async def evaluate_prepared_members(
2388
+ self,
2389
+ prepared: PreparedPortfolioVariationWave,
2390
+ candidate_ids: tuple[CandidateId, ...],
2391
+ *,
2392
+ reward_binding: RewardPolicyBinding | None = None,
2393
+ ) -> tuple[EvaluatedPreparedPortfolioMember, ...]:
2394
+ """Evaluate exactly the requested subset of one prepared portfolio."""
2395
+
2396
+ if type(prepared) is not PreparedPortfolioVariationWave:
2397
+ raise TypeError("prepared must be an exact prepared portfolio")
2398
+ PreparedPortfolioVariationWave.__post_init__(prepared)
2399
+ if (
2400
+ type(candidate_ids) is not tuple
2401
+ or not candidate_ids
2402
+ or any(type(value) is not CandidateId for value in candidate_ids)
2403
+ ):
2404
+ raise TypeError("candidate_ids must be a non-empty exact tuple")
2405
+ for value in candidate_ids:
2406
+ CandidateId.__post_init__(value)
2407
+ if len(set(candidate_ids)) != len(candidate_ids):
2408
+ raise ValueError("candidate_ids must be unique")
2409
+ if reward_binding is not None:
2410
+ if type(reward_binding) is not RewardPolicyBinding:
2411
+ raise TypeError("reward_binding must be exact or None")
2412
+ RewardPolicyBinding.__post_init__(reward_binding)
2413
+ index = {
2414
+ invocation.candidate_id: (
2415
+ selected,
2416
+ invocation,
2417
+ materialization,
2418
+ )
2419
+ for selected, invocation, materialization in zip(
2420
+ prepared.selection.decision.members,
2421
+ prepared.invocations,
2422
+ prepared.materializations,
2423
+ strict=True,
2424
+ )
2425
+ }
2426
+ try:
2427
+ selected_rows = tuple(index[value] for value in candidate_ids)
2428
+ except KeyError as error:
2429
+ raise ValueError(
2430
+ "requested evaluation is outside the prepared portfolio"
2431
+ ) from error
2432
+ outcomes = await self.engine.run_materialized_invocations(
2433
+ tuple(value[1] for value in selected_rows),
2434
+ reward_binding=reward_binding,
2435
+ )
2436
+ if type(outcomes) is not tuple or len(outcomes) != len(selected_rows):
2437
+ raise ValueError("engine returned a partial portfolio outcome wave")
2438
+ evaluated: list[EvaluatedPreparedPortfolioMember] = []
2439
+ for (selected, invocation, materialization), outcome in zip(
2440
+ selected_rows,
2441
+ outcomes,
2442
+ strict=True,
2443
+ ):
2444
+ receipt = self._join_outcome(
2445
+ invocation,
2446
+ materialization,
2447
+ outcome,
2448
+ )
2449
+ evaluated.append(
2450
+ EvaluatedPreparedPortfolioMember(
2451
+ selected=selected,
2452
+ invocation=invocation,
2453
+ materialization=materialization,
2454
+ outcome=outcome,
2455
+ receipt=receipt,
2456
+ )
2457
+ )
2458
+ return tuple(evaluated)
2459
+
2460
+ @staticmethod
2461
+ def _join_outcome(
2462
+ invocation: MaterializedInvocation,
2463
+ materialization: PortfolioMemberMaterializationReceipt,
2464
+ outcome: InvocationOutcome,
2465
+ ) -> PortfolioVariationMemberReceipt:
2466
+ if type(outcome) is not InvocationOutcome:
2467
+ raise TypeError("engine outcomes must be exact InvocationOutcome values")
2468
+ InvocationOutcome.__post_init__(outcome)
2469
+ prepared = outcome.prepared
2470
+ candidate = outcome.candidate
2471
+ if (
2472
+ prepared.plan != invocation.plan
2473
+ or prepared.proposal_authority is not ProposalAuthority.ENGINE
2474
+ or prepared.call_id is not None
2475
+ or prepared.candidate_id != materialization.candidate_id
2476
+ or prepared.materialization_policy_id != PORTFOLIO_MATERIALIZATION_POLICY_ID
2477
+ or prepared.materialization_policy_version
2478
+ != PORTFOLIO_MATERIALIZATION_POLICY_VERSION
2479
+ or prepared.materialization_receipt_hash != materialization.receipt_sha256
2480
+ ):
2481
+ raise ValueError("engine outcome differs from its materialized member")
2482
+ if (
2483
+ outcome.failure_stage is not None
2484
+ or candidate is None
2485
+ or not candidate.operator_compliant
2486
+ or not candidate.evidence_compliant
2487
+ ):
2488
+ raise ValueError("portfolio wave contains a failed or partial member")
2489
+ if (
2490
+ candidate.candidate_id != materialization.candidate_id
2491
+ or candidate.occurrence.configuration_hash
2492
+ != materialization.child_configuration_sha256
2493
+ or not typed_json_equal(
2494
+ candidate.configuration,
2495
+ freeze_json(invocation.draft.configuration),
2496
+ )
2497
+ or candidate.parent_ids != (materialization.parent_candidate_id,)
2498
+ or candidate.parent_patch_hashes != (materialization.patch_sha256,)
2499
+ or candidate.call_telemetry is not None
2500
+ or tuple((item.path, item.source) for item in candidate.source_attribution)
2501
+ != tuple((path, "mutation") for path in materialization.changed_paths)
2502
+ ):
2503
+ raise ValueError("portfolio candidate differs from exact materialization")
2504
+ detailed = candidate.detailed_evaluation
2505
+ disposition = PortfolioMemberDisposition.SCORED
2506
+ candidate_failure: PortfolioCandidateFailureEvidence | None = None
2507
+ if not candidate.valid:
2508
+ if detailed is None or detailed.failure is None:
2509
+ raise ValueError(
2510
+ "candidate infeasibility requires detailed evaluator evidence"
2511
+ )
2512
+ candidate_failure = PortfolioCandidateFailureEvidence.from_failure_record(
2513
+ detailed.failure,
2514
+ detailed_evaluation_sha256=detailed.evidence_sha256,
2515
+ )
2516
+ disposition = PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
2517
+ if outcome.parent_relations:
2518
+ raise ValueError(
2519
+ "candidate infeasibility cannot publish parent relations"
2520
+ )
2521
+ if outcome.dominates_any_parent or outcome.better_than_any_parent:
2522
+ raise ValueError(
2523
+ "candidate infeasibility cannot publish improvement flags"
2524
+ )
2525
+ return PortfolioVariationMemberReceipt(
2526
+ materialization=materialization,
2527
+ operator_invocation_id=prepared.operator_invocation_id,
2528
+ reward_definition_sha256=(prepared.variation_case.reward_definition_hash),
2529
+ reward=outcome.reward,
2530
+ parent_relations=outcome.parent_relations,
2531
+ detailed_evaluation_sha256=(
2532
+ None if detailed is None else detailed.evidence_sha256
2533
+ ),
2534
+ dominates_any_parent=outcome.dominates_any_parent,
2535
+ better_than_any_parent=outcome.better_than_any_parent,
2536
+ disposition=disposition,
2537
+ candidate_failure=candidate_failure,
2538
+ )
2539
+
2540
+ @staticmethod
2541
+ def _join_action_attribution(
2542
+ request: PortfolioSelectionRequest,
2543
+ decision: RankedPortfolioDecision,
2544
+ selected: RankedPortfolioMember,
2545
+ member: PortfolioVariationMemberReceipt,
2546
+ ) -> PortfolioActionAttributionReceipt:
2547
+ """Bind one selected member to its exact cards and evaluated outcome."""
2548
+
2549
+ if type(request) is not PortfolioSelectionRequest:
2550
+ raise TypeError("request must be an exact PortfolioSelectionRequest")
2551
+ PortfolioSelectionRequest.__post_init__(request)
2552
+ if type(decision) is not RankedPortfolioDecision:
2553
+ raise TypeError("decision must be an exact RankedPortfolioDecision")
2554
+ RankedPortfolioDecision.__post_init__(decision)
2555
+ if type(selected) is not RankedPortfolioMember:
2556
+ raise TypeError("selected must be an exact RankedPortfolioMember")
2557
+ RankedPortfolioMember.__post_init__(selected)
2558
+ if type(member) is not PortfolioVariationMemberReceipt:
2559
+ raise TypeError("member must be an exact portfolio member receipt")
2560
+ PortfolioVariationMemberReceipt.__post_init__(member)
2561
+ materialization = member.materialization
2562
+ if (
2563
+ decision.request_sha256 != request.request_sha256
2564
+ or decision.card_snapshot_sha256 != request.card_snapshot_sha256
2565
+ or selected.rank != materialization.rank
2566
+ or selected.option_id != materialization.option_id
2567
+ or selected.option_identity_sha256 != materialization.option_identity_sha256
2568
+ or selected.child_configuration_sha256
2569
+ != materialization.child_configuration_sha256
2570
+ ):
2571
+ raise ValueError(
2572
+ "selected member differs from its request or materialization"
2573
+ )
2574
+ card_by_key = {card.card_key: card for card in request.cards}
2575
+ supporting_cards = tuple(
2576
+ PortfolioActionCardAttribution(
2577
+ card_key=card_key,
2578
+ reference=card_by_key[card_key].reference,
2579
+ content_sha256=card_by_key[card_key].content_sha256,
2580
+ evidence_sha256=card_by_key[card_key].evidence_sha256,
2581
+ )
2582
+ for card_key in selected.supporting_card_keys
2583
+ )
2584
+ return PortfolioActionAttributionReceipt(
2585
+ request_sha256=request.request_sha256,
2586
+ decision_sha256=decision.decision_sha256,
2587
+ card_snapshot_sha256=request.card_snapshot_sha256,
2588
+ rank=selected.rank,
2589
+ option_id=selected.option_id,
2590
+ option_identity_sha256=selected.option_identity_sha256,
2591
+ child_configuration_sha256=selected.child_configuration_sha256,
2592
+ family=selected.family,
2593
+ supporting_cards=supporting_cards,
2594
+ effect_predictions=selected.effect_predictions,
2595
+ design_rationale_sha256=hashlib.sha256(
2596
+ selected.design_rationale.encode("utf-8", errors="strict")
2597
+ ).hexdigest(),
2598
+ materialization_receipt_sha256=materialization.receipt_sha256,
2599
+ outcome_sha256=member.outcome_sha256,
2600
+ operator_invocation_id=member.operator_invocation_id,
2601
+ candidate_id=materialization.candidate_id,
2602
+ )
2603
+
2604
+ def prepare_pending_memory_credit_batch(
2605
+ self,
2606
+ results: tuple[PortfolioVariationWaveResult, ...],
2607
+ ) -> PortfolioMemoryCreditBatchPreparation:
2608
+ """Preview a complete concurrent stage without mutating memory.
2609
+
2610
+ Result order remains the caller's stable decision-slot order. Only the
2611
+ prospective memory mutation and batch receipt are canonicalized by
2612
+ preassigned credit-unit ID, so task completion order cannot affect the
2613
+ later commit. All estimator and ownership validation runs here.
2614
+ """
2615
+
2616
+ if type(results) is not tuple:
2617
+ raise TypeError("results must be an exact tuple")
2618
+ if any(type(result) is not PortfolioVariationWaveResult for result in results):
2619
+ raise TypeError("results must contain exact portfolio wave results")
2620
+ for result in results:
2621
+ PortfolioVariationWaveResult.__post_init__(result)
2622
+ pending_by_result = tuple(
2623
+ result.pending_memory_credit for result in results
2624
+ )
2625
+ pending = tuple(value for value in pending_by_result if value is not None)
2626
+ generations = {result.receipt.generation for result in results}
2627
+ if len(generations) != 1:
2628
+ raise ValueError("one memory-credit batch cannot span generations")
2629
+ generation = next(iter(generations))
2630
+ source_receipts = tuple(result.receipt.receipt_sha256 for result in results)
2631
+ source_pending = tuple(
2632
+ None if value is None else value.pending_sha256
2633
+ for value in pending_by_result
2634
+ )
2635
+ if not pending:
2636
+ return PortfolioMemoryCreditBatchPreparation(
2637
+ generation=generation,
2638
+ source_result_receipt_sha256s=source_receipts,
2639
+ source_pending_credit_sha256s=source_pending,
2640
+ prepared_results=results,
2641
+ expected_trials=(),
2642
+ prior_memory_trials=(
2643
+ () if self.memory is None else self.memory.trials
2644
+ ),
2645
+ batch_receipt=None,
2646
+ )
2647
+ if self.memory is None:
2648
+ raise ValueError("pending portfolio memory credit requires a memory bank")
2649
+ if any(result.receipt.memory_credit is not None for result in results):
2650
+ raise ValueError("cannot mix committed and pending stage memory credits")
2651
+
2652
+ canonical_pending = tuple(
2653
+ sorted(pending, key=lambda value: value.credit_unit_id.value)
2654
+ )
2655
+ canonical_ids = tuple(
2656
+ value.credit_unit_id.value for value in canonical_pending
2657
+ )
2658
+ if len(set(canonical_ids)) != len(canonical_ids):
2659
+ raise ValueError("pending stage repeats a memory credit unit")
2660
+ committed_receipts = tuple(
2661
+ value.to_committed_receipt() for value in canonical_pending
2662
+ )
2663
+ receipt_by_unit = {
2664
+ value.credit_unit_id: value for value in committed_receipts
2665
+ }
2666
+ committed_results = tuple(
2667
+ result
2668
+ if result.pending_memory_credit is None
2669
+ else replace(
2670
+ result,
2671
+ receipt=replace(
2672
+ result.receipt,
2673
+ memory_credit=receipt_by_unit[
2674
+ result.pending_memory_credit.credit_unit_id
2675
+ ],
2676
+ ),
2677
+ pending_memory_credit=None,
2678
+ )
2679
+ for result in results
2680
+ )
2681
+ prior_memory_trials = self.memory.trials
2682
+ trial_count_before = len(prior_memory_trials)
2683
+ batch_receipt = PortfolioMemoryCreditBatchReceipt(
2684
+ generation=generation,
2685
+ credits=committed_receipts,
2686
+ memory_trial_count_before=trial_count_before,
2687
+ memory_trial_count_after=trial_count_before + len(committed_receipts),
2688
+ )
2689
+ expected_trials = tuple(value.to_trial() for value in canonical_pending)
2690
+ # Exercise the complete bank estimator validation now. The bank method
2691
+ # is deliberately non-mutating; commit rechecks the retained exact prior
2692
+ # trial tuple before applying the same canonical batch.
2693
+ previewed_trials = self.memory.preview_trials_batch(expected_trials)
2694
+ if previewed_trials != expected_trials:
2695
+ raise RuntimeError("memory bank changed canonical preview order")
2696
+ return PortfolioMemoryCreditBatchPreparation(
2697
+ generation=generation,
2698
+ source_result_receipt_sha256s=source_receipts,
2699
+ source_pending_credit_sha256s=source_pending,
2700
+ prepared_results=committed_results,
2701
+ expected_trials=expected_trials,
2702
+ prior_memory_trials=prior_memory_trials,
2703
+ batch_receipt=batch_receipt,
2704
+ )
2705
+
2706
+ def commit_prepared_memory_credit_batch(
2707
+ self,
2708
+ preparation: PortfolioMemoryCreditBatchPreparation,
2709
+ ) -> tuple[
2710
+ tuple[PortfolioVariationWaveResult, ...],
2711
+ PortfolioMemoryCreditBatchReceipt | None,
2712
+ ]:
2713
+ """Commit one exact prevalidated generation publication."""
2714
+
2715
+ if type(preparation) is not PortfolioMemoryCreditBatchPreparation:
2716
+ raise TypeError("preparation must be exact")
2717
+ PortfolioMemoryCreditBatchPreparation.__post_init__(preparation)
2718
+ batch_receipt = preparation.batch_receipt
2719
+ if batch_receipt is None:
2720
+ return preparation.prepared_results, None
2721
+ if self.memory is None:
2722
+ raise ValueError("prepared memory credit requires a memory bank")
2723
+ if self.memory.trials != preparation.prior_memory_trials:
2724
+ raise RuntimeError("memory bank changed after credit preparation")
2725
+ committed_trials = self.memory.record_trials_batch(
2726
+ preparation.expected_trials
2727
+ )
2728
+ if committed_trials != preparation.expected_trials:
2729
+ raise RuntimeError("memory bank changed canonical pending-credit order")
2730
+ if len(self.memory.trials) != batch_receipt.memory_trial_count_after:
2731
+ raise RuntimeError("memory bank trial count differs from batch receipt")
2732
+ return preparation.prepared_results, batch_receipt
2733
+
2734
+ def commit_pending_memory_credit_batch(
2735
+ self,
2736
+ results: tuple[PortfolioVariationWaveResult, ...],
2737
+ ) -> tuple[
2738
+ tuple[PortfolioVariationWaveResult, ...],
2739
+ PortfolioMemoryCreditBatchReceipt | None,
2740
+ ]:
2741
+ """Backward-compatible prepare-and-commit convenience boundary."""
2742
+
2743
+ preparation = self.prepare_pending_memory_credit_batch(results)
2744
+ committed_results, batch_receipt = (
2745
+ self.commit_prepared_memory_credit_batch(preparation)
2746
+ )
2747
+ return committed_results, batch_receipt
2748
+
2749
+ async def run(
2750
+ self,
2751
+ wave: PortfolioVariationWaveRequest,
2752
+ *,
2753
+ reward_binding: RewardPolicyBinding | None = None,
2754
+ defer_memory_credit: bool = False,
2755
+ ) -> PortfolioVariationWaveResult:
2756
+ """Select once, materialize all members, and evaluate one concurrent wave."""
2757
+
2758
+ if type(wave) is not PortfolioVariationWaveRequest:
2759
+ raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
2760
+ PortfolioVariationWaveRequest.__post_init__(wave)
2761
+ if reward_binding is not None:
2762
+ if type(reward_binding) is not RewardPolicyBinding:
2763
+ raise TypeError("reward_binding must be exact or None")
2764
+ RewardPolicyBinding.__post_init__(reward_binding)
2765
+ if type(defer_memory_credit) is not bool:
2766
+ raise TypeError("defer_memory_credit must be an exact boolean")
2767
+ credit = wave.memory_credit
2768
+ if credit is not None:
2769
+ if self.memory is None:
2770
+ raise ValueError("portfolio memory credit requires a memory bank")
2771
+ eligible_entries = self.memory.entries_for(credit.decision.eligible)
2772
+ entry_by_reference = {
2773
+ entry.reference: entry for entry in eligible_entries
2774
+ }
2775
+ for card in wave.selection_request.cards:
2776
+ entry = entry_by_reference[card.reference]
2777
+ if card.content_sha256 != entry.draft.content_sha256:
2778
+ raise ValueError(
2779
+ "memory credit card content differs from its memory entry"
2780
+ )
2781
+ if (
2782
+ entry.origin is InsightOrigin.REFLECTION
2783
+ and (
2784
+ card.source_binding is None
2785
+ or wave.selection_request.source_registry is None
2786
+ )
2787
+ ):
2788
+ raise ValueError(
2789
+ "reflection memory credit requires source-admitted cards"
2790
+ )
2791
+ quarantine_entries = tuple(
2792
+ entry
2793
+ for entry in eligible_entries
2794
+ if entry.lifecycle_state is InsightLifecycleState.QUARANTINED
2795
+ )
2796
+ if quarantine_entries:
2797
+ admission = credit.quarantine_admission
2798
+ if admission is None:
2799
+ raise ValueError(
2800
+ "quarantine memory credit requires bank-issued admission"
2801
+ )
2802
+ self.memory.validate_quarantine_test_admission(
2803
+ admission,
2804
+ eligible_references=credit.decision.eligible,
2805
+ subset_authorization_sha256=(
2806
+ credit.quarantine_admission_subset_authorization_sha256
2807
+ ),
2808
+ )
2809
+ elif credit.quarantine_admission is not None:
2810
+ raise ValueError(
2811
+ "normal memory credit cannot carry quarantine admission"
2812
+ )
2813
+ if any(
2814
+ entry.lifecycle_state is InsightLifecycleState.DEPRECATED
2815
+ for entry in eligible_entries
2816
+ ):
2817
+ raise ValueError("memory credit eligible set contains deprecated insight")
2818
+
2819
+ request_sha256 = wave.selection_request.request_sha256
2820
+ parent_sha256 = wave.parent.occurrence.configuration_hash
2821
+ prepared = await self.prepare(wave)
2822
+ result = prepared.selection
2823
+ telemetry = result.telemetry
2824
+ telemetry_sha256 = prepared.selection_telemetry_sha256
2825
+ candidate_ids = tuple(
2826
+ value.candidate_id for value in prepared.invocations
2827
+ )
2828
+ evaluated = await self.evaluate_prepared_members(
2829
+ prepared,
2830
+ candidate_ids,
2831
+ reward_binding=reward_binding,
2832
+ )
2833
+ outcomes = tuple(value.outcome for value in evaluated)
2834
+ members = tuple(value.receipt for value in evaluated)
2835
+ if len({member.operator_invocation_id for member in members}) != len(members):
2836
+ raise ValueError("portfolio outcomes contain colliding invocations")
2837
+ action_attributions = tuple(
2838
+ self._join_action_attribution(
2839
+ wave.selection_request,
2840
+ result.decision,
2841
+ selected,
2842
+ member,
2843
+ )
2844
+ for selected, member in zip(
2845
+ result.decision.members,
2846
+ members,
2847
+ strict=True,
2848
+ )
2849
+ )
2850
+
2851
+ credit_receipt: PortfolioMemoryCreditReceipt | None = None
2852
+ pending_credit: PortfolioPendingMemoryCredit | None = None
2853
+ if credit is not None:
2854
+ assert self.memory is not None
2855
+ context_projection = credit.resolve_context_projection(
2856
+ wave.selection_request.context
2857
+ )
2858
+ if credit.credit_unit_id in {
2859
+ member.operator_invocation_id for member in members
2860
+ }:
2861
+ raise ValueError("memory credit unit collides with a child invocation")
2862
+ aggregate_reward = credit.aggregation.aggregate(outcomes)
2863
+ if type(aggregate_reward) is not float or not math.isfinite(
2864
+ aggregate_reward
2865
+ ):
2866
+ raise TypeError("portfolio aggregate reward must be a finite float")
2867
+ pending_credit = PortfolioPendingMemoryCredit(
2868
+ credit_unit_id=credit.credit_unit_id,
2869
+ decision=credit.decision,
2870
+ candidate_ids=candidate_ids,
2871
+ aggregation=credit.aggregation,
2872
+ context_projection=context_projection,
2873
+ reward=aggregate_reward,
2874
+ treatment_binding_sha256=credit.treatment_binding_sha256,
2875
+ generation=wave.generation,
2876
+ )
2877
+ if not defer_memory_credit:
2878
+ before = len(self.memory.trials)
2879
+ trial = self.memory.record_trials_batch(
2880
+ (pending_credit.to_trial(),)
2881
+ )[0]
2882
+ if (
2883
+ len(self.memory.trials) != before + 1
2884
+ or self.memory.trials[-1] is not trial
2885
+ or trial.candidate_ids != candidate_ids
2886
+ or trial.reward != aggregate_reward
2887
+ ):
2888
+ raise RuntimeError(
2889
+ "portfolio memory credit was not one exact trial"
2890
+ )
2891
+ credit_receipt = pending_credit.to_committed_receipt()
2892
+ pending_credit = None
2893
+
2894
+ receipt = PortfolioVariationWaveReceipt(
2895
+ selection_call_id=wave.selection_request.call_id,
2896
+ request_sha256=request_sha256,
2897
+ decision_sha256=result.decision.decision_sha256,
2898
+ selection_policy_id=result.decision.policy_id,
2899
+ selection_policy_version=result.decision.policy_version,
2900
+ selection_policy_definition_sha256=(
2901
+ result.decision.policy_definition_sha256
2902
+ ),
2903
+ selection_telemetry=telemetry,
2904
+ selection_telemetry_sha256=telemetry_sha256,
2905
+ parent_candidate_id=wave.parent.candidate_id,
2906
+ parent_configuration_sha256=parent_sha256,
2907
+ generation=wave.generation,
2908
+ members=members,
2909
+ action_attributions=action_attributions,
2910
+ memory_credit=credit_receipt,
2911
+ )
2912
+ PortfolioVariationWaveReceipt.__post_init__(receipt)
2913
+ return PortfolioVariationWaveResult(
2914
+ receipt=receipt,
2915
+ outcomes=outcomes,
2916
+ selection_decision=result.decision,
2917
+ supplemental_selection_audit=result.supplemental_audit,
2918
+ pending_memory_credit=pending_credit,
2919
+ )
2920
+
2921
+
2922
+ __all__ = [
2923
+ "EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256",
2924
+ "MEMORY_ESTIMAND_CONTEXT_KEY",
2925
+ "MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256",
2926
+ "PORTFOLIO_MATERIALIZATION_POLICY_ID",
2927
+ "PORTFOLIO_MATERIALIZATION_POLICY_VERSION",
2928
+ "EvaluatedPreparedPortfolioMember",
2929
+ "MaterializedPortfolioEngine",
2930
+ "PreparedPortfolioVariationWave",
2931
+ "PortfolioActionAttributionReceipt",
2932
+ "PortfolioActionCardAttribution",
2933
+ "PortfolioCandidateFailureEvidence",
2934
+ "PortfolioEvolution",
2935
+ "PortfolioMemberMaterializationReceipt",
2936
+ "PortfolioMemberDisposition",
2937
+ "PortfolioMemoryCreditBatchReceipt",
2938
+ "PortfolioMemoryCreditBatchPreparation",
2939
+ "PortfolioMemoryCreditPlan",
2940
+ "PortfolioMemoryCreditReceipt",
2941
+ "PortfolioMemoryContextProjectionBinding",
2942
+ "PortfolioMemoryMatchedControlWavePlan",
2943
+ "PortfolioPendingMemoryCredit",
2944
+ "PortfolioRewardAggregationBinding",
2945
+ "PortfolioVariationMemberReceipt",
2946
+ "PortfolioVariationWaveReceipt",
2947
+ "PortfolioVariationWaveResult",
2948
+ "PortfolioVariationWaveRequest",
2949
+ "portfolio_selection_telemetry_sha256",
2950
+ ]