#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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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']}")
|
||||
|
|
|
|||
|
|
@ -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