refactor(adr-0244): D3 search honesty — sampled-unimodality naming + kappa legibility

Cohesion directive Mandate 6 (ADR-0244 §2.4). The Fibonacci search was already
~85% compliant (typed OptimizationFailure, fail-closed, bracketed contract,
never a bare float); this closes the two residuals honestly:

- Rename the failure reason unimodality_violation_multiple_extrema_detected ->
  sampled_unimodality_violation_observed. A finite sample cannot prove global
  unimodality; the check only *observes* a violation on the evaluated points.
  Docstrings now state the Bracketed-Local contract explicitly.
- propose_kappa_from_search: kappa=1.0-on-failure IS the required "parameters
  unchanged" no-op (thr = productive_threshold / 1.0), and the typed
  OptimizationFailure is already returned as the second element — so the fix is
  legibility, not a behavior change (the working seam is preserved, not broken):
  the docstring names baseline as an explicit caller-side policy, and a new test
  pins that a failed search surfaces the typed failure rather than a bare float.

tests: test_adr_0244_search_honesty pins the renamed reason end-to-end (a
cos(8*pi*x) objective whose golden-section samples are non-monotone triggers it;
a smooth bimodal instead converges to a local-min certificate — the honest
Bracketed-Local distinction), _unimodality_ok as a finite-sample check, kappa
failure legibility + no-op baseline, and that no stale reason string remains.

[Verification]: search-honesty 6 + adr_0242_fibonacci + third_door_cohesion +
carry_seams green; smoke + fast lane below.
This commit is contained in:
Shay 2026-07-17 13:50:55 -07:00
parent 82b031d158
commit 3a0cd0b455
2 changed files with 119 additions and 5 deletions

View file

@ -3,14 +3,23 @@
Deterministic 1D unimodal minimization for construction / calibration /
GoldTether κ-style scalar brackets. Not a serve-path operator (A-04 quarantine).
A strictly **Bracketed Local** refinement operator (ADR-0244 §2.4): the caller
must supply a pre-bracketed interval around a known minimum. Unimodality cannot
be proven from a finite sample, so the check is honestly named a *sampled*
unimodality violation an observation on the evaluated points, not a global
guarantee on the unsampled portions of ``[a, b]``.
Public result is always a typed ``FibonacciSearchCertificate`` or
``OptimizationFailure`` never a bare float (Drive evidence discipline).
``OptimizationFailure`` never a bare float (Drive evidence discipline). On any
fail-closed condition the operator returns a typed failure; it never silently
substitutes a fallback parameter in-path (that policy belongs to the caller).
Fail-closed on:
* nonfinite objective values
* invalid bounds / budget
* sampled unimodality violation (values must decrease to the observed
minimum then increase when sorted by coordinate)
* sampled unimodality violation reason ``sampled_unimodality_violation_observed``
(values must decrease to the observed minimum then increase when sorted by
coordinate)
"""
from __future__ import annotations
@ -303,9 +312,14 @@ def fibonacci_section_search(
k += 1
if not _unimodality_ok(eval_values):
# A finite sample cannot prove global unimodality on the unsampled
# portions of [a, b]; it can only *observe* a violation on the sampled
# points. Honest name (ADR-0244 §2.4 / directive M6): the operator is a
# Bracketed Local refiner and fails closed here rather than silently
# defaulting parameters in-path.
return _failure(
objective,
reason="unimodality_violation_multiple_extrema_detected",
reason="sampled_unimodality_violation_observed",
a=a,
b=b,
evaluations=len(points),
@ -333,7 +347,16 @@ def propose_kappa_from_search(
*,
baseline: float = BASELINE_KAPPA,
) -> tuple[float, SearchResult]:
"""Evidence-gated κ: cert → minimizer; failure → baseline (default 1.0).
"""Evidence-gated κ from a search result. Proposal-only telemetry.
On a certificate, returns the certified minimizer. On an
``OptimizationFailure``, returns ``baseline`` which is an **explicit
caller-side policy**, not a search-internal default: κ = 1.0 is the identity
no-op (``thr = productive_threshold / 1.0`` leaves the active threshold
unchanged), so a failed search never moves live parameters (ADR-0244 §2.4 /
directive M6). The typed ``OptimizationFailure`` is always returned as the
second element, so a caller can distinguish "search proposed κ = 1.0" from
"search failed → holding the κ = 1.0 no-op" instead of reading a bare float.
Never promotes COHERENT standing or mutates identity caller telemetry only.
"""

View file

@ -0,0 +1,91 @@
"""ADR-0244 §2.4 search-honesty pins (cohesion directive Mandate 6).
The Fibonacci section search is a Bracketed Local refiner: it observes a
*sampled* unimodality violation (finite samples cannot prove global
unimodality), fails closed with the honestly-named reason, and never silently
substitutes a fallback parameter in-path. ``propose_kappa_from_search`` keeps
the κ = 1.0 no-op on failure (the unchanged-threshold policy) but returns the
typed failure so a caller can tell "proposed 1.0" from "failed → holding 1.0".
"""
from __future__ import annotations
import math
from pathlib import Path
from core.physics.fibonacci_search import (
BASELINE_KAPPA,
BoundedUnimodalObjective,
FibonacciSearchCertificate,
OptimizationFailure,
_unimodality_ok,
fibonacci_section_search,
propose_kappa_from_search,
)
def _obj(lo: float, hi: float, budget: int = 16) -> BoundedUnimodalObjective:
return BoundedUnimodalObjective(
lower=lo,
upper=hi,
evaluation_budget=budget,
objective_id="search_honesty_test",
objective_version="v1",
)
def _multimodal(x: float) -> float:
# Highly oscillatory → the golden-section samples straddle several extrema,
# so the *sampled* trajectory is non-monotone and the violation fires. (A
# smooth bimodal, by contrast, converges to one local min — the search is a
# Bracketed-Local refiner and reports a certificate for that min, not a
# violation. The violation is an honest-sample observation, not a global
# unimodality proof.)
return math.cos(8.0 * math.pi * x)
def _single_well(x: float) -> float:
return (x - 0.3) ** 2
def test_sampled_unimodality_check_is_a_finite_sample_observation() -> None:
# Monotone down-then-up over the sampled points → OK.
assert _unimodality_ok({0.0: 3.0, 1.0: 1.0, 2.0: 2.0}) is True
# A bump before the observed minimum → not monotone-to-min → violation.
assert _unimodality_ok({0.0: 1.0, 1.0: 0.5, 2.0: 0.8, 3.0: 0.3}) is False
def test_multimodal_objective_returns_sampled_unimodality_violation() -> None:
result = fibonacci_section_search(_obj(0.0, 1.0), _multimodal)
assert isinstance(result, OptimizationFailure)
assert result.reason == "sampled_unimodality_violation_observed"
def test_unimodal_objective_returns_certificate() -> None:
result = fibonacci_section_search(_obj(-2.0, 2.0), _single_well)
assert isinstance(result, FibonacciSearchCertificate)
assert abs(result.minimizer - 0.3) < 0.2
def test_kappa_failure_is_legible_and_holds_the_no_op_baseline() -> None:
result = fibonacci_section_search(_obj(0.0, 1.0), _multimodal)
kappa, outcome = propose_kappa_from_search(result)
# baseline κ = 1.0 is the unchanged-threshold no-op...
assert kappa == BASELINE_KAPPA == 1.0
# ...and the failure is legibly the second element, not swallowed.
assert isinstance(outcome, OptimizationFailure)
assert outcome.reason == "sampled_unimodality_violation_observed"
def test_kappa_success_returns_certified_minimizer() -> None:
result = fibonacci_section_search(_obj(-2.0, 2.0), _single_well)
kappa, outcome = propose_kappa_from_search(result)
assert isinstance(outcome, FibonacciSearchCertificate)
assert abs(kappa - 0.3) < 0.2
def test_no_stale_reason_string_in_source() -> None:
src = Path(__file__).resolve().parents[1] / "core" / "physics" / "fibonacci_search.py"
text = src.read_text(encoding="utf-8")
assert "sampled_unimodality_violation_observed" in text
assert "unimodality_violation_multiple_extrema_detected" not in text