arbiter-engine 0.2.6__tar.gz → 0.2.7__tar.gz

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 (332) hide show
  1. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CHANGELOG.md +36 -1
  2. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CITATION.cff +2 -2
  3. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/PKG-INFO +4 -4
  4. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/README.md +3 -3
  5. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/STANCE.md +22 -0
  6. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/api.py +90 -4
  7. arbiter_engine-0.2.7/arbiter_engine/inference/hypothesis.py +182 -0
  8. arbiter_engine-0.2.7/arbiter_engine/producers/__init__.py +25 -0
  9. arbiter_engine-0.2.7/arbiter_engine/producers/baseline_learner.py +163 -0
  10. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/surprises.py +100 -0
  11. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/transition_learner.py +7 -0
  12. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/pyproject.toml +1 -1
  13. arbiter_engine-0.2.7/tests/test_a_finding_says_where_to_look_next.py +160 -0
  14. arbiter_engine-0.2.7/tests/test_a_proposal_says_what_adopting_it_would_have_caught.py +192 -0
  15. arbiter_engine-0.2.7/tests/test_a_vertical_may_adopt_and_this_engine_may_not.py +224 -0
  16. arbiter_engine-0.2.7/tests/test_the_engine_ships_an_opponent_worth_beating.py +166 -0
  17. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_stance_page_says_what_no_other_page_says.py +37 -0
  18. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.gitattributes +0 -0
  19. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/PUBLISH_FROM_CI +0 -0
  21. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/release.yml +0 -0
  23. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/tests.yml +0 -0
  24. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/verify-published.yml +0 -0
  25. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/verify-tag-artifact.yml +0 -0
  26. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.gitignore +0 -0
  27. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/AI_ATTRIBUTION.md +0 -0
  28. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/AUTHORS.md +0 -0
  29. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/BRIDGES.md +0 -0
  30. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CODE_OF_CONDUCT.md +0 -0
  31. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/COMPATIBILITY.md +0 -0
  32. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CONTRIBUTING.md +0 -0
  33. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/LICENSE +0 -0
  34. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/MODELING.md +0 -0
  35. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/NOTICE +0 -0
  36. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/PRIVACY.md +0 -0
  37. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/ROADMAP.md +0 -0
  38. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/SECURITY.md +0 -0
  39. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/SUPPORT.md +0 -0
  40. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/TRADEMARK.md +0 -0
  41. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/__init__.py +0 -0
  42. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/assumptions.py +0 -0
  43. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/axiom_thresholds.py +0 -0
  44. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/__init__.py +0 -0
  45. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/discovery.py +0 -0
  46. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/granger.py +0 -0
  47. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/leadlag.py +0 -0
  48. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/clock.py +0 -0
  49. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/__init__.py +0 -0
  50. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/indicator.py +0 -0
  51. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/parser.py +0 -0
  52. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/envelope.py +0 -0
  53. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/battery_pack.yaml +0 -0
  54. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/factory_line.yaml +0 -0
  55. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/kubernetes_node.yaml +0 -0
  56. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/margin_book.yaml +0 -0
  57. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/pump_tank_dynamics.yaml +0 -0
  58. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/pump_tank_planning.yaml +0 -0
  59. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/substation_feeder.yaml +0 -0
  60. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/substation_feeder_surprises.yaml +0 -0
  61. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/water_tank.yaml +0 -0
  62. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/fire_frequency.py +0 -0
  63. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/__init__.py +0 -0
  64. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/contract.py +0 -0
  65. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/envelope.py +0 -0
  66. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/ingest.py +0 -0
  67. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/monitor.py +0 -0
  68. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/shadow.py +0 -0
  69. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/__init__.py +0 -0
  70. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/calendar.py +0 -0
  71. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation.py +0 -0
  72. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation_production.py +0 -0
  73. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation_source_wiring.py +0 -0
  74. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/readiness.py +0 -0
  75. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/sqlite_store.py +0 -0
  76. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/__init__.py +0 -0
  77. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/causal.py +0 -0
  78. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/runner.py +0 -0
  79. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/ve.py +0 -0
  80. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/interfaces.py +0 -0
  81. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/mcp/__init__.py +0 -0
  82. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/mcp/server.py +0 -0
  83. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/__init__.py +0 -0
  84. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axiom_verdicts_production.py +0 -0
  85. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/__init__.py +0 -0
  86. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/boundedness.py +0 -0
  87. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/connectivity.py +0 -0
  88. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/conservation.py +0 -0
  89. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/consistency.py +0 -0
  90. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/extensions.py +0 -0
  91. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/homeostasis.py +0 -0
  92. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/monotonicity.py +0 -0
  93. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/peers.py +0 -0
  94. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/responsiveness.py +0 -0
  95. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/roles.py +0 -0
  96. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/stability.py +0 -0
  97. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/domain_loader.py +0 -0
  98. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/entail.py +0 -0
  99. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/loader.py +0 -0
  100. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/prediction_production.py +0 -0
  101. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/reasoner.py +0 -0
  102. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/schemas/health_meta_ontology.ttl +0 -0
  103. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/__init__.py +0 -0
  104. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/projector.py +0 -0
  105. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/runner.py +0 -0
  106. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/__init__.py +0 -0
  107. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/impact_estimator.py +0 -0
  108. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/lp_confidence.py +0 -0
  109. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/mcts_root_cause.py +0 -0
  110. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/root_cause.py +0 -0
  111. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/weight_learner.py +0 -0
  112. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/rca/__init__.py +0 -0
  113. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/rca/greedy_set_cover.py +0 -0
  114. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/replay.py +0 -0
  115. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/__init__.py +0 -0
  116. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/predict_vs_mirror.py +0 -0
  117. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/sqlite_ledger.py +0 -0
  118. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/schema/envelope.schema.json +0 -0
  119. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/__init__.py +0 -0
  120. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/benchmark_check.py +0 -0
  121. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/surprise_benchmark.py +0 -0
  122. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/subenvelope.py +0 -0
  123. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/__init__.py +0 -0
  124. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/temporal_edge.py +0 -0
  125. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/trend_projection.py +0 -0
  126. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/__init__.py +0 -0
  127. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/action_clears_problem.py +0 -0
  128. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/actions.py +0 -0
  129. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/builder.py +0 -0
  130. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/gap.py +0 -0
  131. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/hypothesis_generator.py +0 -0
  132. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/hypothesis_production.py +0 -0
  133. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/kernel_pipeline_executor.py +0 -0
  134. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/monte_carlo_predictor.py +0 -0
  135. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/optimization_production.py +0 -0
  136. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/pipeline_production.py +0 -0
  137. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/planner.py +0 -0
  138. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/rollout.py +0 -0
  139. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/topology.py +0 -0
  140. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/topology_optimizer.py +0 -0
  141. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/traverser.py +0 -0
  142. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/traverser_production.py +0 -0
  143. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/types.py +0 -0
  144. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/README.md +0 -0
  145. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/alpha1_evidence_pack.md +0 -0
  146. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/customer_deployment_runbook.md +0 -0
  147. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/extended_fault_scenarios.md +0 -0
  148. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/l5_surprise_synthesis.md +0 -0
  149. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/observability_handoff_guide.md +0 -0
  150. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/tech_brief.md +0 -0
  151. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/use-case-catalogue.md +0 -0
  152. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/battery_pack.yaml +0 -0
  153. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/factory_line.yaml +0 -0
  154. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/kubernetes_node.yaml +0 -0
  155. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/margin_book.yaml +0 -0
  156. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/pump_tank_dynamics.yaml +0 -0
  157. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/pump_tank_planning.yaml +0 -0
  158. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/substation_feeder.yaml +0 -0
  159. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/substation_feeder_surprises.yaml +0 -0
  160. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/water_tank.yaml +0 -0
  161. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/schema/envelope.schema.json +0 -0
  162. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/conftest.py +0 -0
  163. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_balance_may_cross_an_entity_boundary.py +0 -0
  164. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_bound_can_belong_to_one_instance.py +0 -0
  165. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_calibration_figure_names_its_population.py +0 -0
  166. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_calibration_says_how_many_trajectories_it_saw.py +0 -0
  167. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_chain_of_lags_says_how_it_was_composed.py +0 -0
  168. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_changelog_section_says_whether_you_can_install_it.py +0 -0
  169. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_check_that_cannot_run_says_so.py +0 -0
  170. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_check_that_declines_is_heard.py +0 -0
  171. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_checker_asks_no_entity_type.py +0 -0
  172. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_clean_rollout_still_counts_what_it_evaluated.py +0 -0
  173. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_companion_beside_the_models_is_not_one.py +0 -0
  174. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_correct_gain_is_not_contradicted_by_a_lagged_series.py +0 -0
  175. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_block_is_checked_too.py +0 -0
  176. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_is_graded_on_its_own_projections.py +0 -0
  177. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_learns_how_its_projections_fared.py +0 -0
  178. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_bad_state_is_not_silent.py +0 -0
  179. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_calendar_is_actually_read.py +0 -0
  180. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_edge_reaches_the_published_surface.py +0 -0
  181. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_floor_is_checked_on_every_path.py +0 -0
  182. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_gain_moves_the_downstream_value.py +0 -0
  183. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_number_is_the_first_breach.py +0 -0
  184. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_offset_waits_for_the_delay.py +0 -0
  185. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_source_matches_the_guide.py +0 -0
  186. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_spread_reaches_the_value.py +0 -0
  187. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_decline_states_the_number_it_computed.py +0 -0
  188. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_derived_indicator_is_computed_not_fed.py +0 -0
  189. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_discipline_reports_its_own_denominator.py +0 -0
  190. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_dropped_declaration_is_not_silent.py +0 -0
  191. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_filed_source_says_who_is_not_a_producer.py +0 -0
  192. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_batch_is_filed_or_accounted_for.py +0 -0
  193. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_check_that_refused_says_so_out_loud.py +0 -0
  194. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_graded_at_its_horizon.py +0 -0
  195. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_measured_against_a_random_walk.py +0 -0
  196. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_scored_not_just_stored.py +0 -0
  197. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_keeps_its_doubt.py +0 -0
  198. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_keeps_its_doubt_when_its_median_stands_still.py +0 -0
  199. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_states_its_own_distribution.py +0 -0
  200. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_widens_and_says_what_it_assumed.py +0 -0
  201. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecaster_is_judged_by_the_same_eight_axioms.py +0 -0
  202. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_freeze_is_found_inside_its_window.py +0 -0
  203. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_latency_without_a_threshold_says_so.py +0 -0
  204. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_lazy_import_did_not_move_a_name.py +0 -0
  205. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_lead_lag_p_value_means_what_it_says.py +0 -0
  206. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_learned_gain_is_a_proposal.py +0 -0
  207. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_margin_is_measured_against_every_line.py +0 -0
  208. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_nested_block_is_checked_too.py +0 -0
  209. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_pair_can_beat_the_best_single_action.py +0 -0
  210. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_per_instance_bound_reaches_every_reader.py +0 -0
  211. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_plan_files_the_row_that_is_a_forecast.py +0 -0
  212. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_plan_ranks_only_when_told_how.py +0 -0
  213. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_posterior_is_arithmetic_not_assumption.py +0 -0
  214. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_prediction_ledger_outlives_the_process.py +0 -0
  215. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_projection_declines_rather_than_guessing.py +0 -0
  216. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_proposed_spread_says_what_it_assumed.py +0 -0
  217. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_ranking_says_how_close_the_call_was.py +0 -0
  218. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_reading_can_be_impossible_without_being_extreme.py +0 -0
  219. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_refused_block_says_which_rule_it_was_on.py +0 -0
  220. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_refused_coupling_is_a_question_not_a_silence.py +0 -0
  221. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_response_model_nobody_recognises_is_not_exponential.py +0 -0
  222. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_can_be_graded_later.py +0 -0
  223. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_carries_a_transient_past_the_action_step.py +0 -0
  224. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_steps_the_model_forward.py +0 -0
  225. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_second_action_on_a_moved_property_superposes.py +0 -0
  226. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_sentence_the_changelog_closed_is_gone.py +0 -0
  227. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_set_aside_forecast_says_who_set_it_aside.py +0 -0
  228. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_source_pinned_at_zero_passes_no_doubt_on.py +0 -0
  229. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_spread_nobody_declared_is_not_a_declared_zero.py +0 -0
  230. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_starved_join_says_which_operand_starved.py +0 -0
  231. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_surprise_the_store_never_saw_is_not_a_miss.py +0 -0
  232. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_tie_break_knows_which_side_of_the_line.py +0 -0
  233. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_tie_is_broken_by_the_declared_margin.py +0 -0
  234. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_transition_into_an_acted_property_still_arrives.py +0 -0
  235. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_two_stage_chain_composes_exactly.py +0 -0
  236. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_value_is_final_before_anything_reads_it.py +0 -0
  237. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_warning_is_a_finding_on_every_surface.py +0 -0
  238. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_what_if_is_evaluated_at_the_horizon_it_asked_for.py +0 -0
  239. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_window_cannot_see_past_the_frozen_clock.py +0 -0
  240. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_withdrawn_check_is_not_silent.py +0 -0
  241. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_zero_allowance_still_reports.py +0 -0
  242. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_absent_data_regressions.py +0 -0
  243. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_agreement_declines_what_it_used_to_guess.py +0 -0
  244. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_may_arrive_as_a_mapping.py +0 -0
  245. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_on_a_coupled_property_reads_the_value_it_has.py +0 -0
  246. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_outlives_the_forecast_it_overrides.py +0 -0
  247. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_that_never_runs_says_so.py +0 -0
  248. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_imagined_breach_is_not_a_live_one_in_the_telemetry.py +0 -0
  249. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_imagined_finding_is_not_a_live_one.py +0 -0
  250. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_invented_time_course_says_so.py +0 -0
  251. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_undeclared_transition_projects_nothing.py +0 -0
  252. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_closed_reader.py +0 -0
  253. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_connectivity_required_property.py +0 -0
  254. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_conservation_absent_output.py +0 -0
  255. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_cross_signal_consistency.py +0 -0
  256. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_decline_contract.py +0 -0
  257. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_did_you_mean_is_case_insensitive.py +0 -0
  258. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_discovery_proposes_and_never_promotes.py +0 -0
  259. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_documented_paths_the_suite_never_ran.py +0 -0
  260. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_entailment_derives_and_refuses.py +0 -0
  261. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_envelope_schema.py +0 -0
  262. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_assumption_stamp_has_one_name.py +0 -0
  263. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_declared_tool_has_a_wrapper.py +0 -0
  264. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_decline_reason_has_a_producer.py +0 -0
  265. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_discipline_decline_has_one_name.py +0 -0
  266. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_forecast_gets_a_yardstick.py +0 -0
  267. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_reader_takes_the_same_history.py +0 -0
  268. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_shipped_example_actually_runs.py +0 -0
  269. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_simulation_decline_has_an_input.py +0 -0
  270. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_gated_cell_declines.py +0 -0
  271. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_history_outlives_the_process_and_the_weekend.py +0 -0
  272. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_homeostasis_setpoint.py +0 -0
  273. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_importing_the_package_does_not_pull_an_optional_extra.py +0 -0
  274. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_lower_bounds.py +0 -0
  275. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_mcp_feeders.py +0 -0
  276. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_mcp_roundtrip.py +0 -0
  277. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_monotonicity_reversals.py +0 -0
  278. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_axiom_retires_a_check.py +0 -0
  279. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_cell_is_judged_twice.py +0 -0
  280. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_identifier_names_a_private_record.py +0 -0
  281. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_test_anchors_time_at_import.py +0 -0
  282. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_declared_spread_is_counted_once.py +0 -0
  283. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_question_gets_one_projection.py +0 -0
  284. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_set_of_names_the_model_reads.py +0 -0
  285. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_role_comes_from_the_model.py +0 -0
  286. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_slow_oscillation.py +0 -0
  287. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_axioms_run_over_the_forecast_itself.py +0 -0
  288. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_causal_verb_has_a_model_to_run_against.py +0 -0
  289. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_clock_can_be_moved.py +0 -0
  290. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_config_blocks_are_documented.py +0 -0
  291. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_documented_priorities_are_the_computed_ones.py +0 -0
  292. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_declares_no_domain_it_does_not_serve.py +0 -0
  293. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_invents_no_indicators.py +0 -0
  294. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_races_its_own_forecasts.py +0 -0
  295. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_says_what_its_approximation_cost.py +0 -0
  296. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_scores_its_own_declared_spread.py +0 -0
  297. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engines_own_forecast_is_not_a_producers.py +0 -0
  298. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_envelope_legs_are_documented.py +0 -0
  299. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_evidence_floor_is_declared_or_disclosed.py +0 -0
  300. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_forecasts_leg_says_how_many_were_expected.py +0 -0
  301. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_indicator_keys_are_documented.py +0 -0
  302. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_ingest_tag_names_the_caller_not_the_funnel.py +0 -0
  303. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_lane_cleans_up_after_itself.py +0 -0
  304. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_ledger_is_reachable_without_a_deep_import.py +0 -0
  305. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_legal_files_ship_together.py +0 -0
  306. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_namespaces_point_at_a_graph_that_ships.py +0 -0
  307. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_policy_document_and_the_changelog_agree.py +0 -0
  308. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_published_cascade_formula_is_the_one_that_runs.py +0 -0
  309. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_question_ranking_is_one_scale.py +0 -0
  310. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_rate_arm_declines_what_it_used_to_guess.py +0 -0
  311. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_decline_count_is_derived.py +0 -0
  312. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_name_count_is_derived.py +0 -0
  313. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_names_the_forms_load_model_takes.py +0 -0
  314. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_rename_table_names_real_things.py +0 -0
  315. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_sample_floor_says_whether_it_is_reachable.py +0 -0
  316. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_schema_names_every_sub_envelope.py +0 -0
  317. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_schema_names_what_a_sub_envelope_carries.py +0 -0
  318. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_session_keeps_its_own_ledger.py +0 -0
  319. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_verb_that_used_the_number_names_it.py +0 -0
  320. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_world_model_can_be_exercised_over_mcp.py +0 -0
  321. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_threshold_overrides.py +0 -0
  322. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_timestamped_ingestion.py +0 -0
  323. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_traverse_vocabularies.py +0 -0
  324. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_trend_widens_and_does_not_point.py +0 -0
  325. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_actions_at_two_instants_are_not_one.py +0 -0
  326. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_bare_series_are_joinable.py +0 -0
  327. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_claims_on_one_edge_are_two_questions.py +0 -0
  328. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_offset_couplings_from_one_source_are_both_charged.py +0 -0
  329. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_plans_that_cost_the_same_compare_equal.py +0 -0
  330. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_rules_can_close_a_loop_neither_closes_alone.py +0 -0
  331. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_settings_of_one_property_do_not_add.py +0 -0
  332. {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_unread_properties.py +0 -0
@@ -34,7 +34,42 @@ useful-looking document and the less trustworthy one.
34
34
 
35
35
  ## [Unreleased]
36
36
 
37
- _Nothing yet._
37
+ ## [0.2.7] — 2026-09-25
38
+
39
+ ### Added
40
+
41
+ - **Every fitted transition proposal now carries a `replay`**: what adopting it
42
+ would have caught, as counts against the corpus that recorded what happened.
43
+ `n` and `r_squared` say how well a gain fits the data it was fitted on, which
44
+ is a different question.
45
+ - **`replay_unavailable` (`REPLAY_UNAVAILABLE`) is the answer when no corpus was
46
+ supplied.** It carries its reason and no counts: a proposal nobody could test
47
+ is not a proposal that failed, and a zero cannot tell those apart. No rate is
48
+ reported, for the reason the surprise score reports none.
49
+ - **`hypothesize(session, entity_id)` — what could explain a finding, ranked.**
50
+ The inverse of `gaps`: that verb says what the model does not declare, this
51
+ says what the world might be doing given what it does. Each candidate is a
52
+ DECLARED causal ancestor, scored by the same inference `infer` runs, carrying
53
+ the reading that would discriminate it and the declared action that could test
54
+ it. `MAX_HOPS` bounds the walk. It ranks nothing the author did not connect.
55
+ - **`arbiter_engine.producers.baseline_learner`, a reference producer.** A
56
+ damped-trend exponential smoother, shipped because the engine's own baseline
57
+ is a random walk and a random walk is the right floor and a poor opponent. It
58
+ gets no privilege: it reads a session through `reading_history()` and files
59
+ through `ingest_forecasts` like any outside forecaster. `MINIMUM_POINTS` is
60
+ the series length below which it returns nothing rather than a fitted number.
61
+ - **`model_describe(session, surprises=...)`** takes the corpus to replay
62
+ against. The model is restored afterwards whether or not the replay ran --
63
+ deriving and adopting stay separate.
64
+
65
+ ### Changed
66
+
67
+ - **The proposal surface now records WHO may write a fitted gain down, and it
68
+ is not this package.** A tool downstream -- a separate distribution, a
69
+ separate command, a proposal named by a person, a basis recorded in the file
70
+ -- may adopt one into a model it owns. Nothing here opens a model for
71
+ writing, and a test compares the file's bytes rather than reading this source
72
+ for a call. The first such writer is `bmc-sensor-audit adopt`.
38
73
 
39
74
  ## [0.2.6] — 2026-09-24
40
75
 
@@ -9,8 +9,8 @@ authors:
9
9
  alias: "james-sheen"
10
10
  - name: "The Arbiter Project Authors"
11
11
  license: Apache-2.0
12
- version: "0.2.6"
13
- date-released: "2026-09-24"
12
+ version: "0.2.7"
13
+ date-released: "2026-09-25"
14
14
  repository-code: "https://github.com/james-sheen/arbiter"
15
15
  url: "https://github.com/james-sheen/arbiter"
16
16
  keywords:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arbiter-engine
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: A detection engine that reports what it did not check.
5
5
  Project-URL: Homepage, https://github.com/james-sheen/arbiter
6
6
  Project-URL: Source, https://github.com/james-sheen/arbiter
@@ -514,7 +514,7 @@ The engine is open. The knowledge and the operations are not.
514
514
 
515
515
  ## Status
516
516
 
517
- **v0.2.** 95 Python files, 93 modules importing on the declared dependencies alone, 15 supported
517
+ **v0.2.** 98 Python files, 96 modules importing on the declared dependencies alone, 15 supported
518
518
  names — **counted in this repository**, which is the package you are holding.
519
519
 
520
520
  That basis is stated because it is easy to get wrong in a way nobody notices. The build adds one
@@ -523,7 +523,7 @@ are holding — and this line published the smaller figure until 2026-08-12, whe
523
523
  falsify it with `find . -name '*.py' | wc -l`. A checkable false claim, in the Status section of a
524
524
  project whose subject is checkable claims. Count the artifact, never an earlier stage of it.
525
525
 
526
- The import figure carries the same hazard one layer down, and it depends on what you have installed. Sweeping the package where `scipy` happens to be present imports 94; on the declared dependencies alone it is the 93 above, because `propagation.lp_confidence` is the one module that needs `scipy` and it is a deep path outside the supported surface. Count the artifact **in the state the reader will have it**, not in the state the person measuring happens to be standing in — this line quoted the with-`scipy` figure until 2026-08-12, which no reader installing normally could reproduce.
526
+ The import figure carries the same hazard one layer down, and it depends on what you have installed. Sweeping the package where `scipy` happens to be present imports 97; on the declared dependencies alone it is the 96 above, because `propagation.lp_confidence` is the one module that needs `scipy` and it is a deep path outside the supported surface. Count the artifact **in the state the reader will have it**, not in the state the person measuring happens to be standing in — this line quoted the with-`scipy` figure until 2026-08-12, which no reader installing normally could reproduce.
527
527
 
528
528
  **The supported-name count went stale in exactly the way this section warns about.** It read
529
529
  three short for the release that added `SqliteObservationHistory`, `SessionCalendar` and
@@ -540,7 +540,7 @@ committed by the paragraph. The figures are described here now rather than quote
540
540
  rule the changelog states for its own errata: a correction that reproduces the string it corrects
541
541
  is that string to a checker.
542
542
 
543
- **And the count is of SUBMODULES: the root package is not one of them.** Walking `arbiter_engine` for what it contains gives 93; adding the package you imported to reach them gives 94. Both are honest and they are answers to different questions, so a reader who recounts and gets one more has not found a defect — they have used the other convention. Stated because someone did exactly that from outside, and a number published without its predicate can only be agreed with or disagreed with, never checked. **Every figure in this paragraph moved by one when `assumptions` landed, and nothing went red for two rounds** -- the guard that compares them to the artifact only runs when a built tree exists, so it is silent in every session that does not build one. A check conditioned on an artifact is not a check that runs.
543
+ **And the count is of SUBMODULES: the root package is not one of them.** Walking `arbiter_engine` for what it contains gives 96; adding the package you imported to reach them gives 97. Both are honest and they are answers to different questions, so a reader who recounts and gets one more has not found a defect — they have used the other convention. Stated because someone did exactly that from outside, and a number published without its predicate can only be agreed with or disagreed with, never checked. **Every figure in this paragraph moved by TWO when the reference producer landed, and by one when `assumptions` did, and that earlier move went unnoticed for two rounds** -- the guard that compares them to the artifact only runs when a built tree exists, so it is silent in every session that does not build one. A check conditioned on an artifact is not a check that runs.
544
544
 
545
545
  Honest boundaries, stated because you would otherwise find them yourself:
546
546
 
@@ -476,7 +476,7 @@ The engine is open. The knowledge and the operations are not.
476
476
 
477
477
  ## Status
478
478
 
479
- **v0.2.** 95 Python files, 93 modules importing on the declared dependencies alone, 15 supported
479
+ **v0.2.** 98 Python files, 96 modules importing on the declared dependencies alone, 15 supported
480
480
  names — **counted in this repository**, which is the package you are holding.
481
481
 
482
482
  That basis is stated because it is easy to get wrong in a way nobody notices. The build adds one
@@ -485,7 +485,7 @@ are holding — and this line published the smaller figure until 2026-08-12, whe
485
485
  falsify it with `find . -name '*.py' | wc -l`. A checkable false claim, in the Status section of a
486
486
  project whose subject is checkable claims. Count the artifact, never an earlier stage of it.
487
487
 
488
- The import figure carries the same hazard one layer down, and it depends on what you have installed. Sweeping the package where `scipy` happens to be present imports 94; on the declared dependencies alone it is the 93 above, because `propagation.lp_confidence` is the one module that needs `scipy` and it is a deep path outside the supported surface. Count the artifact **in the state the reader will have it**, not in the state the person measuring happens to be standing in — this line quoted the with-`scipy` figure until 2026-08-12, which no reader installing normally could reproduce.
488
+ The import figure carries the same hazard one layer down, and it depends on what you have installed. Sweeping the package where `scipy` happens to be present imports 97; on the declared dependencies alone it is the 96 above, because `propagation.lp_confidence` is the one module that needs `scipy` and it is a deep path outside the supported surface. Count the artifact **in the state the reader will have it**, not in the state the person measuring happens to be standing in — this line quoted the with-`scipy` figure until 2026-08-12, which no reader installing normally could reproduce.
489
489
 
490
490
  **The supported-name count went stale in exactly the way this section warns about.** It read
491
491
  three short for the release that added `SqliteObservationHistory`, `SessionCalendar` and
@@ -502,7 +502,7 @@ committed by the paragraph. The figures are described here now rather than quote
502
502
  rule the changelog states for its own errata: a correction that reproduces the string it corrects
503
503
  is that string to a checker.
504
504
 
505
- **And the count is of SUBMODULES: the root package is not one of them.** Walking `arbiter_engine` for what it contains gives 93; adding the package you imported to reach them gives 94. Both are honest and they are answers to different questions, so a reader who recounts and gets one more has not found a defect — they have used the other convention. Stated because someone did exactly that from outside, and a number published without its predicate can only be agreed with or disagreed with, never checked. **Every figure in this paragraph moved by one when `assumptions` landed, and nothing went red for two rounds** -- the guard that compares them to the artifact only runs when a built tree exists, so it is silent in every session that does not build one. A check conditioned on an artifact is not a check that runs.
505
+ **And the count is of SUBMODULES: the root package is not one of them.** Walking `arbiter_engine` for what it contains gives 96; adding the package you imported to reach them gives 97. Both are honest and they are answers to different questions, so a reader who recounts and gets one more has not found a defect — they have used the other convention. Stated because someone did exactly that from outside, and a number published without its predicate can only be agreed with or disagreed with, never checked. **Every figure in this paragraph moved by TWO when the reference producer landed, and by one when `assumptions` did, and that earlier move went unnoticed for two rounds** -- the guard that compares them to the artifact only runs when a built tree exists, so it is silent in every session that does not build one. A check conditioned on an artifact is not a check that runs.
506
506
 
507
507
  Honest boundaries, stated because you would otherwise find them yourself:
508
508
 
@@ -79,6 +79,28 @@ declaration.
79
79
  its own stated spread. It does not order producers, recommend one, or carry a
80
80
  reputation between sessions.
81
81
 
82
+ ## The reference producer, which gets none of the above either
83
+
84
+ The package ships one: `arbiter_engine.producers.baseline_learner`, a
85
+ damped-trend exponential smoother. It exists because the engine's own baseline
86
+ is a **random walk** — it predicts the last value and widens with the horizon —
87
+ and a random walk is the right floor and a poor opponent. Anything that notices
88
+ a series is going somewhere beats it, so *beats the baseline* against a random
89
+ walk alone says almost nothing.
90
+
91
+ **It is a producer, not a feature.** It reads a session through
92
+ `reading_history()`, the accessor this engine tells every reader to use, and
93
+ files ordinary records through `ingest_forecasts`. It imports no checker, is
94
+ handed no privilege an outside forecaster would be refused, and is scored by
95
+ exactly the rules above — including `not_a_producers_submission` if its records
96
+ carry a `source`. A baseline with access the competition lacks is not a
97
+ baseline, and the suite asserts its import surface as well as its score.
98
+
99
+ **It refuses rather than guessing.** A series shorter than its minimum gets no
100
+ forecast, and one whose points are not ordered in time is refused rather than
101
+ sorted — re-ordering a series nobody gave you that way is how a producer comes
102
+ to score well on data it invented.
103
+
82
104
  ## Why the engine sets its own records aside
83
105
 
84
106
  The engine is a producer too. `rollout` files what it projected; every
@@ -754,7 +754,16 @@ _FEEDERS_ON_THE_SUPPORTED_SURFACE = (
754
754
  )
755
755
 
756
756
 
757
- def _proposed_transitions(session: EngineSession) -> Dict[str, Any]:
757
+ def _replay_proposal(session: EngineSession, proposal: Any,
758
+ surprises: Any) -> Dict[str, Any]:
759
+ """One proposal's replay, or the refusal that stands in for it."""
760
+ from arbiter_engine.surprises import replay_proposal
761
+
762
+ return replay_proposal(session, proposal, surprises)
763
+
764
+
765
+ def _proposed_transitions(session: EngineSession,
766
+ surprises: Any = None) -> Dict[str, Any]:
758
767
  """Fitted gains, their support, and any that contradict a declaration.
759
768
 
760
769
  Reported from `model_describe` rather than from a verb of its
@@ -767,6 +776,32 @@ def _proposed_transitions(session: EngineSession) -> Dict[str, Any]:
767
776
  author's claim about the system, and an engine that silently replaced it
768
777
  with a fitted number would leave nobody able to say what the model
769
778
  asserts -- the file would no longer be the model.
779
+
780
+ RULED 2026-09-25, A VERTICAL MAY WRITE ONE DOWN; THIS ENGINE
781
+ STILL MAY NOT. The phase plan asked for an `adopt <proposal_id>` verb,
782
+ and this package had refused one twice -- here and in
783
+ `causal/discovery.py`. The ruling separates the two things that question
784
+ runs together. What is refused above is an ENGINE acting on its own
785
+ measurement: a process handed a file to read, rewriting it, on a number
786
+ nobody asked it for. What is now permitted is a tool DOWNSTREAM of this
787
+ one, whose own author maintains the file, invoked by a person who typed
788
+ the proposal's name, writing the number with a basis beside it saying
789
+ where it came from. The file still says what the model asserts. It now
790
+ says the author chose to assert a fitted number, and says so in the file.
791
+
792
+ THE DISTANCE IS THE WHOLE OF THE RULING, AND NONE OF IT IS HERE: a
793
+ separate distribution, a separate command, a named proposal, a recorded
794
+ basis, and a gate that refuses a number which would have caught nothing.
795
+ Take any one of those away and what is left is the thing refused above.
796
+
797
+ So nothing in this package opens a model for writing, and the shipped
798
+ suite holds that by RUNNING this surface -- the replay included, which
799
+ substitutes a proposal to measure it -- and comparing the model file's
800
+ bytes, size and modification time afterwards. Not by reading this source
801
+ for a `write` call, which is a check on text about a claim concerning
802
+ behaviour. The first downstream writer is `bmc-sensor-audit adopt`, which
803
+ writes a fitted `gain` and its `gain_basis` into the supplemental file that
804
+ vertical's operator already maintains by hand.
770
805
  """
771
806
  from arbiter_engine.twin.transition_learner import (
772
807
  MINIMUM_PAIRED_SAMPLES, learn_transitions,
@@ -816,7 +851,14 @@ def _proposed_transitions(session: EngineSession) -> Dict[str, Any]:
816
851
  # author's behalf is the one thing this surface does not do.
817
852
  "residual_autocorrelation": p.residual_autocorrelation,
818
853
  "gain_sigma_assumes_independent_residuals":
819
- p.standard_error_assumes_independence}
854
+ p.standard_error_assumes_independence,
855
+ # WOULD ADOPTING THIS HAVE CAUGHT MORE. A proposal is a
856
+ # number somebody has to decide about, and `n` and `r_squared`
857
+ # answer how well it fits the data it was fitted on, which is not
858
+ # the same question. Without a corpus this is a REFUSAL carrying
859
+ # its reason, never a zero: a proposal nobody could test is not a
860
+ # proposal that failed.
861
+ "replay": _replay_proposal(session, p, surprises)}
820
862
  for p in proposals],
