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.
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CHANGELOG.md +36 -1
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CITATION.cff +2 -2
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/PKG-INFO +4 -4
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/README.md +3 -3
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/STANCE.md +22 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/api.py +90 -4
- arbiter_engine-0.2.7/arbiter_engine/inference/hypothesis.py +182 -0
- arbiter_engine-0.2.7/arbiter_engine/producers/__init__.py +25 -0
- arbiter_engine-0.2.7/arbiter_engine/producers/baseline_learner.py +163 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/surprises.py +100 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/transition_learner.py +7 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/pyproject.toml +1 -1
- arbiter_engine-0.2.7/tests/test_a_finding_says_where_to_look_next.py +160 -0
- arbiter_engine-0.2.7/tests/test_a_proposal_says_what_adopting_it_would_have_caught.py +192 -0
- arbiter_engine-0.2.7/tests/test_a_vertical_may_adopt_and_this_engine_may_not.py +224 -0
- arbiter_engine-0.2.7/tests/test_the_engine_ships_an_opponent_worth_beating.py +166 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_stance_page_says_what_no_other_page_says.py +37 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.gitattributes +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/PUBLISH_FROM_CI +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/release.yml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/tests.yml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/verify-published.yml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.github/workflows/verify-tag-artifact.yml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/.gitignore +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/AI_ATTRIBUTION.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/AUTHORS.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/BRIDGES.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CODE_OF_CONDUCT.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/COMPATIBILITY.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/CONTRIBUTING.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/LICENSE +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/MODELING.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/NOTICE +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/PRIVACY.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/ROADMAP.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/SECURITY.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/SUPPORT.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/TRADEMARK.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/assumptions.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/axiom_thresholds.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/discovery.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/granger.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/causal/leadlag.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/clock.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/indicator.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/derived/parser.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/envelope.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/battery_pack.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/factory_line.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/kubernetes_node.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/margin_book.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/pump_tank_dynamics.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/pump_tank_planning.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/substation_feeder.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/substation_feeder_surprises.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/examples/water_tank.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/fire_frequency.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/contract.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/envelope.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/ingest.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/monitor.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/forecast/shadow.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/calendar.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/observation_source_wiring.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/readiness.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/history/sqlite_store.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/causal.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/runner.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/inference/ve.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/interfaces.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/mcp/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/mcp/server.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axiom_verdicts_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/boundedness.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/connectivity.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/conservation.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/consistency.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/extensions.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/homeostasis.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/monotonicity.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/peers.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/responsiveness.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/roles.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/axioms/stability.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/domain_loader.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/entail.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/loader.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/prediction_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/reasoner.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/ontology/schemas/health_meta_ontology.ttl +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/projector.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/projection/runner.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/impact_estimator.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/lp_confidence.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/mcts_root_cause.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/root_cause.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/propagation/weight_learner.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/rca/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/rca/greedy_set_cover.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/replay.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/predict_vs_mirror.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/residual/sqlite_ledger.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/schema/envelope.schema.json +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/benchmark_check.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/scripts/surprise_benchmark.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/subenvelope.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/temporal_edge.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/temporal/trend_projection.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/__init__.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/action_clears_problem.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/actions.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/builder.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/gap.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/hypothesis_generator.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/hypothesis_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/kernel_pipeline_executor.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/monte_carlo_predictor.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/optimization_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/pipeline_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/planner.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/rollout.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/topology.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/topology_optimizer.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/traverser.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/twin/traverser_production.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/arbiter_engine/types.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/README.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/alpha1_evidence_pack.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/customer_deployment_runbook.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/extended_fault_scenarios.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/l5_surprise_synthesis.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/observability_handoff_guide.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/tech_brief.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/evidence/use-case-catalogue.md +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/battery_pack.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/factory_line.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/kubernetes_node.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/margin_book.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/pump_tank_dynamics.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/pump_tank_planning.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/substation_feeder.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/substation_feeder_surprises.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/examples/water_tank.yaml +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/schema/envelope.schema.json +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/conftest.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_balance_may_cross_an_entity_boundary.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_bound_can_belong_to_one_instance.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_calibration_figure_names_its_population.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_calibration_says_how_many_trajectories_it_saw.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_chain_of_lags_says_how_it_was_composed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_changelog_section_says_whether_you_can_install_it.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_check_that_cannot_run_says_so.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_check_that_declines_is_heard.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_checker_asks_no_entity_type.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_clean_rollout_still_counts_what_it_evaluated.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_companion_beside_the_models_is_not_one.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_block_is_checked_too.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_is_graded_on_its_own_projections.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_coupling_learns_how_its_projections_fared.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_bad_state_is_not_silent.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_calendar_is_actually_read.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_edge_reaches_the_published_surface.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_floor_is_checked_on_every_path.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_gain_moves_the_downstream_value.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_number_is_the_first_breach.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_offset_waits_for_the_delay.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_source_matches_the_guide.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_declared_spread_reaches_the_value.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_decline_states_the_number_it_computed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_derived_indicator_is_computed_not_fed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_discipline_reports_its_own_denominator.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_dropped_declaration_is_not_silent.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_filed_source_says_who_is_not_a_producer.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_batch_is_filed_or_accounted_for.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_check_that_refused_says_so_out_loud.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_graded_at_its_horizon.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_measured_against_a_random_walk.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_is_scored_not_just_stored.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_keeps_its_doubt.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_states_its_own_distribution.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecast_widens_and_says_what_it_assumed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_forecaster_is_judged_by_the_same_eight_axioms.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_freeze_is_found_inside_its_window.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_latency_without_a_threshold_says_so.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_lazy_import_did_not_move_a_name.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_lead_lag_p_value_means_what_it_says.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_learned_gain_is_a_proposal.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_margin_is_measured_against_every_line.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_nested_block_is_checked_too.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_pair_can_beat_the_best_single_action.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_per_instance_bound_reaches_every_reader.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_plan_files_the_row_that_is_a_forecast.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_plan_ranks_only_when_told_how.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_posterior_is_arithmetic_not_assumption.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_prediction_ledger_outlives_the_process.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_projection_declines_rather_than_guessing.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_proposed_spread_says_what_it_assumed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_ranking_says_how_close_the_call_was.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_reading_can_be_impossible_without_being_extreme.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_refused_block_says_which_rule_it_was_on.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_refused_coupling_is_a_question_not_a_silence.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_response_model_nobody_recognises_is_not_exponential.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_can_be_graded_later.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_carries_a_transient_past_the_action_step.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_rollout_steps_the_model_forward.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_second_action_on_a_moved_property_superposes.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_sentence_the_changelog_closed_is_gone.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_set_aside_forecast_says_who_set_it_aside.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_source_pinned_at_zero_passes_no_doubt_on.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_spread_nobody_declared_is_not_a_declared_zero.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_starved_join_says_which_operand_starved.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_tie_break_knows_which_side_of_the_line.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_tie_is_broken_by_the_declared_margin.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_transition_into_an_acted_property_still_arrives.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_two_stage_chain_composes_exactly.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_value_is_final_before_anything_reads_it.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_warning_is_a_finding_on_every_surface.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_window_cannot_see_past_the_frozen_clock.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_withdrawn_check_is_not_silent.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_a_zero_allowance_still_reports.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_absent_data_regressions.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_agreement_declines_what_it_used_to_guess.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_may_arrive_as_a_mapping.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_outlives_the_forecast_it_overrides.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_action_that_never_runs_says_so.py +0 -0
- {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
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_imagined_finding_is_not_a_live_one.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_invented_time_course_says_so.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_an_undeclared_transition_projects_nothing.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_closed_reader.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_connectivity_required_property.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_conservation_absent_output.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_cross_signal_consistency.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_decline_contract.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_did_you_mean_is_case_insensitive.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_discovery_proposes_and_never_promotes.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_documented_paths_the_suite_never_ran.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_entailment_derives_and_refuses.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_envelope_schema.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_assumption_stamp_has_one_name.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_declared_tool_has_a_wrapper.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_decline_reason_has_a_producer.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_discipline_decline_has_one_name.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_forecast_gets_a_yardstick.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_reader_takes_the_same_history.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_shipped_example_actually_runs.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_every_simulation_decline_has_an_input.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_gated_cell_declines.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_history_outlives_the_process_and_the_weekend.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_homeostasis_setpoint.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_importing_the_package_does_not_pull_an_optional_extra.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_lower_bounds.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_mcp_feeders.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_mcp_roundtrip.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_monotonicity_reversals.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_axiom_retires_a_check.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_cell_is_judged_twice.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_identifier_names_a_private_record.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_no_test_anchors_time_at_import.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_declared_spread_is_counted_once.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_question_gets_one_projection.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_one_set_of_names_the_model_reads.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_role_comes_from_the_model.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_slow_oscillation.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_axioms_run_over_the_forecast_itself.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_causal_verb_has_a_model_to_run_against.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_clock_can_be_moved.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_config_blocks_are_documented.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_documented_priorities_are_the_computed_ones.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_declares_no_domain_it_does_not_serve.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_invents_no_indicators.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_races_its_own_forecasts.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_says_what_its_approximation_cost.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engine_scores_its_own_declared_spread.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_engines_own_forecast_is_not_a_producers.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_envelope_legs_are_documented.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_evidence_floor_is_declared_or_disclosed.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_forecasts_leg_says_how_many_were_expected.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_indicator_keys_are_documented.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_ingest_tag_names_the_caller_not_the_funnel.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_lane_cleans_up_after_itself.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_ledger_is_reachable_without_a_deep_import.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_legal_files_ship_together.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_namespaces_point_at_a_graph_that_ships.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_policy_document_and_the_changelog_agree.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_published_cascade_formula_is_the_one_that_runs.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_question_ranking_is_one_scale.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_rate_arm_declines_what_it_used_to_guess.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_decline_count_is_derived.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_name_count_is_derived.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_readme_names_the_forms_load_model_takes.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_rename_table_names_real_things.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_sample_floor_says_whether_it_is_reachable.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_schema_names_every_sub_envelope.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_schema_names_what_a_sub_envelope_carries.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_session_keeps_its_own_ledger.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_verb_that_used_the_number_names_it.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_the_world_model_can_be_exercised_over_mcp.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_threshold_overrides.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_timestamped_ingestion.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_traverse_vocabularies.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_trend_widens_and_does_not_point.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_actions_at_two_instants_are_not_one.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_bare_series_are_joinable.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_claims_on_one_edge_are_two_questions.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_offset_couplings_from_one_source_are_both_charged.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_plans_that_cost_the_same_compare_equal.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_rules_can_close_a_loop_neither_closes_alone.py +0 -0
- {arbiter_engine-0.2.6 → arbiter_engine-0.2.7}/tests/test_two_settings_of_one_property_do_not_add.py +0 -0
- {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
|
-
|
|
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.
|
|
13
|
-
date-released: "2026-09-
|
|
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.
|
|
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.**
|
|
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
|
|
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
|
|
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.**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|