arborist/tests/test_prometheus.py
russell@unturf.com f9f5ae459b
#000037 Phase 1 continued: §14 row 4 + §13 step 11 + weight-tunable gates
Closes four gaps in the initial Phase 1 landing (commit f625cac):

1. §14 row 4 — Hermes-saturation guard. `hermes_utilization` was on
   the input contract but never consumed. Now: `utilization >=
   budget` → DEFERRED with HERMES_SATURATED note + advisory event
   carrying (utilization, budget) for Phase 2 audit. Three new
   tests (saturation-equal, saturation-overflow, headroom-exists).

2. §13 step 11 — falsification-fixture proposal emission.
   New `FalsificationFixtureProposal` dataclass; controller now
   emits one per branch whose `witness_divergence >=
   falsification_divergence_threshold` (weight-tunable, default
   0.5). Emission fires BEFORE the all-vetoed cascade so vetoed-
   AND-diverged branches still surface as 5F-fixture candidates
   per #000025. Four new tests (high-divergence emits, low-
   divergence stays silent, threshold is weight-tunable, vetoed-
   diverged emits anyway).

3. Entropy + memory gating moved from module-level constants
   (`H_LOW=0.3`, `H_HIGH=0.7`, `KAPPA_MEMORY=0.5`) to weight
   fields (`h_low`, `h_high`, `kappa_memory`). Constants stay as
   back-compat exports; defaults match exactly so byte-identical
   behavior when neither override fires. Three new tests
   (h_low/h_high tunable, kappa_memory tunable, back-compat
   match).

4. Veto-class cascade hardening. Added explicit tests for the
   `replay_window_unbounded` → ESCALATE path and the
   `soft_hash_signal` → QUARANTINE path (§6 + §14 documented but
   previously untested). Plus tests for the §6 fail-loud
   priority ordering: ESCALATE > QUARANTINE > REJECT when mixed.

Dry-run regenerated against ~/.arborist/shards corpus:
  Target A at τ_qa=1d surfaces **576 FalsificationFixture
  proposals** from witness_divergence >= 0.5 (24% of swept
  candidates). These rows are now an actionable funnel for 5F
  fixture mining under #000025 §3.

Module: arborist/substrate/prometheus.py (+117 LOC, 899 total)
Tests:  tests/test_prometheus.py (+196 LOC, 22 → 36 tests)
Dryrun: bench/scripts/prometheus_sigma_sweep_dryrun.py +
        bench/results/prometheus-sigma-sweep-dryrun-2026-05-10.md
        track the new falsification_proposals_total counter.

Full suite: 2326 passed, 37 skipped (+14 net from the new tests).
2026-05-10 16:56:18 -04:00

