#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:
parent
61424370bd
commit
f9f5ae459b
4 changed files with 330 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue