#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

@ -183,6 +183,21 @@ class ControllerWeights:
min_difficulty: float = 0.0
max_difficulty: float = 10.0
# §5 entropy gating + §14 memory-invalidation κ.
# Promoted from module-level constants to weight fields so
# deployments can tune them via the profile mechanism without
# patching the module. Defaults match :data:`H_LOW` /
# :data:`H_HIGH` / :data:`KAPPA_MEMORY`.
h_low: float = 0.3
h_high: float = 0.7
kappa_memory: float = 0.5
# §13 Step 11 — falsification-fixture proposal threshold. When a
# branch's ``witness_divergence`` exceeds this, the controller
# emits a :class:`FalsificationFixtureProposal` for downstream
# 5F-fixture mining. 0.5 = "divergence dominates verifier signal."
falsification_divergence_threshold: float = 0.5
@dataclass(frozen=True)
class ControllerInput:
@ -236,6 +251,23 @@ class SelfModelUpdateProposal:
reason: str
@dataclass(frozen=True)
class FalsificationFixtureProposal:
"""§13 Step 11 — proposal record for a candidate 5F fixture.
Emitted when a branch's ``witness_divergence`` exceeds
:attr:`ControllerWeights.falsification_divergence_threshold`.
The 5F battery's falsification fixture-set (#000025) is the
intended sink. Phase 1 only proposes; downstream fixture
authors decide whether to admit the proposal.
"""
organism_root: str
branch_id: str
witness_divergence: float
reason: str
@dataclass(frozen=True)
class ControllerDecision:
"""§13 step-9 output. Advisory; Phase 2 persists ``advisory_events``."""
@ -249,6 +281,7 @@ class ControllerDecision:
notes: tuple[str, ...]
memory_proposals: tuple[MemoryRootUpdateProposal, ...] = ()
selfmodel_proposals: tuple[SelfModelUpdateProposal, ...] = ()
falsification_proposals: tuple[FalsificationFixtureProposal, ...] = ()
advisory_events: tuple[tuple[str, dict], ...] = ()
@ -483,6 +516,17 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
notes=("NO_CANDIDATE_BRANCHES",),
)
# §13 Step 11 — falsification-fixture proposals fire BEFORE the
# all-vetoed early-return. Divergence is an empirical signal
# independent of accept/reject; a vetoed-AND-diverged branch is
# still a 5F-fixture candidate. Emission lives in a helper so the
# all-vetoed and normal paths use identical logic.
falsification_proposals = _emit_falsification_proposals(
inp.organism_root,
branches,
threshold=inp.weights.falsification_divergence_threshold,
)
# ---- Step 1 + Step 3: hard-veto sweep ----
unvetoed = [b for b in branches if not b.hard_vetoes]
if not unvetoed:
@ -514,6 +558,7 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
entropy=0.0,
notes=("ALL_BRANCHES_VETOED",),
memory_proposals=memory_proposals,
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
@ -537,6 +582,36 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
# ---- Step 8: Kelly-bounded allocation under B ----
allocations: dict[str, float] = {b.branch_id: 0.0 for b in branches}
# §14 row 4: Hermes unavailable / saturated → DEFERRED. Modeled
# as ``hermes_utilization >= budget`` (no headroom for new calls).
# Sleep-time scheduling can pick the work back up once the
# endpoint frees up.
if inp.hermes_utilization >= inp.budget and inp.budget > 0:
advisory_events = (
(
"controller_decision",
{
"organism_root": inp.organism_root,
"label": "DEFERRED",
"reason": "HERMES_SATURATED",
"entropy": h_norm,
"hermes_utilization": inp.hermes_utilization,
"budget": inp.budget,
},
),
)
return ControllerDecision(
selected_branch_id=None,
label="DEFERRED",
allocations=allocations,
difficulty_next=difficulty_next,
veto_reasons=veto_reasons,
entropy=h_norm,
notes=("HERMES_SATURATED",),
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
# §14 row: Budget B = 0 → DEFERRED for everything.
if inp.budget == 0:
advisory_events = (
@ -558,6 +633,7 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
veto_reasons=veto_reasons,
entropy=h_norm,
notes=("ZERO_BUDGET",),
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
@ -607,6 +683,7 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
veto_reasons=veto_reasons,
entropy=h_norm,
notes=("ZERO_RAW_ALLOCATION",),
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
@ -633,6 +710,7 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
veto_reasons=veto_reasons,
entropy=h_norm,
notes=("NUMERIC_INSTABILITY",),
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
@ -656,13 +734,13 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
elif selected_u <= 0.0:
label = "REJECT"
reason_notes.append("NON_POSITIVE_UTILITY")
elif selected.memory_invalidation >= KAPPA_MEMORY:
elif selected.memory_invalidation >= w.kappa_memory:
label = "ESCALATE"
reason_notes.append("MEMORY_INVALIDATION_ABOVE_KAPPA")
elif h_norm <= H_LOW:
elif h_norm <= w.h_low:
label = "ACCEPT"
reason_notes.append("LOW_ENTROPY")
elif h_norm >= H_HIGH:
elif h_norm >= w.h_high:
label = "MARGINAL"
reason_notes.append("HIGH_ENTROPY")
else:
@ -684,6 +762,11 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
),
)
# Falsification proposals were emitted at the top of
# controller_decide (above the all-vetoed cascade) so vetoed-AND-
# diverged branches still produce 5F fixture candidates. The value
# captured up there is the one we return.
# ---- Advisory event (§13 step 10; persisted by Phase 2) ----
advisory_events: tuple[tuple[str, dict], ...] = (
(
@ -728,6 +811,7 @@ def controller_decide(inp: ControllerInput) -> ControllerDecision:
notes=tuple(notes),
memory_proposals=memory_proposals,
selfmodel_proposals=selfmodel_proposals,
falsification_proposals=falsification_proposals,
advisory_events=advisory_events,
)
@ -754,6 +838,32 @@ def _emit_memory_proposals(
return tuple(proposals)
def _emit_falsification_proposals(
organism_root: str,
branches: Sequence[ControllerBranch],
*,
threshold: float,
) -> tuple[FalsificationFixtureProposal, ...]:
"""§13 Step 11 — propose 5F-fixture candidates from divergence.
Any branch whose ``witness_divergence`` meets or exceeds the
configured threshold gets a :class:`FalsificationFixtureProposal`
record. Emission is independent of veto state divergence is an
empirical signal that a fixture could capture regardless of
whether the controller would accept the branch otherwise.
"""
return tuple(
FalsificationFixtureProposal(
organism_root=organism_root,
branch_id=b.branch_id,
witness_divergence=b.witness_divergence,
reason="WITNESS_DIVERGENCE_EXCEEDS_THRESHOLD",
)
for b in branches
if b.witness_divergence >= threshold
)
__all__ = [
# Constants
"VETO_CLASS",
@ -772,6 +882,7 @@ __all__ = [
"ControllerDecision",
"MemoryRootUpdateProposal",
"SelfModelUpdateProposal",
"FalsificationFixtureProposal",
# Weight profile factories
"safe_weights",
"conservative_weights",

View file

@ -1,6 +1,6 @@
# Prometheus-Σ Phase 3 sleep-sweep dry-run
Generated: 2026-05-10T20:42:47Z UTC
Generated: 2026-05-10T20:54:12Z UTC
Script: `bench/scripts/prometheus_sigma_sweep_dryrun.py`
Ticket: #000037 Phase 3 (read-only simulation)
@ -14,16 +14,16 @@ Ticket: #000037 Phase 3 (read-only simulation)
## Target A — providence_cache sweep candidates
- Total candidates (rows older than τ_qa): **2379**
- Sweep chunks (size 4): 595
- Controller runtime: 23.20 ms
- Total candidates (rows older than τ_qa): **2381**
- Sweep chunks (size 4): 596
- Controller runtime: 51.93 ms
### Audit-mode distribution of candidates
| audit_mode | count |
|---|---|
| HYBRID | 872 |
| STRICT | 863 |
| HYBRID | 873 |
| STRICT | 864 |
| UNGROUNDED | 635 |
| CANONICAL_PROJECTION | 9 |
@ -32,7 +32,7 @@ Ticket: #000037 Phase 3 (read-only simulation)
| label | chunks |
|---|---|
| DEFERRED | 322 |
| REJECT | 269 |
| REJECT | 270 |
| MARGINAL | 4 |
### Veto kinds observed
@ -45,13 +45,14 @@ Ticket: #000037 Phase 3 (read-only simulation)
- MemoryRoot proposals: 0
- SelfModel proposals: 0
- Advisory event entries (would write to `controller_events` under Phase 2): 1141
- FalsificationFixture proposals (§13 Step 11 — high-divergence → 5F-fixture funnel): **576**
- Advisory event entries (would write to `controller_events` under Phase 2): 1144
## Target B — document sweep sample
- Total sampled candidates: **2000**
- Sweep chunks (size 4): 500
- Controller runtime: 26.17 ms
- Controller runtime: 43.37 ms
### Canonical-shape regex prefilter hits
@ -85,9 +86,9 @@ Ticket: #000037 Phase 3 (read-only simulation)
## Total simulation cost
- Total branches scored: 4379
- Total controller runtime: 49.37 ms
- Mean per-branch latency: 11.27 µs
- Total branches scored: 4381
- Total controller runtime: 95.30 ms
- Mean per-branch latency: 21.75 µs
## Findings & fixes (dry-run iteration log)

View file

@ -306,6 +306,7 @@ def sweep_target_a(shards_dir: Path, tau_qa_seconds: int, now: int, chunk_size:
veto_kinds: Counter[str] = Counter()
memory_proposals = 0
selfmodel_proposals = 0
falsification_proposals = 0
advisory_event_count = 0
candidates_total = 0
chunks_total = 0
@ -355,6 +356,7 @@ def sweep_target_a(shards_dir: Path, tau_qa_seconds: int, now: int, chunk_size:
label_counts[decision.label] += 1
memory_proposals += len(decision.memory_proposals)
selfmodel_proposals += len(decision.selfmodel_proposals)
falsification_proposals += len(decision.falsification_proposals)
advisory_event_count += len(decision.advisory_events)
for reasons in decision.veto_reasons.values():
for r in reasons:
@ -378,6 +380,7 @@ def sweep_target_a(shards_dir: Path, tau_qa_seconds: int, now: int, chunk_size:
label_counts[decision.label] += 1
memory_proposals += len(decision.memory_proposals)
selfmodel_proposals += len(decision.selfmodel_proposals)
falsification_proposals += len(decision.falsification_proposals)
advisory_event_count += len(decision.advisory_events)
for reasons in decision.veto_reasons.values():
for r in reasons:
@ -394,6 +397,7 @@ def sweep_target_a(shards_dir: Path, tau_qa_seconds: int, now: int, chunk_size:
"veto_kinds": dict(veto_kinds),
"memory_proposals_total": memory_proposals,
"selfmodel_proposals_total": selfmodel_proposals,
"falsification_proposals_total": falsification_proposals,
"advisory_event_count_total": advisory_event_count,
"runtime_total_ms": round(runtime_total_ms, 2),
"chunk_size": chunk_size,
@ -573,6 +577,9 @@ def render_markdown(a_results: dict, b_results: dict, opts: dict) -> str:
f"{a_results['memory_proposals_total']}")
lines.append(f"- SelfModel proposals: "
f"{a_results['selfmodel_proposals_total']}")
lines.append(f"- FalsificationFixture proposals "
f"(§13 Step 11 — high-divergence → 5F-fixture funnel): "
f"**{a_results.get('falsification_proposals_total', 0)}**")
lines.append(f"- Advisory event entries "
f"(would write to `controller_events` under Phase 2): "
f"{a_results['advisory_event_count_total']}")

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