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,1981 @@
1
+ """Deterministic derivation, replay, inversion, and three-way classification.
2
+
3
+ No function in this module calls a model or performs a merge. Three-way
4
+ classification produces explicit evidence obligations; a later operator may
5
+ choose or synthesize resolutions, but it cannot make conflicts disappear.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+
14
+ from agent_evolve.domain.ids import CandidateId
15
+ from agent_evolve.domain.lineage import (
16
+ MAX_PRESERVATION_OBLIGATIONS,
17
+ AbsenceContextKind,
18
+ AbsenceFailureKind,
19
+ CandidateOccurrence,
20
+ PreservationClaim,
21
+ PreservationExpectation,
22
+ PreservationObligation,
23
+ PreservationSource,
24
+ VariationCase,
25
+ VariationKind,
26
+ validate_variation_case,
27
+ )
28
+ from agent_evolve.domain.patch import (
29
+ DEFAULT_PATCH_LIMITS,
30
+ MAX_PATCH_OPERATIONS,
31
+ ArrayIndex,
32
+ DeleteSequenceItem,
33
+ InsertSequenceItem,
34
+ JsonPath,
35
+ ObjectKey,
36
+ PatchLimits,
37
+ PatchOperation,
38
+ PermuteSequence,
39
+ ReplaceScalar,
40
+ ReplaceSubtree,
41
+ TypedPatch,
42
+ canonical_path_bytes,
43
+ operation_effect_bytes,
44
+ operation_effect_sha256,
45
+ operation_occurrence_bytes,
46
+ operation_kind,
47
+ operation_sort_key,
48
+ require_sha256,
49
+ validate_json_path,
50
+ validate_patch_limits,
51
+ validate_patch_operation,
52
+ validate_semantic_component,
53
+ validate_typed_patch,
54
+ )
55
+ from agent_evolve.domain.typed_json import (
56
+ FrozenJsonArray,
57
+ FrozenJsonObject,
58
+ FrozenJsonValue,
59
+ TypedJsonLimits,
60
+ canonical_typed_json_bytes,
61
+ freeze_json,
62
+ is_json_scalar,
63
+ typed_json_equal,
64
+ typed_json_sha256,
65
+ validate_typed_json_limits,
66
+ )
67
+
68
+
69
+ class PatchPreconditionError(ValueError):
70
+ """The supplied base or an operation-local old value was stale."""
71
+
72
+
73
+ class PatchPostconditionError(ValueError):
74
+ """An operation or whole patch did not produce its declared endpoint."""
75
+
76
+
77
+ class PreservationError(ValueError):
78
+ """A parent-use or preservation claim was absent, fabricated, or false."""
79
+
80
+
81
+ @dataclass(frozen=True, slots=True, eq=False)
82
+ class ComponentTagAssignment:
83
+ """Benchmark-supplied semantic component for one exact operation path."""
84
+
85
+ path: JsonPath
86
+ component: str
87
+
88
+ def __post_init__(self) -> None:
89
+ validate_json_path(self.path)
90
+ if type(self.component) is not str:
91
+ raise TypeError("component tag must be an exact string")
92
+ validate_semantic_component(self.component)
93
+
94
+ def __eq__(self, other: object) -> bool:
95
+ if (
96
+ type(self) is not ComponentTagAssignment
97
+ or type(other) is not ComponentTagAssignment
98
+ ):
99
+ return False
100
+ ComponentTagAssignment.__post_init__(self)
101
+ ComponentTagAssignment.__post_init__(other)
102
+ return (
103
+ canonical_path_bytes(self.path),
104
+ self.component,
105
+ ) == (
106
+ canonical_path_bytes(other.path),
107
+ other.component,
108
+ )
109
+
110
+ __hash__ = None
111
+
112
+
113
+ def _component_assignments(
114
+ values: tuple[ComponentTagAssignment, ...],
115
+ *,
116
+ max_assignments: int,
117
+ ) -> dict[bytes, str]:
118
+ if type(values) is not tuple:
119
+ raise TypeError(
120
+ "component_tags must be an exact tuple of ComponentTagAssignment values"
121
+ )
122
+ if len(values) > max_assignments:
123
+ raise ValueError("component_tags exceeds the patch max_operations bound")
124
+ if any(type(value) is not ComponentTagAssignment for value in values):
125
+ raise TypeError(
126
+ "component_tags must be an exact tuple of ComponentTagAssignment values"
127
+ )
128
+ for value in values:
129
+ ComponentTagAssignment.__post_init__(value)
130
+ canonical = tuple(sorted(values, key=lambda value: canonical_path_bytes(value.path)))
131
+ if values != canonical:
132
+ raise ValueError("component_tags must use canonical typed-path order")
133
+ result: dict[bytes, str] = {}
134
+ for value in values:
135
+ key = canonical_path_bytes(value.path)
136
+ if key in result:
137
+ raise ValueError("component_tags cannot duplicate an operation path")
138
+ result[key] = value.component
139
+ return result
140
+
141
+
142
+ def _object_dict(value: FrozenJsonObject) -> dict[str, FrozenJsonValue]:
143
+ return {key: item for key, item in value.items}
144
+
145
+
146
+ def _stable_permutation(
147
+ before: FrozenJsonArray,
148
+ after: FrozenJsonArray,
149
+ *,
150
+ limits: TypedJsonLimits,
151
+ ) -> tuple[int, ...] | None:
152
+ if len(before.items) != len(after.items):
153
+ return None
154
+ positions: dict[bytes, list[int]] = {}
155
+ for index, item in enumerate(before.items):
156
+ item_bytes = canonical_typed_json_bytes(item, limits=limits)
157
+ positions.setdefault(item_bytes, []).append(index)
158
+ cursors: dict[bytes, int] = {}
159
+ result: list[int] = []
160
+ for item in after.items:
161
+ item_bytes = canonical_typed_json_bytes(item, limits=limits)
162
+ cursor = cursors.get(item_bytes, 0)
163
+ candidates = positions.get(item_bytes)
164
+ if candidates is None or cursor >= len(candidates):
165
+ return None
166
+ result.append(candidates[cursor])
167
+ cursors[item_bytes] = cursor + 1
168
+ if any(cursors.get(key, 0) != len(indices) for key, indices in positions.items()):
169
+ return None
170
+ return tuple(result)
171
+
172
+
173
+ def _item_bytes(
174
+ sequence: FrozenJsonArray,
175
+ *,
176
+ limits: TypedJsonLimits,
177
+ ) -> tuple[bytes, ...]:
178
+ return tuple(
179
+ canonical_typed_json_bytes(item, limits=limits) for item in sequence.items
180
+ )
181
+
182
+
183
+ def _single_insertion_index(
184
+ before: FrozenJsonArray,
185
+ after: FrozenJsonArray,
186
+ *,
187
+ limits: TypedJsonLimits,
188
+ ) -> int | None:
189
+ """Find the lowest valid insertion index in linear comparisons."""
190
+
191
+ if len(after.items) != len(before.items) + 1:
192
+ return None
193
+ before_items = _item_bytes(before, limits=limits)
194
+ after_items = _item_bytes(after, limits=limits)
195
+ size = len(before_items)
196
+ suffix_ok = [False] * (size + 1)
197
+ suffix_ok[size] = True
198
+ for index in range(size - 1, -1, -1):
199
+ suffix_ok[index] = (
200
+ before_items[index] == after_items[index + 1]
201
+ and suffix_ok[index + 1]
202
+ )
203
+ prefix_ok = True
204
+ for index in range(size + 1):
205
+ if prefix_ok and suffix_ok[index]:
206
+ return index
207
+ if index < size:
208
+ prefix_ok = prefix_ok and before_items[index] == after_items[index]
209
+ return None
210
+
211
+
212
+ def _single_deletion_index(
213
+ before: FrozenJsonArray,
214
+ after: FrozenJsonArray,
215
+ *,
216
+ limits: TypedJsonLimits,
217
+ ) -> int | None:
218
+ """Find the lowest valid deletion index in linear comparisons."""
219
+
220
+ if len(before.items) != len(after.items) + 1:
221
+ return None
222
+ before_items = _item_bytes(before, limits=limits)
223
+ after_items = _item_bytes(after, limits=limits)
224
+ size = len(after_items)
225
+ suffix_ok = [False] * (size + 1)
226
+ suffix_ok[size] = True
227
+ for index in range(size - 1, -1, -1):
228
+ suffix_ok[index] = (
229
+ before_items[index + 1] == after_items[index]
230
+ and suffix_ok[index + 1]
231
+ )
232
+ prefix_ok = True
233
+ for index in range(size + 1):
234
+ if prefix_ok and suffix_ok[index]:
235
+ return index
236
+ if index < size:
237
+ prefix_ok = prefix_ok and before_items[index] == after_items[index]
238
+ return None
239
+
240
+
241
+ def derive_patch(
242
+ base: object,
243
+ target: object,
244
+ *,
245
+ base_candidate_id: CandidateId,
246
+ target_candidate_id: CandidateId,
247
+ component_tags: tuple[ComponentTagAssignment, ...] = (),
248
+ limits: PatchLimits = DEFAULT_PATCH_LIMITS,
249
+ ) -> TypedPatch:
250
+ """Derive one deterministic bounded patch between exact typed trees.
251
+
252
+ Object key-set changes are represented as an explicit subtree replacement;
253
+ the algebra has no implicit or path-created object insertion. A sequence
254
+ gets a dedicated operation for a pure permutation or exactly one insertion
255
+ or deletion. More complex structural edits fall back to an explicit
256
+ subtree replacement rather than an ambiguous order-dependent script.
257
+ """
258
+
259
+ if type(base_candidate_id) is not CandidateId or type(
260
+ target_candidate_id
261
+ ) is not CandidateId:
262
+ raise TypeError("patch endpoint IDs must be exact CandidateId values")
263
+ CandidateId.__post_init__(base_candidate_id)
264
+ CandidateId.__post_init__(target_candidate_id)
265
+ validate_patch_limits(limits)
266
+ frozen_base = freeze_json(base, limits=limits.json_limits)
267
+ frozen_target = freeze_json(target, limits=limits.json_limits)
268
+ tag_by_path = _component_assignments(
269
+ component_tags,
270
+ max_assignments=limits.max_operations,
271
+ )
272
+ used_tag_paths: set[bytes] = set()
273
+ operations: list[PatchOperation] = []
274
+
275
+ def tag(path: JsonPath) -> str | None:
276
+ path_key = canonical_path_bytes(path)
277
+ value = tag_by_path.get(path_key)
278
+ if value is not None:
279
+ used_tag_paths.add(path_key)
280
+ return value
281
+
282
+ def add(operation: PatchOperation) -> None:
283
+ operations.append(operation)
284
+ if len(operations) > limits.max_operations:
285
+ raise ValueError("derived patch exceeds max_operations")
286
+
287
+ def replace_at(
288
+ path: JsonPath,
289
+ before: FrozenJsonValue,
290
+ after: FrozenJsonValue,
291
+ ) -> None:
292
+ component = tag(path)
293
+ if is_json_scalar(before) and is_json_scalar(after):
294
+ add(
295
+ ReplaceScalar(
296
+ path,
297
+ before,
298
+ after,
299
+ base_candidate_id,
300
+ component,
301
+ )
302
+ )
303
+ else:
304
+ add(
305
+ ReplaceSubtree(
306
+ path,
307
+ before,
308
+ after,
309
+ base_candidate_id,
310
+ component,
311
+ )
312
+ )
313
+
314
+ def visit(
315
+ before: FrozenJsonValue,
316
+ after: FrozenJsonValue,
317
+ path: JsonPath,
318
+ ) -> None:
319
+ if typed_json_equal(before, after, limits=limits.json_limits):
320
+ return
321
+ if len(path.segments) >= limits.max_path_segments:
322
+ replace_at(path, before, after)
323
+ return
324
+
325
+ if type(before) is FrozenJsonObject and type(after) is FrozenJsonObject:
326
+ before_items = _object_dict(before)
327
+ after_items = _object_dict(after)
328
+ if before_items.keys() != after_items.keys():
329
+ replace_at(path, before, after)
330
+ return
331
+ changed_children: list[
332
+ tuple[str, FrozenJsonValue, FrozenJsonValue, JsonPath]
333
+ ] = []
334
+ for key, before_item in before.items:
335
+ after_item = after_items[key]
336
+ if typed_json_equal(
337
+ before_item,
338
+ after_item,
339
+ limits=limits.json_limits,
340
+ ):
341
+ continue
342
+ try:
343
+ child_path = path.child_key(key)
344
+ except ValueError:
345
+ # The typed-JSON domain intentionally admits longer keys
346
+ # than one path segment. Preserve derivation totality by
347
+ # bubbling the edit to the nearest representable parent.
348
+ replace_at(path, before, after)
349
+ return
350
+ changed_children.append((key, before_item, after_item, child_path))
351
+ for _, before_item, after_item, child_path in changed_children:
352
+ visit(before_item, after_item, child_path)
353
+ return
354
+
355
+ if type(before) is FrozenJsonArray and type(after) is FrozenJsonArray:
356
+ permutation = _stable_permutation(
357
+ before,
358
+ after,
359
+ limits=limits.json_limits,
360
+ )
361
+ if permutation is not None and permutation != tuple(range(len(before.items))):
362
+ add(
363
+ PermuteSequence(
364
+ path,
365
+ permutation,
366
+ before,
367
+ after,
368
+ base_candidate_id,
369
+ tag(path),
370
+ )
371
+ )
372
+ return
373
+
374
+ if len(after.items) == len(before.items) + 1:
375
+ index = _single_insertion_index(
376
+ before,
377
+ after,
378
+ limits=limits.json_limits,
379
+ )
380
+ if index is not None:
381
+ add(
382
+ InsertSequenceItem(
383
+ path,
384
+ index,
385
+ after.items[index],
386
+ before,
387
+ after,
388
+ base_candidate_id,
389
+ tag(path),
390
+ )
391
+ )
392
+ return
393
+ replace_at(path, before, after)
394
+ return
395
+
396
+ if len(before.items) == len(after.items) + 1:
397
+ index = _single_deletion_index(
398
+ before,
399
+ after,
400
+ limits=limits.json_limits,
401
+ )
402
+ if index is not None:
403
+ add(
404
+ DeleteSequenceItem(
405
+ path,
406
+ index,
407
+ before.items[index],
408
+ before,
409
+ after,
410
+ base_candidate_id,
411
+ tag(path),
412
+ )
413
+ )
414
+ return
415
+ replace_at(path, before, after)
416
+ return
417
+
418
+ if len(before.items) == len(after.items):
419
+ for index, (before_item, after_item) in enumerate(
420
+ zip(before.items, after.items)
421
+ ):
422
+ visit(before_item, after_item, path.child_index(index))
423
+ return
424
+
425
+ replace_at(path, before, after)
426
+ return
427
+
428
+ replace_at(path, before, after)
429
+
430
+ visit(frozen_base, frozen_target, JsonPath())
431
+ unused_tags = set(tag_by_path) - used_tag_paths
432
+ if unused_tags:
433
+ raise ValueError("component_tags contains a path that is not a derived operation")
434
+ ordered = tuple(sorted(operations, key=operation_sort_key))
435
+ return TypedPatch(
436
+ base_candidate_id=base_candidate_id,
437
+ target_candidate_id=target_candidate_id,
438
+ base_hash=typed_json_sha256(frozen_base, limits=limits.json_limits),
439
+ target_hash=typed_json_sha256(frozen_target, limits=limits.json_limits),
440
+ operations=ordered,
441
+ limits=limits,
442
+ )
443
+
444
+
445
+ def value_at_path(value: FrozenJsonValue, path: JsonPath) -> FrozenJsonValue:
446
+ """Resolve an existing typed path without invoking container subclasses."""
447
+
448
+ validate_json_path(path)
449
+ current = freeze_json(value)
450
+ if current is not value:
451
+ raise TypeError("value_at_path requires an already frozen typed-JSON value")
452
+ for segment in path.segments:
453
+ if type(segment) is ObjectKey:
454
+ if type(current) is not FrozenJsonObject:
455
+ raise PatchPreconditionError("object-key segment reached a non-object")
456
+ found = False
457
+ for key, item in current.items:
458
+ if key == segment.value:
459
+ current = item
460
+ found = True
461
+ break
462
+ if not found:
463
+ raise PatchPreconditionError("object-key path does not exist")
464
+ elif type(segment) is ArrayIndex:
465
+ if type(current) is not FrozenJsonArray:
466
+ raise PatchPreconditionError("array-index segment reached a non-array")
467
+ if segment.value >= len(current.items):
468
+ raise PatchPreconditionError("array-index path is out of bounds")
469
+ current = current.items[segment.value]
470
+ else: # pragma: no cover - JsonPath closes this union.
471
+ raise AssertionError("unsupported path segment")
472
+ return current
473
+
474
+
475
+ def _replace_existing_path(
476
+ value: FrozenJsonValue,
477
+ path: JsonPath,
478
+ replacement: FrozenJsonValue,
479
+ ) -> FrozenJsonValue:
480
+ if not path.segments:
481
+ return replacement
482
+ head = path.segments[0]
483
+ tail = JsonPath(path.segments[1:])
484
+ if type(head) is ObjectKey:
485
+ if type(value) is not FrozenJsonObject:
486
+ raise PatchPreconditionError("object-key replacement reached a non-object")
487
+ found = False
488
+ updated: list[tuple[str, FrozenJsonValue]] = []
489
+ for key, item in value.items:
490
+ if key == head.value:
491
+ found = True
492
+ updated.append((key, _replace_existing_path(item, tail, replacement)))
493
+ else:
494
+ updated.append((key, item))
495
+ if not found:
496
+ raise PatchPreconditionError(
497
+ "object-key replacement cannot insert a missing key"
498
+ )
499
+ return FrozenJsonObject(tuple(updated))
500
+ if type(head) is ArrayIndex:
501
+ if type(value) is not FrozenJsonArray:
502
+ raise PatchPreconditionError("array-index replacement reached a non-array")
503
+ if head.value >= len(value.items):
504
+ raise PatchPreconditionError("array-index replacement is out of bounds")
505
+ items = list(value.items)
506
+ items[head.value] = _replace_existing_path(items[head.value], tail, replacement)
507
+ return FrozenJsonArray(tuple(items))
508
+ raise AssertionError("unsupported path segment")
509
+
510
+
511
+ def replace_existing_path(
512
+ value: FrozenJsonValue,
513
+ path: JsonPath,
514
+ replacement: FrozenJsonValue,
515
+ ) -> FrozenJsonValue:
516
+ """Return an immutable tree with one existing typed path replaced.
517
+
518
+ This is the narrow construction primitive used before deriving a canonical
519
+ patch. It cannot insert a key or grow an array, and it accepts only values
520
+ that are already inside the frozen typed-JSON boundary.
521
+ """
522
+
523
+ validate_json_path(path)
524
+ frozen_value = freeze_json(value)
525
+ if frozen_value is not value:
526
+ raise TypeError("value must already be frozen typed JSON")
527
+ frozen_replacement = freeze_json(replacement)
528
+ if frozen_replacement is not replacement:
529
+ raise TypeError("replacement must already be frozen typed JSON")
530
+ # Resolve first so every missing/stale non-root path fails before any
531
+ # provisional target is constructed (the root is itself an existing path).
532
+ value_at_path(value, path)
533
+ return _replace_existing_path(value, path, replacement)
534
+
535
+
536
+ def apply_patch(base: object, patch: TypedPatch) -> FrozenJsonValue:
537
+ """Apply a patch only when every local and global precondition is exact."""
538
+
539
+ validate_typed_patch(patch)
540
+ current = freeze_json(base, limits=patch.limits.json_limits)
541
+ if typed_json_sha256(current, limits=patch.limits.json_limits) != patch.base_hash:
542
+ raise PatchPreconditionError("base hash does not match patch.base_hash")
543
+
544
+ for operation in patch.operations:
545
+ observed = value_at_path(current, operation.path)
546
+ if type(operation) in (ReplaceScalar, ReplaceSubtree):
547
+ expected_before = operation.old_value
548
+ replacement = operation.new_value
549
+ else:
550
+ expected_before = operation.before_sequence
551
+ if type(observed) is not FrozenJsonArray:
552
+ raise PatchPreconditionError("sequence operation reached a non-array")
553
+ if type(operation) is InsertSequenceItem:
554
+ replacement = FrozenJsonArray(
555
+ observed.items[: operation.index]
556
+ + (operation.item,)
557
+ + observed.items[operation.index :]
558
+ )
559
+ elif type(operation) is DeleteSequenceItem:
560
+ if operation.index >= len(observed.items):
561
+ raise PatchPreconditionError("delete index is stale")
562
+ if not typed_json_equal(
563
+ observed.items[operation.index],
564
+ operation.item,
565
+ limits=patch.limits.json_limits,
566
+ ):
567
+ raise PatchPreconditionError("deleted item precondition is stale")
568
+ replacement = FrozenJsonArray(
569
+ observed.items[: operation.index]
570
+ + observed.items[operation.index + 1 :]
571
+ )
572
+ elif type(operation) is PermuteSequence:
573
+ if len(observed.items) != len(operation.permutation):
574
+ raise PatchPreconditionError("permutation length is stale")
575
+ replacement = FrozenJsonArray(
576
+ tuple(observed.items[index] for index in operation.permutation)
577
+ )
578
+ else: # pragma: no cover - TypedPatch closes operation types.
579
+ raise AssertionError("unsupported patch operation")
580
+ if not typed_json_equal(
581
+ observed,
582
+ expected_before,
583
+ limits=patch.limits.json_limits,
584
+ ):
585
+ raise PatchPreconditionError("operation old-value precondition is stale")
586
+ expected_after = (
587
+ operation.new_value
588
+ if type(operation) in (ReplaceScalar, ReplaceSubtree)
589
+ else operation.after_sequence
590
+ )
591
+ if not typed_json_equal(
592
+ replacement,
593
+ expected_after,
594
+ limits=patch.limits.json_limits,
595
+ ):
596
+ raise PatchPostconditionError("operation result violates its postcondition")
597
+ current = _replace_existing_path(current, operation.path, replacement)
598
+ if not typed_json_equal(
599
+ value_at_path(current, operation.path),
600
+ expected_after,
601
+ limits=patch.limits.json_limits,
602
+ ):
603
+ raise PatchPostconditionError("operation path was not replaced exactly")
604
+
605
+ actual_target_hash = typed_json_sha256(current, limits=patch.limits.json_limits)
606
+ if actual_target_hash != patch.target_hash:
607
+ raise PatchPostconditionError("patch result does not match target_hash")
608
+ return current
609
+
610
+
611
+ def invert_patch(patch: TypedPatch) -> TypedPatch:
612
+ """Build the canonical exact inverse; applying it restores the base tree."""
613
+
614
+ validate_typed_patch(patch)
615
+ inverse: list[PatchOperation] = []
616
+ source = patch.target_candidate_id
617
+ for operation in patch.operations:
618
+ if type(operation) is ReplaceScalar:
619
+ inverse.append(
620
+ ReplaceScalar(
621
+ operation.path,
622
+ operation.new_value,
623
+ operation.old_value,
624
+ source,
625
+ operation.semantic_component,
626
+ )
627
+ )
628
+ elif type(operation) is ReplaceSubtree:
629
+ inverse.append(
630
+ ReplaceSubtree(
631
+ operation.path,
632
+ operation.new_value,
633
+ operation.old_value,
634
+ source,
635
+ operation.semantic_component,
636
+ )
637
+ )
638
+ elif type(operation) is InsertSequenceItem:
639
+ inverse.append(
640
+ DeleteSequenceItem(
641
+ operation.path,
642
+ operation.index,
643
+ operation.item,
644
+ operation.after_sequence,
645
+ operation.before_sequence,
646
+ source,
647
+ operation.semantic_component,
648
+ )
649
+ )
650
+ elif type(operation) is DeleteSequenceItem:
651
+ inverse.append(
652
+ InsertSequenceItem(
653
+ operation.path,
654
+ operation.index,
655
+ operation.item,
656
+ operation.after_sequence,
657
+ operation.before_sequence,
658
+ source,
659
+ operation.semantic_component,
660
+ )
661
+ )
662
+ elif type(operation) is PermuteSequence:
663
+ inverse_permutation = _stable_permutation(
664
+ operation.after_sequence,
665
+ operation.before_sequence,
666
+ limits=patch.limits.json_limits,
667
+ )
668
+ if inverse_permutation is None: # pragma: no cover - constructor proves it.
669
+ raise AssertionError("validated permutation had no inverse")
670
+ inverse.append(
671
+ PermuteSequence(
672
+ operation.path,
673
+ inverse_permutation,
674
+ operation.after_sequence,
675
+ operation.before_sequence,
676
+ source,
677
+ operation.semantic_component,
678
+ )
679
+ )
680
+ else: # pragma: no cover - TypedPatch closes operation types.
681
+ raise AssertionError("unsupported patch operation")
682
+ return TypedPatch(
683
+ base_candidate_id=patch.target_candidate_id,
684
+ target_candidate_id=patch.base_candidate_id,
685
+ base_hash=patch.target_hash,
686
+ target_hash=patch.base_hash,
687
+ operations=tuple(sorted(inverse, key=operation_sort_key)),
688
+ limits=patch.limits,
689
+ )
690
+
691
+
692
+ class ThreeWayRelationKind(str, Enum):
693
+ IDENTICAL = "identical"
694
+ DISJOINT = "disjoint"
695
+ COMPATIBLE_SAME_COMPONENT = "compatible_same_component"
696
+ CONFLICT = "conflict"
697
+ INVALIDATED = "invalidated"
698
+
699
+
700
+ @dataclass(frozen=True, slots=True)
701
+ class _CrossPathIndex:
702
+ equal_pairs: tuple[tuple[int, int], ...]
703
+ strict_edges: tuple[tuple[int, int], ...]
704
+ work_units: int
705
+
706
+
707
+ def _validate_operation_tuple(
708
+ operations: tuple[PatchOperation, ...],
709
+ *,
710
+ name: str,
711
+ ) -> None:
712
+ if type(operations) is not tuple:
713
+ raise TypeError(f"{name} must be an exact tuple")
714
+ if len(operations) > MAX_PATCH_OPERATIONS:
715
+ raise ValueError(f"{name} exceeds MAX_PATCH_OPERATIONS")
716
+ for operation in operations:
717
+ validate_patch_operation(operation)
718
+ if operations != tuple(sorted(operations, key=operation_sort_key)):
719
+ raise ValueError(f"{name} must use canonical operation order")
720
+ terminal = object()
721
+ root: dict[object, object] = {}
722
+ for operation in operations:
723
+ node = root
724
+ for segment in operation.path.segments:
725
+ if terminal in node:
726
+ raise ValueError(f"{name} contains overlapping operation paths")
727
+ child = node.setdefault(segment, {})
728
+ if type(child) is not dict: # pragma: no cover - local trie invariant.
729
+ raise AssertionError("invalid relation-side path trie")
730
+ node = child
731
+ if terminal in node or node:
732
+ raise ValueError(f"{name} contains overlapping operation paths")
733
+ node[terminal] = True
734
+
735
+
736
+ def _index_cross_path_relations(
737
+ left: tuple[PatchOperation, ...],
738
+ right: tuple[PatchOperation, ...],
739
+ ) -> _CrossPathIndex:
740
+ """Index equal and strict-prefix pairs in linear trie work plus output edges.
741
+
742
+ Each side is already internally prefix-free. Consequently the descendant
743
+ subtrees visited for distinct right-side terminals are disjoint, so the
744
+ accumulated ``work_units`` is linear in total path length and emitted
745
+ strict-overlap edges rather than the Cartesian product of branch sizes.
746
+ """
747
+
748
+ _validate_operation_tuple(left, name="left_operations")
749
+ _validate_operation_tuple(right, name="right_operations")
750
+ terminal = object()
751
+ root: dict[object, object] = {}
752
+ work_units = 0
753
+ for index, operation in enumerate(left):
754
+ node = root
755
+ for segment in operation.path.segments:
756
+ work_units += 1
757
+ child = node.setdefault(segment, {})
758
+ if type(child) is not dict: # pragma: no cover - local trie invariant.
759
+ raise AssertionError("invalid cross-branch path trie")
760
+ node = child
761
+ node[terminal] = index
762
+
763
+ equal_pairs: list[tuple[int, int]] = []
764
+ strict_edges: list[tuple[int, int]] = []
765
+ for right_index, operation in enumerate(right):
766
+ node = root
767
+ found = True
768
+ for segment in operation.path.segments:
769
+ work_units += 1
770
+ left_ancestor = node.get(terminal)
771
+ if type(left_ancestor) is int:
772
+ strict_edges.append((left_ancestor, right_index))
773
+ child = node.get(segment)
774
+ if type(child) is not dict:
775
+ found = False
776
+ break
777
+ node = child
778
+ if not found:
779
+ continue
780
+ exact = node.get(terminal)
781
+ if type(exact) is int:
782
+ equal_pairs.append((exact, right_index))
783
+ continue
784
+ stack = [
785
+ child
786
+ for key, child in node.items()
787
+ if key is not terminal and type(child) is dict
788
+ ]
789
+ while stack:
790
+ descendant = stack.pop()
791
+ work_units += 1
792
+ left_descendant = descendant.get(terminal)
793
+ if type(left_descendant) is int:
794
+ strict_edges.append((left_descendant, right_index))
795
+ stack.extend(
796
+ child
797
+ for key, child in descendant.items()
798
+ if key is not terminal and type(child) is dict
799
+ )
800
+ return _CrossPathIndex(
801
+ tuple(sorted(equal_pairs)),
802
+ tuple(sorted(set(strict_edges))),
803
+ work_units,
804
+ )
805
+
806
+
807
+ @dataclass(frozen=True, slots=True, eq=False)
808
+ class PatchRelation:
809
+ kind: ThreeWayRelationKind
810
+ left_operations: tuple[PatchOperation, ...]
811
+ right_operations: tuple[PatchOperation, ...]
812
+ semantic_component: str | None = None
813
+
814
+ def __post_init__(self) -> None:
815
+ # Validate before any equality or encoding. A string-like object with
816
+ # hostile __eq__/encode methods must never influence canonical replay or
817
+ # relation identity.
818
+ validate_semantic_component(self.semantic_component)
819
+ if type(self.kind) is not ThreeWayRelationKind:
820
+ raise TypeError("kind must be a ThreeWayRelationKind")
821
+ for name, operations in (
822
+ ("left_operations", self.left_operations),
823
+ ("right_operations", self.right_operations),
824
+ ):
825
+ _validate_operation_tuple(operations, name=name)
826
+ left = self.left_operations
827
+ right = self.right_operations
828
+ if self.kind is ThreeWayRelationKind.IDENTICAL:
829
+ if len(left) != 1 or len(right) != 1:
830
+ raise ValueError("identical relations require one operation per branch")
831
+ if operation_effect_bytes(left[0]) != operation_effect_bytes(right[0]):
832
+ raise ValueError("identical relation operations are not exact effects")
833
+ elif self.kind is ThreeWayRelationKind.CONFLICT:
834
+ if len(left) != 1 or len(right) != 1 or left[0].path != right[0].path:
835
+ raise ValueError("conflicts require different effects at one exact path")
836
+ if operation_effect_bytes(left[0]) == operation_effect_bytes(right[0]):
837
+ raise ValueError("identical effects cannot be classified as conflicts")
838
+ elif self.kind is ThreeWayRelationKind.INVALIDATED:
839
+ if not left or not right:
840
+ raise ValueError("invalidated relations require operations on both branches")
841
+ indexed = _index_cross_path_relations(left, right)
842
+ if indexed.equal_pairs:
843
+ raise ValueError("invalidated relation cannot contain equal paths")
844
+ incident_left = {left_index for left_index, _ in indexed.strict_edges}
845
+ incident_right = {right_index for _, right_index in indexed.strict_edges}
846
+ if incident_left != set(range(len(left))) or incident_right != set(
847
+ range(len(right))
848
+ ):
849
+ raise ValueError("invalidated relation lacks strict prefix invalidation")
850
+ graph: dict[tuple[str, int], set[tuple[str, int]]] = {}
851
+ for left_index, right_index in indexed.strict_edges:
852
+ left_node = ("left", left_index)
853
+ right_node = ("right", right_index)
854
+ graph.setdefault(left_node, set()).add(right_node)
855
+ graph.setdefault(right_node, set()).add(left_node)
856
+ visited: set[tuple[str, int]] = set()
857
+ stack = [next(iter(graph))]
858
+ while stack:
859
+ node = stack.pop()
860
+ if node in visited:
861
+ continue
862
+ visited.add(node)
863
+ stack.extend(graph[node] - visited)
864
+ if len(visited) != len(left) + len(right):
865
+ raise ValueError("invalidated relation must be one connected component")
866
+ elif self.kind is ThreeWayRelationKind.COMPATIBLE_SAME_COMPONENT:
867
+ if not left or not right or self.semantic_component is None:
868
+ raise ValueError(
869
+ "compatible same-component relations require both branches and a tag"
870
+ )
871
+ if any(
872
+ operation.semantic_component != self.semantic_component
873
+ for operation in left + right
874
+ ):
875
+ raise ValueError("compatible relation component tags do not agree")
876
+ indexed = _index_cross_path_relations(left, right)
877
+ if indexed.equal_pairs or indexed.strict_edges:
878
+ raise ValueError("overlapping paths are not compatible disjoint edits")
879
+ elif self.kind is ThreeWayRelationKind.DISJOINT:
880
+ if len(left) + len(right) != 1:
881
+ raise ValueError("a disjoint relation contains one unmatched branch edit")
882
+ else: # pragma: no cover - enum closes cases.
883
+ raise AssertionError("unsupported relation kind")
884
+ if self.kind is not ThreeWayRelationKind.COMPATIBLE_SAME_COMPONENT and (
885
+ self.semantic_component is not None
886
+ ):
887
+ raise ValueError("semantic_component is only stored on compatible relations")
888
+
889
+ def __eq__(self, other: object) -> bool:
890
+ if type(self) is not PatchRelation or type(other) is not PatchRelation:
891
+ return False
892
+ return _relation_fingerprint(self) == _relation_fingerprint(other)
893
+
894
+ __hash__ = None
895
+
896
+ @property
897
+ def relation_id(self) -> str:
898
+ validate_patch_relation(self)
899
+ digest = hashlib.sha256()
900
+ digest.update(b"agent-evolve:three-way-relation:v1\x00")
901
+ digest.update(self.kind.value.encode("ascii", errors="strict"))
902
+ component = (
903
+ b""
904
+ if self.semantic_component is None
905
+ else self.semantic_component.encode("utf-8", errors="strict")
906
+ )
907
+ digest.update(len(component).to_bytes(8, "big", signed=False))
908
+ digest.update(component)
909
+ for marker, operations in (
910
+ (b"L", self.left_operations),
911
+ (b"R", self.right_operations),
912
+ ):
913
+ digest.update(marker)
914
+ digest.update(len(operations).to_bytes(8, "big", signed=False))
915
+ for operation in operations:
916
+ occurrence = operation_occurrence_bytes(operation)
917
+ digest.update(len(occurrence).to_bytes(8, "big", signed=False))
918
+ digest.update(occurrence)
919
+ return digest.hexdigest()
920
+
921
+
922
+ def validate_patch_relation(relation: PatchRelation) -> None:
923
+ """Revalidate an exact relation and every nested operation."""
924
+
925
+ if type(relation) is not PatchRelation:
926
+ raise TypeError("relation must be an exact PatchRelation")
927
+ PatchRelation.__post_init__(relation)
928
+
929
+
930
+ def _relation_sort_key(relation: PatchRelation) -> tuple[bytes, str, str]:
931
+ paths = [
932
+ canonical_path_bytes(operation.path)
933
+ for operation in relation.left_operations + relation.right_operations
934
+ ]
935
+ return min(paths), relation.kind.value, relation.relation_id
936
+
937
+
938
+ def _derive_three_way_relations(
939
+ left_patch: TypedPatch,
940
+ right_patch: TypedPatch,
941
+ ) -> tuple[PatchRelation, ...]:
942
+ """Derive the one canonical global relation partition from exact patches."""
943
+
944
+ validate_typed_patch(left_patch)
945
+ validate_typed_patch(right_patch)
946
+ left = left_patch.operations
947
+ right = right_patch.operations
948
+ indexed = _index_cross_path_relations(left, right)
949
+ relations: list[PatchRelation] = []
950
+ consumed_left: set[int] = set()
951
+ consumed_right: set[int] = set()
952
+
953
+ for left_index, right_index in indexed.equal_pairs:
954
+ left_operation = left[left_index]
955
+ right_operation = right[right_index]
956
+ kind = (
957
+ ThreeWayRelationKind.IDENTICAL
958
+ if operation_effect_bytes(left_operation)
959
+ == operation_effect_bytes(right_operation)
960
+ else ThreeWayRelationKind.CONFLICT
961
+ )
962
+ relations.append(PatchRelation(kind, (left_operation,), (right_operation,)))
963
+ consumed_left.add(left_index)
964
+ consumed_right.add(right_index)
965
+
966
+ adjacency: dict[tuple[str, int], set[tuple[str, int]]] = {}
967
+ for left_index, right_index in indexed.strict_edges:
968
+ left_node = ("left", left_index)
969
+ right_node = ("right", right_index)
970
+ adjacency.setdefault(left_node, set()).add(right_node)
971
+ adjacency.setdefault(right_node, set()).add(left_node)
972
+ visited: set[tuple[str, int]] = set()
973
+ for seed in sorted(adjacency):
974
+ if seed in visited:
975
+ continue
976
+ stack = [seed]
977
+ component_nodes: set[tuple[str, bytes]] = set()
978
+ while stack:
979
+ node = stack.pop()
980
+ if node in component_nodes:
981
+ continue
982
+ component_nodes.add(node)
983
+ stack.extend(adjacency.get(node, ()))
984
+ visited.update(component_nodes)
985
+ left_indices = sorted(index for side, index in component_nodes if side == "left")
986
+ right_indices = sorted(index for side, index in component_nodes if side == "right")
987
+ consumed_left.update(left_indices)
988
+ consumed_right.update(right_indices)
989
+ relations.append(
990
+ PatchRelation(
991
+ ThreeWayRelationKind.INVALIDATED,
992
+ tuple(left[index] for index in left_indices),
993
+ tuple(right[index] for index in right_indices),
994
+ )
995
+ )
996
+
997
+ left_components: dict[str, list[int]] = {}
998
+ right_components: dict[str, list[int]] = {}
999
+ for index, operation in enumerate(left):
1000
+ if index not in consumed_left and operation.semantic_component is not None:
1001
+ left_components.setdefault(operation.semantic_component, []).append(index)
1002
+ for index, operation in enumerate(right):
1003
+ if index not in consumed_right and operation.semantic_component is not None:
1004
+ right_components.setdefault(operation.semantic_component, []).append(index)
1005
+ for component in sorted(set(left_components) & set(right_components)):
1006
+ left_group = sorted(left_components[component])
1007
+ right_group = sorted(right_components[component])
1008
+ relations.append(
1009
+ PatchRelation(
1010
+ ThreeWayRelationKind.COMPATIBLE_SAME_COMPONENT,
1011
+ tuple(left[index] for index in left_group),
1012
+ tuple(right[index] for index in right_group),
1013
+ component,
1014
+ )
1015
+ )
1016
+ consumed_left.update(left_group)
1017
+ consumed_right.update(right_group)
1018
+
1019
+ for index, operation in enumerate(left):
1020
+ if index not in consumed_left:
1021
+ relations.append(
1022
+ PatchRelation(ThreeWayRelationKind.DISJOINT, (operation,), ())
1023
+ )
1024
+ for index, operation in enumerate(right):
1025
+ if index not in consumed_right:
1026
+ relations.append(
1027
+ PatchRelation(ThreeWayRelationKind.DISJOINT, (), (operation,))
1028
+ )
1029
+ return tuple(sorted(relations, key=_relation_sort_key))
1030
+
1031
+
1032
+ def _relation_fingerprint(relation: PatchRelation) -> tuple[object, ...]:
1033
+ validate_patch_relation(relation)
1034
+ return (
1035
+ relation.kind.value,
1036
+ relation.semantic_component,
1037
+ tuple(operation_occurrence_bytes(value) for value in relation.left_operations),
1038
+ tuple(operation_occurrence_bytes(value) for value in relation.right_operations),
1039
+ )
1040
+
1041
+
1042
+ def _assert_replayed_canonical_classification(
1043
+ ancestor: FrozenJsonValue,
1044
+ left_patch: TypedPatch,
1045
+ right_patch: TypedPatch,
1046
+ relations: tuple[PatchRelation, ...],
1047
+ ) -> None:
1048
+ validate_typed_patch(left_patch)
1049
+ validate_typed_patch(right_patch)
1050
+ if type(relations) is not tuple:
1051
+ raise TypeError("relations must be an exact tuple")
1052
+ for relation in relations:
1053
+ validate_patch_relation(relation)
1054
+ if left_patch.limits != right_patch.limits:
1055
+ raise ValueError("three-way branch patches must share exact algebra limits")
1056
+ if (
1057
+ left_patch.base_candidate_id != right_patch.base_candidate_id
1058
+ or left_patch.base_hash != right_patch.base_hash
1059
+ ):
1060
+ raise ValueError("three-way branch patches must share the exact ancestor")
1061
+ if left_patch.target_candidate_id == right_patch.target_candidate_id:
1062
+ raise ValueError("three-way branches must target distinct occurrences")
1063
+ apply_patch(ancestor, left_patch)
1064
+ apply_patch(ancestor, right_patch)
1065
+ expected = _derive_three_way_relations(left_patch, right_patch)
1066
+ if tuple(map(_relation_fingerprint, relations)) != tuple(
1067
+ map(_relation_fingerprint, expected)
1068
+ ):
1069
+ raise ValueError(
1070
+ "relations are not the canonical global classification of the replayed patches"
1071
+ )
1072
+
1073
+
1074
+ @dataclass(frozen=True, slots=True, eq=False)
1075
+ class ThreeWayPatchClassification:
1076
+ ancestor: FrozenJsonValue
1077
+ ancestor_candidate_id: CandidateId
1078
+ ancestor_hash: str
1079
+ left_patch_hash: str
1080
+ right_patch_hash: str
1081
+ relations: tuple[PatchRelation, ...]
1082
+ left_patch: TypedPatch
1083
+ right_patch: TypedPatch
1084
+
1085
+ def __post_init__(self) -> None:
1086
+ if type(self.left_patch) is not TypedPatch or type(
1087
+ self.right_patch
1088
+ ) is not TypedPatch:
1089
+ raise TypeError("classification patches must be exact TypedPatch values")
1090
+ validate_typed_patch(self.left_patch)
1091
+ validate_typed_patch(self.right_patch)
1092
+ frozen_ancestor = freeze_json(
1093
+ self.ancestor,
1094
+ limits=self.left_patch.limits.json_limits,
1095
+ )
1096
+ object.__setattr__(self, "ancestor", frozen_ancestor)
1097
+ if type(self.ancestor_candidate_id) is not CandidateId:
1098
+ raise TypeError("ancestor_candidate_id must be an exact CandidateId")
1099
+ CandidateId.__post_init__(self.ancestor_candidate_id)
1100
+ for value, name in (
1101
+ (self.ancestor_hash, "ancestor_hash"),
1102
+ (self.left_patch_hash, "left_patch_hash"),
1103
+ (self.right_patch_hash, "right_patch_hash"),
1104
+ ):
1105
+ require_sha256(value, name)
1106
+ if (
1107
+ self.left_patch.base_candidate_id != self.ancestor_candidate_id
1108
+ or self.right_patch.base_candidate_id != self.ancestor_candidate_id
1109
+ or self.left_patch.base_hash != self.ancestor_hash
1110
+ or self.right_patch.base_hash != self.ancestor_hash
1111
+ or self.left_patch.patch_hash != self.left_patch_hash
1112
+ or self.right_patch.patch_hash != self.right_patch_hash
1113
+ ):
1114
+ raise ValueError("classification endpoint or patch hashes do not bind")
1115
+ if type(self.relations) is not tuple:
1116
+ raise TypeError("relations must be an exact tuple of PatchRelation values")
1117
+ relation_bound = len(self.left_patch.operations) + len(
1118
+ self.right_patch.operations
1119
+ )
1120
+ if len(self.relations) > relation_bound:
1121
+ raise ValueError("relations exceeds the branch-operation bound")
1122
+ if any(type(relation) is not PatchRelation for relation in self.relations):
1123
+ raise TypeError("relations must be an exact tuple of PatchRelation values")
1124
+ for relation in self.relations:
1125
+ validate_patch_relation(relation)
1126
+ if self.relations != tuple(sorted(self.relations, key=_relation_sort_key)):
1127
+ raise ValueError("relations must use canonical order")
1128
+ if len({relation.relation_id for relation in self.relations}) != len(
1129
+ self.relations
1130
+ ):
1131
+ raise ValueError("classification contains duplicate relation identities")
1132
+
1133
+ expected: dict[tuple[str, bytes], int] = {}
1134
+ for side, patch in (("left", self.left_patch), ("right", self.right_patch)):
1135
+ for operation in patch.operations:
1136
+ key = (side, operation_occurrence_bytes(operation))
1137
+ expected[key] = expected.get(key, 0) + 1
1138
+ observed: dict[tuple[str, bytes], int] = {}
1139
+ for relation in self.relations:
1140
+ for side, operations in (
1141
+ ("left", relation.left_operations),
1142
+ ("right", relation.right_operations),
1143
+ ):
1144
+ for operation in operations:
1145
+ if operation.source_candidate_id != self.ancestor_candidate_id:
1146
+ raise ValueError(
1147
+ "relation operation does not bind the common ancestor"
1148
+ )
1149
+ key = (side, operation_occurrence_bytes(operation))
1150
+ observed[key] = observed.get(key, 0) + 1
1151
+ if observed != expected:
1152
+ raise ValueError("relations do not partition the two branch patches exactly")
1153
+ _assert_replayed_canonical_classification(
1154
+ self.ancestor,
1155
+ self.left_patch,
1156
+ self.right_patch,
1157
+ self.relations,
1158
+ )
1159
+
1160
+ def revalidate(self) -> None:
1161
+ """Replay both branches and recompute the complete global partition."""
1162
+
1163
+ if type(self) is not ThreeWayPatchClassification:
1164
+ raise TypeError("classification must be exact ThreeWayPatchClassification")
1165
+ ThreeWayPatchClassification.__post_init__(self)
1166
+
1167
+ def __eq__(self, other: object) -> bool:
1168
+ if (
1169
+ type(self) is not ThreeWayPatchClassification
1170
+ or type(other) is not ThreeWayPatchClassification
1171
+ ):
1172
+ return False
1173
+ self.revalidate()
1174
+ other.revalidate()
1175
+ left = (
1176
+ canonical_typed_json_bytes(
1177
+ self.ancestor,
1178
+ limits=self.left_patch.limits.json_limits,
1179
+ ),
1180
+ self.ancestor_candidate_id.value,
1181
+ self.ancestor_hash,
1182
+ self.left_patch_hash,
1183
+ self.right_patch_hash,
1184
+ tuple(relation.relation_id for relation in self.relations),
1185
+ )
1186
+ right = (
1187
+ canonical_typed_json_bytes(
1188
+ other.ancestor,
1189
+ limits=other.left_patch.limits.json_limits,
1190
+ ),
1191
+ other.ancestor_candidate_id.value,
1192
+ other.ancestor_hash,
1193
+ other.left_patch_hash,
1194
+ other.right_patch_hash,
1195
+ tuple(relation.relation_id for relation in other.relations),
1196
+ )
1197
+ return left == right
1198
+
1199
+ __hash__ = None
1200
+
1201
+ def of_kind(self, kind: ThreeWayRelationKind) -> tuple[PatchRelation, ...]:
1202
+ if type(kind) is not ThreeWayRelationKind:
1203
+ raise TypeError("kind must be a ThreeWayRelationKind")
1204
+ self.revalidate()
1205
+ return tuple(relation for relation in self.relations if relation.kind is kind)
1206
+
1207
+
1208
+ def classify_three_way_patches(
1209
+ ancestor: object,
1210
+ left_patch: TypedPatch,
1211
+ right_patch: TypedPatch,
1212
+ ) -> ThreeWayPatchClassification:
1213
+ """Verify and partition two ancestor-derived patches without merging."""
1214
+
1215
+ validate_typed_patch(left_patch)
1216
+ validate_typed_patch(right_patch)
1217
+ if (
1218
+ left_patch.base_candidate_id != right_patch.base_candidate_id
1219
+ or left_patch.base_hash != right_patch.base_hash
1220
+ ):
1221
+ raise ValueError("three-way branch patches must share the exact ancestor")
1222
+ if left_patch.limits != right_patch.limits:
1223
+ raise ValueError("three-way branch patches must share exact algebra limits")
1224
+ if left_patch.target_candidate_id == right_patch.target_candidate_id:
1225
+ raise ValueError("three-way branches must target distinct occurrences")
1226
+ frozen_ancestor = freeze_json(ancestor, limits=left_patch.limits.json_limits)
1227
+ ordered = _derive_three_way_relations(left_patch, right_patch)
1228
+ return ThreeWayPatchClassification(
1229
+ ancestor=frozen_ancestor,
1230
+ ancestor_candidate_id=left_patch.base_candidate_id,
1231
+ ancestor_hash=left_patch.base_hash,
1232
+ left_patch_hash=left_patch.patch_hash,
1233
+ right_patch_hash=right_patch.patch_hash,
1234
+ relations=ordered,
1235
+ left_patch=left_patch,
1236
+ right_patch=right_patch,
1237
+ )
1238
+
1239
+
1240
+ @dataclass(frozen=True, slots=True, eq=False)
1241
+ class PreservationObligationRequest:
1242
+ """One requested path within a replay-verified three-way relation effect."""
1243
+
1244
+ relation_id: str
1245
+ source: PreservationSource
1246
+ path: JsonPath
1247
+
1248
+ def __post_init__(self) -> None:
1249
+ require_sha256(self.relation_id, "relation_id")
1250
+ if type(self.source) is not PreservationSource:
1251
+ raise TypeError("source must be a PreservationSource")
1252
+ validate_json_path(self.path)
1253
+
1254
+ def __eq__(self, other: object) -> bool:
1255
+ if (
1256
+ type(self) is not PreservationObligationRequest
1257
+ or type(other) is not PreservationObligationRequest
1258
+ ):
1259
+ return False
1260
+ PreservationObligationRequest.__post_init__(self)
1261
+ PreservationObligationRequest.__post_init__(other)
1262
+ return (
1263
+ self.relation_id,
1264
+ self.source.value,
1265
+ canonical_path_bytes(self.path),
1266
+ ) == (
1267
+ other.relation_id,
1268
+ other.source.value,
1269
+ canonical_path_bytes(other.path),
1270
+ )
1271
+
1272
+ __hash__ = None
1273
+
1274
+
1275
+ _ABSENCE_CONTEXT_SHAPE_DOMAIN = b"agent-evolve:absence-context-shape:v1\x00"
1276
+
1277
+
1278
+ def _context_shape_hash(value: FrozenJsonValue) -> str:
1279
+ digest = hashlib.sha256()
1280
+ digest.update(_ABSENCE_CONTEXT_SHAPE_DOMAIN)
1281
+ if type(value) is FrozenJsonObject:
1282
+ digest.update(b"object")
1283
+ digest.update(len(value.items).to_bytes(8, "big"))
1284
+ for key, _ in value.items:
1285
+ encoded = key.encode("utf-8", errors="strict")
1286
+ digest.update(len(encoded).to_bytes(8, "big"))
1287
+ digest.update(encoded)
1288
+ elif type(value) is FrozenJsonArray:
1289
+ digest.update(b"array")
1290
+ digest.update(len(value.items).to_bytes(8, "big"))
1291
+ else:
1292
+ raise TypeError("absence context must be an object or array")
1293
+ return digest.hexdigest()
1294
+
1295
+
1296
+ @dataclass(frozen=True, slots=True)
1297
+ class _ObservedPathState:
1298
+ state: PreservationExpectation
1299
+ value: FrozenJsonValue | None
1300
+ value_hash: str | None
1301
+ absence_context_path: JsonPath | None = None
1302
+ absence_context_kind: AbsenceContextKind | None = None
1303
+ absence_context_shape_hash: str | None = None
1304
+ absence_failure_kind: AbsenceFailureKind | None = None
1305
+
1306
+
1307
+ def _path_state(
1308
+ value: FrozenJsonValue,
1309
+ path: JsonPath,
1310
+ *,
1311
+ limits: TypedJsonLimits,
1312
+ ) -> _ObservedPathState:
1313
+ validate_json_path(path)
1314
+ frozen = freeze_json(value, limits=limits)
1315
+ if frozen is not value:
1316
+ raise TypeError("path-state inspection requires a frozen typed-JSON value")
1317
+ current = frozen
1318
+ traversed: list[ObjectKey | ArrayIndex] = []
1319
+ for segment in path.segments:
1320
+ context_path = JsonPath(tuple(traversed))
1321
+ if type(segment) is ObjectKey:
1322
+ if type(current) is not FrozenJsonObject:
1323
+ return _ObservedPathState(PreservationExpectation.ABSENT, None, None)
1324
+ found = False
1325
+ for key, item in current.items:
1326
+ if key == segment.value:
1327
+ current = item
1328
+ found = True
1329
+ break
1330
+ if not found:
1331
+ return _ObservedPathState(
1332
+ PreservationExpectation.ABSENT,
1333
+ None,
1334
+ None,
1335
+ context_path,
1336
+ AbsenceContextKind.OBJECT,
1337
+ _context_shape_hash(current),
1338
+ AbsenceFailureKind.MISSING_OBJECT_KEY,
1339
+ )
1340
+ elif type(segment) is ArrayIndex:
1341
+ if type(current) is not FrozenJsonArray:
1342
+ return _ObservedPathState(PreservationExpectation.ABSENT, None, None)
1343
+ if segment.value >= len(current.items):
1344
+ return _ObservedPathState(
1345
+ PreservationExpectation.ABSENT,
1346
+ None,
1347
+ None,
1348
+ context_path,
1349
+ AbsenceContextKind.ARRAY,
1350
+ _context_shape_hash(current),
1351
+ AbsenceFailureKind.ARRAY_INDEX_OUT_OF_BOUNDS,
1352
+ )
1353
+ current = current.items[segment.value]
1354
+ else: # pragma: no cover - validate_json_path closes the union.
1355
+ raise AssertionError("unsupported path segment")
1356
+ traversed.append(segment)
1357
+ return _ObservedPathState(
1358
+ PreservationExpectation.PRESENT,
1359
+ current,
1360
+ typed_json_sha256(current, limits=limits),
1361
+ )
1362
+
1363
+
1364
+ def _state_identity(
1365
+ state: _ObservedPathState,
1366
+ ) -> tuple[
1367
+ PreservationExpectation,
1368
+ str | None,
1369
+ JsonPath | None,
1370
+ AbsenceContextKind | None,
1371
+ str | None,
1372
+ AbsenceFailureKind | None,
1373
+ ]:
1374
+ return (
1375
+ state.state,
1376
+ state.value_hash,
1377
+ state.absence_context_path,
1378
+ state.absence_context_kind,
1379
+ state.absence_context_shape_hash,
1380
+ state.absence_failure_kind,
1381
+ )
1382
+
1383
+
1384
+ def _states_equal(
1385
+ left: _ObservedPathState,
1386
+ right: _ObservedPathState,
1387
+ *,
1388
+ limits: TypedJsonLimits,
1389
+ ) -> bool:
1390
+ if left.state is not right.state:
1391
+ return False
1392
+ if left.state is PreservationExpectation.ABSENT:
1393
+ return True
1394
+ if left.value is None or right.value is None: # pragma: no cover - state invariant.
1395
+ raise AssertionError("present state lacked a typed value")
1396
+ return typed_json_equal(left.value, right.value, limits=limits)
1397
+
1398
+
1399
+ def _operation_prefix_index(
1400
+ operations: tuple[PatchOperation, ...],
1401
+ ) -> dict[object, object]:
1402
+ terminal = object()
1403
+ root: dict[object, object] = {"__terminal_marker__": terminal}
1404
+ for operation in operations:
1405
+ node = root
1406
+ for segment in operation.path.segments:
1407
+ child = node.setdefault(segment, {})
1408
+ if type(child) is not dict: # pragma: no cover - prefix-free invariant.
1409
+ raise AssertionError("invalid operation-prefix trie")
1410
+ node = child
1411
+ node[terminal] = operation
1412
+ return root
1413
+
1414
+
1415
+ def _covering_effect(
1416
+ index: dict[object, object],
1417
+ path: JsonPath,
1418
+ ) -> PatchOperation:
1419
+ terminal = index["__terminal_marker__"]
1420
+ node = index
1421
+ match: PatchOperation | None = None
1422
+ for segment in path.segments:
1423
+ candidate = node.get(terminal)
1424
+ if type(candidate) in (
1425
+ ReplaceScalar,
1426
+ ReplaceSubtree,
1427
+ InsertSequenceItem,
1428
+ DeleteSequenceItem,
1429
+ PermuteSequence,
1430
+ ):
1431
+ match = candidate
1432
+ break
1433
+ child = node.get(segment)
1434
+ if type(child) is not dict:
1435
+ break
1436
+ node = child
1437
+ else:
1438
+ candidate = node.get(terminal)
1439
+ if type(candidate) in (
1440
+ ReplaceScalar,
1441
+ ReplaceSubtree,
1442
+ InsertSequenceItem,
1443
+ DeleteSequenceItem,
1444
+ PermuteSequence,
1445
+ ):
1446
+ match = candidate
1447
+ if match is None:
1448
+ raise ValueError(
1449
+ "obligation path must lie within exactly one source-branch operation effect"
1450
+ )
1451
+ return match
1452
+
1453
+
1454
+ def derive_preservation_obligations(
1455
+ classification: ThreeWayPatchClassification,
1456
+ requests: tuple[PreservationObligationRequest, ...],
1457
+ ) -> tuple[PreservationObligation, ...]:
1458
+ """Derive immutable obligations only from replayed branch innovations.
1459
+
1460
+ A branch-specific request must identify a value/presence change that differs
1461
+ from both the common ancestor and the other branch. An identical edit may
1462
+ be requested only once as neutral evidence and never yields parent credit.
1463
+ """
1464
+
1465
+ if type(classification) is not ThreeWayPatchClassification:
1466
+ raise TypeError("classification must be exact ThreeWayPatchClassification")
1467
+ classification.revalidate()
1468
+ if type(requests) is not tuple:
1469
+ raise TypeError(
1470
+ "requests must be an exact tuple of PreservationObligationRequest values"
1471
+ )
1472
+ if len(requests) > MAX_PRESERVATION_OBLIGATIONS:
1473
+ raise ValueError("requests exceeds MAX_PRESERVATION_OBLIGATIONS")
1474
+ if any(type(request) is not PreservationObligationRequest for request in requests):
1475
+ raise TypeError(
1476
+ "requests must be an exact tuple of PreservationObligationRequest values"
1477
+ )
1478
+ for request in requests:
1479
+ PreservationObligationRequest.__post_init__(request)
1480
+ request_keys = tuple(
1481
+ (
1482
+ request.relation_id,
1483
+ request.source.value,
1484
+ canonical_path_bytes(request.path),
1485
+ )
1486
+ for request in requests
1487
+ )
1488
+ if len(set(request_keys)) != len(request_keys):
1489
+ raise ValueError("preservation requests cannot duplicate an exact source/path")
1490
+
1491
+ relations = {relation.relation_id: relation for relation in classification.relations}
1492
+ relation_indexes = {
1493
+ relation.relation_id: (
1494
+ _operation_prefix_index(relation.left_operations),
1495
+ _operation_prefix_index(relation.right_operations),
1496
+ )
1497
+ for relation in classification.relations
1498
+ }
1499
+ limits = classification.left_patch.limits.json_limits
1500
+ left_target = apply_patch(classification.ancestor, classification.left_patch)
1501
+ right_target = apply_patch(classification.ancestor, classification.right_patch)
1502
+ obligations: list[PreservationObligation] = []
1503
+
1504
+ for request in requests:
1505
+ relation = relations.get(request.relation_id)
1506
+ if relation is None:
1507
+ raise ValueError("preservation request names an unknown relation identity")
1508
+ ancestor_state = _path_state(
1509
+ classification.ancestor,
1510
+ request.path,
1511
+ limits=limits,
1512
+ )
1513
+
1514
+ if request.source is PreservationSource.IDENTICAL_NEUTRAL:
1515
+ if relation.kind is not ThreeWayRelationKind.IDENTICAL:
1516
+ raise ValueError("only an identical relation may create a neutral obligation")
1517
+ left_effect = _covering_effect(
1518
+ relation_indexes[relation.relation_id][0], request.path
1519
+ )
1520
+ right_effect = _covering_effect(
1521
+ relation_indexes[relation.relation_id][1], request.path
1522
+ )
1523
+ left_state = _path_state(left_target, request.path, limits=limits)
1524
+ right_state = _path_state(right_target, request.path, limits=limits)
1525
+ if not _states_equal(left_state, right_state, limits=limits):
1526
+ raise ValueError("identical relation branches disagree at obligation path")
1527
+ expected_state = left_state
1528
+ source_parent_ids = (
1529
+ classification.left_patch.target_candidate_id,
1530
+ classification.right_patch.target_candidate_id,
1531
+ )
1532
+ patch_hashes = (
1533
+ classification.left_patch_hash,
1534
+ classification.right_patch_hash,
1535
+ )
1536
+ effect_hashes = (
1537
+ operation_effect_sha256(left_effect, limits=limits),
1538
+ operation_effect_sha256(right_effect, limits=limits),
1539
+ )
1540
+ else:
1541
+ if relation.kind is ThreeWayRelationKind.IDENTICAL:
1542
+ raise ValueError("identical edits are neutral and cannot earn branch credit")
1543
+ if request.source is PreservationSource.LEFT_BRANCH:
1544
+ operation_index = relation_indexes[relation.relation_id][0]
1545
+ target = left_target
1546
+ other_target = right_target
1547
+ patch = classification.left_patch
1548
+ elif request.source is PreservationSource.RIGHT_BRANCH:
1549
+ operation_index = relation_indexes[relation.relation_id][1]
1550
+ target = right_target
1551
+ other_target = left_target
1552
+ patch = classification.right_patch
1553
+ else: # pragma: no cover - enum closes sources.
1554
+ raise AssertionError("unsupported preservation source")
1555
+ effect = _covering_effect(operation_index, request.path)
1556
+ expected_state = _path_state(target, request.path, limits=limits)
1557
+ other_state = _path_state(other_target, request.path, limits=limits)
1558
+ if _states_equal(expected_state, ancestor_state, limits=limits):
1559
+ raise ValueError(
1560
+ "branch-specific obligation is not an actual change from the ancestor"
1561
+ )
1562
+ if _states_equal(expected_state, other_state, limits=limits):
1563
+ raise ValueError(
1564
+ "branch-specific obligation is not discriminative from the other branch"
1565
+ )
1566
+ source_parent_ids = (patch.target_candidate_id,)
1567
+ patch_hashes = (patch.patch_hash,)
1568
+ effect_hashes = (operation_effect_sha256(effect, limits=limits),)
1569
+
1570
+ if (
1571
+ expected_state.state is PreservationExpectation.ABSENT
1572
+ and expected_state.absence_context_path is None
1573
+ ):
1574
+ raise ValueError(
1575
+ "absence obligations require a missing key/index in a surviving container"
1576
+ )
1577
+
1578
+ obligations.append(
1579
+ PreservationObligation(
1580
+ source=request.source,
1581
+ source_parent_candidate_ids=source_parent_ids,
1582
+ branch_patch_hashes=patch_hashes,
1583
+ operation_effect_hashes=effect_hashes,
1584
+ relation_id=relation.relation_id,
1585
+ path=request.path,
1586
+ expected_state=expected_state.state,
1587
+ expected_value_hash=expected_state.value_hash,
1588
+ ancestor_state=ancestor_state.state,
1589
+ ancestor_value_hash=ancestor_state.value_hash,
1590
+ absence_context_path=expected_state.absence_context_path,
1591
+ absence_context_kind=expected_state.absence_context_kind,
1592
+ absence_context_shape_hash=expected_state.absence_context_shape_hash,
1593
+ absence_failure_kind=expected_state.absence_failure_kind,
1594
+ )
1595
+ )
1596
+ ordered = tuple(sorted(obligations, key=lambda value: value.obligation_id))
1597
+ if len({value.obligation_id for value in ordered}) != len(ordered):
1598
+ raise ValueError("derived preservation obligations have duplicate identities")
1599
+ return ordered
1600
+
1601
+
1602
+ class ResolutionChoice(str, Enum):
1603
+ CHOOSE_LEFT = "choose_left"
1604
+ CHOOSE_RIGHT = "choose_right"
1605
+ SYNTHESIZE = "synthesize"
1606
+ DROP_BOTH = "drop_both"
1607
+
1608
+
1609
+ @dataclass(frozen=True, slots=True, eq=False)
1610
+ class PatchResolution:
1611
+ relation_id: str
1612
+ choice: ResolutionChoice
1613
+ synthesized_result_hash: str | None = None
1614
+
1615
+ def __post_init__(self) -> None:
1616
+ require_sha256(self.relation_id, "relation_id")
1617
+ if type(self.choice) is not ResolutionChoice:
1618
+ raise TypeError("choice must be a ResolutionChoice")
1619
+ if self.choice is ResolutionChoice.SYNTHESIZE:
1620
+ if self.synthesized_result_hash is None:
1621
+ raise ValueError("synthesize resolution requires a result hash")
1622
+ require_sha256(
1623
+ self.synthesized_result_hash,
1624
+ "synthesized_result_hash",
1625
+ )
1626
+ elif self.synthesized_result_hash is not None:
1627
+ raise ValueError("only synthesize resolutions may carry a result hash")
1628
+
1629
+ def __eq__(self, other: object) -> bool:
1630
+ if type(self) is not PatchResolution or type(other) is not PatchResolution:
1631
+ return False
1632
+ PatchResolution.__post_init__(self)
1633
+ PatchResolution.__post_init__(other)
1634
+ return (
1635
+ self.relation_id,
1636
+ self.choice.value,
1637
+ self.synthesized_result_hash,
1638
+ ) == (
1639
+ other.relation_id,
1640
+ other.choice.value,
1641
+ other.synthesized_result_hash,
1642
+ )
1643
+
1644
+ __hash__ = None
1645
+
1646
+
1647
+ def validate_three_way_resolutions(
1648
+ classification: ThreeWayPatchClassification,
1649
+ resolutions: tuple[PatchResolution, ...],
1650
+ ) -> tuple[PatchResolution, ...]:
1651
+ """Require exactly one declared choice for every conflict/invalidation."""
1652
+
1653
+ if type(classification) is not ThreeWayPatchClassification:
1654
+ raise TypeError("classification must be exact ThreeWayPatchClassification")
1655
+ classification.revalidate()
1656
+ if type(resolutions) is not tuple:
1657
+ raise TypeError("resolutions must be an exact tuple of PatchResolution values")
1658
+ if len(resolutions) > len(classification.left_patch.operations) + len(
1659
+ classification.right_patch.operations
1660
+ ):
1661
+ raise ValueError("resolutions exceeds the branch-operation bound")
1662
+ if any(type(resolution) is not PatchResolution for resolution in resolutions):
1663
+ raise TypeError("resolutions must be an exact tuple of PatchResolution values")
1664
+ for resolution in resolutions:
1665
+ PatchResolution.__post_init__(resolution)
1666
+ if resolutions != tuple(sorted(resolutions, key=lambda value: value.relation_id)):
1667
+ raise ValueError("resolutions must use canonical relation-id order")
1668
+ ids = tuple(resolution.relation_id for resolution in resolutions)
1669
+ if len(set(ids)) != len(ids):
1670
+ raise ValueError("duplicate or conflicting resolutions are prohibited")
1671
+ required = {
1672
+ relation.relation_id
1673
+ for relation in classification.relations
1674
+ if relation.kind
1675
+ in (ThreeWayRelationKind.CONFLICT, ThreeWayRelationKind.INVALIDATED)
1676
+ }
1677
+ supplied = set(ids)
1678
+ if supplied != required:
1679
+ missing = required - supplied
1680
+ unknown = supplied - required
1681
+ reason = "missing" if missing else "unknown"
1682
+ raise ValueError(f"resolution set contains {reason} relation identities")
1683
+ return resolutions
1684
+
1685
+
1686
+ @dataclass(frozen=True, slots=True, eq=False)
1687
+ class ParentConfiguration:
1688
+ occurrence: CandidateOccurrence
1689
+ configuration: FrozenJsonValue
1690
+
1691
+ def __post_init__(self) -> None:
1692
+ if type(self.occurrence) is not CandidateOccurrence:
1693
+ raise TypeError("occurrence must be an exact CandidateOccurrence")
1694
+ CandidateOccurrence.__post_init__(self.occurrence)
1695
+ frozen = freeze_json(self.configuration)
1696
+ if frozen is not self.configuration:
1697
+ raise TypeError("configuration must already be a frozen typed-JSON value")
1698
+ if typed_json_sha256(frozen) != self.occurrence.configuration_hash:
1699
+ raise ValueError("configuration does not match its occurrence hash")
1700
+
1701
+ def __eq__(self, other: object) -> bool:
1702
+ if type(self) is not ParentConfiguration or type(other) is not ParentConfiguration:
1703
+ return False
1704
+ ParentConfiguration.__post_init__(self)
1705
+ ParentConfiguration.__post_init__(other)
1706
+ return (
1707
+ self.occurrence._validated_values(),
1708
+ canonical_typed_json_bytes(self.configuration),
1709
+ ) == (
1710
+ other.occurrence._validated_values(),
1711
+ canonical_typed_json_bytes(other.configuration),
1712
+ )
1713
+
1714
+ __hash__ = None
1715
+
1716
+
1717
+ def bind_parent_configuration(
1718
+ occurrence: CandidateOccurrence,
1719
+ configuration: object,
1720
+ *,
1721
+ limits: TypedJsonLimits,
1722
+ ) -> ParentConfiguration:
1723
+ if type(occurrence) is not CandidateOccurrence:
1724
+ raise TypeError("occurrence must be an exact CandidateOccurrence")
1725
+ CandidateOccurrence.__post_init__(occurrence)
1726
+ frozen = freeze_json(configuration, limits=limits)
1727
+ if typed_json_sha256(frozen, limits=limits) != occurrence.configuration_hash:
1728
+ raise ValueError("configuration does not match its occurrence hash")
1729
+ return ParentConfiguration(occurrence, frozen)
1730
+
1731
+
1732
+ @dataclass(frozen=True, slots=True, eq=False)
1733
+ class PreservationVerification:
1734
+ child_hash: str
1735
+ verified_claims: tuple[PreservationClaim, ...]
1736
+ discriminatively_used_parent_ids: tuple[CandidateId, ...]
1737
+
1738
+ def __post_init__(self) -> None:
1739
+ require_sha256(self.child_hash, "child_hash")
1740
+ if type(self.verified_claims) is not tuple:
1741
+ raise TypeError("verified_claims must contain exact PreservationClaim values")
1742
+ if not 2 <= len(self.verified_claims) <= MAX_PRESERVATION_OBLIGATIONS:
1743
+ raise ValueError(
1744
+ "verified_claims must contain 2 to "
1745
+ f"{MAX_PRESERVATION_OBLIGATIONS} claims"
1746
+ )
1747
+ if any(type(claim) is not PreservationClaim for claim in self.verified_claims):
1748
+ raise TypeError("verified_claims must contain exact PreservationClaim values")
1749
+ for claim in self.verified_claims:
1750
+ PreservationClaim.__post_init__(claim)
1751
+ claim_ids = tuple(claim.obligation_id for claim in self.verified_claims)
1752
+ if claim_ids != tuple(sorted(claim_ids)):
1753
+ raise ValueError("verified_claims must use canonical obligation-id order")
1754
+ if len(set(claim_ids)) != len(claim_ids):
1755
+ raise ValueError("verified_claims cannot duplicate an obligation identity")
1756
+ if type(self.discriminatively_used_parent_ids) is not tuple:
1757
+ raise TypeError("used parent IDs must be an exact CandidateId tuple")
1758
+ if len(self.discriminatively_used_parent_ids) != 2:
1759
+ raise ValueError(
1760
+ "three-way preservation requires exactly two used parent IDs"
1761
+ )
1762
+ if any(
1763
+ type(value) is not CandidateId
1764
+ for value in self.discriminatively_used_parent_ids
1765
+ ):
1766
+ raise TypeError("used parent IDs must be an exact CandidateId tuple")
1767
+ for value in self.discriminatively_used_parent_ids:
1768
+ CandidateId.__post_init__(value)
1769
+ if len(set(self.discriminatively_used_parent_ids)) != 2:
1770
+ raise ValueError("used parent IDs must be distinct")
1771
+
1772
+ def __eq__(self, other: object) -> bool:
1773
+ if (
1774
+ type(self) is not PreservationVerification
1775
+ or type(other) is not PreservationVerification
1776
+ ):
1777
+ return False
1778
+ validate_preservation_verification(self)
1779
+ validate_preservation_verification(other)
1780
+ left = (
1781
+ self.child_hash,
1782
+ tuple(claim.obligation_id for claim in self.verified_claims),
1783
+ tuple(value.value for value in self.discriminatively_used_parent_ids),
1784
+ )
1785
+ right = (
1786
+ other.child_hash,
1787
+ tuple(claim.obligation_id for claim in other.verified_claims),
1788
+ tuple(value.value for value in other.discriminatively_used_parent_ids),
1789
+ )
1790
+ return left == right
1791
+
1792
+ __hash__ = None
1793
+
1794
+
1795
+ def validate_preservation_verification(value: PreservationVerification) -> None:
1796
+ """Recursively validate one non-authoritative preservation receipt.
1797
+
1798
+ Structural validity does not prove that the receipt came from
1799
+ :func:`verify_preservation_claims`; a future durable evidence graph must
1800
+ bind it to the exact variation case, classification, child, and claims.
1801
+ """
1802
+
1803
+ if type(value) is not PreservationVerification:
1804
+ raise TypeError("value must be an exact PreservationVerification")
1805
+ PreservationVerification.__post_init__(value)
1806
+
1807
+
1808
+ def verify_preservation_claims(
1809
+ variation_case: VariationCase,
1810
+ classification: ThreeWayPatchClassification,
1811
+ parent_configurations: tuple[ParentConfiguration, ...],
1812
+ child: object,
1813
+ *,
1814
+ claims: tuple[PreservationClaim, ...],
1815
+ limits: TypedJsonLimits,
1816
+ ) -> PreservationVerification:
1817
+ """Replay and verify exact predeclared three-way innovation obligations."""
1818
+
1819
+ validate_variation_case(variation_case)
1820
+ if variation_case.variation_kind is not VariationKind.THREE_WAY_RECOMBINATION:
1821
+ raise ValueError(
1822
+ "this preservation verifier accepts only verified three-way cases"
1823
+ )
1824
+ if type(classification) is not ThreeWayPatchClassification:
1825
+ raise TypeError("classification must be exact ThreeWayPatchClassification")
1826
+ classification.revalidate()
1827
+ if type(limits) is not TypedJsonLimits:
1828
+ raise TypeError("limits must be an exact TypedJsonLimits value")
1829
+ validate_typed_json_limits(limits)
1830
+ if limits != classification.left_patch.limits.json_limits:
1831
+ raise ValueError("verification limits must match the branch patch algebra")
1832
+ if variation_case.common_ancestor is None:
1833
+ raise ValueError("three-way preservation requires a common ancestor")
1834
+ if (
1835
+ classification.ancestor_candidate_id
1836
+ != variation_case.common_ancestor.candidate_id
1837
+ or classification.ancestor_hash
1838
+ != variation_case.common_ancestor.configuration_hash
1839
+ or tuple(
1840
+ patch.patch_hash for patch in variation_case.ancestor_to_parent_patches
1841
+ )
1842
+ != (classification.left_patch_hash, classification.right_patch_hash)
1843
+ ):
1844
+ raise ValueError("classification does not bind the exact variation case")
1845
+ if type(parent_configurations) is not tuple:
1846
+ raise TypeError(
1847
+ "parent_configurations must be an exact tuple of ParentConfiguration values"
1848
+ )
1849
+ if len(parent_configurations) != 2:
1850
+ raise ValueError("three-way preservation requires exactly two parent configurations")
1851
+ if any(type(value) is not ParentConfiguration for value in parent_configurations):
1852
+ raise TypeError(
1853
+ "parent_configurations must be an exact tuple of ParentConfiguration values"
1854
+ )
1855
+ for value in parent_configurations:
1856
+ ParentConfiguration.__post_init__(value)
1857
+ if type(claims) is not tuple:
1858
+ raise TypeError("claims must be an exact tuple of PreservationClaim values")
1859
+ if len(claims) > MAX_PRESERVATION_OBLIGATIONS:
1860
+ raise ValueError("claims exceeds MAX_PRESERVATION_OBLIGATIONS")
1861
+ if any(type(claim) is not PreservationClaim for claim in claims):
1862
+ raise TypeError("claims must be an exact tuple of PreservationClaim values")
1863
+ for claim in claims:
1864
+ PreservationClaim.__post_init__(claim)
1865
+ claim_ids = tuple(claim.obligation_id for claim in claims)
1866
+ if claim_ids != tuple(sorted(claim_ids)):
1867
+ raise ValueError("claims must use canonical obligation-id order")
1868
+ if len(set(claim_ids)) != len(claim_ids):
1869
+ raise ValueError("claims cannot duplicate an obligation identity")
1870
+ expected_ids = tuple(
1871
+ parent.occurrence.candidate_id for parent in variation_case.parents
1872
+ )
1873
+ actual_ids = tuple(value.occurrence.candidate_id for value in parent_configurations)
1874
+ if actual_ids != expected_ids:
1875
+ raise ValueError("parent configurations must follow exact variation-case order")
1876
+ for binding, value in zip(variation_case.parents, parent_configurations):
1877
+ if value.occurrence != binding.occurrence:
1878
+ raise ValueError("parent configuration occurrence does not match the case")
1879
+ replayed_targets = (
1880
+ apply_patch(classification.ancestor, classification.left_patch),
1881
+ apply_patch(classification.ancestor, classification.right_patch),
1882
+ )
1883
+ for supplied, replayed in zip(parent_configurations, replayed_targets):
1884
+ if not typed_json_equal(supplied.configuration, replayed, limits=limits):
1885
+ raise ValueError("parent configuration does not match its replayed branch patch")
1886
+
1887
+ requests = tuple(
1888
+ PreservationObligationRequest(
1889
+ obligation.relation_id,
1890
+ obligation.source,
1891
+ obligation.path,
1892
+ )
1893
+ for obligation in variation_case.preservation_obligations
1894
+ )
1895
+ rederived_obligations = derive_preservation_obligations(classification, requests)
1896
+ if rederived_obligations != variation_case.preservation_obligations:
1897
+ raise PreservationError(
1898
+ "predeclared obligations are not exact replay-derived branch effects"
1899
+ )
1900
+ child_value = freeze_json(child, limits=limits)
1901
+ by_id = {
1902
+ value.occurrence.candidate_id: value.configuration
1903
+ for value in parent_configurations
1904
+ }
1905
+ required_ids = expected_ids
1906
+ obligations = {
1907
+ obligation.obligation_id: obligation
1908
+ for obligation in variation_case.preservation_obligations
1909
+ }
1910
+ if set(claim_ids) != set(obligations):
1911
+ raise PreservationError(
1912
+ "preservation claims must cover every predeclared obligation exactly once"
1913
+ )
1914
+ discriminative: set[CandidateId] = set()
1915
+ for claim in claims:
1916
+ obligation = obligations.get(claim.obligation_id)
1917
+ if obligation is None: # pragma: no cover - exact set check above.
1918
+ raise PreservationError("claim does not name a predeclared obligation")
1919
+ child_state = _path_state(child_value, obligation.path, limits=limits)
1920
+ if _state_identity(child_state) != (
1921
+ obligation.expected_state,
1922
+ obligation.expected_value_hash,
1923
+ obligation.absence_context_path,
1924
+ obligation.absence_context_kind,
1925
+ obligation.absence_context_shape_hash,
1926
+ obligation.absence_failure_kind,
1927
+ ):
1928
+ raise PreservationError(
1929
+ "child does not preserve the obligation's exact presence-aware state"
1930
+ )
1931
+ if obligation.expected_state is PreservationExpectation.PRESENT:
1932
+ source_parent = by_id[obligation.source_parent_candidate_ids[0]]
1933
+ expected_value = value_at_path(source_parent, obligation.path)
1934
+ if child_state.value is None or not typed_json_equal(
1935
+ child_state.value,
1936
+ expected_value,
1937
+ limits=limits,
1938
+ ):
1939
+ raise PreservationError("child value does not match the replayed source branch")
1940
+ if obligation.source is not PreservationSource.IDENTICAL_NEUTRAL:
1941
+ discriminative.add(obligation.source_parent_candidate_ids[0])
1942
+
1943
+ if set(required_ids) != discriminative:
1944
+ raise PreservationError(
1945
+ "one or more branch parents have no verified innovation-preservation claim"
1946
+ )
1947
+ return PreservationVerification(
1948
+ child_hash=typed_json_sha256(child_value, limits=limits),
1949
+ verified_claims=claims,
1950
+ discriminatively_used_parent_ids=tuple(
1951
+ candidate_id for candidate_id in required_ids if candidate_id in discriminative
1952
+ ),
1953
+ )
1954
+
1955
+
1956
+ __all__ = [
1957
+ "ComponentTagAssignment",
1958
+ "ParentConfiguration",
1959
+ "PatchPostconditionError",
1960
+ "PatchPreconditionError",
1961
+ "PatchRelation",
1962
+ "PatchResolution",
1963
+ "PreservationObligationRequest",
1964
+ "PreservationError",
1965
+ "PreservationVerification",
1966
+ "ResolutionChoice",
1967
+ "ThreeWayPatchClassification",
1968
+ "ThreeWayRelationKind",
1969
+ "apply_patch",
1970
+ "bind_parent_configuration",
1971
+ "classify_three_way_patches",
1972
+ "derive_patch",
1973
+ "derive_preservation_obligations",
1974
+ "invert_patch",
1975
+ "replace_existing_path",
1976
+ "validate_three_way_resolutions",
1977
+ "validate_patch_relation",
1978
+ "validate_preservation_verification",
1979
+ "value_at_path",
1980
+ "verify_preservation_claims",
1981
+ ]