The #000025 §10.14 calibration showed _delta_5{s,t,f} mean over a battery's 5 subs, so a single-sub gain weighs 1/5 of face value (the 5× dilution). #000047 ships the knob to pick the aggregation, default unchanged. WeightSet.delta_aggregator ∈ {"mean","max","sum"} (default "mean") — a categorical field, validated in __post_init__ against DELTA_AGGREGATORS; from_dict takes it as a string. Default unchanged → ScoredFork output byte-identical → no fork_score.ESTIMATOR_VERSION bump. fork_score._aggregate(deltas, how): mean = arithmetic mean, max = max(0.0, max_i Δ_i), sum = Σ Δ_i; empty → 0.0. _delta_5s/_delta_5t/ _delta_5f take an aggregator arg (default "mean"); the 5F efficiency bonus is added after the aggregated base (aggregator-independent). fork_score passes weights.delta_aggregator. The per-sub HARD_REGRESSION_FLOOR flags are computed before aggregation, so a single-sub regression still forces REJECT under max/sum. The chosen aggregator is recorded in ScoredFork.weights["delta_aggregator"] (via WeightSet.as_dict()); fork_score_branches traceability stays via the opaque weights_id — no schema migration. bench/scripts/fivef_threshold_calibration.py gained §5 — runs the #000046 below-ceiling pack (5f/falsification at 0.333) and shows the verdict / γ·Δ5f under each aggregator; bench/results/5f-threshold- calibration-2026-05-11.md §5 is the captured record. Default stays "mean" — the conservative, noise-robust, regression-symmetric choice matching docs/bench-maxing.md's per-rate floor framing; v8 picks max/sum per-deployment. Tests: 8 new in tests/test_fork_score.py + 1 anchor in tests/test_fivef_threshold_calibration.py; tests/test_weights.py as_dict field-set test updated to include delta_aggregator; test_fork_score.py AUTOCOUNT tags (#000012 §286, warrant-substrate- cookbook.md ×2) bumped 23 → 31. #000047 closed; #000012 §8 §3 + TICKETS.md row updated. Full suite: 2330 passed, 28 skipped.
182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
"""Tests for ``arborist.substrate.weights`` — WeightSet defaults +
|
||
``from_dict`` adapter (#000012 Phase 1a).
|
||
|
||
Pure-stdlib dataclass + classmethod-style adapter; small surface.
|
||
This pins:
|
||
|
||
- documented defaults (lifted from the module docstring)
|
||
- greek-letter and Python-safe key aliases ("lambda" ↔ "lambda_")
|
||
- missing-keys fall-through to defaults
|
||
- extra-keys silently ignored
|
||
- string values coerce to float
|
||
- frozen-dataclass invariant (mutation raises)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import dataclasses
|
||
|
||
import pytest
|
||
|
||
from arborist.substrate.weights import (
|
||
DEFAULT_WEIGHTS,
|
||
WeightSet,
|
||
from_dict,
|
||
)
|
||
|
||
|
||
# --- defaults pinned to the docstring ------------------------------
|
||
|
||
|
||
def test_default_weights_match_documented_values():
|
||
"""Each weight in the dataclass docstring is pinned. If a future
|
||
PR changes a default, this test fires and forces an explicit
|
||
update of both the docstring AND the test."""
|
||
assert DEFAULT_WEIGHTS.alpha == 1.0
|
||
assert DEFAULT_WEIGHTS.beta == 1.0
|
||
assert DEFAULT_WEIGHTS.gamma == 1.0
|
||
assert DEFAULT_WEIGHTS.delta == 0.5
|
||
assert DEFAULT_WEIGHTS.epsilon == 0.3
|
||
assert DEFAULT_WEIGHTS.zeta == 0.0 # validator-diversity off in single-validator
|
||
assert DEFAULT_WEIGHTS.eta == 2.0 # regression > improvement weight
|
||
assert DEFAULT_WEIGHTS.theta == 0.5
|
||
assert DEFAULT_WEIGHTS.iota == 0.0 # reserved
|
||
assert DEFAULT_WEIGHTS.kappa == 0.0 # reserved
|
||
assert DEFAULT_WEIGHTS.lambda_ == 0.5
|
||
|
||
|
||
def test_eta_is_heavier_than_alpha():
|
||
"""Documented invariant from the docstring:
|
||
'η (RegressionPenalty) heavier than improvement weights so the
|
||
scorer is biased toward refusing regressions.'"""
|
||
assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.alpha
|
||
assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.beta
|
||
assert DEFAULT_WEIGHTS.eta > DEFAULT_WEIGHTS.gamma
|
||
|
||
|
||
def test_three_batteries_equal_by_default():
|
||
"""Documented invariant: 'α = β = γ (3 batteries weighted equally
|
||
by default).'"""
|
||
assert DEFAULT_WEIGHTS.alpha == DEFAULT_WEIGHTS.beta == DEFAULT_WEIGHTS.gamma
|
||
|
||
|
||
def test_reserved_weights_are_zero_phase_1a():
|
||
"""Phase 1a discipline: validator-diversity / security / complexity
|
||
are zero by default. v8 multi-validator deployments must opt in."""
|
||
assert DEFAULT_WEIGHTS.zeta == 0.0
|
||
assert DEFAULT_WEIGHTS.iota == 0.0
|
||
assert DEFAULT_WEIGHTS.kappa == 0.0
|
||
|
||
|
||
# --- as_dict --------------------------------------------------------
|
||
|
||
|
||
def test_as_dict_returns_all_fields():
|
||
d = DEFAULT_WEIGHTS.as_dict()
|
||
expected_keys = {
|
||
"alpha", "beta", "gamma", "delta", "epsilon", "zeta",
|
||
"eta", "theta", "iota", "kappa", "lambda_",
|
||
"delta_aggregator", # #000047 — categorical, not a numeric weight
|
||
}
|
||
assert set(d.keys()) == expected_keys
|
||
assert d["delta_aggregator"] == "mean"
|
||
|
||
|
||
def test_as_dict_uses_lambda_underscore_key():
|
||
"""``lambda`` is a Python keyword → dataclass uses ``lambda_``;
|
||
as_dict() carries it forward unchanged. Callers serializing to
|
||
JSON / YAML rename to bare ``lambda`` if needed."""
|
||
d = DEFAULT_WEIGHTS.as_dict()
|
||
assert "lambda_" in d
|
||
assert "lambda" not in d
|
||
|
||
|
||
# --- from_dict adapter ---------------------------------------------
|
||
|
||
|
||
def test_from_dict_uses_greek_letter_keys():
|
||
ws = from_dict({"alpha": 0.7, "beta": 0.8, "gamma": 0.9})
|
||
assert ws.alpha == 0.7
|
||
assert ws.beta == 0.8
|
||
assert ws.gamma == 0.9
|
||
# Unspecified keys fall through to defaults.
|
||
assert ws.delta == DEFAULT_WEIGHTS.delta
|
||
|
||
|
||
def test_from_dict_translates_lambda_to_lambda_underscore():
|
||
"""JSON/YAML configs use bare ``lambda`` (the Greek letter, not
|
||
the keyword). from_dict translates to the Python-safe field
|
||
name ``lambda_``."""
|
||
ws = from_dict({"lambda": 0.99})
|
||
assert ws.lambda_ == 0.99
|
||
# Bare 'lambda' did not survive into a hidden field.
|
||
assert "lambda" not in ws.as_dict()
|
||
|
||
|
||
def test_from_dict_lambda_underscore_key_also_accepted():
|
||
"""For Python callers writing config dicts directly, ``lambda_``
|
||
is the natural key name; from_dict accepts it too."""
|
||
ws = from_dict({"lambda_": 0.42})
|
||
assert ws.lambda_ == 0.42
|
||
|
||
|
||
def test_from_dict_missing_keys_fall_through_to_defaults():
|
||
"""Empty dict → identical to DEFAULT_WEIGHTS."""
|
||
ws = from_dict({})
|
||
assert ws == DEFAULT_WEIGHTS
|
||
|
||
|
||
def test_from_dict_partial_override_preserves_other_defaults():
|
||
ws = from_dict({"alpha": 0.5})
|
||
assert ws.alpha == 0.5
|
||
# Every other field is unchanged.
|
||
assert ws.beta == DEFAULT_WEIGHTS.beta
|
||
assert ws.eta == DEFAULT_WEIGHTS.eta
|
||
assert ws.lambda_ == DEFAULT_WEIGHTS.lambda_
|
||
|
||
|
||
def test_from_dict_ignores_unknown_keys():
|
||
"""Defensive: extra keys (e.g. "mu" — never defined) are
|
||
silently dropped rather than raising. Forward-compat with
|
||
config files that include vendor-specific tags."""
|
||
ws = from_dict({"alpha": 0.6, "mu": 1.234, "extra_metadata": "ignore"})
|
||
assert ws.alpha == 0.6
|
||
# mu / extra_metadata silently dropped.
|
||
assert not hasattr(ws, "mu")
|
||
|
||
|
||
def test_from_dict_coerces_string_to_float():
|
||
"""JSON/YAML may pass numeric weights as strings. from_dict
|
||
coerces via float()."""
|
||
ws = from_dict({"alpha": "0.7", "lambda": "0.3"})
|
||
assert ws.alpha == 0.7
|
||
assert ws.lambda_ == 0.3
|
||
|
||
|
||
def test_from_dict_int_input_becomes_float():
|
||
ws = from_dict({"alpha": 1})
|
||
assert ws.alpha == 1.0
|
||
assert isinstance(ws.alpha, float)
|
||
|
||
|
||
# --- frozen invariant ----------------------------------------------
|
||
|
||
|
||
def test_weight_set_is_frozen():
|
||
"""WeightSet is dataclass(frozen=True) — mutation raises
|
||
FrozenInstanceError. Pinned so a future @dataclass change
|
||
forces this test to be updated explicitly."""
|
||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||
DEFAULT_WEIGHTS.alpha = 99.0
|
||
|
||
|
||
def test_weight_sets_equal_by_value():
|
||
"""Two WeightSet instances with identical fields compare equal —
|
||
standard dataclass equality. Used by tests + audit-trail
|
||
serialization."""
|
||
a = WeightSet(alpha=0.5, beta=0.6)
|
||
b = WeightSet(alpha=0.5, beta=0.6)
|
||
assert a == b
|
||
# Different values → not equal.
|
||
c = WeightSet(alpha=0.5, beta=0.7)
|
||
assert a != c
|