#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).
This commit is contained in:
russell@unturf.com 2026-05-10 16:56:18 -04:00
parent 61424370bd
commit f9f5ae459b
No known key found for this signature in database
4 changed files with 330 additions and 15 deletions

View file

@ -606,3 +606,199 @@ def test_decision_is_a_controllerdecision():
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