821
863
  "not_fitted": [
822
864
  {"edge": r.location, "reason": r.reason, "detail": r.detail}
@@ -1051,7 +1093,8 @@ def _transition_coverage(model, session: Optional[EngineSession] = None
1051
1093
  }
1052
1094
 
1053
1095
 
1054
- def model_describe(session: EngineSession) -> Envelope:
1096
+ def model_describe(session: EngineSession,
1097
+ surprises: Any = None) -> Envelope:
1055
1098
  """What domain is loaded: entity types, indicators, declared axioms.
1056
1099
 
1057
1100
  This is the grounding tool. An agent calls it before reasoning so it
@@ -1150,7 +1193,7 @@ def model_describe(session: EngineSession) -> Envelope:
1150
1193
  # ones the author declared. Proposals: nothing here has changed the
1151
1194
  # model, and a `gain: estimate` transition projects no value until a
1152
1195
  # number is adopted into the YAML.
1153
- "proposed_transitions": _proposed_transitions(session),
1196
+ "proposed_transitions": _proposed_transitions(session, surprises),
1154
1197
  # the list, mounted where a MODEL fact belongs. `check` has
1155
1198
  # carried it since it was added; this verb, whose whole question is
1156
1199
  # *did my model load the way I wrote it*, did not -- so a reader
@@ -2461,6 +2504,49 @@ def entail(session: EngineSession, adopt: bool = False) -> Envelope:
2461
2504
  return _WithPayload(envelope, payload)
2462
2505
 
2463
2506
 
2507
+ def hypothesize(session: EngineSession, entity_id: str,
2508
+ report_above: Optional[float] = None) -> Envelope:
2509
+ """What could explain a finding on `entity_id`, ranked, with what to read next.
2510
+
2511
+ THE INVERSE OF `gaps`. That verb says what the MODEL does not declare; this
2512
+ says what the WORLD might be doing given what it does — the same question
2513
+ turned around, and the one an operator has after a finding names an entity
2514
+ that is wrong.
2515
+
2516
+ Each candidate is a DECLARED causal ancestor, scored by the same inference
2517
+ `infer` runs, and carries the reading that would discriminate it and the
2518
+ declared action that could test it. Nothing here searches for a cause
2519
+ outside the graph: an engine proposing causes nobody declared would be doing
2520
+ the inference this project removed from `role:` and from flow direction.
2521
+
2522
+ **It is only as good as the evidence severity.** With every breach at
2523
+ `warning` and no `causal.evidence_severity:` declared, no finding counts as
2524
+ evidence, every posterior sits at its prior, and the ranking is an ordering
2525
+ of priors rather than of explanations. Measured on the shipped substation
2526
+ specimen: with both panels in the warning band the supply outranks the
2527
+ feeder 0.0104 to 0.0018, and with both below critical the feeder ranks first
2528
+ at 0.962. Same topology, same question, opposite answers — so read the
2529
+ `evidence_severity_not_declared` stamp before reading this ranking.
2530
+ """
2531
+ from arbiter_engine.inference.hypothesis import (
2532
+ hypothesize as _hypothesize)
2533
+
2534
+ if session.model is None:
2535
+ return unavailable_envelope("no domain model loaded")
2536
+ try:
2537
+ sub, ranked = _hypothesize(session, entity_id, report_above=report_above)
2538
+ except Exception as exc: # noqa: BLE001 - see `_raised`
2539
+ sub, ranked = _raised("inference", exc, {"candidates": 0}), []
2540
+
2541
+ envelope = Envelope(
2542
+ checked=CheckedSummary(invariants=0, entities=len(session.entities)),
2543
+ findings=list(sub.findings), questions=[_q(q) for q in sub.questions])
2544
+ payload = envelope.to_dict()
2545
+ payload["hypothesis"] = sub.to_dict()
2546
+ payload["hypothesis"]["candidates"] = ranked
2547
+ return _WithPayload(envelope, payload)
2548
+
2549
+
2464
2550
  def infer(session: EngineSession, target: str,
2465
2551
  do: Optional[Dict[str, int]] = None,
2466
2552
  report_above: Optional[float] = None) -> Envelope:
@@ -0,0 +1,182 @@
1
+ """What could explain a finding, and what reading would tell the candidates apart.
2
+
3
+ Phase B4. `gaps` says what the MODEL does not declare. This says what
4
+ the WORLD might be doing given what it does declare, which is the same question
5
+ turned around: a finding names an entity that is wrong, and an operator's next
6
+ move is to decide where to look.
7
+
8
+ THE PHASE PLAN CALLED THIS A PROMOTION AND IT IS NOT. `twin/hypothesis_generator.py`
9
+ was named as the thing to promote. Measured: it walks a topology for STRUCTURAL
10
+ patterns -- conservation, feedback loops, property bounds, monotonicity -- and
11
+ emits a `TopologyHypothesis` carrying a `precondition_pattern`, a `confidence`
12
+ and a `tenant_id`. None of that is `(cause, evidence_needed, test_action)` for a
13
+ finding, and `tenant_id` is an orchestrator concept this engine does not have.
14
+ Promoting it would have shipped the wrong verb wearing the right name, and
15
+ carried a multi-tenant field into a domain-free package.
16
+
17
+ So this is a composition over machinery that already exists, which is the
18
+ cheaper and more honest build: the causal subgraph supplies the candidates, the
19
+ inference runner scores them, and the model supplies the two things neither of
20
+ those knows -- which reading would discriminate, and whether an action is
21
+ declared that could test it.
22
+
23
+ IT RUNS UNDER THE `inference` DISCIPLINE AND DECLARES NO VOCABULARY OF ITS OWN.
24
+ The discipline set is closed and so is each discipline's decline vocabulary, and
25
+ BOTH refused an eighth member when this verb first asked for one -- which is the
26
+ refuse-what-you-cannot-read rule working on its own author. Using `inference` is
27
+ also the truer answer: this runs that discipline's machinery on a causal
28
+ question, and an eighth name for the seventh engine would be a second record of
29
+ one thing.
30
+
31
+ Its two refusals therefore share `not_identifiable`, distinguished by their
32
+ detail. This project normally SPLITS a reason into named arms rather than
33
+ sharing one, so that is a debt and not a design: growing a published closed
34
+ vocabulary has a measured cost on a fail-closed consumer -- one broke this month
35
+ on exactly such an addition -- and that decision should not ride along inside
36
+ the commit that ships a feature.
37
+
38
+ WHAT IT REFUSES. It ranks only DECLARED causal ancestors. It does not search for
39
+ a cause outside the graph, does not invent an edge, and does not rank an entity
40
+ the author never connected -- the same refusal `infer` makes, for the same
41
+ reason: an engine that proposed causes nobody declared would be doing the
42
+ inference this project removed from `role:` and from flow direction.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from typing import Any, Dict, List, Optional, Set, Tuple
48
+
49
+ from ..subenvelope import Decline, SubEnvelope
50
+ from .causal import CausalGraph, causal_subgraph
51
+ from .runner import Query, run_inference
52
+
53
+ #: How far upstream a hypothesis may reach. BOUNDED, and the bound is the point:
54
+ #: an unbounded ancestor walk over a graph with a cycle does not return, and the
55
+ #: project rule is that verification stays in P. The graph's own `cycle()` check
56
+ #: guards the declaration; this guards the walk.
57
+ MAX_HOPS = 4
58
+
59
+
60
+ def _ancestors(graph: CausalGraph, node: str,
61
+ max_hops: int = MAX_HOPS) -> List[Tuple[str, int]]:
62
+ """`(node, hops)` upstream of `node`, nearest first, each reported once."""
63
+ seen: Set[str] = {node}
64
+ out: List[Tuple[str, int]] = []
65
+ frontier = [node]
66
+ for hop in range(1, max_hops + 1):
67
+ nxt: List[str] = []
68
+ for current in frontier:
69
+ for parent in graph.parents.get(current, ()) or ():
70
+ if parent in seen:
71
+ continue
72
+ seen.add(parent)
73
+ out.append((parent, hop))
74
+ nxt.append(parent)
75
+ if not nxt:
76
+ break
77
+ frontier = nxt
78
+ return out
79
+
80
+
81
+ def _readable_properties(model, entity_type: str) -> List[str]:
82
+ """What the model says can be read on this type, in declared order."""
83
+ indicators = (getattr(model, "indicators", None) or {}).get(entity_type) or []
84
+ names: List[str] = []
85
+ for indicator in indicators:
86
+ name = getattr(indicator, "name", None)
87
+ if name is None and isinstance(indicator, dict):
88
+ name = indicator.get("name")
89
+ if name:
90
+ names.append(str(name))
91
+ return names
92
+
93
+
94
+ def _test_action(model, entity_type: str) -> Optional[str]:
95
+ """A declared action that applies to this type, or None.
96
+
97
+ NAMED, NEVER INVENTED. An engine that suggested an action nobody declared
98
+ would be recommending a thing it cannot know is safe, on a system it cannot
99
+ see. `None` here means the model declares no way to test this candidate,
100
+ which is a fact about the model and is worth reporting as one.
101
+ """
102
+ for template in (getattr(model, "action_templates", None) or ()):
103
+ applies = getattr(template, "applies_to", None)
104
+ name = getattr(template, "name", None)
105
+ if applies is None and isinstance(template, dict):
106
+ applies, name = template.get("applies_to"), template.get("name")
107
+ if applies == entity_type and name:
108
+ return str(name)
109
+ return None
110
+
111
+
112
+ def hypothesize(session: Any, entity_id: str, *,
113
+ report_above: Optional[float] = None
114
+ ) -> Tuple[SubEnvelope, List[Dict[str, Any]]]:
115
+ """Rank the declared causes of a finding on `entity_id`.
116
+
117
+ Returns the sub-envelope and the ranked candidates, which the caller
118
+ attaches -- the shape `entail` already uses, because `SubEnvelope` is
119
+ frozen and a verb that mutated one would be the only thing here that did.
120
+
121
+ Each candidate carries the posterior `infer` computes for it, the reading
122
+ that would discriminate it, and the declared action that could test it --
123
+ or an honest `None` where the model offers neither.
124
+ """
125
+ checked: Dict[str, Any] = {"candidates": 0, "ranked": 0, "max_hops": MAX_HOPS}
126
+ declines: List[Decline] = []
127
+
128
+ if getattr(session, "model", None) is None:
129
+ return (SubEnvelope("inference", {"candidates": 0}, source="unavailable",
130
+ reason="no domain model loaded"), [])
131
+
132
+ graph = causal_subgraph(session.model, session.graph, session.entities)
133
+ if entity_id not in graph.nodes:
134
+ declines.append(Decline(
135
+ "not_identifiable", {"entity": entity_id},
136
+ detail=(f"`{entity_id}` is not in the declared causal subgraph, so "
137
+ f"there is nothing upstream of it to rank. An edge enters "
138
+ f"that graph by declaring `edge_direction: causal`; this "
139
+ f"verb does not search for one.")))
140
+ return SubEnvelope("inference", checked, not_checked=declines), []
141
+
142
+ candidates = _ancestors(graph, entity_id)
143
+ checked["candidates"] = len(candidates)
144
+ if not candidates:
145
+ declines.append(Decline(
146
+ "not_identifiable", {"entity": entity_id},
147
+ detail=(f"`{entity_id}` has no declared causal ancestor within "
148
+ f"{MAX_HOPS} hops, so the model offers nothing that could "
149
+ f"explain a finding on it. That is a statement about the "
150
+ f"model and not about the system.")))
151
+ return SubEnvelope("inference", checked, not_checked=declines), []
152
+
153
+ ranked: List[Dict[str, Any]] = []
154
+ for node, hops in candidates:
155
+ sub = run_inference(session, Query(target=node), report_above=None)
156
+ payload = sub.to_dict()
157
+ posterior = (payload.get("checked") or {}).get("posterior")
158
+ entity_type = graph.entity_type.get(node, "")
159
+ properties = _readable_properties(session.model, entity_type)
160
+ ranked.append({
161
+ "cause": node,
162
+ "entity_type": entity_type,
163
+ "hops": hops,
164
+ "posterior": posterior,
165
+ # The FIRST declared readable property, which is the model's own
166
+ # ordering rather than this verb's opinion about which matters.
167
+ "evidence_needed": (f"{node}.{properties[0]}" if properties else None),
168
+ "test_action": _test_action(session.model, entity_type),
169
+ })
170
+
171
+ # Nearest first on a tie, because a cause two hops away explains a finding
172
+ # only through one that is nearer, and asking about the nearer one first is
173
+ # how an operator narrows rather than guesses.
174
+ ranked.sort(key=lambda row: (-(row["posterior"] or 0.0), row["hops"],
175
+ row["cause"]))
176
+ checked["ranked"] = len(ranked)
177
+ if report_above is not None:
178
+ ranked = [r for r in ranked
179
+ if r["posterior"] is not None and r["posterior"] >= report_above]
180
+ checked["reported"] = len(ranked)
181
+
182
+ return SubEnvelope("inference", checked, not_checked=declines), ranked
@@ -0,0 +1,25 @@
1
+ """Reference producers. They have no special status and that is the point.
2
+
3
+ Phase B3. `STANCE.md` says what a producer is and what this engine owes
4
+ one: a record naming a `source` is filed, fitted a baseline, and graded, and the
5
+ only thing the source changes is whether the eight shadow axioms run over it.
6
+ Until now nothing in this repository walked through that door, so the whole
7
+ producer path was exercised by fixtures and a consumer asking *what does a
8
+ reasonable forecaster score here* had nothing to compare against.
9
+
10
+ WHAT MAKES THIS A PRODUCER AND NOT AN ENGINE FEATURE. Everything here reads the
11
+ session through `reading_history()` -- the accessor the engine tells every
12
+ reader to use -- and emits ordinary forecast records through
13
+ `ingest_forecasts`. It imports no checker, touches no envelope, and is handed no
14
+ privilege the engine would refuse an outside forecaster. If any of that stopped
15
+ being true the comparison it exists to support would stop meaning anything: a
16
+ baseline with access the competition lacks is not a baseline.
17
+ """
18
+
19
+ from .baseline_learner import (
20
+ BASELINE_MODEL_ID, MINIMUM_POINTS, SeriesRefused, forecast_series,
21
+ forecast_session,
22
+ )
23
+
24
+ __all__ = ["BASELINE_MODEL_ID", "MINIMUM_POINTS", "SeriesRefused",
25
+ "forecast_series", "forecast_session"]
@@ -0,0 +1,163 @@
1
+ """A damped-trend exponential smoother, stated as quantiles.
2
+
3
+ The engine's own baseline is a random walk -- it predicts the last
4
+ value and widens with the horizon -- and every producer this engine grades is
5
+ scored against it. A random walk is the right FLOOR and a poor opponent: it is
6
+ beaten by anything that notices a series is going somewhere, so a producer that
7
+ beats it has shown almost nothing.
8
+
9
+ So this is the next thing up, and deliberately not further: Holt's linear method
10
+ with a damping factor, which is the smallest model that carries a trend and does
11
+ not extrapolate it forever. It is fitted by grid search over the three
12
+ parameters against one-step-ahead error on the series itself, which is honest
13
+ for a baseline and would not be for a claim.
14
+
15
+ WHAT IT REFUSES. A series shorter than `MINIMUM_POINTS` gets no forecast rather
16
+ than a fitted one -- three points can be fitted to perfectly and say nothing. A
17
+ series whose points are not strictly ordered in time is refused outright rather
18
+ than sorted, because re-ordering somebody's series silently is how a producer
19
+ scores well on data nobody gave it.
20
+
21
+ THE SPREAD IS THE RESIDUAL SPREAD, not a confidence interval. The quantiles come
22
+ from the one-step residuals scaled by the square root of the horizon in steps --
23
+ the random walk's own widening rule, applied to a better centre. Naming it an
24
+ interval would claim a distributional result this does not have.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import math
30
+ from datetime import datetime, timedelta
31
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
32
+
33
+ #: What the records say they came from. A producer names itself; the engine does
34
+ #: not name it, and `ingest_forecasts(source=)` is a separate statement about
35
+ #: who filed rather than about who computed.
36
+ BASELINE_MODEL_ID = "baseline-learner/holt-damped/1"
37
+
38
+ #: Below this a fit is arithmetic rather than evidence.
39
+ MINIMUM_POINTS = 12
40
+
41
+ #: The grid. Small on purpose: a baseline that is tuned finely is no longer a
42
+ #: baseline, it is an entry.
43
+ _ALPHAS = (0.2, 0.4, 0.6, 0.8)
44
+ _BETAS = (0.05, 0.15, 0.3)
45
+ _PHIS = (0.8, 0.9, 0.98)
46
+
47
+ #: The two the contract requires, plus the median the shadow check reads.
48
+ _Z = {"q05": -1.6448536269514722, "q50": 0.0, "q95": 1.6448536269514722}
49
+
50
+
51
+ class SeriesRefused(ValueError):
52
+ """The series cannot be forecast, and the reason is not a number."""
53
+
54
+
55
+ def _ordered(points: Sequence[Tuple[datetime, float]]
56
+ ) -> List[Tuple[datetime, float]]:
57
+ rows = list(points)
58
+ for earlier, later in zip(rows, rows[1:]):
59
+ if later[0] <= earlier[0]:
60
+ raise SeriesRefused(
61
+ "the series is not strictly ordered in time; refusing rather "
62
+ "than sorting, because re-ordering a series nobody gave you "
63
+ "that way is how a producer scores well on data it invented")
64
+ return rows
65
+
66
+
67
+ def _holt(values: Sequence[float], alpha: float, beta: float,
68
+ phi: float) -> Tuple[float, float, List[float]]:
69
+ """Level, trend and the one-step residuals, in one pass."""
70
+ level = values[0]
71
+ trend = values[1] - values[0]
72
+ residuals: List[float] = []
73
+ for actual in values[1:]:
74
+ predicted = level + phi * trend
75
+ residuals.append(actual - predicted)
76
+ previous = level
77
+ level = alpha * actual + (1.0 - alpha) * predicted
78
+ trend = beta * (level - previous) + (1.0 - beta) * phi * trend
79
+ return level, trend, residuals
80
+
81
+
82
+ def _fit(values: Sequence[float]) -> Tuple[float, float, float, float]:
83
+ """`(level, trend, phi, sigma)` for the grid point with least squared error."""
84
+ best = None
85
+ for alpha in _ALPHAS:
86
+ for beta in _BETAS:
87
+ for phi in _PHIS:
88
+ level, trend, residuals = _holt(values, alpha, beta, phi)
89
+ sse = sum(r * r for r in residuals)
90
+ if best is None or sse < best[0]:
91
+ n = max(1, len(residuals))
92
+ best = (sse, level, trend, phi, math.sqrt(sse / n))
93
+ _sse, level, trend, phi, sigma = best
94
+ return level, trend, phi, sigma
95
+
96
+
97
+ def forecast_series(points: Sequence[Tuple[datetime, float]], *,
98
+ entity_id: str, property_name: str,
99
+ horizon_s: float,
100
+ issued_at: Optional[datetime] = None
101
+ ) -> Optional[Dict[str, Any]]:
102
+ """One forecast record, or `None` when the series cannot support one.
103
+
104
+ Returns a plain dict in the shape `ingest_forecasts` reads. Nothing here
105
+ constructs an engine object: a producer that had to import the contract to
106
+ file would be a producer only this repository could write.
107
+ """
108
+ rows = _ordered(points)
109
+ if len(rows) < MINIMUM_POINTS:
110
+ return None
111
+
112
+ values = [float(v) for _when, v in rows]
113
+ level, trend, phi, sigma = _fit(values)
114
+
115
+ step_s = (rows[-1][0] - rows[-2][0]).total_seconds()
116
+ if step_s <= 0:
117
+ raise SeriesRefused("the last two points share an instant")
118
+ steps = max(1.0, horizon_s / step_s)
119
+
120
+ # Damped trend summed over the horizon, which is what `phi` is for: the
121
+ # contribution of each further step shrinks, so the forecast flattens
122
+ # instead of running away.
123
+ ahead = level + sum(phi ** k for k in range(1, int(steps) + 1)) * trend
124
+ spread = sigma * math.sqrt(steps)
125
+
126
+ return {
127
+ "model_id": BASELINE_MODEL_ID,
128
+ "entity_id": entity_id,
129
+ "property": property_name,
130
+ "issued_at": issued_at or rows[-1][0],
131
+ "horizon_s": float(horizon_s),
132
+ "quantiles": {name: ahead + z * spread for name, z in _Z.items()},
133
+ "sample_count": len(rows),
134
+ }
135
+
136
+
137
+ def forecast_session(session: Any, *, horizon_s: float,
138
+ issued_at: Optional[datetime] = None
139
+ ) -> List[Dict[str, Any]]:
140
+ """Every numeric series this session can support a forecast for.
141
+
142
+ Reads `reading_history()`, which is the store the engine tells every reader
143
+ to ask -- `session.history` is what was FED, and the two differ whenever a
144
+ calendar or a derived indicator is declared.
145
+ """
146
+ history = session.reading_history()
147
+ out: List[Dict[str, Any]] = []
148
+ for key, points in (history.get_all_numeric_series() or {}).items():
149
+ # `entity_id.property_name`, split on the LAST dot: a property name
150
+ # carries none in this engine and an entity id may, so splitting on the
151
+ # first would silently forecast a property nobody named.
152
+ entity_id, _, property_name = str(key).rpartition(".")
153
+ if not entity_id or not property_name:
154
+ continue
155
+ try:
156
+ record = forecast_series(
157
+ points, entity_id=entity_id, property_name=property_name,
158
+ horizon_s=horizon_s, issued_at=issued_at)
159
+ except SeriesRefused:
160
+ continue
161
+ if record is not None:
162
+ out.append(record)
163
+ return out