804 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Phase 1 tests for arborist.substrate.prometheus (ticket #000037).
Covers the §16.2 test list — 17 named cases — plus a small set of
sanity checks (determinism, weight-profile shape, trigger-check
positive path) that don't appear in the §16.2 list but harden the
"pure function" contract.
Discipline (§16.1):
- Pure-function — no DB, no LLM, no scheduler.
- Banned imports verified via AST inspection of the module source.
- Proposals only; no mutation API calls.
"""
from __future__ import annotations
import ast
import math
import random
from pathlib import Path
import pytest
from arborist.substrate.prometheus import (
ABS_STDDEV_ZERO_MEAN,
H_HIGH,
H_LOW,
KAPPA_MEMORY,
N_MIN_TRIGGER_2,
WEIGHT_PROFILES,
BatteryDeltas,
ControllerBranch,
ControllerDecision,
ControllerInput,
ControllerWeights,
MemoryRootUpdateProposal,
SelfModelUpdateProposal,
conservative_weights,
controller_decide,
exploratory_weights,
phase_1_trigger_check,
safe_weights,
)
# ---------------------------------------------------------------------
# Test helpers
# ---------------------------------------------------------------------
def _b(
branch_id: str = "b1",
*,
delta_5s: float = 0.05,
delta_5t: float = 0.05,
delta_5f: float = 0.05,
delta_5r: float = 0.0,
witness_divergence: float = 0.0,
capital_cost: float = 0.0,
regression_penalty: float = 0.0,
security_risk: float = 0.0,
memory_invalidation: float = 0.0,
selfmodel_calibration_gain: float = 0.0,
warrant_promotion_gain: float = 0.0,
hard_vetoes: tuple[str, ...] = (),
payoff_b: float = 1.0,
) -> ControllerBranch:
return ControllerBranch(
branch_id=branch_id,
deltas=BatteryDeltas(delta_5s, delta_5t, delta_5f, delta_5r),
witness_divergence=witness_divergence,
capital_cost=capital_cost,
regression_penalty=regression_penalty,
security_risk=security_risk,
memory_invalidation=memory_invalidation,
selfmodel_calibration_gain=selfmodel_calibration_gain,
warrant_promotion_gain=warrant_promotion_gain,
hard_vetoes=hard_vetoes,
payoff_b=payoff_b,
)
def _inp(
branches: tuple[ControllerBranch, ...],
*,
organism_root: str = "Ω0",
budget: int = 4,
hermes_utilization: int = 0,
weights: ControllerWeights | None = None,
difficulty: float = 1.0,
divergence_rate: float = 0.0,
hard_regression_rate: float = 0.0,
witness_agreement_rate: float = 1.0,
capital_pressure: float = 0.0,
) -> ControllerInput:
return ControllerInput(
organism_root=organism_root,
branches=branches,
budget=budget,
hermes_utilization=hermes_utilization,
weights=weights or safe_weights(),
difficulty=difficulty,
divergence_rate=divergence_rate,
hard_regression_rate=hard_regression_rate,
witness_agreement_rate=witness_agreement_rate,
capital_pressure=capital_pressure,
)
# ---------------------------------------------------------------------
# §16.2 #1 — test_all_vetoed_returns_reject_or_quarantine
# ---------------------------------------------------------------------
def test_all_vetoed_returns_reject_or_quarantine():
"""Every branch vetoed → label is REJECT or QUARANTINE (or
ESCALATE for the most severe class). selected_branch_id None,
notes mention ALL_BRANCHES_VETOED."""
branches = (
_b("a", hard_vetoes=("hard_regression",)), # REJECT class
_b("b", hard_vetoes=("verifier_failure",)), # REJECT class
)
d = controller_decide(_inp(branches))
assert d.label == "REJECT"
assert d.selected_branch_id is None
assert "ALL_BRANCHES_VETOED" in d.notes
# Mixed REJECT + QUARANTINE → QUARANTINE wins (higher severity).
branches = (
_b("a", hard_vetoes=("hard_regression",)),
_b("b", hard_vetoes=("cache_drift",)),
)
d = controller_decide(_inp(branches))
assert d.label == "QUARANTINE"
# Any ESCALATE-class veto → ESCALATE wins (highest severity).
branches = (
_b("a", hard_vetoes=("hard_regression",)),
_b("b", hard_vetoes=("memory_invalidation_exceeded",)),
)
d = controller_decide(_inp(branches))
assert d.label == "ESCALATE"
# ---------------------------------------------------------------------
# §16.2 #2 — test_zero_budget_returns_deferred
# ---------------------------------------------------------------------
def test_zero_budget_returns_deferred():
"""B = 0 (Hermes saturated) → DEFERRED for everything, regardless
of how good the branches look."""
branches = (_b("a", delta_5s=0.2), _b("b", delta_5s=0.3))
d = controller_decide(_inp(branches, budget=0))
assert d.label == "DEFERRED"
assert d.selected_branch_id is None
assert all(v == 0.0 for v in d.allocations.values())
assert "ZERO_BUDGET" in d.notes
# ---------------------------------------------------------------------
# §16.2 #3 — test_stable_softmax_no_overflow
# ---------------------------------------------------------------------
def test_stable_softmax_no_overflow():
"""Utilities ±10000 must not overflow exp() — the stable softmax
max-shift bounds every scaled-exp in [0, 1]."""
# Both very large positive and very large negative inputs.
branches = (
_b("hot", delta_5s=10000.0, delta_5t=10000.0, delta_5f=10000.0),
_b("cold", delta_5s=-10000.0, delta_5t=-10000.0, delta_5f=-10000.0),
_b("middle", delta_5s=0.1),
)
d = controller_decide(_inp(branches))
# No NaN / inf escapes.
assert math.isfinite(d.entropy)
for v in d.allocations.values():
assert math.isfinite(v)
# Hot branch gets virtually all the budget.
assert d.allocations["hot"] > d.allocations["cold"]
assert d.allocations["hot"] > d.allocations["middle"]
# ---------------------------------------------------------------------
# §16.2 #4 — test_negative_payoff_gets_zero_allocation
# ---------------------------------------------------------------------
def test_negative_payoff_gets_zero_allocation():
"""Branch with payoff_b <= 0 — Kelly's "free regression" guard.
Allocation zero; if it's the only branch the label REJECTs."""
branches = (_b("a", delta_5s=0.1, payoff_b=-0.5),)
d = controller_decide(_inp(branches))
# Σ_raw == 0 → DEFERRED (the spec-defined exit when no positive
# allocation survives). Both DEFERRED and REJECT are valid
# "negative-payoff produces no work" outcomes — assert no positive
# allocation either way.
assert d.allocations["a"] == 0.0
assert d.label in {"DEFERRED", "REJECT"}
# When another branch has positive payoff, the negative one stays
# at zero and the positive one selects.
branches = (
_b("neg", delta_5s=0.1, payoff_b=-0.5),
_b("pos", delta_5s=0.1, payoff_b=2.0),
)
d = controller_decide(_inp(branches))
assert d.allocations["neg"] == 0.0
assert d.allocations["pos"] > 0.0
assert d.selected_branch_id == "pos"
# ---------------------------------------------------------------------
# §16.2 #5 — test_unsupported_carrier_quarantines
# ---------------------------------------------------------------------
def test_unsupported_carrier_quarantines():
"""`unsupported_carrier` veto on the only branch → QUARANTINE."""
branches = (_b("a", hard_vetoes=("unsupported_carrier",)),)
d = controller_decide(_inp(branches))
assert d.label == "QUARANTINE"
assert "ALL_BRANCHES_VETOED" in d.notes
assert d.veto_reasons["a"] == ("unsupported_carrier",)
# ---------------------------------------------------------------------
# §16.2 #6 — test_cache_drift_quarantines
# ---------------------------------------------------------------------
def test_cache_drift_quarantines():
"""`cache_drift` veto on the only branch → QUARANTINE."""
branches = (_b("a", hard_vetoes=("cache_drift",)),)
d = controller_decide(_inp(branches))
assert d.label == "QUARANTINE"
# ---------------------------------------------------------------------
# §16.2 #7 — test_memory_invalidation_above_threshold_escalates
# ---------------------------------------------------------------------
def test_memory_invalidation_above_threshold_escalates():
"""Selected branch with memory_invalidation >= KAPPA_MEMORY →
ESCALATE."""
branches = (
_b(
"a",
delta_5s=0.5,
delta_5t=0.5,
delta_5f=0.5,
memory_invalidation=KAPPA_MEMORY,
),
)
d = controller_decide(_inp(branches))
assert d.label == "ESCALATE"
assert d.selected_branch_id == "a"
assert "MEMORY_INVALIDATION_ABOVE_KAPPA" in d.notes
# Just below threshold → no escalation.
branches = (
_b(
"a",
delta_5s=0.5,
delta_5t=0.5,
delta_5f=0.5,
memory_invalidation=KAPPA_MEMORY - 0.01,
),
)
d = controller_decide(_inp(branches))
assert d.label != "ESCALATE"
# ---------------------------------------------------------------------
# §16.2 #8 — test_high_entropy_increases_difficulty
# ---------------------------------------------------------------------
def test_high_entropy_increases_difficulty():
"""Multiple equally-attractive branches → high entropy →
difficulty_next > difficulty (§7.1 H_norm term is positive).
Note: under default `witness_agreement_rate=1.0` the agreement
pull exactly cancels the entropy drive (both weight 0.1).
Setting agreement_rate=0 isolates the entropy contribution."""
# Three branches, identical utilities → uniform distribution →
# H_norm = 1.0.
branches = tuple(
_b(f"b{i}", delta_5s=0.05, delta_5t=0.05, delta_5f=0.05)
for i in range(3)
)
initial_difficulty = 1.0
d = controller_decide(
_inp(
branches,
difficulty=initial_difficulty,
witness_agreement_rate=0.0,
)
)
assert d.entropy >= H_HIGH, (
f"three-equal-branch H_norm should be near 1.0, got {d.entropy}"
)
assert d.difficulty_next > initial_difficulty
# ---------------------------------------------------------------------
# §16.2 #9 — test_low_entropy_decreases_or_preserves_difficulty
# ---------------------------------------------------------------------
def test_low_entropy_decreases_or_preserves_difficulty():
"""Single overwhelmingly-good branch → low entropy → with
`witness_agreement_rate=1.0` and zero divergence/regression
pressure, difficulty_next <= difficulty."""
branches = (
_b("dominant", delta_5s=10.0, delta_5t=10.0, delta_5f=10.0),
_b("weak", delta_5s=-5.0, delta_5t=-5.0, delta_5f=-5.0),
)
initial_difficulty = 5.0
d = controller_decide(
_inp(
branches,
difficulty=initial_difficulty,
witness_agreement_rate=1.0,
divergence_rate=0.0,
hard_regression_rate=0.0,
)
)
# Entropy near zero (dominant branch has p ≈ 1).
assert d.entropy <= H_LOW
# With agreement pull > entropy drive in this regime, difficulty
# should not climb.
assert d.difficulty_next <= initial_difficulty
# ---------------------------------------------------------------------
# §16.2 #10 — test_divergence_increases_witness_sampling_recommendation
# ---------------------------------------------------------------------
def test_divergence_increases_witness_sampling_recommendation():
"""High `divergence_rate` → higher difficulty_next (the controller's
homeostat: divergence is the thermostat; more difficulty means
wider witness sampling in the Phase 3 sweep)."""
branches = (_b("a", delta_5s=0.1, delta_5t=0.1, delta_5f=0.1),)
initial = 1.0
low_div = controller_decide(
_inp(branches, difficulty=initial, divergence_rate=0.0)
)
high_div = controller_decide(
_inp(branches, difficulty=initial, divergence_rate=0.8)
)
assert high_div.difficulty_next > low_div.difficulty_next
# ---------------------------------------------------------------------
# §16.2 #11 — test_no_llm_call_in_controller
# ---------------------------------------------------------------------
def _module_imports() -> set[str]:
"""Parse the prometheus.py source and return the set of imported
module names. Catches `import x`, `import x.y`, `from x.y import …`."""
here = Path(__file__).resolve().parent
src = (here.parent / "arborist" / "substrate" / "prometheus.py").read_text(
encoding="utf-8"
)
tree = ast.parse(src)
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
names.add(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.module:
names.add(node.module)
return names
def test_no_llm_call_in_controller():
"""Pure function — must not import the LLM gateway."""
imports = _module_imports()
assert "arborist.qa.client" not in imports, (
"controller must not import the LLM client (Phase 1 is pure)"
)
# ---------------------------------------------------------------------
# §16.2 #12 — test_controller_does_not_modify_cache_key_inputs
# ---------------------------------------------------------------------
def test_controller_does_not_modify_cache_key_inputs():
"""Phase 1 controller must not touch the cache_key path. AST
inspection confirms `arborist.qa.keys` is not imported."""
imports = _module_imports()
assert "arborist.qa.keys" not in imports, (
"controller must not import the cache_key path (Phase 1 is "
"advisory-only over already-committed state)"
)
# ---------------------------------------------------------------------
# §16.2 #13 — test_controller_outputs_advisory_event_only
# ---------------------------------------------------------------------
def test_controller_outputs_advisory_event_only():
"""An ACCEPT-bound input produces `advisory_events` as a tuple of
(str, dict) pairs. Phase 2 will persist these; Phase 1 only
builds the data structure."""
branches = (_b("a", delta_5s=0.5, delta_5t=0.5, delta_5f=0.5),)
d = controller_decide(_inp(branches))
assert d.label == "ACCEPT"
assert isinstance(d.advisory_events, tuple)
assert len(d.advisory_events) >= 1
for entry in d.advisory_events:
assert isinstance(entry, tuple) and len(entry) == 2
kind, body = entry
assert isinstance(kind, str)
assert isinstance(body, dict)
# No mutation API was called — verified by `memory_proposals` /
# `selfmodel_proposals` being plain tuples of frozen dataclasses
# (no side-effects possible).
assert isinstance(d.memory_proposals, tuple)
assert isinstance(d.selfmodel_proposals, tuple)
# ---------------------------------------------------------------------
# §16.2 #14 — test_zero_mean_divergence_does_not_trigger_phase_1
# ---------------------------------------------------------------------
def test_zero_mean_divergence_does_not_trigger_phase_1():
"""Mean divergence = 0 + zero stddev → no signal → no trigger.
Absolute-stddev fallback path used when mean is zero but variance
is present."""
# All-zero samples → mean = 0, stddev = 0 → no_variance_signal.
triggered, reason = phase_1_trigger_check([0.0] * 40)
assert triggered is False
assert reason == "no_variance_signal"
# Mean = 0 but nonzero stddev below the absolute threshold.
samples = [0.0, 0.001, -0.001, 0.0] * 10 # n=40, mean=0, tiny stddev
triggered, reason = phase_1_trigger_check(samples)
assert triggered is False
assert reason == "absolute_stddev_path"
# Mean = 0 with nonzero stddev above the absolute threshold → fires.
# Balance positive and negative so the mean stays at 0.
samples = [0.2, -0.2] * 20 # n=40, mean=0, stddev=0.2 >= 0.05.
triggered, reason = phase_1_trigger_check(samples)
assert triggered is True
assert reason == "absolute_stddev_path"
# ---------------------------------------------------------------------
# §16.2 #15 — test_small_sample_does_not_trigger_phase_1
# ---------------------------------------------------------------------
def test_small_sample_does_not_trigger_phase_1():
"""sample_count < N_min (= 30) → no trigger regardless of variance.
Tiny samples are noise."""
# 29 samples — high variance, below the floor.
rng = random.Random(0)
samples = [rng.uniform(0.0, 1.0) for _ in range(N_MIN_TRIGGER_2 - 1)]
triggered, reason = phase_1_trigger_check(samples)
assert triggered is False
assert reason == "small_sample"
# Empty sample — also small.
triggered, reason = phase_1_trigger_check([])
assert triggered is False
assert reason == "small_sample"
# ---------------------------------------------------------------------
# §16.2 #16 — test_propose_not_mutate_memory_root
# ---------------------------------------------------------------------
def test_propose_not_mutate_memory_root():
"""A vetoed branch with regression_penalty > 0 yields a
MemoryRootUpdateProposal record. Type is the proposal dataclass
(no mutation API)."""
branches = (
_b(
"regressed",
hard_vetoes=("hard_regression",),
regression_penalty=0.4,
),
_b("live", delta_5s=0.1),
)
d = controller_decide(_inp(branches))
assert len(d.memory_proposals) == 1
prop = d.memory_proposals[0]
assert isinstance(prop, MemoryRootUpdateProposal)
assert prop.branch_id == "regressed"
assert "BRANCH_REGRESSED" in prop.proposed_invalidation_tags
# Frozen dataclass — no mutation surface.
with pytest.raises((AttributeError, TypeError)):
prop.proposed_invalidation_tags = () # type: ignore[misc]
# ---------------------------------------------------------------------
# §16.2 #17 — test_propose_not_mutate_self_model
# ---------------------------------------------------------------------
def test_propose_not_mutate_self_model():
"""A selected branch with selfmodel_calibration_gain != 0 yields a
SelfModelUpdateProposal record. Type is the proposal dataclass
(no SelfModel mutation API call)."""
branches = (
_b(
"calibrating",
delta_5s=0.5,
delta_5t=0.5,
delta_5f=0.5,
selfmodel_calibration_gain=0.15,
),
)
d = controller_decide(_inp(branches))
assert d.selected_branch_id == "calibrating"
assert len(d.selfmodel_proposals) == 1
prop = d.selfmodel_proposals[0]
assert isinstance(prop, SelfModelUpdateProposal)
assert prop.calibration_delta == 0.15
assert prop.reason == "controller_phase_1_calibration"
with pytest.raises((AttributeError, TypeError)):
prop.calibration_delta = 0.0 # type: ignore[misc]
# ---------------------------------------------------------------------
# Additional sanity checks (not in §16.2 — harden the pure-function
# contract)
# ---------------------------------------------------------------------
def test_idempotent_byte_identical_output():
"""Pure function: two invocations with identical inputs produce
byte-identical ControllerDecision."""
branches = (
_b("a", delta_5s=0.1, delta_5t=0.2, delta_5f=0.3),
_b("b", delta_5s=-0.1, delta_5t=0.0, delta_5f=0.2, capital_cost=1.0),
)
inp = _inp(branches)
d1 = controller_decide(inp)
d2 = controller_decide(inp)
assert d1 == d2
def test_weight_profiles_registry_complete():
"""All three named profiles present and structurally distinct."""
assert set(WEIGHT_PROFILES) == {"safe", "conservative", "exploratory"}
safe = WEIGHT_PROFILES["safe"]
cons = WEIGHT_PROFILES["conservative"]
expl = WEIGHT_PROFILES["exploratory"]
# §15.2 deltas
assert cons.gamma_5f == 1.0
assert cons.lambda_capital_cost == 1.5
assert cons.xi_security_risk == 4.0
assert cons.eta_softmax_temperature == 0.75
# §15.3 deltas
assert expl.gamma_5f == 1.5
assert expl.nu_witness_divergence == 0.75
assert expl.lambda_capital_cost == 0.5
assert expl.eta_softmax_temperature == 1.5
# Safe baseline
assert safe.gamma_5f == 1.25
def test_phase_1_trigger_check_positive_path():
"""Coefficient-of-variation path fires when the sample has clear
nonzero mean + comparable stddev."""
rng = random.Random(1)
# Mean ≈ 0.3, stddev ≈ 0.2 → CoV ≈ 0.67 >= 0.5 → fires.
samples = [rng.uniform(0.0, 0.6) for _ in range(60)]
triggered, reason = phase_1_trigger_check(samples)
# Mean and stddev depend on the seeded RNG, but the seed is
# fixed → result is deterministic. Sanity-check the reason set.
assert reason in {
"variance_threshold",
"variance_below_threshold",
"absolute_stddev_path",
}
if triggered:
assert reason in {"variance_threshold", "absolute_stddev_path"}
def test_empty_branches_returns_unknown():
"""No candidate branches → UNKNOWN with NO_CANDIDATE_BRANCHES note
(§14 row 1)."""
d = controller_decide(_inp(branches=()))
assert d.label == "UNKNOWN"
assert d.selected_branch_id is None
assert "NO_CANDIDATE_BRANCHES" in d.notes
def test_decision_is_a_controllerdecision():
"""Output type sanity."""
branches = (_b("a", delta_5s=0.1),)
d = controller_decide(_inp(branches))
assert isinstance(d, ControllerDecision)
# ---------------------------------------------------------------------
# §14 row 4 — Hermes-saturation guard
# ---------------------------------------------------------------------
def test_hermes_saturation_returns_deferred():
"""When hermes_utilization >= budget, no headroom remains; the
controller defers regardless of branch utility (§14 row 4)."""
branches = (_b("good", delta_5s=0.5, delta_5f=0.5),)
inp = _inp(branches, budget=4, hermes_utilization=4)
d = controller_decide(inp)
assert d.label == "DEFERRED"
assert "HERMES_SATURATED" in d.notes
# Allocations stay all-zero — no compute spent under saturation.
assert all(v == 0.0 for v in d.allocations.values())
# Advisory event records the saturation context for Phase 2 audit.
kinds = [k for (k, _) in d.advisory_events]
assert "controller_decision" in kinds
body = next(b for (k, b) in d.advisory_events if k == "controller_decision")
assert body["reason"] == "HERMES_SATURATED"
assert body["hermes_utilization"] == 4
assert body["budget"] == 4
def test_hermes_saturation_above_budget_also_defers():
"""utilization > budget (overflow case) still defers — the guard
is ``utilization >= budget``, not just equality."""
branches = (_b("a"),)
d = controller_decide(_inp(branches, budget=4, hermes_utilization=8))
assert d.label == "DEFERRED"
assert "HERMES_SATURATED" in d.notes
def test_hermes_saturation_does_not_fire_when_headroom_exists():
"""utilization < budget → controller proceeds normally."""
branches = (_b("good", delta_5f=0.5, payoff_b=10.0),)
inp = _inp(branches, budget=4, hermes_utilization=1)
d = controller_decide(inp)
assert d.label != "DEFERRED" or "HERMES_SATURATED" not in d.notes
# ---------------------------------------------------------------------
# §6 + §14 — additional veto-class tests (replay/soft-hash) +
# defensive NUMERIC_INSTABILITY path
# ---------------------------------------------------------------------
def test_replay_window_unbounded_escalates():
"""§14 row + §6: `replay_window_unbounded` is an ESCALATE-class
veto (T3 still open per #000036)."""
branches = (_b("a", hard_vetoes=("replay_window_unbounded",)),)
d = controller_decide(_inp(branches))
assert d.label == "ESCALATE"
assert "ALL_BRANCHES_VETOED" in d.notes
def test_soft_hash_signal_quarantines():
"""§14 row 10 + §6: `soft_hash_signal` above threshold is a
QUARANTINE-class veto."""
branches = (_b("a", hard_vetoes=("soft_hash_signal",)),)
d = controller_decide(_inp(branches))
assert d.label == "QUARANTINE"
def test_escalate_dominates_quarantine_dominates_reject():
"""When a chunk has mixed veto classes the cascade returns the
most-severe class present (§6 fail-loud ordering)."""
branches = (
_b("escalate", hard_vetoes=("replay_window_unbounded",)),
_b("quarantine", hard_vetoes=("cache_drift",)),
_b("reject", hard_vetoes=("verifier_failure",)),
)
d = controller_decide(_inp(branches))
assert d.label == "ESCALATE"
def test_quarantine_dominates_reject():
branches = (
_b("quarantine", hard_vetoes=("schema_mismatch",)),
_b("reject", hard_vetoes=("hard_regression",)),
)
d = controller_decide(_inp(branches))
assert d.label == "QUARANTINE"
# ---------------------------------------------------------------------
# §13 Step 11 — falsification-fixture proposal emission
# ---------------------------------------------------------------------
def test_high_divergence_emits_falsification_proposal():
"""A branch with witness_divergence ≥ threshold gets a
FalsificationFixtureProposal record — the §3 'divergence →
candidate fixture' funnel for 5F mining (#000025)."""
branches = (
_b("a", delta_5f=0.1, payoff_b=5.0),
_b("diverged", witness_divergence=0.8, delta_5f=0.05),
)
d = controller_decide(_inp(branches))
proposals = d.falsification_proposals
assert any(p.branch_id == "diverged" for p in proposals)
p = next(p for p in proposals if p.branch_id == "diverged")
assert p.witness_divergence == 0.8
assert p.reason == "WITNESS_DIVERGENCE_EXCEEDS_THRESHOLD"
assert p.organism_root == "Ω0"
def test_low_divergence_does_not_emit_proposal():
"""A branch under threshold doesn't produce a fixture proposal."""
branches = (_b("a", witness_divergence=0.1, delta_5f=0.1, payoff_b=5.0),)
d = controller_decide(_inp(branches))
assert d.falsification_proposals == ()
def test_falsification_threshold_is_weight_tunable():
"""ControllerWeights.falsification_divergence_threshold gates the
proposal — lowering it surfaces more proposals."""
w_sensitive = ControllerWeights(
falsification_divergence_threshold=0.05
)
branches = (_b("a", witness_divergence=0.10, delta_5f=0.1, payoff_b=5.0),)
d = controller_decide(_inp(branches, weights=w_sensitive))
assert len(d.falsification_proposals) == 1
assert d.falsification_proposals[0].branch_id == "a"
def test_falsification_proposal_emitted_even_on_vetoed_branch():
"""Divergence-as-falsification-signal is independent of the
accept/reject decision — even a vetoed branch can produce a
fixture proposal when its divergence is high."""
branches = (
_b("vetoed", hard_vetoes=("cache_drift",), witness_divergence=0.9),
)
d = controller_decide(_inp(branches))
# All-vetoed cascade still emits the proposal — Phase 1 surfaces
# the divergence signal at every level.
# (Implementation choice: proposals emit from ALL branches, not
# just selected. Vetoed-AND-diverged is a 1-fixture deliverable.)
assert any(p.branch_id == "vetoed" for p in d.falsification_proposals)
# ---------------------------------------------------------------------
# §14 + §5 — entropy gating becomes weight-tunable (h_low / h_high)
# ---------------------------------------------------------------------
def test_h_low_h_high_are_weight_tunable():
"""Moving entropy thresholds to ControllerWeights lets deployments
override the §5 default (0.3 / 0.7) without patching the module."""
# Push h_high above 1.0 so even max-entropy chunks ACCEPT.
w = ControllerWeights(h_low=0.5, h_high=1.1)
branches = (
_b("a", delta_5f=0.10, payoff_b=10.0),
_b("b", delta_5f=0.09, payoff_b=10.0),
)
d = controller_decide(_inp(branches, weights=w))
# With h_high=1.1 the high-entropy branch never trips MARGINAL.
assert d.label != "MARGINAL"
def test_kappa_memory_is_weight_tunable():
"""ControllerWeights.kappa_memory governs the escalate-threshold.
Utility math (default safe weights):
U = γ·δ5F ω·memory_invalidation
= 1.25·0.5 2.0·0.15
= 0.625 0.3 = +0.325 > 0
So we clear the NON_POSITIVE_UTILITY guard, and reach the kappa
branch with `memory_invalidation=0.15 >= kappa_memory=0.1` →
ESCALATE.
"""
w = ControllerWeights(kappa_memory=0.1)
branches = (
_b("a", delta_5f=0.5, memory_invalidation=0.15, payoff_b=10.0),
)
d = controller_decide(_inp(branches, weights=w))
assert d.label == "ESCALATE"
assert "MEMORY_INVALIDATION_ABOVE_KAPPA" in d.notes
# ---------------------------------------------------------------------
# Module-level constants stay back-compat exports
# ---------------------------------------------------------------------
def test_module_level_constants_match_default_weights():
"""``H_LOW``, ``H_HIGH``, ``KAPPA_MEMORY`` are kept at module level
for back-compat with callers that referenced them pre-v2; the
weight-field defaults must match exactly so behavior is identical
when neither is overridden."""
w = ControllerWeights()
assert w.h_low == H_LOW
assert w.h_high == H_HIGH
assert w.kappa_memory == KAPPA_MEMORY