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,1804 @@
1
+ """Lifecycle-gated insight memory built around randomized subset credit.
2
+
3
+ The bank stores immutable versions, logs one assignment per operator invocation,
4
+ and updates contextual retrieval scores only from identified selected-vs-unselected
5
+ contrasts. Unselected insights are never mechanically penalized, and untested
6
+ reflection output cannot enter retrieval without a recorded promotion.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import math
13
+ import re
14
+ from dataclasses import dataclass, field, replace
15
+ from enum import Enum
16
+ from fractions import Fraction
17
+ from typing import Mapping, Sequence
18
+
19
+ from agent_evolve.domain.ids import (
20
+ CandidateId,
21
+ LLMCallId,
22
+ OperatorInvocationId,
23
+ )
24
+ from agent_evolve.domain.finite_variation import FiniteActionEvidenceBinding
25
+ from agent_evolve.domain.insight import InsightRef
26
+ from agent_evolve.domain.patch import require_sha256
27
+ from agent_evolve.domain.typed_json import (
28
+ FrozenJsonObject,
29
+ canonical_typed_json_bytes,
30
+ freeze_json,
31
+ thaw_json,
32
+ )
33
+ from agent_evolve.policies.memory.randomized_subset import (
34
+ EpsilonGreedySubsetSelector,
35
+ InsightSelectionDecision,
36
+ InsightTrial,
37
+ estimate_marginal_effect,
38
+ )
39
+ from agent_evolve.ports.agentic_generator import (
40
+ InsightDraft,
41
+ MAX_REFLECTION_SEMANTIC_VOCABULARY_ENTRIES,
42
+ ReflectionConsumerScope,
43
+ )
44
+
45
+
46
+ _SPACE = re.compile(r"\s+")
47
+ _OPERATOR_KIND_TOKEN = re.compile(r"^[a-z][a-z0-9_]*$")
48
+ _CANONICAL_JSON_PATH = re.compile(
49
+ r"^\$\.[^.\[\]\s]+(?:\.[^.\[\]\s]+|\[(?:0|[1-9][0-9]*)\])*$"
50
+ )
51
+ _LOWER_SHA256 = re.compile(r"^[0-9a-f]{64}$")
52
+ _EVIDENCE_REFERENCE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$")
53
+ _FACT_SCHEMA_ID = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
54
+ _FACTOR_CAPABILITY_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,191}$")
55
+ _INSIGHT_EVIDENCE_LINEAGE_DOMAIN = b"agent-evolve:insight-evidence-lineage:v1\x00"
56
+ _INSIGHT_EVIDENCE_LINEAGE_V2_DOMAIN = (
57
+ b"agent-evolve:insight-evidence-lineage:v2-empirical-snapshots\x00"
58
+ )
59
+ _EMPIRICAL_EVIDENCE_SNAPSHOT_DOMAIN = b"agent-evolve:empirical-evidence-snapshot:v1\x00"
60
+ _QUARANTINE_TEST_ADMISSION_DOMAIN = b"agent-evolve:quarantine-test-admission:v1\x00"
61
+
62
+
63
+ class QuarantineAssignmentStructuralError(ValueError):
64
+ """A valid owned quarantine card cannot act within the requested scope."""
65
+
66
+
67
+ class InsightLifecycleState(str, Enum):
68
+ """Explicit retrieval lifecycle for one exact insight version."""
69
+
70
+ SEED = "seed"
71
+ QUARANTINED = "quarantined"
72
+ PROMOTED = "promoted"
73
+ DEPRECATED = "deprecated"
74
+
75
+ @property
76
+ def retrievable(self) -> bool:
77
+ return self in {type(self).SEED, type(self).PROMOTED}
78
+
79
+
80
+ class InsightOrigin(str, Enum):
81
+ """How an insight entered the bank; lifecycle transitions do not alter it."""
82
+
83
+ SEED = "seed"
84
+ REFLECTION = "reflection"
85
+ MANUAL = "manual"
86
+
87
+
88
+ class InsightRelationKind(str, Enum):
89
+ """Declared semantic relationships; the bank never infers these."""
90
+
91
+ REVISES = "revises"
92
+ DUPLICATES = "duplicates"
93
+ CONTRADICTS = "contradicts"
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class EmpiricalEvidenceSnapshot:
98
+ """Engine-issued facts for one exact contrast, never model-authored prose."""
99
+
100
+ contrast_id: str
101
+ fact_schema_id: str
102
+ fact_schema_version: int
103
+ fact_schema_definition_sha256: str
104
+ facts: FrozenJsonObject
105
+ optimization_semantics_definition_sha256: str | None = None
106
+ action_semantics_definition_sha256: str | None = None
107
+
108
+ def __post_init__(self) -> None:
109
+ if (
110
+ type(self.contrast_id) is not str
111
+ or _LOWER_SHA256.fullmatch(self.contrast_id) is None
112
+ ):
113
+ raise ValueError("contrast_id must be a lowercase SHA-256 ID")
114
+ if (
115
+ type(self.fact_schema_id) is not str
116
+ or _FACT_SCHEMA_ID.fullmatch(self.fact_schema_id) is None
117
+ ):
118
+ raise ValueError("fact_schema_id must use the closed lowercase grammar")
119
+ if type(self.fact_schema_version) is not int or self.fact_schema_version <= 0:
120
+ raise ValueError("fact_schema_version must be a positive exact integer")
121
+ if (
122
+ type(self.fact_schema_definition_sha256) is not str
123
+ or _LOWER_SHA256.fullmatch(self.fact_schema_definition_sha256) is None
124
+ ):
125
+ raise ValueError(
126
+ "fact_schema_definition_sha256 must be a lowercase SHA-256 ID"
127
+ )
128
+ if type(self.facts) is not FrozenJsonObject:
129
+ raise TypeError("facts must be an exact FrozenJsonObject")
130
+ if freeze_json(self.facts) is not self.facts:
131
+ raise TypeError("facts must already be frozen typed JSON")
132
+ if not self.facts.items:
133
+ raise ValueError("facts must be non-empty")
134
+ for name in (
135
+ "optimization_semantics_definition_sha256",
136
+ "action_semantics_definition_sha256",
137
+ ):
138
+ value = getattr(self, name)
139
+ if value is not None and (
140
+ type(value) is not str or _LOWER_SHA256.fullmatch(value) is None
141
+ ):
142
+ raise ValueError(f"{name} must be a lowercase SHA-256 ID or None")
143
+
144
+ def _identity_record(self) -> dict[str, object]:
145
+ self.__post_init__()
146
+ return {
147
+ "schema_version": 1,
148
+ "contrast_id": self.contrast_id,
149
+ "fact_schema_id": self.fact_schema_id,
150
+ "fact_schema_version": self.fact_schema_version,
151
+ "fact_schema_definition_sha256": (self.fact_schema_definition_sha256),
152
+ "facts": thaw_json(self.facts),
153
+ "optimization_semantics_definition_sha256": (
154
+ self.optimization_semantics_definition_sha256
155
+ ),
156
+ "action_semantics_definition_sha256": (
157
+ self.action_semantics_definition_sha256
158
+ ),
159
+ }
160
+
161
+ @property
162
+ def snapshot_sha256(self) -> str:
163
+ return hashlib.sha256(
164
+ _EMPIRICAL_EVIDENCE_SNAPSHOT_DOMAIN
165
+ + canonical_typed_json_bytes(freeze_json(self._identity_record()))
166
+ ).hexdigest()
167
+
168
+ def to_record(self) -> dict[str, object]:
169
+ return {**self._identity_record(), "snapshot_sha256": self.snapshot_sha256}
170
+
171
+
172
+ @dataclass(frozen=True, slots=True)
173
+ class InsightRelation:
174
+ kind: InsightRelationKind
175
+ target: InsightRef
176
+ note: str | None = None
177
+
178
+ def __post_init__(self) -> None:
179
+ if not isinstance(self.kind, InsightRelationKind):
180
+ raise TypeError("relation kind must be an InsightRelationKind")
181
+ if not isinstance(self.target, InsightRef):
182
+ raise TypeError("relation target must be an InsightRef")
183
+ if self.note is not None and (
184
+ type(self.note) is not str
185
+ or not self.note.strip()
186
+ or self.note != self.note.strip()
187
+ ):
188
+ raise ValueError("relation note must be non-empty canonical text")
189
+
190
+ def to_record(self) -> dict[str, object]:
191
+ """Return the complete deterministic durable relation projection."""
192
+
193
+ self.__post_init__()
194
+ InsightRef.__post_init__(self.target)
195
+ return {
196
+ "kind": self.kind.value,
197
+ "target": {
198
+ "insight_id": self.target.insight_id.value,
199
+ "version": self.target.version,
200
+ },
201
+ "note": self.note,
202
+ }
203
+
204
+
205
+ @dataclass(frozen=True, slots=True)
206
+ class InsightEvidenceLineage:
207
+ """Machine-verifiable evidence boundary supplied to one reflection call.
208
+
209
+ ``available_contrast_ids`` records what the reflector was allowed to use;
210
+ ``cited_contrast_ids`` records only exact full IDs supplied through that
211
+ draft's structured citation field. The distinction avoids treating prompt
212
+ availability or incidental prose substrings as an evidence citation.
213
+ """
214
+
215
+ reflection_call_id: LLMCallId
216
+ source_operator_invocation_ids: tuple[OperatorInvocationId, ...]
217
+ source_candidate_ids: tuple[CandidateId, ...]
218
+ available_contrast_ids: tuple[str, ...]
219
+ cited_contrast_ids: tuple[str, ...] = ()
220
+ finite_action_bindings: tuple[FiniteActionEvidenceBinding, ...] = ()
221
+ empirical_evidence: tuple[EmpiricalEvidenceSnapshot, ...] = ()
222
+
223
+ def __post_init__(self) -> None:
224
+ if type(self.reflection_call_id) is not LLMCallId:
225
+ raise TypeError("reflection_call_id must be an exact LLMCallId")
226
+ LLMCallId.__post_init__(self.reflection_call_id)
227
+ _validate_exact_sorted_ids(
228
+ self.source_operator_invocation_ids,
229
+ OperatorInvocationId,
230
+ name="source_operator_invocation_ids",
231
+ )
232
+ _validate_exact_sorted_ids(
233
+ self.source_candidate_ids,
234
+ CandidateId,
235
+ name="source_candidate_ids",
236
+ )
237
+ for name in ("available_contrast_ids", "cited_contrast_ids"):
238
+ values = getattr(self, name)
239
+ if type(values) is not tuple or any(
240
+ type(value) is not str or _LOWER_SHA256.fullmatch(value) is None
241
+ for value in values
242
+ ):
243
+ raise TypeError(f"{name} must be a tuple of lowercase SHA-256 IDs")
244
+ if values != tuple(sorted(set(values))):
245
+ raise ValueError(f"{name} must be unique and canonically sorted")
246
+ if not set(self.cited_contrast_ids).issubset(self.available_contrast_ids):
247
+ raise ValueError("cited_contrast_ids must be available to the reflection")
248
+ if type(self.finite_action_bindings) is not tuple or any(
249
+ type(binding) is not FiniteActionEvidenceBinding
250
+ for binding in self.finite_action_bindings
251
+ ):
252
+ raise TypeError(
253
+ "finite_action_bindings must be an exact tuple of "
254
+ "FiniteActionEvidenceBinding values"
255
+ )
256
+ for binding in self.finite_action_bindings:
257
+ FiniteActionEvidenceBinding.__post_init__(binding)
258
+ binding_contrast_ids = tuple(
259
+ binding.contrast_id for binding in self.finite_action_bindings
260
+ )
261
+ if binding_contrast_ids != tuple(sorted(set(binding_contrast_ids))):
262
+ raise ValueError(
263
+ "finite_action_bindings must have unique canonical contrast order"
264
+ )
265
+ if not set(binding_contrast_ids).issubset(self.cited_contrast_ids):
266
+ raise ValueError(
267
+ "finite action evidence must bind a cited reflection contrast"
268
+ )
269
+ if type(self.empirical_evidence) is not tuple or any(
270
+ type(value) is not EmpiricalEvidenceSnapshot
271
+ for value in self.empirical_evidence
272
+ ):
273
+ raise TypeError(
274
+ "empirical_evidence must be an exact tuple of "
275
+ "EmpiricalEvidenceSnapshot values"
276
+ )
277
+ for snapshot in self.empirical_evidence:
278
+ EmpiricalEvidenceSnapshot.__post_init__(snapshot)
279
+ empirical_contrast_ids = tuple(
280
+ value.contrast_id for value in self.empirical_evidence
281
+ )
282
+ if empirical_contrast_ids != tuple(sorted(set(empirical_contrast_ids))):
283
+ raise ValueError(
284
+ "empirical_evidence must have unique canonical contrast order"
285
+ )
286
+ if self.empirical_evidence and empirical_contrast_ids != (
287
+ self.cited_contrast_ids
288
+ ):
289
+ raise ValueError(
290
+ "empirical evidence must exactly cover every cited contrast"
291
+ )
292
+
293
+ def _identity_record(self) -> dict[str, object]:
294
+ self.__post_init__()
295
+ record: dict[str, object] = {
296
+ "schema_version": 2 if self.empirical_evidence else 1,
297
+ "reflection_call_id": self.reflection_call_id.value,
298
+ "source_operator_invocation_ids": [
299
+ value.value for value in self.source_operator_invocation_ids
300
+ ],
301
+ "source_candidate_ids": [
302
+ value.value for value in self.source_candidate_ids
303
+ ],
304
+ "available_contrast_ids": list(self.available_contrast_ids),
305
+ "cited_contrast_ids": list(self.cited_contrast_ids),
306
+ "finite_action_bindings": [
307
+ binding.to_record() for binding in self.finite_action_bindings
308
+ ],
309
+ }
310
+ if self.empirical_evidence:
311
+ record["empirical_evidence"] = [
312
+ snapshot.to_record() for snapshot in self.empirical_evidence
313
+ ]
314
+ return record
315
+
316
+ @property
317
+ def identity_sha256(self) -> str:
318
+ """Bind citations, source IDs, and exact action-conditioned evidence."""
319
+
320
+ domain = (
321
+ _INSIGHT_EVIDENCE_LINEAGE_V2_DOMAIN
322
+ if self.empirical_evidence
323
+ else _INSIGHT_EVIDENCE_LINEAGE_DOMAIN
324
+ )
325
+ return hashlib.sha256(
326
+ domain + canonical_typed_json_bytes(freeze_json(self._identity_record()))
327
+ ).hexdigest()
328
+
329
+ def to_record(self) -> dict[str, object]:
330
+ """Return deterministic durable provenance without benchmark objects."""
331
+
332
+ return {
333
+ **self._identity_record(),
334
+ "lineage_identity_sha256": self.identity_sha256,
335
+ }
336
+
337
+ @property
338
+ def portfolio_action_evidence(
339
+ self,
340
+ ) -> tuple[FiniteActionEvidenceBinding, ...]:
341
+ """Immutable framework-neutral projection for portfolio card adapters."""
342
+
343
+ self.__post_init__()
344
+ return self.finite_action_bindings
345
+
346
+
347
+ def compose_epistemic_prompt_payload(
348
+ *,
349
+ empirical_evidence: tuple[EmpiricalEvidenceSnapshot, ...],
350
+ hypothesis: FrozenJsonObject,
351
+ ) -> FrozenJsonObject:
352
+ """Keep trusted observations structurally separate from model hypotheses.
353
+
354
+ Benchmark adapters may scrub or otherwise project a hypothesis before this
355
+ call, but they cannot author empirical snapshots. The returned typed JSON
356
+ is suitable for a card prompt payload and makes the epistemic boundary
357
+ machine-inspectable rather than relying on cautionary prose.
358
+ """
359
+
360
+ if type(empirical_evidence) is not tuple or any(
361
+ type(value) is not EmpiricalEvidenceSnapshot for value in empirical_evidence
362
+ ):
363
+ raise TypeError("empirical_evidence must be an exact tuple of snapshots")
364
+ for snapshot in empirical_evidence:
365
+ EmpiricalEvidenceSnapshot.__post_init__(snapshot)
366
+ contrast_ids = tuple(value.contrast_id for value in empirical_evidence)
367
+ if contrast_ids != tuple(sorted(set(contrast_ids))):
368
+ raise ValueError("empirical_evidence must have unique canonical contrast order")
369
+ if type(hypothesis) is not FrozenJsonObject:
370
+ raise TypeError("hypothesis must be an exact FrozenJsonObject")
371
+ if freeze_json(hypothesis) is not hypothesis:
372
+ raise TypeError("hypothesis must already be frozen typed JSON")
373
+ hypothesis_record = thaw_json(hypothesis)
374
+ if hypothesis_record.get("epistemic_status") != "unverified_hypothesis":
375
+ raise ValueError(
376
+ "hypothesis must declare epistemic_status=unverified_hypothesis"
377
+ )
378
+ payload = freeze_json(
379
+ {
380
+ "schema_version": 1,
381
+ "empirical_facts": [
382
+ snapshot.to_record() for snapshot in empirical_evidence
383
+ ],
384
+ "hypothesis": hypothesis_record,
385
+ "interpretation_policy": {
386
+ "empirical_facts_are_observations": True,
387
+ "hypothesis_is_observation": False,
388
+ "mechanism_requires_independent_validation": True,
389
+ },
390
+ }
391
+ )
392
+ if type(payload) is not FrozenJsonObject:
393
+ raise AssertionError("epistemic prompt payload must freeze to an object")
394
+ return payload
395
+
396
+
397
+ def _validate_reflection_action_lineage(
398
+ draft: InsightDraft,
399
+ lineage: InsightEvidenceLineage,
400
+ ) -> None:
401
+ """Reject action recommendations detached from their cited finite evidence."""
402
+
403
+ if type(draft) is not InsightDraft:
404
+ raise TypeError("draft must be an exact InsightDraft")
405
+ InsightDraft.__post_init__(draft)
406
+ if type(lineage) is not InsightEvidenceLineage:
407
+ raise TypeError("lineage must be an exact InsightEvidenceLineage")
408
+ InsightEvidenceLineage.__post_init__(lineage)
409
+ binding_contrast_ids = tuple(
410
+ binding.contrast_id for binding in lineage.finite_action_bindings
411
+ )
412
+ if not set(binding_contrast_ids).issubset(draft.evidence_contrast_ids):
413
+ raise ValueError(
414
+ "finite action evidence must bind a contrast cited by the draft"
415
+ )
416
+ if not draft.recommended_option_ids:
417
+ return
418
+ if draft.evidence_contrast_ids != lineage.cited_contrast_ids:
419
+ raise ValueError(
420
+ "exact-action draft citations differ from their evidence lineage"
421
+ )
422
+ if binding_contrast_ids != lineage.cited_contrast_ids:
423
+ raise ValueError(
424
+ "exact-action reflection requires one finite action binding per citation"
425
+ )
426
+ bound_option_ids = tuple(
427
+ sorted({binding.option_id for binding in lineage.finite_action_bindings})
428
+ )
429
+ if bound_option_ids != draft.recommended_option_ids:
430
+ raise ValueError(
431
+ "exact-action recommendation differs from its evidence action bindings"
432
+ )
433
+
434
+
435
+ @dataclass(frozen=True, slots=True)
436
+ class ReflectedInsightBatchItem:
437
+ """One staged reflection card and its independently verified lineage."""
438
+
439
+ draft: InsightDraft
440
+ evidence_lineage: InsightEvidenceLineage
441
+
442
+ def __post_init__(self) -> None:
443
+ if type(self.draft) is not InsightDraft:
444
+ raise TypeError("draft must be an exact InsightDraft")
445
+ InsightDraft.__post_init__(self.draft)
446
+ if type(self.evidence_lineage) is not InsightEvidenceLineage:
447
+ raise TypeError("evidence_lineage must be an exact InsightEvidenceLineage")
448
+ InsightEvidenceLineage.__post_init__(self.evidence_lineage)
449
+ _validate_reflection_action_lineage(self.draft, self.evidence_lineage)
450
+
451
+
452
+ @dataclass(frozen=True, slots=True)
453
+ class InsightLifecycleTransition:
454
+ """Append-only audit record for an explicit lifecycle change."""
455
+
456
+ sequence: int
457
+ reference: InsightRef
458
+ prior_state: InsightLifecycleState
459
+ new_state: InsightLifecycleState
460
+ reason: str
461
+ supporting_evidence: tuple[str, ...] = ()
462
+
463
+ def __post_init__(self) -> None:
464
+ if type(self.sequence) is not int or self.sequence <= 0:
465
+ raise ValueError("transition sequence must be positive")
466
+ if not isinstance(self.reference, InsightRef):
467
+ raise TypeError("transition reference must be an InsightRef")
468
+ if not isinstance(self.prior_state, InsightLifecycleState) or not isinstance(
469
+ self.new_state, InsightLifecycleState
470
+ ):
471
+ raise TypeError("transition states must be InsightLifecycleState values")
472
+ if (
473
+ type(self.reason) is not str
474
+ or not self.reason.strip()
475
+ or self.reason != self.reason.strip()
476
+ or len(self.reason) > 1_024
477
+ ):
478
+ raise ValueError("transition reason must be canonical non-empty text")
479
+ if type(self.supporting_evidence) is not tuple:
480
+ raise TypeError("supporting_evidence must be an exact tuple")
481
+ if _supporting_evidence(self.supporting_evidence) != self.supporting_evidence:
482
+ raise ValueError("supporting_evidence must use canonical sorted order")
483
+
484
+
485
+ @dataclass(frozen=True, slots=True)
486
+ class InsightLifecycleChangeRequest:
487
+ """One proposed lifecycle change for atomic batch publication."""
488
+
489
+ reference: InsightRef
490
+ new_state: InsightLifecycleState
491
+ reason: str
492
+ supporting_evidence: tuple[str, ...] = ()
493
+
494
+ def __post_init__(self) -> None:
495
+ if type(self.reference) is not InsightRef:
496
+ raise TypeError("reference must be an exact InsightRef")
497
+ InsightRef.__post_init__(self.reference)
498
+ if self.new_state not in {
499
+ InsightLifecycleState.PROMOTED,
500
+ InsightLifecycleState.DEPRECATED,
501
+ }:
502
+ raise ValueError("new_state must be promoted or deprecated")
503
+ if (
504
+ type(self.reason) is not str
505
+ or not self.reason.strip()
506
+ or self.reason != self.reason.strip()
507
+ or len(self.reason) > 1_024
508
+ ):
509
+ raise ValueError("reason must be canonical non-empty text")
510
+ if type(self.supporting_evidence) is not tuple:
511
+ raise TypeError("supporting_evidence must be an exact tuple")
512
+ if _supporting_evidence(self.supporting_evidence) != self.supporting_evidence:
513
+ raise ValueError("supporting_evidence must use canonical sorted order")
514
+
515
+
516
+ def _validate_exact_sorted_ids(
517
+ values: tuple, expected_type: type, *, name: str
518
+ ) -> None:
519
+ if type(values) is not tuple or any(
520
+ type(value) is not expected_type for value in values
521
+ ):
522
+ raise TypeError(
523
+ f"{name} must be an exact tuple of {expected_type.__name__} values"
524
+ )
525
+ for value in values:
526
+ expected_type.__post_init__(value)
527
+ if values != tuple(sorted(set(values))):
528
+ raise ValueError(f"{name} must be unique and canonically sorted")
529
+
530
+
531
+ def _supporting_evidence(values: Sequence[str]) -> tuple[str, ...]:
532
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
533
+ raise TypeError("supporting_evidence must be a sequence of evidence IDs")
534
+ canonical = tuple(values)
535
+ if any(
536
+ type(value) is not str or _EVIDENCE_REFERENCE.fullmatch(value) is None
537
+ for value in canonical
538
+ ):
539
+ raise ValueError("supporting_evidence must contain bounded evidence-ID tokens")
540
+ if len(set(canonical)) != len(canonical):
541
+ raise ValueError("supporting_evidence cannot contain duplicates")
542
+ return tuple(sorted(canonical))
543
+
544
+
545
+ def _claim_key(value: str) -> str:
546
+ return _SPACE.sub(" ", value.strip().casefold())
547
+
548
+
549
+ def _operator_kind_token(value: str) -> str:
550
+ if type(value) is not str or _OPERATOR_KIND_TOKEN.fullmatch(value) is None:
551
+ raise ValueError(
552
+ "operator kinds must be lowercase tokens containing only letters, "
553
+ "digits, and underscores"
554
+ )
555
+ return value
556
+
557
+
558
+ def _applicable_operator_kinds(values: Sequence[str]) -> tuple[str, ...]:
559
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
560
+ raise TypeError("applicable_operator_kinds must be a sequence of strings")
561
+ canonical = tuple(sorted(_operator_kind_token(value) for value in values))
562
+ if len(set(canonical)) != len(canonical):
563
+ raise ValueError("applicable_operator_kinds cannot contain duplicates")
564
+ return canonical
565
+
566
+
567
+ def _canonical_paths(values: Sequence[str], *, name: str) -> tuple[str, ...]:
568
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
569
+ raise TypeError(f"{name} must be a sequence of canonical JSON paths")
570
+ paths = tuple(values)
571
+ if any(
572
+ type(path) is not str or _CANONICAL_JSON_PATH.fullmatch(path) is None
573
+ for path in paths
574
+ ):
575
+ raise ValueError(
576
+ f"{name} must contain exact canonical JSON paths beginning with $."
577
+ )
578
+ if len(set(paths)) != len(paths):
579
+ raise ValueError(f"{name} cannot contain duplicates")
580
+ return tuple(sorted(paths))
581
+
582
+
583
+ def _canonical_factor_capabilities(
584
+ values: Sequence[str] | None,
585
+ ) -> tuple[str, ...] | None:
586
+ if values is None:
587
+ return None
588
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
589
+ raise TypeError("factor_capabilities must be a sequence of strings or None")
590
+ capabilities = tuple(values)
591
+ if any(
592
+ type(value) is not str or _FACTOR_CAPABILITY_TOKEN.fullmatch(value) is None
593
+ for value in capabilities
594
+ ):
595
+ raise ValueError("factor_capabilities must contain canonical capability tokens")
596
+ if len(set(capabilities)) != len(capabilities):
597
+ raise ValueError("factor_capabilities cannot contain duplicates")
598
+ if len(capabilities) > MAX_REFLECTION_SEMANTIC_VOCABULARY_ENTRIES:
599
+ raise ValueError("factor_capabilities exceeds the semantic vocabulary cap")
600
+ return tuple(sorted(capabilities))
601
+
602
+
603
+ def _paths_overlap(first: str, second: str) -> bool:
604
+ """Whether either canonical JSON path is an ancestor of the other."""
605
+
606
+ return (
607
+ first == second
608
+ or first.startswith(second + ".")
609
+ or first.startswith(second + "[")
610
+ or second.startswith(first + ".")
611
+ or second.startswith(first + "[")
612
+ )
613
+
614
+
615
+ @dataclass(frozen=True, slots=True)
616
+ class QuarantineTestAdmissionReceipt:
617
+ """Bank-issued authority for randomized use of exact quarantine versions."""
618
+
619
+ references: tuple[InsightRef, ...]
620
+ operator_kind: str
621
+ editable_paths: tuple[str, ...]
622
+ source_admission_request_sha256: str
623
+ memory_trial_count_cutoff: int
624
+ receipt_sha256: str = field(init=False)
625
+
626
+ def __post_init__(self) -> None:
627
+ if (
628
+ type(self.references) is not tuple
629
+ or not self.references
630
+ or any(type(value) is not InsightRef for value in self.references)
631
+ ):
632
+ raise ValueError("references must be a non-empty exact tuple")
633
+ if self.references != tuple(sorted(set(self.references))):
634
+ raise ValueError("references must be unique and canonical")
635
+ operator = _operator_kind_token(self.operator_kind)
636
+ if operator != self.operator_kind:
637
+ raise ValueError("operator_kind must be canonical")
638
+ paths = _canonical_paths(self.editable_paths, name="editable_paths")
639
+ if paths != self.editable_paths:
640
+ raise ValueError("editable_paths must be canonical")
641
+ if (
642
+ type(self.source_admission_request_sha256) is not str
643
+ or _LOWER_SHA256.fullmatch(self.source_admission_request_sha256) is None
644
+ ):
645
+ raise ValueError(
646
+ "source_admission_request_sha256 must be a lowercase SHA-256 ID"
647
+ )
648
+ if (
649
+ type(self.memory_trial_count_cutoff) is not int
650
+ or self.memory_trial_count_cutoff < 0
651
+ ):
652
+ raise ValueError("memory_trial_count_cutoff must be non-negative")
653
+ object.__setattr__(
654
+ self,
655
+ "receipt_sha256",
656
+ hashlib.sha256(
657
+ _QUARANTINE_TEST_ADMISSION_DOMAIN
658
+ + canonical_typed_json_bytes(freeze_json(self._unsigned_record()))
659
+ ).hexdigest(),
660
+ )
661
+
662
+ def _unsigned_record(self) -> dict[str, object]:
663
+ return {
664
+ "schema_version": 1,
665
+ "references": [
666
+ {
667
+ "insight_id": value.insight_id.value,
668
+ "version": value.version,
669
+ }
670
+ for value in self.references
671
+ ],
672
+ "operator_kind": self.operator_kind,
673
+ "editable_paths": list(self.editable_paths),
674
+ "source_admission_request_sha256": (self.source_admission_request_sha256),
675
+ "memory_trial_count_cutoff": self.memory_trial_count_cutoff,
676
+ "scope": "quarantine_diagnostic_only",
677
+ }
678
+
679
+ def to_record(self) -> dict[str, object]:
680
+ return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
681
+
682
+
683
+ @dataclass(frozen=True, slots=True)
684
+ class InsightMemoryEntry:
685
+ reference: InsightRef
686
+ draft: InsightDraft
687
+ initial_score: float
688
+ applicable_operator_kinds: tuple[str, ...] = ()
689
+ lifecycle_state: InsightLifecycleState = InsightLifecycleState.SEED
690
+ origin: InsightOrigin = InsightOrigin.SEED
691
+ evidence_lineage: InsightEvidenceLineage | None = None
692
+ relations: tuple[InsightRelation, ...] = ()
693
+
694
+ def __post_init__(self) -> None:
695
+ if type(self.reference) is not InsightRef:
696
+ raise TypeError("reference must be an exact InsightRef")
697
+ InsightRef.__post_init__(self.reference)
698
+ if type(self.draft) is not InsightDraft:
699
+ raise TypeError("draft must be an exact InsightDraft")
700
+ InsightDraft.__post_init__(self.draft)
701
+ if type(self.initial_score) is not float or not math.isfinite(
702
+ self.initial_score
703
+ ):
704
+ raise TypeError("initial_score must be a finite canonical float")
705
+ canonical = _applicable_operator_kinds(self.applicable_operator_kinds)
706
+ if canonical != self.applicable_operator_kinds:
707
+ raise ValueError(
708
+ "applicable_operator_kinds must use canonical sorted order"
709
+ )
710
+ if not isinstance(self.lifecycle_state, InsightLifecycleState):
711
+ raise TypeError("lifecycle_state must be an InsightLifecycleState")
712
+ if not isinstance(self.origin, InsightOrigin):
713
+ raise TypeError("origin must be an InsightOrigin")
714
+ if self.lifecycle_state is InsightLifecycleState.SEED:
715
+ if self.origin is not InsightOrigin.SEED:
716
+ raise ValueError(
717
+ "only seed-origin insights may have seed lifecycle state"
718
+ )
719
+ elif self.origin is InsightOrigin.SEED and self.lifecycle_state not in {
720
+ InsightLifecycleState.SEED,
721
+ InsightLifecycleState.DEPRECATED,
722
+ }:
723
+ raise ValueError(
724
+ "seed-origin insights can only remain seed or be deprecated"
725
+ )
726
+ if self.origin is InsightOrigin.REFLECTION:
727
+ if self.evidence_lineage is None:
728
+ raise ValueError("reflection-origin insights require evidence lineage")
729
+ elif self.evidence_lineage is not None:
730
+ raise ValueError("reflection evidence lineage requires reflection origin")
731
+ if self.evidence_lineage is not None:
732
+ if type(self.evidence_lineage) is not InsightEvidenceLineage:
733
+ raise TypeError(
734
+ "evidence_lineage must be an exact InsightEvidenceLineage"
735
+ )
736
+ InsightEvidenceLineage.__post_init__(self.evidence_lineage)
737
+ _validate_reflection_action_lineage(
738
+ self.draft,
739
+ self.evidence_lineage,
740
+ )
741
+ if type(self.relations) is not tuple or any(
742
+ not isinstance(relation, InsightRelation) for relation in self.relations
743
+ ):
744
+ raise TypeError("relations must be a tuple of InsightRelation values")
745
+ relation_keys = tuple(
746
+ (relation.target, relation.kind.value) for relation in self.relations
747
+ )
748
+ if relation_keys != tuple(sorted(set(relation_keys))):
749
+ raise ValueError("relations must be unique and canonically sorted")
750
+
751
+ @property
752
+ def retrievable(self) -> bool:
753
+ return self.lifecycle_state.retrievable
754
+
755
+ def to_record(self) -> dict[str, object]:
756
+ """Return a complete, lossless, JSON-ready durable projection.
757
+
758
+ The draft record carries every immutable model-authored field while
759
+ its hashes make later content drift directly detectable. Scores use
760
+ hexadecimal float text so a write/read cycle cannot change the exact
761
+ binary value. Lifecycle eligibility is emitted explicitly because it
762
+ is operational state, even though it is derivable from the lifecycle
763
+ enum.
764
+ """
765
+
766
+ self.__post_init__()
767
+ return {
768
+ "schema_version": 1,
769
+ "reference": {
770
+ "insight_id": self.reference.insight_id.value,
771
+ "version": self.reference.version,
772
+ },
773
+ "draft": self.draft.content_record(),
774
+ "draft_content_sha256": self.draft.content_sha256,
775
+ "draft_hypothesis_sha256": self.draft.hypothesis_sha256,
776
+ "initial_score_hex": self.initial_score.hex(),
777
+ "applicable_operator_kinds": list(self.applicable_operator_kinds),
778
+ "lifecycle_state": self.lifecycle_state.value,
779
+ "retrievable": self.retrievable,
780
+ "origin": self.origin.value,
781
+ "evidence_lineage": (
782
+ None
783
+ if self.evidence_lineage is None
784
+ else self.evidence_lineage.to_record()
785
+ ),
786
+ "relations": [relation.to_record() for relation in self.relations],
787
+ }
788
+
789
+
790
+ class InsightMemoryBank:
791
+ """Small replaceable policy object for online development experiments."""
792
+
793
+ def __init__(
794
+ self,
795
+ *,
796
+ id_factory,
797
+ exploration_probability: Fraction = Fraction(1, 2),
798
+ shrinkage_effective_sample_size: float = 4.0,
799
+ ) -> None:
800
+ if shrinkage_effective_sample_size <= 0:
801
+ raise ValueError("shrinkage_effective_sample_size must be positive")
802
+ self._ids = id_factory
803
+ self._selector = EpsilonGreedySubsetSelector(exploration_probability)
804
+ self._shrinkage_ess = float(shrinkage_effective_sample_size)
805
+ self._entries: dict[InsightRef, InsightMemoryEntry] = {}
806
+ self._claim_index: dict[str, InsightRef] = {}
807
+ self._scores: dict[tuple[str, InsightRef], float] = {}
808
+ self._trials: list[InsightTrial] = []
809
+ self._transitions: list[InsightLifecycleTransition] = []
810
+ self._quarantine_admissions: dict[
811
+ str,
812
+ QuarantineTestAdmissionReceipt,
813
+ ] = {}
814
+ self._quarantine_admission_by_reference: dict[InsightRef, str] = {}
815
+
816
+ @property
817
+ def entries(self) -> tuple[InsightMemoryEntry, ...]:
818
+ return tuple(self._entries[key] for key in sorted(self._entries))
819
+
820
+ @property
821
+ def trials(self) -> tuple[InsightTrial, ...]:
822
+ return tuple(self._trials)
823
+
824
+ @property
825
+ def transitions(self) -> tuple[InsightLifecycleTransition, ...]:
826
+ return tuple(self._transitions)
827
+
828
+ def entries_for(
829
+ self, references: Sequence[InsightRef]
830
+ ) -> tuple[InsightMemoryEntry, ...]:
831
+ """Return immutable exact-version entries in caller-supplied order."""
832
+
833
+ owned = self._owned_reference_sequence(references, name="references")
834
+ return tuple(self._entries[reference] for reference in owned)
835
+
836
+ def add(
837
+ self,
838
+ draft: InsightDraft,
839
+ *,
840
+ initial_score: float | None = None,
841
+ applicable_operator_kinds: Sequence[str] = (),
842
+ origin: InsightOrigin = InsightOrigin.SEED,
843
+ lifecycle_state: InsightLifecycleState | None = None,
844
+ evidence_lineage: InsightEvidenceLineage | None = None,
845
+ relations: Sequence[InsightRelation] = (),
846
+ ) -> tuple[InsightMemoryEntry, bool]:
847
+ """Add one immutable v1 insight, or return an exact-claim match.
848
+
849
+ Existing callers create eligible seed priors by default. Reflection and
850
+ manual origins are forced to start quarantined; promotion is a separate,
851
+ recorded operation.
852
+ """
853
+
854
+ if type(draft) is not InsightDraft:
855
+ raise TypeError("draft must be an exact InsightDraft")
856
+ InsightDraft.__post_init__(draft)
857
+ _canonical_paths(draft.affected_paths, name="draft.affected_paths")
858
+ operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
859
+ state = self._initial_state(origin, lifecycle_state, evidence_lineage)
860
+ if origin is InsightOrigin.REFLECTION:
861
+ assert evidence_lineage is not None
862
+ _validate_reflection_action_lineage(draft, evidence_lineage)
863
+ canonical_relations = self._canonical_relations(relations)
864
+ if any(
865
+ relation.kind is InsightRelationKind.REVISES
866
+ for relation in canonical_relations
867
+ ):
868
+ raise ValueError("use add_revision to create a revises relationship")
869
+ key = _claim_key(draft.claim)
870
+ existing = self._claim_index.get(key)
871
+ if existing is not None:
872
+ return self._entries[existing], False
873
+ score = 0.0 if initial_score is None else float(initial_score)
874
+ if not math.isfinite(score):
875
+ raise ValueError("initial_score must be finite")
876
+ reference = InsightRef(self._ids.new_insight_id(), 1)
877
+ entry = InsightMemoryEntry(
878
+ reference=reference,
879
+ draft=draft,
880
+ initial_score=score,
881
+ applicable_operator_kinds=operator_kinds,
882
+ lifecycle_state=state,
883
+ origin=origin,
884
+ evidence_lineage=evidence_lineage,
885
+ relations=canonical_relations,
886
+ )
887
+ self._insert(entry)
888
+ return entry, True
889
+
890
+ def add_reflection_batch(
891
+ self,
892
+ items: tuple[ReflectedInsightBatchItem, ...],
893
+ *,
894
+ initial_score: float = 0.0,
895
+ applicable_operator_kinds: Sequence[str] = (),
896
+ ) -> tuple[InsightMemoryEntry, ...]:
897
+ """Publish a complete reflection batch atomically or publish nothing.
898
+
899
+ Reflection shards are independently generated, but a scientific batch
900
+ is useful only when every expected origin yielded a new quarantine
901
+ card. All validation and duplicate checks therefore precede entry
902
+ construction and insertion.
903
+ """
904
+
905
+ if type(items) is not tuple or not items:
906
+ raise ValueError("a reflection batch must be a non-empty exact tuple")
907
+ if any(type(item) is not ReflectedInsightBatchItem for item in items):
908
+ raise TypeError(
909
+ "reflection batch must contain exact ReflectedInsightBatchItem values"
910
+ )
911
+ score = float(initial_score)
912
+ if not math.isfinite(score):
913
+ raise ValueError("initial_score must be finite")
914
+ operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
915
+
916
+ claim_keys: list[str] = []
917
+ for item in items:
918
+ ReflectedInsightBatchItem.__post_init__(item)
919
+ _canonical_paths(item.draft.affected_paths, name="draft.affected_paths")
920
+ self._initial_state(
921
+ InsightOrigin.REFLECTION,
922
+ InsightLifecycleState.QUARANTINED,
923
+ item.evidence_lineage,
924
+ )
925
+ claim_keys.append(_claim_key(item.draft.claim))
926
+ if len(set(claim_keys)) != len(claim_keys):
927
+ raise ValueError("reflection batch contains duplicate normalized claims")
928
+ if any(key in self._claim_index for key in claim_keys):
929
+ raise ValueError("reflection batch collides with an existing claim")
930
+
931
+ staged = tuple(
932
+ InsightMemoryEntry(
933
+ reference=InsightRef(self._ids.new_insight_id(), 1),
934
+ draft=item.draft,
935
+ initial_score=score,
936
+ applicable_operator_kinds=operator_kinds,
937
+ lifecycle_state=InsightLifecycleState.QUARANTINED,
938
+ origin=InsightOrigin.REFLECTION,
939
+ evidence_lineage=item.evidence_lineage,
940
+ )
941
+ for item in items
942
+ )
943
+ staged_references = tuple(entry.reference for entry in staged)
944
+ if len(set(staged_references)) != len(staged_references) or any(
945
+ reference in self._entries for reference in staged_references
946
+ ):
947
+ raise ValueError(
948
+ "reflection batch ID allocation collided before publication"
949
+ )
950
+ for entry in staged:
951
+ self._insert(entry)
952
+ return staged
953
+
954
+ def extend(
955
+ self,
956
+ drafts: Sequence[InsightDraft],
957
+ *,
958
+ initial_score: float | None = None,
959
+ applicable_operator_kinds: Sequence[str] = (),
960
+ origin: InsightOrigin = InsightOrigin.SEED,
961
+ lifecycle_state: InsightLifecycleState | None = None,
962
+ evidence_lineage: InsightEvidenceLineage | None = None,
963
+ relations: Sequence[InsightRelation] = (),
964
+ ) -> tuple[InsightMemoryEntry, ...]:
965
+ added: list[InsightMemoryEntry] = []
966
+ for draft in drafts:
967
+ entry, is_new = self.add(
968
+ draft,
969
+ initial_score=initial_score,
970
+ applicable_operator_kinds=applicable_operator_kinds,
971
+ origin=origin,
972
+ lifecycle_state=lifecycle_state,
973
+ evidence_lineage=evidence_lineage,
974
+ relations=relations,
975
+ )
976
+ if is_new:
977
+ added.append(entry)
978
+ return tuple(added)
979
+
980
+ def add_revision(
981
+ self,
982
+ predecessor: InsightRef,
983
+ draft: InsightDraft,
984
+ *,
985
+ initial_score: float | None = None,
986
+ applicable_operator_kinds: Sequence[str] = (),
987
+ origin: InsightOrigin = InsightOrigin.MANUAL,
988
+ evidence_lineage: InsightEvidenceLineage | None = None,
989
+ relations: Sequence[InsightRelation] = (),
990
+ revision_note: str = "explicit semantic revision",
991
+ ) -> InsightMemoryEntry:
992
+ """Create a quarantined next version without superseding its predecessor.
993
+
994
+ The caller must separately promote this version and deprecate the old
995
+ version after validation. This keeps semantic edits from silently
996
+ inheriting the predecessor's retrieval eligibility or credit.
997
+ """
998
+
999
+ prior = self._owned_entry(predecessor)
1000
+ versions = tuple(
1001
+ reference.version
1002
+ for reference in self._entries
1003
+ if reference.insight_id == predecessor.insight_id
1004
+ )
1005
+ if predecessor.version != max(versions):
1006
+ raise ValueError("predecessor must be the latest version of its insight")
1007
+ if origin is InsightOrigin.SEED:
1008
+ raise ValueError("revisions must use manual or reflection origin")
1009
+ if type(draft) is not InsightDraft:
1010
+ raise TypeError("draft must be an exact InsightDraft")
1011
+ InsightDraft.__post_init__(draft)
1012
+ _canonical_paths(draft.affected_paths, name="draft.affected_paths")
1013
+ operator_kinds = _applicable_operator_kinds(applicable_operator_kinds)
1014
+ state = self._initial_state(origin, None, evidence_lineage)
1015
+ if origin is InsightOrigin.REFLECTION:
1016
+ assert evidence_lineage is not None
1017
+ _validate_reflection_action_lineage(draft, evidence_lineage)
1018
+ user_relations = self._canonical_relations(relations)
1019
+ if any(
1020
+ relation.kind is InsightRelationKind.REVISES for relation in user_relations
1021
+ ):
1022
+ raise ValueError("add_revision declares its predecessor automatically")
1023
+ declared = (
1024
+ *user_relations,
1025
+ InsightRelation(
1026
+ InsightRelationKind.REVISES,
1027
+ predecessor,
1028
+ revision_note,
1029
+ ),
1030
+ )
1031
+ canonical_relations = self._canonical_relations(declared)
1032
+ score = 0.0 if initial_score is None else float(initial_score)
1033
+ if not math.isfinite(score):
1034
+ raise ValueError("initial_score must be finite")
1035
+ reference = InsightRef(predecessor.insight_id, predecessor.version + 1)
1036
+ claim_match = self._claim_index.get(_claim_key(draft.claim))
1037
+ if claim_match is not None and claim_match != predecessor:
1038
+ raise ValueError(
1039
+ "revision claim exactly matches a different insight version"
1040
+ )
1041
+ entry = InsightMemoryEntry(
1042
+ reference=reference,
1043
+ draft=draft,
1044
+ initial_score=score,
1045
+ applicable_operator_kinds=operator_kinds,
1046
+ lifecycle_state=state,
1047
+ origin=origin,
1048
+ evidence_lineage=evidence_lineage,
1049
+ relations=canonical_relations,
1050
+ )
1051
+ self._insert(entry)
1052
+ # Retain the predecessor's exact-claim lookup if the prose changed; for
1053
+ # an unchanged claim, future exact matches resolve to the latest version.
1054
+ if _claim_key(prior.draft.claim) == _claim_key(draft.claim):
1055
+ self._claim_index[_claim_key(draft.claim)] = reference
1056
+ return entry
1057
+
1058
+ def apply_lifecycle_batch(
1059
+ self,
1060
+ requests: tuple[InsightLifecycleChangeRequest, ...],
1061
+ ) -> tuple[InsightMemoryEntry, ...]:
1062
+ """Validate and publish independent lifecycle changes atomically.
1063
+
1064
+ Reflection blocks and generation barriers can resolve several cards at
1065
+ once. Publishing one transition at a time would leave a partially
1066
+ mutated bank if a later card were foreign or in the wrong state. This
1067
+ method stages every updated entry and transition first, then commits in
1068
+ canonical reference order so gather completion order has no meaning.
1069
+ """
1070
+
1071
+ if type(requests) is not tuple or not requests:
1072
+ raise ValueError("lifecycle batch must be a non-empty exact tuple")
1073
+ if any(type(value) is not InsightLifecycleChangeRequest for value in requests):
1074
+ raise TypeError(
1075
+ "lifecycle batch must contain exact InsightLifecycleChangeRequest values"
1076
+ )
1077
+ for value in requests:
1078
+ InsightLifecycleChangeRequest.__post_init__(value)
1079
+ canonical = tuple(sorted(requests, key=lambda value: value.reference))
1080
+ references = tuple(value.reference for value in canonical)
1081
+ if len(set(references)) != len(references):
1082
+ raise ValueError("lifecycle batch cannot repeat an insight reference")
1083
+
1084
+ updated_entries: list[InsightMemoryEntry] = []
1085
+ transitions: list[InsightLifecycleTransition] = []
1086
+ for offset, request in enumerate(canonical, start=1):
1087
+ entry = self._owned_entry(request.reference)
1088
+ if request.new_state is InsightLifecycleState.PROMOTED:
1089
+ if entry.lifecycle_state is not InsightLifecycleState.QUARANTINED:
1090
+ raise ValueError("only a quarantined insight can be promoted")
1091
+ if not request.supporting_evidence:
1092
+ raise ValueError(
1093
+ "promotion requires at least one supporting evidence ID"
1094
+ )
1095
+ elif entry.lifecycle_state is InsightLifecycleState.DEPRECATED:
1096
+ raise ValueError("a deprecated insight cannot transition again")
1097
+ transition = InsightLifecycleTransition(
1098
+ sequence=len(self._transitions) + offset,
1099
+ reference=request.reference,
1100
+ prior_state=entry.lifecycle_state,
1101
+ new_state=request.new_state,
1102
+ reason=request.reason,
1103
+ supporting_evidence=request.supporting_evidence,
1104
+ )
1105
+ transitions.append(transition)
1106
+ updated_entries.append(replace(entry, lifecycle_state=request.new_state))
1107
+
1108
+ for entry in updated_entries:
1109
+ self._entries[entry.reference] = entry
1110
+ self._transitions.extend(transitions)
1111
+ return tuple(updated_entries)
1112
+
1113
+ def promote(
1114
+ self,
1115
+ reference: InsightRef,
1116
+ *,
1117
+ reason: str,
1118
+ supporting_evidence: Sequence[str],
1119
+ ) -> InsightMemoryEntry:
1120
+ """Promote one tested quarantine entry into retrieval eligibility."""
1121
+
1122
+ evidence = _supporting_evidence(supporting_evidence)
1123
+ return self.apply_lifecycle_batch(
1124
+ (
1125
+ InsightLifecycleChangeRequest(
1126
+ reference=reference,
1127
+ new_state=InsightLifecycleState.PROMOTED,
1128
+ reason=reason,
1129
+ supporting_evidence=evidence,
1130
+ ),
1131
+ )
1132
+ )[0]
1133
+
1134
+ def deprecate(
1135
+ self,
1136
+ reference: InsightRef,
1137
+ *,
1138
+ reason: str,
1139
+ supporting_evidence: Sequence[str] = (),
1140
+ ) -> InsightMemoryEntry:
1141
+ """Make a seed, quarantine, or promoted entry permanently ineligible."""
1142
+
1143
+ return self.apply_lifecycle_batch(
1144
+ (
1145
+ InsightLifecycleChangeRequest(
1146
+ reference=reference,
1147
+ new_state=InsightLifecycleState.DEPRECATED,
1148
+ reason=reason,
1149
+ supporting_evidence=_supporting_evidence(supporting_evidence),
1150
+ ),
1151
+ )
1152
+ )[0]
1153
+
1154
+ def _initial_state(
1155
+ self,
1156
+ origin: InsightOrigin,
1157
+ lifecycle_state: InsightLifecycleState | None,
1158
+ evidence_lineage: InsightEvidenceLineage | None,
1159
+ ) -> InsightLifecycleState:
1160
+ if not isinstance(origin, InsightOrigin):
1161
+ raise TypeError("origin must be an InsightOrigin")
1162
+ expected = (
1163
+ InsightLifecycleState.SEED
1164
+ if origin is InsightOrigin.SEED
1165
+ else InsightLifecycleState.QUARANTINED
1166
+ )
1167
+ state = expected if lifecycle_state is None else lifecycle_state
1168
+ if not isinstance(state, InsightLifecycleState):
1169
+ raise TypeError("lifecycle_state must be an InsightLifecycleState")
1170
+ if state is not expected:
1171
+ raise ValueError(
1172
+ f"{origin.value}-origin insights must start in {expected.value} state"
1173
+ )
1174
+ if origin is InsightOrigin.REFLECTION:
1175
+ if evidence_lineage is None:
1176
+ raise ValueError("reflection-origin insights require evidence lineage")
1177
+ if type(evidence_lineage) is not InsightEvidenceLineage:
1178
+ raise TypeError(
1179
+ "evidence_lineage must be an exact InsightEvidenceLineage"
1180
+ )
1181
+ InsightEvidenceLineage.__post_init__(evidence_lineage)
1182
+ elif evidence_lineage is not None:
1183
+ raise ValueError("evidence_lineage is only valid for reflection origin")
1184
+ return state
1185
+
1186
+ def _canonical_relations(
1187
+ self, relations: Sequence[InsightRelation]
1188
+ ) -> tuple[InsightRelation, ...]:
1189
+ if isinstance(relations, (str, bytes)) or not isinstance(relations, Sequence):
1190
+ raise TypeError("relations must be a sequence of InsightRelation values")
1191
+ values = tuple(relations)
1192
+ if any(not isinstance(relation, InsightRelation) for relation in values):
1193
+ raise TypeError("relations must contain InsightRelation values")
1194
+ keys = tuple((relation.target, relation.kind.value) for relation in values)
1195
+ if len(set(keys)) != len(keys):
1196
+ raise ValueError("relations cannot contain duplicate target-kind pairs")
1197
+ foreign = tuple(
1198
+ relation.target
1199
+ for relation in values
1200
+ if relation.target not in self._entries
1201
+ )
1202
+ if foreign:
1203
+ raise ValueError("relations cannot target a foreign insight reference")
1204
+ return tuple(sorted(values, key=lambda item: (item.target, item.kind.value)))
1205
+
1206
+ def _insert(self, entry: InsightMemoryEntry) -> None:
1207
+ if entry.reference in self._entries:
1208
+ raise ValueError("insight reference already exists")
1209
+ self._entries[entry.reference] = entry
1210
+ self._claim_index[_claim_key(entry.draft.claim)] = entry.reference
1211
+
1212
+ def _owned_entry(self, reference: InsightRef) -> InsightMemoryEntry:
1213
+ if not isinstance(reference, InsightRef):
1214
+ raise TypeError("reference must be an InsightRef")
1215
+ try:
1216
+ return self._entries[reference]
1217
+ except KeyError as exc:
1218
+ raise ValueError("reference is foreign to this insight bank") from exc
1219
+
1220
+ def _transition(
1221
+ self,
1222
+ reference: InsightRef,
1223
+ new_state: InsightLifecycleState,
1224
+ *,
1225
+ reason: str,
1226
+ supporting_evidence: tuple[str, ...],
1227
+ ) -> InsightMemoryEntry:
1228
+ entry = self._owned_entry(reference)
1229
+ if new_state is InsightLifecycleState.PROMOTED:
1230
+ if entry.lifecycle_state is not InsightLifecycleState.QUARANTINED:
1231
+ raise ValueError("only a quarantined insight can be promoted")
1232
+ elif new_state is InsightLifecycleState.DEPRECATED:
1233
+ if entry.lifecycle_state is InsightLifecycleState.DEPRECATED:
1234
+ raise ValueError("a deprecated insight cannot transition again")
1235
+ else: # pragma: no cover - private method has two public call sites.
1236
+ raise ValueError("unsupported insight lifecycle transition")
1237
+ transition = InsightLifecycleTransition(
1238
+ sequence=len(self._transitions) + 1,
1239
+ reference=reference,
1240
+ prior_state=entry.lifecycle_state,
1241
+ new_state=new_state,
1242
+ reason=reason,
1243
+ supporting_evidence=supporting_evidence,
1244
+ )
1245
+ updated = replace(entry, lifecycle_state=new_state)
1246
+ self._entries[reference] = updated
1247
+ self._transitions.append(transition)
1248
+ return updated
1249
+
1250
+ def _eligible_reference_subset(
1251
+ self,
1252
+ eligible_references: Sequence[InsightRef] | None,
1253
+ ) -> tuple[InsightRef, ...]:
1254
+ if eligible_references is None:
1255
+ return tuple(
1256
+ reference
1257
+ for reference, entry in sorted(self._entries.items())
1258
+ if entry.retrievable
1259
+ )
1260
+ if isinstance(eligible_references, (str, bytes)) or not isinstance(
1261
+ eligible_references, Sequence
1262
+ ):
1263
+ raise TypeError(
1264
+ "eligible_references must be a sequence of InsightRef values"
1265
+ )
1266
+ references = tuple(eligible_references)
1267
+ if any(not isinstance(reference, InsightRef) for reference in references):
1268
+ raise TypeError("eligible_references must contain InsightRef values")
1269
+ if len(set(references)) != len(references):
1270
+ raise ValueError("eligible_references cannot contain duplicates")
1271
+ foreign = tuple(
1272
+ reference for reference in references if reference not in self._entries
1273
+ )
1274
+ if foreign:
1275
+ raise ValueError("eligible_references contains a foreign insight reference")
1276
+ ineligible = tuple(
1277
+ reference
1278
+ for reference in references
1279
+ if not self._entries[reference].retrievable
1280
+ )
1281
+ if ineligible:
1282
+ raise ValueError(
1283
+ "eligible_references contains a lifecycle-ineligible insight reference"
1284
+ )
1285
+ return tuple(sorted(references))
1286
+
1287
+ def eligible_references(
1288
+ self,
1289
+ *,
1290
+ operator_kind: str,
1291
+ editable_paths: Sequence[str] | None = None,
1292
+ consumer_scope: ReflectionConsumerScope | None = None,
1293
+ factor_capabilities: Sequence[str] | None = None,
1294
+ ) -> tuple[InsightRef, ...]:
1295
+ """Return deterministic structurally and semantically compatible cards.
1296
+
1297
+ Applicability is deliberately structural: natural-language triggers are
1298
+ not interpreted. An empty operator-kind tuple on an entry means that the
1299
+ insight applies to every valid operator token. The v3 semantic filters
1300
+ are opt-in so historical callers retain their exact behavior. When a
1301
+ filter is supplied, semantic-v3 cards fail closed against their declared
1302
+ consumer scopes and required factor capabilities; legacy cards remain
1303
+ governed by their historical operator/path contract.
1304
+ """
1305
+
1306
+ operator = _operator_kind_token(operator_kind)
1307
+ paths = (
1308
+ None
1309
+ if editable_paths is None
1310
+ else _canonical_paths(editable_paths, name="editable_paths")
1311
+ )
1312
+ if consumer_scope is not None and type(consumer_scope) is not (
1313
+ ReflectionConsumerScope
1314
+ ):
1315
+ raise TypeError(
1316
+ "consumer_scope must be an exact ReflectionConsumerScope or None"
1317
+ )
1318
+ capabilities = _canonical_factor_capabilities(factor_capabilities)
1319
+ return tuple(
1320
+ reference
1321
+ for reference, entry in sorted(self._entries.items())
1322
+ if entry.retrievable
1323
+ and self._structurally_applicable(entry, operator, paths)
1324
+ and self._semantically_applicable(
1325
+ entry,
1326
+ consumer_scope=consumer_scope,
1327
+ factor_capabilities=capabilities,
1328
+ )
1329
+ )
1330
+
1331
+ @staticmethod
1332
+ def _semantically_applicable(
1333
+ entry: InsightMemoryEntry,
1334
+ *,
1335
+ consumer_scope: ReflectionConsumerScope | None,
1336
+ factor_capabilities: tuple[str, ...] | None,
1337
+ ) -> bool:
1338
+ draft = entry.draft
1339
+ if not draft.has_semantic_contract:
1340
+ return True
1341
+ return (consumer_scope is None or consumer_scope in draft.consumer_scopes) and (
1342
+ factor_capabilities is None
1343
+ or set(draft.factor_capabilities).issubset(factor_capabilities)
1344
+ )
1345
+
1346
+ @staticmethod
1347
+ def _structurally_applicable(
1348
+ entry: InsightMemoryEntry,
1349
+ operator_kind: str,
1350
+ editable_paths: tuple[str, ...] | None,
1351
+ ) -> bool:
1352
+ return (
1353
+ not entry.applicable_operator_kinds
1354
+ or operator_kind in entry.applicable_operator_kinds
1355
+ ) and (
1356
+ editable_paths is None
1357
+ or any(
1358
+ _paths_overlap(editable, affected)
1359
+ for editable in editable_paths
1360
+ for affected in entry.draft.affected_paths
1361
+ )
1362
+ )
1363
+
1364
+ def validate_quarantine_test_assignment(
1365
+ self,
1366
+ references: Sequence[InsightRef],
1367
+ *,
1368
+ operator_kind: str,
1369
+ editable_paths: Sequence[str] | None = None,
1370
+ ) -> tuple[InsightRef, ...]:
1371
+ """Validate exact quarantined versions for one isolated test invocation.
1372
+
1373
+ This is deliberately separate from :meth:`eligible_references`: a
1374
+ quarantined hypothesis may be named for a controlled test, but remains
1375
+ unavailable to normal retrieval. The returned tuple is an immutable
1376
+ value snapshot in caller-supplied order; no entry or mutable bank state
1377
+ is exposed.
1378
+ """
1379
+
1380
+ assigned = self._owned_reference_sequence(
1381
+ references,
1382
+ name="quarantine_test_insights",
1383
+ )
1384
+ operator = _operator_kind_token(operator_kind)
1385
+ paths = (
1386
+ None
1387
+ if editable_paths is None
1388
+ else _canonical_paths(editable_paths, name="editable_paths")
1389
+ )
1390
+ wrong_lifecycle = tuple(
1391
+ reference
1392
+ for reference in assigned
1393
+ if self._entries[reference].lifecycle_state
1394
+ is not InsightLifecycleState.QUARANTINED
1395
+ )
1396
+ if wrong_lifecycle:
1397
+ raise ValueError(
1398
+ "quarantine_test_insights must contain only quarantined insight "
1399
+ "references"
1400
+ )
1401
+ inapplicable = tuple(
1402
+ reference
1403
+ for reference in assigned
1404
+ if not self._structurally_applicable(
1405
+ self._entries[reference], operator, paths
1406
+ )
1407
+ )
1408
+ if inapplicable:
1409
+ raise QuarantineAssignmentStructuralError(
1410
+ "quarantine_test_insights contains a structurally inapplicable "
1411
+ "insight reference"
1412
+ )
1413
+ return assigned
1414
+
1415
+ def admit_quarantine_test_assignment(
1416
+ self,
1417
+ references: Sequence[InsightRef],
1418
+ *,
1419
+ operator_kind: str,
1420
+ source_admission_request_sha256: str,
1421
+ editable_paths: Sequence[str] | None = None,
1422
+ ) -> QuarantineTestAdmissionReceipt:
1423
+ """Issue one exact diagnostic authority and retain it for later joins."""
1424
+
1425
+ assigned = self.validate_quarantine_test_assignment(
1426
+ references,
1427
+ operator_kind=operator_kind,
1428
+ editable_paths=editable_paths,
1429
+ )
1430
+ if any(
1431
+ reference in self._quarantine_admission_by_reference
1432
+ for reference in assigned
1433
+ ):
1434
+ raise ValueError("a quarantine insight was already admitted for testing")
1435
+ receipt = QuarantineTestAdmissionReceipt(
1436
+ references=tuple(sorted(assigned)),
1437
+ operator_kind=_operator_kind_token(operator_kind),
1438
+ editable_paths=(
1439
+ ()
1440
+ if editable_paths is None
1441
+ else _canonical_paths(editable_paths, name="editable_paths")
1442
+ ),
1443
+ source_admission_request_sha256=source_admission_request_sha256,
1444
+ memory_trial_count_cutoff=len(self._trials),
1445
+ )
1446
+ if receipt.receipt_sha256 in self._quarantine_admissions:
1447
+ raise ValueError("quarantine admission receipt identity collided")
1448
+ self._quarantine_admissions[receipt.receipt_sha256] = receipt
1449
+ for reference in receipt.references:
1450
+ self._quarantine_admission_by_reference[reference] = receipt.receipt_sha256
1451
+ return receipt
1452
+
1453
+ def validate_quarantine_test_admission(
1454
+ self,
1455
+ receipt: QuarantineTestAdmissionReceipt,
1456
+ *,
1457
+ eligible_references: Sequence[InsightRef],
1458
+ subset_authorization_sha256: str | None = None,
1459
+ ) -> tuple[InsightRef, ...]:
1460
+ """Revalidate bank issuance, lifecycle, and the randomized catalog.
1461
+
1462
+ By default the still-active admitted cohort must equal the assignment
1463
+ cohort exactly. A prospectively selected subset is permitted only
1464
+ when its external policy receipt is explicitly bound into memory
1465
+ credit. This preserves the original anti-cherry-picking default while
1466
+ allowing parent-local positivity filtering to happen before dispatch.
1467
+ """
1468
+
1469
+ if type(receipt) is not QuarantineTestAdmissionReceipt:
1470
+ raise TypeError("receipt must be an exact QuarantineTestAdmissionReceipt")
1471
+ QuarantineTestAdmissionReceipt.__post_init__(receipt)
1472
+ issued = self._quarantine_admissions.get(receipt.receipt_sha256)
1473
+ if issued != receipt:
1474
+ raise ValueError("quarantine admission was not issued by this memory bank")
1475
+ if subset_authorization_sha256 is not None:
1476
+ require_sha256(
1477
+ subset_authorization_sha256,
1478
+ "subset_authorization_sha256",
1479
+ )
1480
+ eligible = self._owned_reference_sequence(
1481
+ eligible_references,
1482
+ name="eligible_references",
1483
+ )
1484
+ quarantined = tuple(
1485
+ sorted(
1486
+ reference
1487
+ for reference in eligible
1488
+ if self._entries[reference].lifecycle_state
1489
+ is InsightLifecycleState.QUARANTINED
1490
+ )
1491
+ )
1492
+ currently_quarantined_admitted = tuple(
1493
+ reference
1494
+ for reference in receipt.references
1495
+ if self._entries[reference].lifecycle_state
1496
+ is InsightLifecycleState.QUARANTINED
1497
+ )
1498
+ if subset_authorization_sha256 is None:
1499
+ if quarantined != currently_quarantined_admitted:
1500
+ raise ValueError(
1501
+ "eligible quarantine cards differ from the still-active references "
1502
+ "of the issued admission"
1503
+ )
1504
+ elif not quarantined or not set(quarantined).issubset(
1505
+ currently_quarantined_admitted
1506
+ ):
1507
+ raise ValueError(
1508
+ "authorized quarantine subset must be a non-empty subset of the "
1509
+ "still-active issued admission"
1510
+ )
1511
+ if any(
1512
+ self._quarantine_admission_by_reference.get(reference)
1513
+ != receipt.receipt_sha256
1514
+ for reference in quarantined
1515
+ ):
1516
+ raise ValueError("quarantine admission reference index changed")
1517
+ return quarantined
1518
+
1519
+ def quarantine_test_admission_receipt(
1520
+ self,
1521
+ receipt_sha256: str,
1522
+ ) -> QuarantineTestAdmissionReceipt:
1523
+ """Return one immutable bank-issued diagnostic authority by exact hash."""
1524
+
1525
+ if (
1526
+ type(receipt_sha256) is not str
1527
+ or _LOWER_SHA256.fullmatch(receipt_sha256) is None
1528
+ ):
1529
+ raise ValueError("receipt_sha256 must be a lowercase SHA-256 ID")
1530
+ try:
1531
+ return self._quarantine_admissions[receipt_sha256]
1532
+ except KeyError as exc:
1533
+ raise ValueError(
1534
+ "quarantine admission was not issued by this memory bank"
1535
+ ) from exc
1536
+
1537
+ def _owned_reference_sequence(
1538
+ self,
1539
+ references: Sequence[InsightRef],
1540
+ *,
1541
+ name: str,
1542
+ ) -> tuple[InsightRef, ...]:
1543
+ if isinstance(references, (str, bytes)) or not isinstance(references, Sequence):
1544
+ raise TypeError(f"{name} must be a sequence of InsightRef values")
1545
+ values = tuple(references)
1546
+ if any(not isinstance(reference, InsightRef) for reference in values):
1547
+ raise TypeError(f"{name} must contain InsightRef values")
1548
+ if len(set(values)) != len(values):
1549
+ raise ValueError(f"{name} cannot contain duplicates")
1550
+ if any(reference not in self._entries for reference in values):
1551
+ raise ValueError(f"{name} contains a foreign insight reference")
1552
+ return values
1553
+
1554
+ def score_snapshot(
1555
+ self,
1556
+ context_hash: str,
1557
+ *,
1558
+ eligible_references: Sequence[InsightRef] | None = None,
1559
+ ) -> Mapping[InsightRef, float]:
1560
+ eligible = self._eligible_reference_subset(eligible_references)
1561
+ return {
1562
+ reference: self._scores.get(
1563
+ (context_hash, reference), self._entries[reference].initial_score
1564
+ )
1565
+ for reference in eligible
1566
+ }
1567
+
1568
+ def select(
1569
+ self,
1570
+ *,
1571
+ context_hash: str,
1572
+ subset_size: int,
1573
+ rng,
1574
+ exploration_probability: Fraction | None = None,
1575
+ score_context_hash: str | None = None,
1576
+ eligible_references: Sequence[InsightRef] | None = None,
1577
+ ) -> InsightSelectionDecision:
1578
+ eligible = self._eligible_reference_subset(eligible_references)
1579
+ selector = (
1580
+ self._selector
1581
+ if exploration_probability is None
1582
+ else EpsilonGreedySubsetSelector(exploration_probability)
1583
+ )
1584
+ return selector.select(
1585
+ context_hash=context_hash,
1586
+ eligible=eligible,
1587
+ scores=self.score_snapshot(
1588
+ score_context_hash or context_hash,
1589
+ eligible_references=eligible,
1590
+ ),
1591
+ subset_size=min(subset_size, len(eligible)),
1592
+ rng=rng,
1593
+ )
1594
+
1595
+ def prompt_records(
1596
+ self, references: Sequence[InsightRef]
1597
+ ) -> tuple[dict[str, object], ...]:
1598
+ """Return detached, JSON-ready records for exact owned references.
1599
+
1600
+ Every mapping and nested container is freshly allocated. Mutating a
1601
+ returned prompt record therefore cannot mutate lifecycle, provenance,
1602
+ scores, or any other state held by this bank.
1603
+ """
1604
+
1605
+ owned = self._owned_reference_sequence(references, name="references")
1606
+ records = []
1607
+ for reference in owned:
1608
+ entry = self._entries[reference]
1609
+ record: dict[str, object] = {
1610
+ "insight_id": reference.insight_id.value,
1611
+ "version": reference.version,
1612
+ "claim": entry.draft.claim,
1613
+ "trigger": entry.draft.trigger,
1614
+ "mechanism": entry.draft.mechanism,
1615
+ "evidence_summary": entry.draft.evidence_summary,
1616
+ "affected_paths": list(entry.draft.affected_paths),
1617
+ "confidence_at_creation": entry.draft.confidence,
1618
+ "lifecycle_state": entry.lifecycle_state.value,
1619
+ "origin": entry.origin.value,
1620
+ "semantic_relations": [
1621
+ {
1622
+ "kind": relation.kind.value,
1623
+ "target_insight_id": relation.target.insight_id.value,
1624
+ "target_version": relation.target.version,
1625
+ "note": relation.note,
1626
+ }
1627
+ for relation in entry.relations
1628
+ ],
1629
+ }
1630
+ intervention = entry.draft.intervention_record()
1631
+ if intervention is not None:
1632
+ record.update(intervention)
1633
+ semantic = entry.draft.semantic_record()
1634
+ if semantic is not None:
1635
+ record.update(semantic)
1636
+ records.append(record)
1637
+ return tuple(records)
1638
+
1639
+ def selected_prompt_records(
1640
+ self, decision: InsightSelectionDecision
1641
+ ) -> tuple[dict[str, object], ...]:
1642
+ """Backward-compatible prompt projection for retrieval decisions."""
1643
+
1644
+ return self.prompt_records(decision.selected)
1645
+
1646
+ def record_trial(
1647
+ self,
1648
+ *,
1649
+ credit_unit_id: OperatorInvocationId,
1650
+ candidate_ids: tuple[CandidateId, ...],
1651
+ reward_definition_hash: str,
1652
+ decision: InsightSelectionDecision,
1653
+ reward: float,
1654
+ ) -> InsightTrial:
1655
+ trial = InsightTrial(
1656
+ credit_unit_id=credit_unit_id,
1657
+ candidate_ids=candidate_ids,
1658
+ reward_definition_hash=reward_definition_hash,
1659
+ decision=decision,
1660
+ reward=float(reward),
1661
+ )
1662
+ return self.record_trials_batch((trial,))[0]
1663
+
1664
+ def _prepare_trials_batch(
1665
+ self,
1666
+ trials: tuple[InsightTrial, ...],
1667
+ ) -> tuple[tuple[InsightTrial, ...], dict[tuple[str, InsightRef], float]]:
1668
+ if type(trials) is not tuple or not trials:
1669
+ raise ValueError("trials must be a non-empty exact tuple")
1670
+ if any(type(trial) is not InsightTrial for trial in trials):
1671
+ raise TypeError("trials must contain exact InsightTrial values")
1672
+ for trial in trials:
1673
+ InsightTrial.__post_init__(trial)
1674
+ self.entries_for(trial.decision.eligible)
1675
+
1676
+ canonical = tuple(sorted(trials, key=lambda trial: trial.credit_unit_id.value))
1677
+ proposed = (*self._trials, *canonical)
1678
+ contexts = tuple(sorted({trial.decision.context_hash for trial in canonical}))
1679
+
1680
+ # Estimator validation also rejects duplicate credit/candidate units
1681
+ # across the existing bank and the complete incoming batch. Compute
1682
+ # all score mutations before touching either mutable collection.
1683
+ for trial in canonical:
1684
+ for reference in trial.decision.eligible:
1685
+ estimate_marginal_effect(
1686
+ proposed,
1687
+ reference,
1688
+ context_hash=trial.decision.context_hash,
1689
+ )
1690
+ score_updates: dict[tuple[str, InsightRef], float] = {}
1691
+ for context_hash in contexts:
1692
+ score_updates.update(self._context_score_updates(proposed, context_hash))
1693
+
1694
+ return canonical, score_updates
1695
+
1696
+ def preview_trials_batch(
1697
+ self,
1698
+ trials: tuple[InsightTrial, ...],
1699
+ ) -> tuple[InsightTrial, ...]:
1700
+ """Validate and canonicalize a prospective batch without mutation."""
1701
+
1702
+ canonical, _ = self._prepare_trials_batch(trials)
1703
+ return canonical
1704
+
1705
+ def record_trials_batch(
1706
+ self,
1707
+ trials: tuple[InsightTrial, ...],
1708
+ ) -> tuple[InsightTrial, ...]:
1709
+ """Atomically publish a canonical batch of completed credit units.
1710
+
1711
+ Concurrent campaign arms must not update retrieval scores as each arm
1712
+ happens to finish. This boundary validates the complete proposed
1713
+ state, computes every affected score against that same state, and only
1714
+ then publishes trials in stable credit-unit order. The returned tuple
1715
+ is therefore independent of caller/gather completion order.
1716
+ """
1717
+
1718
+ canonical, score_updates = self._prepare_trials_batch(trials)
1719
+
1720
+ self._trials.extend(canonical)
1721
+ self._scores.update(score_updates)
1722
+ return canonical
1723
+
1724
+ def _refresh_context_scores(self, context_hash: str) -> None:
1725
+ self._scores.update(
1726
+ self._context_score_updates(tuple(self._trials), context_hash)
1727
+ )
1728
+
1729
+ def _context_score_updates(
1730
+ self,
1731
+ trials: Sequence[InsightTrial],
1732
+ context_hash: str,
1733
+ ) -> dict[tuple[str, InsightRef], float]:
1734
+ updates: dict[tuple[str, InsightRef], float] = {}
1735
+ for reference, entry in self._entries.items():
1736
+ estimate = estimate_marginal_effect(
1737
+ trials,
1738
+ reference,
1739
+ context_hash=context_hash,
1740
+ )
1741
+ if not estimate.identified:
1742
+ continue
1743
+ support = min(
1744
+ estimate.treated_effective_sample_size,
1745
+ estimate.control_effective_sample_size,
1746
+ )
1747
+ shrinkage = support / (support + self._shrinkage_ess)
1748
+ updates[(context_hash, reference)] = (
1749
+ entry.initial_score + shrinkage * float(estimate.effect)
1750
+ )
1751
+ return updates
1752
+
1753
+ def score_evidence(self, context_hash: str) -> tuple[dict[str, object], ...]:
1754
+ evidence = []
1755
+ for reference, entry in sorted(self._entries.items()):
1756
+ estimate = estimate_marginal_effect(
1757
+ self._trials,
1758
+ reference,
1759
+ context_hash=context_hash,
1760
+ )
1761
+ evidence.append(
1762
+ {
1763
+ "insight_id": reference.insight_id.value,
1764
+ "version": reference.version,
1765
+ "lifecycle_state": entry.lifecycle_state.value,
1766
+ "retrievable": entry.retrievable,
1767
+ "retrieval_score": self._scores.get(
1768
+ (context_hash, reference), entry.initial_score
1769
+ ),
1770
+ "effect": estimate.effect,
1771
+ "treated_trials": estimate.treated_trials,
1772
+ "control_trials": estimate.control_trials,
1773
+ "treated_ess": estimate.treated_effective_sample_size,
1774
+ "control_ess": estimate.control_effective_sample_size,
1775
+ "identified": estimate.identified,
1776
+ }
1777
+ )
1778
+ return tuple(evidence)
1779
+
1780
+
1781
+ def context_stratum_hash(*, problem_id: str, operator_kind: str, phase: str) -> str:
1782
+ payload = f"{problem_id}\x00{operator_kind}\x00{phase}".encode(
1783
+ "utf-8", errors="strict"
1784
+ )
1785
+ return hashlib.sha256(b"agent-evolve:insight-context:v1\x00" + payload).hexdigest()
1786
+
1787
+
1788
+ __all__ = [
1789
+ "EmpiricalEvidenceSnapshot",
1790
+ "InsightEvidenceLineage",
1791
+ "InsightLifecycleChangeRequest",
1792
+ "InsightLifecycleState",
1793
+ "InsightLifecycleTransition",
1794
+ "InsightMemoryBank",
1795
+ "InsightMemoryEntry",
1796
+ "InsightOrigin",
1797
+ "InsightRelation",
1798
+ "InsightRelationKind",
1799
+ "QuarantineAssignmentStructuralError",
1800
+ "QuarantineTestAdmissionReceipt",
1801
+ "ReflectedInsightBatchItem",
1802
+ "compose_epistemic_prompt_payload",
1803
+ "context_stratum_hash",
1804
+ ]