Skip to content

ci: exact-head verification for #134 - #138

Closed
marcohost33-maker wants to merge 49 commits into
mainfrom
chatgpt/verify-134-exact-head
Closed

ci: exact-head verification for #134#138
marcohost33-maker wants to merge 49 commits into
mainfrom
chatgpt/verify-134-exact-head

Conversation

@marcohost33-maker

Copy link
Copy Markdown
Owner

CI-only verification of the exact commit used by stacked PR #134. Do not merge. The authoritative review remains #134; this draft exists only because main-targeted pull requests are the repository's full CI trigger.

claude and others added 30 commits August 14, 2026 15:31
…ity gate

Additive, report-only slice of issue #102 — no verdict/class/tier/confidence
behaviour changes (726-test suite green, anchors untouched).

- ClassificationResult.hypothesis_matrix: one entry per hypothesis over the
  FULL A1-A12 taxonomy (decision rungs + A12 fallback + schema-reserved
  A6/A7/A9) with supporting measurements, counterevidence, missing required
  evidence, an explicit fail-closed claim_floor and the per-class ordinal
  support_score. RESERVED/UNEVALUABLE floor to UNDEFINED, NOT_SUPPORTED to
  NOT_EXCLUDED (absence of support is not proof of absence, #70 A5),
  SUPPORTED to the verdict the hypothesis would receive as winner — so the
  winner's floor equals the reported verdict exactly (pinned).
- Declarative ladder: _ladder_spec() defines the priority chain as rungs of
  atomic _Condition predicates; _hypothesis_ladder (decision + shadow
  report) and hypothesis_evidence_matrix are both derived from that one
  spec, so the audit surface cannot drift from the decision.
- support_score (issue #102 rename option 1): honestly-named ordinal twin of
  the legacy confidence field; identical value, documented NON-probabilistic
  semantics; calibrated replacement stays gated on the preregistered
  validation design.
- Reachability gate: REACHABLE_A_CLASSES (9) is the coverage denominator;
  RESERVED_A_CLASSES exported; reserved classes excluded from claims.
- Docs (taxonomy explanation, README, tutorial), CHANGELOG, 57 new tests in
  tests/test_hypothesis_matrix.py; ruff + mypy clean.

No MANIFEST_SCHEMA bump: run-manifest contract untouched (additive report
fields with defaults). CITATION.cff untouched: no result or methodology
change — report surface only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…rage guard

CodeQL on PR #107 flagged the constant as an unused global: it was only
re-exported and read by tests. Instead of suppressing the finding, the
constant now does real work in the package: _reachable_coverage_check()
verifies at import that the ladder rungs plus the A12 fallback emit exactly
the reachable taxonomy (A_CLASSES minus RESERVED_A_CLASSES), failing closed
on drift. This is defense in depth for installed environments where the
AST-level reachability test never runs. Two new tests cover the guard's
accept and fail paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
## #108 — zero-mode separation (CHANGES NUMERICAL RESULTS on rescaled input)

The filter deciding which modes are the steady state used an absolute floor
|lambda| > 1e-10, duplicated across five modules. A Liouvillian carries rate
dimension, so this broke in both directions under L -> cL:

* small c: every genuine mode fell below the floor -> D1 collapsed to 0.0
  (firing the gapless F5 reach leg) and D19 overlap collapsed to 0.0, raising
  a FALSE A11/F4 Mpemba candidate on the highest-priority rung. Measured
  end-to-end: verdict moved A12/none NOT_EXCLUDED -> A11/F4 CANDIDATE between
  c=1 and c=1e-10 on a textbook amplitude-damped qubit.
* large c: the round-off zero mode (~eps*||L||) rose above the floor and was
  counted as genuine, yielding a NEGATIVE gap (-1.4e-6 at c=1e10), impossible
  for a GKSL generator by definition.

Canonical correction, no free parameter: spectral_zero_tolerance() derives the
threshold from the spectrum as ZERO_MODE_RTOL * max|lambda| (spectral radius:
homogeneous degree one, unitary-similarity invariant). At unit scale it
reproduces the historical floor, so all 21 anchors and the full suite stay
green byte-identically; only rescaled generators change. Legacy floor kept as
an explicit atol= opt-in on every affected function (mirrors #99). D1
deliberately does not clamp at zero: a positive Re(lambda) after the fix is a
genuinely unstable non-GKSL mode, and masking it would trade one silent
failure for another.

## Codex review follow-ups on #102 matrix

* support_score is None unless the hypothesis is SUPPORTED. _confidence answers
  'what grade as the winner' and keys on a subset of each firing rule, so a
  failed rung printed a confirmation-grade number beside its own
  counterevidence (kreiss=11, petermann_max=1 -> A3 at 0.85).
* Conditions whose own inputs are present are now evaluated even when a sibling
  is unevaluable, so a partial run keeps its usable evidence; the rung status
  still degrades to UNEVALUABLE.
* Trust boundary documented: the matrix reports the ev dict, it does not
  re-validate it; the typed EnsembleEvidence check lives at the diagnose()
  boundary, same contract as classify_mechanism.
* CITATION.cff: extended the 'Pending for the next cut' block per the
  2026-08-09 convention (unreleased capabilities must not enter the abstract of
  the cited release), flagging #108 as a results-changing correction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…red/optional keys, overflow-safe fits

## Zero-mode threshold now scales with eigensolver backward error (P1)

The first #108 fix used tol = 1e-10 * max|lambda|, which imposes a fixed
dynamic-range ceiling of 1e10 and discards genuine slow modes -- exactly the
METASTABLE (A5) regime the library exists to study. Reproduced: two damping
channels at rates 1.0 and 1e-12 (true gap 5e-13) reported a gap of 5e-1, wrong
by ten orders of magnitude.

Corrected to tol = ZERO_MODE_EPS_FACTOR * eps * max|lambda|. A computed
eigenvalue is uncertain to order eps*||L||, so that -- not a fixed ratio -- is
the scale on which 'indistinguishable from zero' is decided. Calibrated by
measurement across amplitude damping, Rabi-driven damping, dephasing, a
strongly non-normal near-defective generator and a 64-dim 3-qubit chain, each
at c in {1, 1e6, 1e12}: the numerical zero mode never exceeded 1.94 * eps *
max|lambda|, so the default factor keeps ~500x headroom while resolving genuine
modes further down than the pre-#108 absolute floor did at unit scale.

## Fitter is overflow-safe on long time grids (P1, was the CI failure)

M0-M3b now clip the exponent. A slow generator in small rate units yields a
legitimately long grid (t up to 5e10); the optimiser probes a negative decay
rate and np.exp overflows -- inf/nan residuals give least-squares no gradient
to step back from, so the failure mode is silent non-convergence on valid
input, not merely a log line. The bound is 345 rather than the ~709 where exp
itself overflows, because M3a multiplies by the polynomial prefactor (A + B t)
and a clip at 709 turns an exp overflow into a multiply overflow one line
later. Bit-identical in the well-conditioned regime, pinned by test.

## Matrix: required vs optional evidence keys (P2)

_Condition now separates keys the predicate INDEXES (required -> UNEVALUABLE
when absent) from keys it reads via .get with a documented default (optional ->
reported in the new missing_optional column, but still decided). Declaring a
defaulted key required made the matrix report UNEVALUABLE for evidence the
ladder happily fires on -- the two disagreeing precisely on the partially
collected evidence the matrix exists to describe. The same key can be required
for one rung and optional for another, which a single mixed list could not
express. A parametrised test now pins matrix/ladder agreement under every
single-key omission.

## Fail-closed: legacy atol no longer bypasses spectrum validation (P2)

The atol opt-in returned early, so liouvillian_gap([0, nan], atol=1e-10)
silently reported 0.0. A compatibility switch may restore the old THRESHOLD; it
must not restore the old silent acceptance of corrupted solver output.

## Claim qualified, not broadened (P2)

CHANGELOG and CITATION.cff no longer say the mechanism verdict is unit-
invariant: the A10/F5 branch still gates on rate-dimensioned henrici_eta (open
in #101). #108 removes a different, independent source of unit dependence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…valuable, alias contract

Five findings, all valid; two were genuine defects in the previous commit.

## Zero-mode eps now follows the spectrum's dtype (P1)

The threshold is a multiple of the eigensolver backward error, but eps was
hard-coded to float64. A single-precision spectrum has round-off ~1e-7
relative, so the threshold sat ~9 decades below that solver's noise and the
numerical steady-state eigenvalue survived as physical -- reintroducing the
negative gap this function exists to prevent. eps is now taken from the real
dtype underlying the spectrum, with a float64 fallback for integer/object
input.

## The exponent cap is one-sided (P2)

Clipping the NEGATIVE side truncated genuine, perfectly representable decay:
exp(-400) = 1.9e-174 was reported as exp(-345) = 1.5e-150, a factor of 1e24,
growing past 1e150 by exponent -700. Every model acquired an artificial
constant tail that distorts residuals, fitted offsets and AICc on
high-dynamic-range trajectories -- trading an overflow for a silent bias. Only
the positive side needs the overflow cap; underflow is exact in the limit and
NumPy's default error state ignores it.

## A12 fallback is unevaluable while any rung is unknown (P2)

A false any_fired does not establish "no mechanism applies" when a rung was
UNEVALUABLE -- the missing evidence could have made it fire. Claiming A12
SUPPORTED there asserts more than the run measured. A fired rung still refutes
A12 outright; only the unknown case degrades.

## support_score inherits confidence on the legacy path (P2)

An older caller supplying only confidence left support_score at its NaN
sentinel, breaking the documented alias contract precisely on the
backward-compatible path: migrated code lost the score for legacy results and
exports emitted a non-finite tag. __post_init__ now fills an omitted value;
an explicitly supplied one is left untouched.

## Compatibility claim corrected (P2)

CHANGELOG and the constant's rationale no longer claim the new default
reproduces the historical floor at unit scale -- it is ~2.2e-13, not 1e-10, so
modes in that band are now classified as genuine WITHOUT any rescaling. That
is the intended improvement (it is what rescues metastable slow modes) and no
anchor system carries a mode in the band, which is why reference behaviour is
unchanged; but "only rescaled generators change" was false.

Full suite 830 passed; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…st invariance claims

Two findings, both valid. The first exposed the #108 defect class in the
FITTING path and, following it up, showed my own end-to-end test was asserting
something the library explicitly does not guarantee.

## Fit seed rate is grid-relative (P1) — CHANGES NUMERICAL RESULTS

initial_guess_m0 floored the seeded decay rate at an absolute 1e-3. A rate is
1/time, so on a grid spanning t = 5e10 the seed sat 1e7 above the true rate, in
a region where exp(-alpha t) has underflowed flat and the optimiser has no
gradient. The fit returned THE FLOOR ITSELF as the measured rate:
beta_D == beta_D_linear == 1e-3 on a system whose true rate was 5e-11 --
corrupting D5/D17 while every spectral quantity looked healthy, and my test
certified that report because it only checked the gap and the verdict.

The floor is now a dimensionless decay depth over the fitted window
(ALPHA_SEED_FLOOR_FRAC / t_span), which reproduces 1e-3 exactly at the
t_span = 5 used throughout the suite, so seeds there and the anchors are
unchanged. The too-few-positive fallback rate is likewise grid-relative.

Model outputs are additionally magnitude-bounded: capping the exponent bounds
exp, but M3a multiplies by (A + B t), unbounded in the parameters, and
least-squares squares the product inside its own normal equations -- the
overflow reappeared in SciPy's trf rather than in this module.

## The end-to-end test asserted invariance the library does not claim

Following the finding up revealed the class is NOT unit-invariant: measured
A10/F5 at c = 10 and c = 1e3 where c = 1 gives A12. That is exactly the
documented #101 henrici_eta scale dependence, so my
test_diagnose_verdict_is_invariant_under_rate_rescale was green by luck on its
particular c values while claiming something the README lists as a known
limitation.

Rebuilt into three honest tests: the spectral/Mpemba evidence #108 actually
fixes IS invariant; fitted rates scale by c over c in [1e-6, 1e10]; and the
class non-invariance is now PINNED, so nobody later mistakes it for an
invariance guarantee. When #101 slice C lands that test should fail and be
replaced by a genuine assertion.

## NaN required evidence is unevaluable (P2)

The matrix used a presence-only check, so a required key holding NaN -- the
library's own "not computed" sentinel -- read as collected evidence: every
comparison against NaN is False, which looks like "threshold not met" and let
the A12 fallback conclude no mechanism applies. Infinities are deliberately NOT
swept in: a floored gap drives gap_to_gns_ratio to inf by design.

## Residual limitation recorded, not asserted away

At c = 1e-10 the least-squares convergence criteria stop tracking the
rescaling, so fitted-rate invariance is asserted only over c in [1e-6, 1e10].
Opened as its own issue; non-dimensionalising the fit is the proper fix and
touches every fitted quantity, so it needs anchor review and its own PR.

Full suite 844 passed; ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
The evidence dict is typed dict[str, float], so the isinstance guard was
unreachable-by-type and failed the enforcing mypy gate. math.isnan alone
carries the same runtime semantics: absent -> unavailable, NaN -> unavailable,
finite or infinite -> available (infinities are legitimate measured values).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…cision

Reverts the dtype branch added in the third review round. That change was made
on an unreproduced claim and introduced a real defect, which the fifth review
round then correctly caught.

## What the measurement shows

NumPy/SciPy solve eigenproblems in DOUBLE regardless of the input dtype. A
complex64 generator returns a complex64 array whose numerical zero mode sits at
~1.5e-19 -- double-level, not single-level. The storage dtype therefore says
nothing about the precision the backward error was actually incurred in.

Deriving eps from it inflated the threshold to ~1.2e-4 relative and discarded
clearly resolved slow modes: a complex64 two-channel generator with rates 1.0
and 1e-4 (true gap 5e-5) reported 5e-1 -- corrupting by four orders of
magnitude exactly the metastable case the widened tolerance existed to protect.

The finding that motivated the dtype branch (a complex64 Rabi-damped generator
scaled by 1e-3 allegedly reporting a gap near -1.7e-11) does not reproduce:
measured, that case returns 2.000000e-04 with the round-off mode at 1.46e-19,
comfortably below any threshold. I noted at the time that it did not reproduce
and changed the code anyway; that was the error.

A genuinely single-precision spectrum (external or GPU solver) is the one case
where a coarser threshold is right, and it is indistinguishable from a downcast
double result by inspection -- so it belongs to the caller, via the existing
rtol argument, and is documented as such.

Tests now pin the measured behaviour in both directions: the tolerance is
independent of storage dtype, a downcast metastable spectrum keeps its slow
branch, and the round-off mode is still excluded so no negative gap returns.

Full suite 845 passed; ruff and mypy clean (exit codes checked directly this
time -- the previous round's mypy failure reached CI because a piped `tail`
masked its exit status).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
## Root fix: dense eig/Schur boundaries promote to complex128

Self-review of the round-5 revert exposed that my public claim "NumPy/SciPy
solve eigenproblems in double regardless of input dtype" was HALF wrong, and
the wrong half is the half the library uses. Measured on a generic matrix
whose entries are not exactly representable:

  numpy.linalg.eigvals(complex64): max|dlambda| = 3.3e-07  (= input casting
    error; numpy._commonType computes in cdouble always, casts result back)
  scipy.linalg.eig(complex64):     max|dlambda| = 1.0e-05  (~30x more: scipy
    dispatches by dtype and genuinely runs single-precision cgeev)

LiouScope's dense paths all go through scipy, and eig_nonhermitian even
documents "Always uses LAPACK zgeev" -- a contract the code did not enforce.
It now does: eig_nonhermitian, the Mpemba layer, Petermann/Henrici and D24
promote to complex128 before solving. This supersedes both dtype review
rounds at the root: every backward-error tolerance is calibrated against the
double solve, and now the double solve is guaranteed. Representation error
already present in caller-supplied single-precision data is the caller's
data quality and is not masked. My round-3 measurement (zero mode at 1.5e-19
for complex64) was an artifact of exactly representable matrix entries, not
evidence of double computation -- recorded so the wrong inference is not
repeated.

## Sixth Codex review, all four adopted

* Stale CHANGELOG claim: the round-3 "eps from the spectrum's own dtype"
  sentence survived the round-5 revert. Now describes the fixed
  double-precision epsilon and the external-solver limitation.
* rtol passthrough: liouvillian_gap / oscillating_mode_gap / spectral_spread /
  lep_proximity now expose the rtol multiplier, so a caller with eigenvalues
  from a genuinely single-precision external/GPU solver can widen the filter
  scale-relatively instead of reverting to an absolute floor. Pinned with the
  reviewer's own example (displaced zero mode at 1e-7 * radius).
* Factor multiplication could overflow before _bounded saw the product
  (amplitude probe 1e200 -> inf on arrival; 0 * inf -> NaN). Models now
  evaluate under a suppressed overflow errstate and _bounded maps non-finite
  intermediates to the saturation bound -- both encode "absurdly far from the
  data", which is what the optimiser needs to hear.
* RESERVED_A_CLASSES (and the other exported taxonomy mappings) are now
  MappingProxyType: a consumer pop("A6") could previously desynchronise the
  taxonomy from the import-time coverage guard that certifies it, since the
  guard compares against the precomputed REACHABLE_A_CLASSES tuple.

Full suite 849 passed; ruff and mypy clean (exit codes checked directly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
The __post_init__ inheritance handled only the NaN sentinel, so a caller
supplying BOTH fields with different values produced a report carrying two
contradictory scores for names documented as aliases -- consumers would read
different values depending on which name they used. My own round-3 test even
pinned that wrong behaviour ('an explicit score must not be overwritten').

A mismatch is a contradiction, not a datum: it now raises ValueError,
fail-closed. Supplying equal values stays allowed. If the fields ever
legitimately diverge (a calibrated score, issue #102 option 2), that is a
semantic change that must ship with its own contract, not leak in through
mismatched constructor arguments.

Full suite 849 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…pendence, M3a slope seed

Three of four findings adopted; the fourth is deferred to #111 with reasoning.

## Fit boundary rejects non-finite data (adopted)

The model saturation added for optimiser overflow probes also laundered a NaN
in CALLER data into a 'successful' fit with finite likelihood -- before the
saturation, the non-finite residual rejected it. fit_gls_ar1 now validates
t/y/p0 as finite up front, fail-closed: overflow recovery is for probes the
optimiser generates itself, data the caller hands in must be measurements.

## NaN evidence encoding == absence, uniformly (adopted)

The matrix treated a NaN-valued OPTIONAL key as collected evidence, so the
same missing measurement produced different claim floors depending on its
encoding: gap absent -> default applied, rung SUPPORTED; gap NaN -> comparison
False, NOT_SUPPORTED -- and the matrix diverged from the ladder. NaN entries
(the library's unavailable sentinel) are now stripped before evaluation in
BOTH the ladder and the matrix, so NaN behaves exactly like absence
everywhere: optional keys fall back to their documented defaults, required
keys make the ladder raise (as absent always did) and the matrix report
UNEVALUABLE. Encoding-equivalence pinned for both key classes.

## M3a slope seed is grid-relative (adopted)

B in (A + B t) exp(-alpha t) carries dimension amplitude/time; the absolute
0.01*A seed put the seeded linear term at 1e5*A on a t=1e7 grid, so the M3a
fit diverged to infinite AICc purely because of the rate unit -- silently
removing the A10/F5 Jordan hypothesis from the model comparison. The seed is
now M3A_SLOPE_SEED_FRAC * A / t_span, which reproduces the historical 0.01*A
exactly at t_span = 5, so canonical-grid seeds and the anchors are unchanged.
Measured: the reviewer's rescaled curve now recovers [1, 0.2c, 0.5c] exactly
at c = 1 and c = 1e6. At c = 1e-6 the fit succeeds and stays finite, but
exact recovery there is bounded by the least-squares convergence
non-invariance already tracked in #111 and is deliberately not asserted.

## Rate bounds / gradient-preserving stabilisation (deferred to #111)

The flat-region termination the review demonstrates (p0 = [1, -1] on a 1e10
grid) starts from a negative SEED rate that no initial_guess in this library
produces (the floor is positive and now grid-relative). The proper fix --
parameter bounds or nondimensionalising the fit so the optimiser works in
O(1) coordinates -- is exactly the #111 resolution and touches every fitted
quantity, so it belongs in that dedicated PR rather than as a ninth
incremental patch here.

Full suite 854 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Freezing A_CLASS_DESCRIPTIONS / F_FAMILY_DESCRIPTIONS /
RESERVED_DIAGNOSTIC_SLOTS as MappingProxyType broke json.dumps and
copy.deepcopy for exports that were public and serialisable long before this
PR -- a breaking change smuggled into a hardening commit. Restored to plain
dicts: mutating them desyncs no decision logic.

RESERVED_A_CLASSES stays frozen: the import-time reachability guard and
REACHABLE_A_CLASSES derive from it, so a consumer-side pop('A6') would
silently desynchronise the taxonomy from the guard that certifies it -- and
it is NEW public API in the same release that freezes it, so nothing
established loses serialisability. Both directions pinned by test.

Full suite 855 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…ation documented

Finding 3 adopted: when the A12 fallback is UNEVALUABLE, its entry now carries
the required keys whose absence left a rung undecided, so a serialised
consumer can see WHICH missing measurement blocked the fallback claim without
walking the rungs.

Finding 2 (P1, conditioning-amplified zero-mode displacement) verified and
escalated to its own issue rather than patched as a threshold tweak. Measured
on a 4-level classical network with rate spread ~1e10 (found by scanning 4000
topologies with the reviewer's rates): the dense 16x16 solve displaces the
zero mode to 7.28e-6 AND loses the genuine slow modes (true gap 1.839e-5,
verified against the accurately solvable 4x4 population block). Decisive for
scope: the legacy absolute 1e-10 floor reports the identical wrong gap, so
this predates #108 and no spectrum-only threshold -- relative or absolute --
can repair it; the information is not in the eigenvalues. The structural fix
(deflating the exactly-known vec(I) left null vector via the #103
ordered-Schur machinery) is tracked in #112; spectral_zero_tolerance's
docstring now states the limitation.

Finding 1 (positional constructor order) declined: ClassificationResult is
kw_only=True -- positional construction has always raised TypeError, so no
previous positional API exists to preserve.

Full suite 855 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…, None-score documented

Finding 2 (real regression from round 8): stripping a NaN required key could
abort REPORT GENERATION with KeyError when one condition passed and its
sibling's predicate then indexed the stripped key (kreiss=10 +
petermann_max=NaN). A crash is worse than any wrong answer, and the
unreachable-by-construction argument only covers _gather_evidence callers,
not the public classify_mechanism surface. A condition whose required keys
are unavailable is now treated as NOT holding instead of invoked -- not
firing is the fail-closed direction (no mechanism claimed), and the matrix
carries the audit trail (UNEVALUABLE + missing keys, A12 inherits the
uncertainty). This replaces the earlier raise-on-absent ladder contract; the
matrix/ladder equivalence test now covers all three statuses directly instead
of via a KeyError branch.

Finding 1: README and taxonomy docs now state that support_score is a number
ONLY for SUPPORTED entries and None otherwise -- consumers following the
README would have treated the field as numeric and failed on the many null
entries of a normal report.

Full suite 856 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…/import-and-import-from)

The uncertified-spectrum test imported liouscope.diagnostics.spectral a
second time under 'import ... as' purely to monkeypatch certified_eigvals,
alongside the module's existing 'from ... import' -- the mixed style CodeQL
flags, plus a manual try/finally restore. The pytest monkeypatch fixture with
a string target needs no second import and restores automatically.

Full suite 914 passed (includes the d3c5049 #109/#112/#113 work); ruff and
mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…tificate machinery

All five findings adopted; they target the certificate/repair-ladder work
that landed in d3c5049.

## Mpemba abstains on unresolved spectra (P1)

_certified_decomposition gated only on certified=False, so a certified
spectrum with AMBIGUOUS in-band modes (resolved=False) was still consumed:
on the stiff fixture at fast rate 1e8 the "slowest" vector had Rayleigh
value ~-5e7 against an analytic slow mode near -1e-5, corrupting D19 on the
classifier's highest-priority A11/F4 rung. The decomposition is now withheld
whenever the certificate is applicable but not resolved. Crucially the
abstention value is NaN, not 0.0 or None->0.0: a zero overlap would itself
read as "the initial state skips the slowest mode" and MANUFACTURE a Mpemba
candidate out of solver failure. NaN flows through the existing
unavailable-sentinel machinery (candidate False, ladder strip, matrix
UNEVALUABLE).

## dgeev-real requires exact realness (P1)

np.allclose(imag, 0) carries an absolute default atol, so a complex stiff
generator in small rate units read as "real" and the repair route silently
deleted its Hamiltonian part -- D3 moved under a pure L -> cL unit change
(the reviewer's measurement). Exact realness (not np.any(L.imag)) is the only
scale-invariant criterion under which solving the real part is solving the
same matrix. Fixed in both ladders; pinned with a complex-H stiff fixture
over c in {1e-10, 1, 1e5}.

## Hermiticity gate is gauge invariant (P1)

The dynamics depend on H only through the commutator, so H + c*I (real c) is
physically identical -- but it inflated max|H| and loosened the
scale-relative gate: H = [[0, 1e-3], [0, 0]] was rejected while H + 1e9*I
passed with the same non-Hermitian part. The defect is still measured on H
itself (a real diagonal shift cannot change H - H^dag); the SCALE now comes
from the gauge-fixed traceless part. Dense and sparse builders in parity,
both pinned.

## Repair ladder is lazy (P2)

_candidates() built an eager list, executing a real eigensolve, a Schur
decomposition, balancing and another eigensolve before the loop looked at
the already-computed primary result -- three additional cubic decompositions
charged to every healthy run. Now a generator; a monkeypatched Schur counter
pins that the healthy path executes no fallback.

## Unresolved certificate floors the verdict (P2)

The certificate was report-only, so a spectrum the solver demonstrably could
not resolve still drove a normal mechanism verdict through gap /
has_complex_pairs / every evidence ratio built on D1. New fail-closed floor
at the VERDICT level, exactly like the non-finite-beta_D floor: applicable
and not resolved -> UNDEFINED / EXPLORATION, class/family preserved as the
best-fit hypothesis, surfaced as evidence key spectral_resolved. The
hypothesis-matrix claim floor applies the same rule, so the winner's floor
still equals the reported verdict. Synthetic certificates keep the pin
independent of which fixtures defeat the current repair ladder. Healthy
(resolved) systems are untouched -- anchors byte-identical.

Full suite 920 passed; ruff and mypy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…NotFoundError)

test_unresolved_certificate_floors_the_verdict_to_undefined imported helper
builders from tests.test_classifier_semantics_debt, but the tests directory is
not an importable package on the CI runner (rootdir-dependent sys.path), so
every matrix job failed with ModuleNotFoundError while the local run passed.
The test now constructs its minimal synthetic results locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…nclusive matrix refutation

All three findings reproduced before changing anything, then adopted.

## One zero-mode scale for certification and filtering (P1)

certified_eigvals accepts a stationary residual up to the true eigensolver
backward error rtol*eps*||L||_2, but every downstream filter used the
spectrum-side proxy rtol*eps*max|lambda|. For a strongly non-normal
trace-preserving operator the two differ by ||L||_2/max|lambda| -- measured
3.9e3 on a 4x4 example -- so a certified-RESOLVED zero mode with residual
between the thresholds survived the radius filter as a spurious genuine
mode: D1 reported ~1e-12 (occasionally NEGATIVE, impossible for GKSL)
instead of the true gap 1, and D9 returned four eigenmodes where three
exist. Reproduced on 20/20 constructions at skew<=1e-3.

Fix: new shared helper operator_zero_tolerance (= the certificate bound,
pinned equal by test); every consumer that holds the OPERATOR filters with
it -- spectral layer (D1/D3/D4, has_complex_pairs), Mpemba layer (slowest
mode + expansion via _certified_decomposition, which now carries the bound),
petermann_factors, D24. Genuine slow modes inside the coarser band are not
silently swallowed: the #113 ambiguity split reports resolved=False and the
verdict floor fires. Radius default untouched for spectrum-only sites;
caller atol still wins. Deterministic 4x4 fixture frozen as exact float64
literals with a guard-skip if a future LAPACK resolves it below the radius
tolerance.

## D24 recomputation is certified (P2)

compute_zhou_predictor filtered raw sla.eig output, retaining exactly the
solver failure the spectral and Mpemba layers repair: on the stiff #112
fixture D24 reported gap 7.28e-6 against the certified 1.074e-5 (~30% shift
of the mixing-time window; reproduced). Now routed through certified_eig
with the certificate bound as filter; an applicable-but-unresolved
certificate returns an honest unconverged record (inf bounds, NaN gap/K),
honouring caller-supplied values exactly like the existing
no-nonzero-modes branch. Supplying both gap and K still bypasses the
eigensolve entirely.

## Conclusively refuted partial rungs are NOT_SUPPORTED (P2)

The rungs are conjunctions: kreiss=1 refutes F1 whether or not
petermann_max was ever measured. The old rule promoted any missing required
key to UNEVALUABLE, which propagated into an UNEVALUABLE A12 fallback while
the decision ladder deterministically returned A12 -- the matrix
contradicting the decision it documents. UNEVALUABLE is now reserved for
the genuinely open case (no evaluated condition false AND missing evidence
could still flip the rung to supported); missing keys stay listed for the
audit trail. The NaN-encoding-equivalence test now pins the open case via
gns_certified=1 and the refuted case is pinned separately.

Full suite 929 passed (920 + 9 new pins); anchors, ruff, mypy clean;
CHANGELOG, CITATION.cff pending block and taxonomy docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…llback for inapplicable certificates

Both findings reproduced before changing anything, then adopted.

## Petermann factors consume the certified eigendecomposition (P1)

petermann_factors recomputed its own raw zgeev decomposition, so on the
stiff #112 fixture it consumed exactly the solver failure the spectral and
Mpemba layers repair -- and no cutoff can restore an eigenvalue that is
ABSENT from the raw spectrum. Reproduced: 16 "non-zero" modes with
petermann_max ~ 622.7 against the certified (dgeev-real) 15 modes with
~ 2.0, corrupting D9 and the D11 input while the spectral certificate
looked resolved -- a possible false F1 signal no verdict floor would catch.
Now routed through certified_eig; on an applicable-but-unresolved
certificate both returned arrays are the NaN unavailable sentinel (warned),
so petermann_max becomes NaN and F1 reads UNEVALUABLE. Deliberately NOT the
empty-array default K_max = 1.0, which would assert perfect normality from
a spectrum the solver demonstrably could not resolve.

## Radius fallback when the certificate is inapplicable (P2)

Without established trace preservation no zero eigenvalue is guaranteed,
so the operator-norm bound is not a valid zero-mode cutoff. Reproduced with
the reviewer's construction (diag(-1,-2,-3,-4) plus a 1e16 off-diagonal):
bound ~ 2.2e3 exceeds the whole spectrum, the round-13 filters discarded
every eigenvalue, and D24 reported gap 0.0 / unconverged for a true gap of
1 (D1 identically through the layer with a supplied steady state). Every
round-13 call site -- spectral layer, Mpemba layer, D9, D24 -- now uses the
certificate bound only when certificate.applicable and falls back to the
radius-based #108 tolerance otherwise.

Full suite 933 passed (929 + 4 new pins); anchors byte-identical; ruff and
mypy clean; CHANGELOG and CITATION.cff pending block updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…nues past ambiguity

Both findings reproduced by scan (4000 random stiff four-level networks
each) before changing anything, then adopted.

## certified_eig validates eigenvector residuals (P1)

The certificate accepted a decomposition solely on eigenvalue evidence,
while D19 and the Petermann factors consume the eigenVECTORS. Reproduced:
valid stiff classical networks with certified=True/resolved=True whose
slow-mode LEFT eigenvector has residual up to 3.1e-4 against a bound of
3.2e-7 -- three orders beyond, not an eigenvector in any usable sense
(right vectors fine; the corruption is the Ahues-Tisseur-deflated Schur
basis, invisible to every magnitude test on eigenvalues).

Acceptance is now PER MODE and RELATIVE:
r_j <= max(VECTOR_RESIDUAL_REL_MAX * |lambda_j|, bound), r_j the larger of
the unit-normalised left/right residuals. r/|lambda| is the first-order
relative error scale of anything computed from the pair; a single
operator-scale cutoff does NOT separate the measured populations (healthy
zgeev over 301 generators <= 2.1e-10; legitimate dgeev-real repairs of the
stiff family 1.2e-2..6.5e-2; corrupt decompositions 2.2e-1..2.9e1). The
boundary at 1e-1 sits between marginal-but-usable and clearly broken, with
fail-closed as the failure direction; an extreme repair at spectral spread
~4e11 carrying a 79% slow-mode error is deliberately failed closed -- that
is not a measurement. Calibration documented at the constant.

A vector-failed candidate does not end the ladder; when no route passes,
certified=False with the OFFENDING residual reported (so the downstream
warning shows a number that actually exceeds the printed bound), and D19,
D9 and D24 withhold. certified_eigvals is deliberately untouched: the
eigenvalues of such a decomposition remain usable for D1/D3/D4, and the
new tests pin exactly that division.

## The ladder continues past ambiguous candidates (P2)

An ambiguous candidate ended the ladder immediately (resolved=False, D1
withheld, verdict floored) even when the next route resolves the generator
cleanly. Reproduced on 3 scan hits: zgeev certified with one ambiguous
mode at 3.5e-7..1.6e-6 while dgeev-real returns a machine-zero stationary
mode (7.3e-17..1.3e-14) with none. Both ladders now accept only
ambiguity-free candidates; the best ambiguous candidate (fewest ambiguous
modes, ties by ladder order) is returned fail-closed only when every route
stays ambiguous. Healthy path unchanged and still lazy (pinned by the
existing Schur-counter test).

Full suite 936 passed (933 + 3 new pins); anchors byte-identical; ruff and
mypy clean; CHANGELOG and CITATION.cff pending block updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…on-matched certificates, D11 decoupling

All four findings reproduced before changing anything, then adopted.

## Prony fallback seeds are grid-relative (P2)

The fallback seeded (beta, omega) = (1, 1) in absolute rate units, so on a
valid non-uniform grid spanning t = 1e7 the M3b fit started seven orders of
magnitude off and least-squares "converged" (success=True) onto the seed
itself -- reproduced: [A, beta, omega] ~ [0.006, 1, 1] for true values 5e-8
and 2e-6, corrupting the fitted rate and suppressing M3b/A8 purely because
of the rate unit. Fallback rates are now 5.0/t_span and the success-path
positivity floors 5e-6/t_span, both reproducing the historical values
exactly at the canonical t_span = 5 (same convention as
ALPHA_SEED_FLOOR_FRAC / M3A_SLOPE_SEED_FRAC). The measured non-uniform
scenario is now unit-invariant across c in [1e-6, 1e6] (pinned end to end).

## D24 uses the eigenvalue certificate when only the gap is missing (P2)

The recomputation required certified_eig even when the caller supplied
petermann_factor -- but that path consumes no eigenvectors. Reproduced: a
stiff network whose eigenvalue certificate resolves a usable gap of
3.32e-6 while only the round-15 eigenvector gate fails returned an
unconverged NaN record. The certificate now matches what is consumed:
certified_eigvals for the gap-only path, certified_eig when the Petermann
factor is recomputed (that path stays fail-closed, pinned).

## D11 decoupled from the D9 vector gate (P2)

The D9 NaN sentinel flowed into bohr_arithmetic_progression, which
silently filtered it and reported the default length 1 as a measured
value. Reproduced: a ladder-Hamiltonian network whose certified spectrum
carries a length-3 progression while D9 is withheld reported 1. D11's
input is now recomputed from the certified spectrum (D11 consumes only
eigenvalues); when the eigenvalues themselves are unresolved,
bohr_ap_length is NaN (field now float-typed, NaN = documented sentinel).

## Release-note correction (P2, documentation)

The atol opt-in for the legacy absolute Hermiticity gate exists on the
standalone is_hermitian predicate only; the builders never accepted it.
The #109 CHANGELOG entry now says so explicitly instead of advertising an
unavailable builder argument.

Full suite 942 passed (936 + 6 new pins); anchors byte-identical; ruff and
mypy clean; CHANGELOG and CITATION.cff pending block updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
…hem (CodeQL py/unused-local-variable)

The gap-only recompute path (round-16) bound vl = vr = None purely to
satisfy definition-on-all-paths, but the Petermann loop is the only
consumer and runs exactly in the need_vectors branch. Moving the
extraction there removes the dead assignments; behaviour unchanged
(suite 942 passed, ruff and mypy clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ML7N7dLW77wzcWKaZAq9q
Findings 9, 15 and 16 were reproduced at head 4a8ae9c before any change and
are each pinned by a regression test whose discrimination is proven (revert
the fix -> test red, restore -> green; 3/3, mutation confirmed to have landed).

[15] The zero-mode band is `1e3 * eps * ||L||_2`, but ||L||_2 is set by the
     OSCILLATION scale while the decision is about DECAY. A two-level system
     with omega = 1 and rates 1e-15 / 1e-14 lost its genuine slowest mode
     (D1 = 1e-15) into the band, was certified resolved with zero_mode_count=2,
     and D1 reported the 20x faster eigenvalue 2.05e-14 with full confidence.
     The code's own comment says the magnitude axis cannot fix this (healthy
     round-off reaches 2.38 eps||L||, unresolved slow modes 4.87 -- a factor of
     two apart). Added the a posteriori backward-error bound as a second,
     independent axis. It is a certificate, not a threshold: a stationary mode
     would satisfy |lambda_hat| <= bound, so exceeding it proves non-stationarity.
     Calibrated on the SAME corpus as the existing factor -- 0 of 132 healthy
     in-band modes reach q = 1 (max 0.472), all 3 stiff members with a genuine
     in-band mode rescued. One-directional by construction: it can only remove
     a mode from the zero set, never add one, so it cannot manufacture the
     false NaN the ambiguity split exists to prevent.

     Note for the record: the comment at ZERO_MODE_AMBIGUITY_FACTOR explains the
     miss as double precision having "genuinely lost the information". Measured,
     that is not what happens here -- L is diagonal and zgeev returns all four
     eigenvalues exactly. The loss was in the decision rule, not the arithmetic.

[9]  The model magnitude guards keep an optimiser probe finite, but the value
     they return is constant, so its derivatives vanish and least_squares
     terminates on "gradient is small". fit_gls_ar1 with p0 = [1, -1] on
     t in [0, 1e10] returned success=True with p0 unchanged and residual norm
     7.9e100. Only the FINAL evaluation is judged; GLSFitOutput.saturated names
     the guard that fired.

[16] CHANGELOG called zero_mode_certificate "report-only" while classify reads
     it and caps verdict/tier via _apply_spectral_certificate_floor. It was
     wrong from the commit that added the floor; corrected rather than dropped,
     and it now carries a second load-bearing path through zero_tolerance.

Gates, this run: pytest 947 passed, anchors 21 passed, ruff clean, mypy clean
(53 files). GitHub Actions allocates no runners account-wide, so these are the
only evidence available -- LOCAL-FIRST, per the standing decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-17 review of PR #121 named four sites that filter with the raw
certificate band after the a posteriori refinement (issue #113 second axis)
had rescued a genuine slow mode from it -- discarding that mode again one
layer later:

* _zhou.py:180        D24 understated the mixing-time window (20x here)
* mpemba.py:115       _slowest_mode/expansion_alpha lost the mode, so D19
                      collapsed to overlap 0.0 -- the FALSE A11/F4 trigger
* nonnormality.py:334 D9 dropped the mode whose conditioning can dominate
                      petermann_max, i.e. a possible wrong F1 input
* nonnormality.py:474 the D11 fallback scanned the pre-refinement complement

Root, not instances. Those four are not four independent mistakes: the same
two-branch expression ("band when applicable, radius proxy otherwise") was
copy-written at FIVE sites in round-13, and only the spectral layer was
migrated when #113 made the raw band the wrong half of it. The generator of
that miss is the docstring of operator_zero_tolerance, which instructed
consumers to filter "equivalently, with the bound of the ZeroModeCertificate"
-- true when written, false since #113, and still read by anyone adding a
sixth site.

Therefore:

* ZeroModeCertificate.zero_set_tolerance() is now the ONLY way any layer
  obtains a cutoff. Applicability, the radius fallback and the refinement are
  decided once, inside the certificate, instead of five times at call sites.
* the ``bound`` field is documented as report/diagnostics only.
* the misleading paragraph in operator_zero_tolerance is corrected.
* _certified_decomposition (mpemba) hands out the CERTIFICATE, not a bare
  number, so its two consumers cannot pick the wrong tolerance.
* spectral.py:271 additionally reported ``bound`` while calling it "the
  zero-mode tolerance"; after a refinement that is not the applied number.
  Not in the review -- found while enumerating every .bound reader.

Proof, per site (Vero/Tools/diskriminierung.py, 7/7 DISKRIMINIERT):
each fix reverted individually turns exactly its own test red. The D11 case
is asserted at the seam the review names -- what reaches
bohr_arithmetic_progression -- because the reported progression length is 1
either way on that fixture, so a number assertion there would be blind.

A structural guard pins the remaining .bound readers to three warning texts
and the two accessors inside the certificate; a sixth filter site fails the
suite instead of surviving six review rounds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s + C4

B1 spectral.py:255 -- fail-open D1. With an applicable certificate whose every
repair route left certified=False, the layer still published a finite gap read
off the candidate spectrum it had just called untrustworthy. The warning does
not reach a caller that consumes SpectralResult.gap, and _gather_evidence
derives ratios from it, so the number outran its caveat; the classifier floor
caps the VERDICT, which is a different guarantee. D1 is now the NaN sentinel
for BOTH unresolved kinds. D3/D4 deliberately unchanged -- widening the
withholding is a physics decision, not a review fix, and is flagged upward.

B2 gls.py:132 -- a saturated fit that could still win. Flipping
GLSFitOutput.success changed nothing, because no consumer read it: the fit kept
a finite AICc and could take choose_model and supply the reported decay rate.
Made NON-SELECTABLE at the choke point (_fit_with_model -> aicc = inf), not
enforced at each consumer: _dominant_rate, compute_relaxation_layer, bootstrap
and holdout are four separate obligations to remember, which is the exact shape
of the four-fold miss the same review round found in the zero-mode filters. With
nothing selectable left, aicc_model is the existing "none" sentinel and beta_D
is NaN rather than read off a failed fit. parametric_bootstrap refuses a
saturated BASE fit (a resample around a non-estimate is not an uncertainty) and
reports how many non-converged replicates it retained -- retaining widens the
interval, dropping them would narrow it, and no retain-fraction threshold is
invented here.

B3 linalg.py:427 -- and 602, which the review did not name. A LinAlgError from
the incumbent zgeev ended the ladder before the real-driver, Schur and balanced
routes ran, i.e. it failed exactly in the nonconvergence case the ladder was
built for. Both ladders now carry the error and re-raise it only if no route
produced a spectrum at all.

C4 _types.py:44 -- the field is load-bearing, not report-only:
classify_mechanism reads it and _apply_spectral_certificate_floor caps the
reported verdict AND tier from it. Documentation only.

C5 _consts.py:139 -- NOT removed. The CodeQL alert is a false positive:
REACHABLE_A_CLASSES is imported in classification.py:65, compared at :705 in
_reachable_coverage_check(), which runs at import (:715), re-exported in
liouscope.__all__ and asserted in two test files. Verified in this run:
_reachable_coverage_check() == frozenset(REACHABLE_A_CLASSES) -> True, 9
classes. Deleting it would remove the import-time reachability gate.

Proof: Vero/Tools/diskriminierung.py, 7/7 DISKRIMINIERT (spec_B). Three cases
first came back "ROT DURCH ABSTURZ -- kein Beleg"; the tests were hardened to
kill on an assertion instead of on the exception/warning, because a test that
dies of a crash proves nothing about the mutation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The full suite caught my own B2 fix. Three validation-system tests went red
(test_d17_gap_coherence: V4 twice, plus the faithful/rank-deficient split),
and the cause was not the review finding -- it was the predicate I chose for
it.

Measured on V4 (thermal two-level), trace-distance curve: all five models
M0..M3b converge with success=True, and all five AICc values are already inf,
because the Geyer-corrected n_eff of that smooth, strongly autocorrelated
residual series is too small for the small-sample correction. A non-finite
AICc is therefore NOT the same event as a failed fit, and filtering on
isfinite(aicc) withheld D17 (beta_D_linear -> NaN) on a system where nothing
had failed.

Corrected: the candidate set is the SUCCEEDED set. choose_model keeps its
documented "every entry inf -> M0" fallback; the only rule enforced here is
that the winner must come from that set, and when the fallback names a model
outside it, the first succeeded model in ladder order is taken instead --
deterministic, and byte-identical to the previous behaviour whenever M0 itself
succeeded (which is the V4 case).

Regression test added for the near-miss itself: every AICc forced to inf while
every fit succeeds must still yield a real winner and a finite beta_D.

Proof: diskriminierung.py 3/3 DISKRIMINIERT -- reverting to isfinite(aicc)
turns both the new guard and the pre-existing V4 test red, in both the
compute_relaxation_layer and the _dominant_rate site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exact number of modes the a posteriori refinement rescues on the stiff
four-level network is a solver detail that can differ across the CI matrix
(3.10-3.14, different SciPy/LAPACK builds). Pinning it to 3 would make the
test red for a reason that is not the defect it guards.

Relaxed to ">= 1", which is the substantive precondition -- without a
refinement the test is vacuous. Deliberately an assertion and not a skip: a
skip that fires on every other platform is a guard that silently proves
nothing. The membership check still runs over whatever was rescued, and the
scanned-set size is compared against the MEASURED applied tolerance, so it
self-adjusts.

Discrimination re-verified after the change: diskriminierung.py 1/1
DISKRIMINIERT on the A4 case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fortzusetzen

Fremder Review-Befund auf PR #121, zutreffend und ueber den Rundgang-17-Fund
hinaus.

Runde 17 schuetzte den PRIMAEREN Solve, sodass eine zgeev-Nichtkonvergenz die
Reparaturkette nicht mehr abbricht. Der unmittelbar folgende dgeev-real-Schritt
in `certified_eigvals` blieb ungeschuetzt -- und anders als der primaere steht
er VOR zwei weiteren Routen. Wirft `np.linalg.eigvals(L_c.real)` fuer einen
exakt reellen Generator, propagiert die Ausnahme aus dem Generator heraus und
verwirft `zgees-schur` und `balanced-zgeev`, obwohl beide das Spektrum noch
haetten zertifizieren koennen.

Die Geschwister-Leiter in `certified_eig` unterdrueckte genau diesen Schritt
bereits. Die Auslassung hier war die Inkonsistenz, nicht der Schutz dort.

5 neue Tests (`tests/test_pr121_review_round18.py`):
- Vorbedingung: der Fixture-Generator ist EXAKT reell -- sonst wird der Zweig
  nie betreten und jedes Gruen darunter waere ohne Aussage.
- Regression: beide eigvals-Aufrufe brechen -> genau `zgees-schur` bleibt
  uebrig, und die Leiter muss ihn erreichen.
- Positiv-Kontrolle: nur der primaere bricht -> `dgeev-real` zertifiziert.
  Ohne sie koennte die Regression bestehen, weil zgees-schur ohnehin gewinnt.
- Fail-closed-Kontrolle: alle Routen brechen -> die urspruengliche Ausnahme
  kommt hoch. Ein Schutz, der Totalversagen in stillen Erfolg verwandelt,
  waere schlimmer als der reparierte Defekt.
- Strukturelle Sperre: liest die Quelle und faellt, wenn EINE der beiden
  dgeev-real-Routen ausserhalb eines Suppression-Blocks steht -- genau das
  Auseinanderdriften, das den Defekt erzeugt hat.

Belege (lokal, Repo-`.venv`, py 3.14.4, Summary-Zeilen abgelesen):
  pytest tests/                          -> "971 passed in 236.04s"
  pytest tests/test_pr121_review_round18.py -> "5 passed in 2.54s"
  Diskriminierung: Schutz entfernt -> 2 failed, 3 passed
  (Regression + strukturelle Sperre fallen); zurueckgenommen -> 5 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
marcohost33-maker and others added 19 commits August 28, 2026 16:22
…atten

Runde-19-Gegenlesung auf den Push von f194c97. Alle drei Befunde haben
dieselbe Bauart wie die vorherigen: eine Schicht entscheidet etwas, das sie
nie gemessen hat, und der Entscheid sieht aus wie eine Messung.

1) `holdout_validate` bewertete die Parameter eines Fits, nach dessen Erfolg
   es nie gefragt hat. Auf dem Saettigungsplateau ist das Modell KONSTANT --
   Trainings- und Holdout-RMSE sind gleich gross, ihr Verhaeltnis ist rund 1
   (gemessen 1.0106 am Beispiel des Pruefers), und das Anti-Overfit-Kriterium
   ist erfuellt. Das Gate, das nicht-generalisierende Modelle fangen soll,
   wurde von einem Nicht-Modell zufriedengestellt. `accept` verlangt jetzt
   `fit.success`; das neue Feld `fit_success` haelt "generalisiert nicht" von
   "wurde nie gefittet" getrennt -- die beiden verlangen verschiedene
   Handlungen.

2) D3, D4 und `has_complex_pairs` wurden weiter aus demselben Spektrum
   veroeffentlicht, das die Warnung der Schicht als unzuverlaessig bezeichnet;
   zurueckgehalten wurde nur D1. Das war keine Teil-Reparatur, sondern eine
   inkonsistente: sie lehrte Verbraucher, dass diese Schicht zurueckhaelt,
   wofuer sie nicht einstehen kann -- und machte die verbliebenen endlichen
   Werte dadurch glaubwuerdiger, nicht weniger. `has_complex_pairs` ist jetzt
   `bool | None`, weil ein bool kein NaN hat und `False` die Aussage "keine
   oszillierenden Moden" waere statt einer unbeantworteten Frage.

3) Der Eich-Abzug lief ueber und NaN entschaerfte das Hermitizitaets-Gate.
   `np.trace(H)` summiert vor dem Teilen und laeuft fuer ein ENDLICHES H mit
   grossen Eintraegen nach `inf` -- die explizite Endlichkeitspruefung
   unmittelbar darueber ist da bereits bestanden. `scale` wurde NaN,
   `defect > EPS * NaN` ist False, und ein zweidimensionales `1e308 * I` mit
   einseitigem `1e290` wurde akzeptiert. Der Abzug ist jetzt
   `sum(diag(H)/d)`, durch `max|diag(H)|` beschraenkt; zusaetzlich wird eine
   nicht-endliche abgeleitete Skala fail-closed abgewiesen, damit kein
   kuenftiger Weg dorthin das Loch still wieder oeffnet.

8 neue Tests (`tests/test_pr121_review_round19.py`), jeder mit Positiv-
Kontrolle bzw. Ueberkorrektur-Sperre: ein gesunder Fit wird weiter akzeptiert,
ein zertifizierter Lauf veroeffentlicht D3/D4 weiter, und ein echt
hermitesches `1e308 * I` geht weiter durch -- die Reparatur ist keine
Groessengrenze.

Belege (lokal, Repo-`.venv`, py 3.14.4, Summary-Zeilen abgelesen):
  pytest                              -> "979 passed in 215.01s"
  pytest tests/test_anchors.py -q     -> "21 passed in 2.06s"
  ruff check src tests benchmarks     -> "All checks passed!"
  mypy src/liouscope                  -> "Success: no issues found in 53 source files"
  Diskriminierung, je Fix einzeln zurueckgenommen (Skript prueft, dass die
  Mutation griff, und dass alle Dateien danach unveraendert sind):
    1 holdout ignoriert success      -> 1 failed, 7 passed
    2 D3/D4 nicht zurueckgehalten    -> 1 failed, 7 passed
    3 Ueberlauf-sicherer Eichabzug   -> 3 failed, 5 passed

MANIFEST_SCHEMA ist nicht betroffen (kein Feld dieser Aenderung kommt darin
vor -- nachgesehen, nicht angenommen). CHANGELOG ergaenzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ld (B1)

Round-20 review, spectral layer. D1 was withheld on ``applicable and not
resolved``; D3, D4 and ``has_complex_pairs`` on the narrower ``applicable and
not certified``. Those are not the same event: a certificate can be
``certified=True`` with ``resolved=False`` when an in-band mode is ambiguous
(#113), and that state passed the narrower gate.

The consequence is worse than a wrong number. The ambiguous mode is excluded
by ``zero_tol`` before D3 and the pair flag ever look at the spectrum, so on
the measured fixture they reported ``oscillating_gap = 0.0`` and
``has_complex_pairs = False`` -- the ABSENCE of the very oscillation that made
the certificate unresolvable. D4 came from a truncated spectrum.

All four now share D1's predicate, so the layer withholds on one condition
rather than on two.

Discrimination proof (Tools/diskriminierung.py, spec 4de678194475): both
guard lines reverted individually -> the regression goes red; restored ->
green. 2/2 diskriminieren, ledger diskriminierung_runs.jsonl.
Local: ruff clean, mypy clean, 77 passed on the spectral-certificate set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aXYnUv5MTwQvcpb1QLtEb
…ng (B2)

Round-20 review, zero-mode certificate. ``residual <= bound`` established that
a zero mode was found; nothing established that the band SEPARATED it. On the
reviewer's counterexample -- a finite 4x4 with cancelling +-1e308 entries and
eigenvalues {1,2,3,4} -- ||L||_2 is 1.41e308, so the band is 3.14e295 and all
four eigenvalues fall inside it. The certificate returned
``certified=True, resolved=True, zero_mode_count=4``: the assertion that the
entire operator space is stationary, and ``resolved=True`` is the half that
tells the spectral layer D1/D3/D4 may be published.

``band_discriminates`` is a second axis, not a tightened threshold: an
acceptance region containing every measured value cannot have rejected, so
passing it is not evidence. Added to BOTH ladders in one commit -- the two
loops drifting apart was the round-16 finding.

NEGATIVE RESULT, reported rather than hidden. The reviewer's first suggested
route -- compute the relative defect with an overflow-safe norm -- was built,
measured and REVERTED: it cannot flip this case. For ||L||_F to overflow,
max|L_ij| must reach ~1e154, and then ANY finite defect (here 4.12) is
relatively zero. The operator IS trace preserving relative to its own scale,
which is the unit-invariant criterion this module deliberately chose (#108,
#111), so ``applicable`` stays True. What is unmeasurable at that scale is
the SPECTRUM, and that is what the guard now says.

The class is not overflow-specific, so a second fixture proves it on ordinary
arithmetic: a nilpotent generator, trace defect exactly 0.0, ||L||_F = 1.4e10,
band 3.1e-3, all four eigenvalues exactly zero -- previously certified as a
four-dimensional stationary manifold.

``bound == 0`` is exempt: a zero-width band accepts only exact zeros, so the
exactly-zero generator ("everything is stationary", measured exactly) still
certifies. That exemption carries its own discrimination case.

Discrimination proof (Tools/diskriminierung.py, spec c2cdf4b654d2): 4/4
diskriminieren -- verdict line, both call sites, and the bound==0 exemption.
Ledger diskriminierung_runs.jsonl. Local: ruff clean, mypy clean,
100 passed on the certificate + input-guard set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aXYnUv5MTwQvcpb1QLtEb
…t a curve (B3)

Round-20 review, uncertainty layer. Two guards, one defect: something is
reported that was never measured.

1. THE ARGUMENT FOR RETAINING FAILED REPLICATES IS BACKWARDS. Round 19 kept
   them because "keeping them widens the interval -- the conservative one". A
   failed ``least_squares`` returns its unchanged STARTING value, which in
   this resample is ``theta_hat``, so every failure deposits mass exactly at
   the centre. Measured, 400 replicates with 40 % failures: BCa width falls to
   0.93x and 0.87x of the interval without them -- NARROWER, in the dangerous
   direction. Dropping them is no alternative either (a fit fails on the
   hardest replicates, not on a random subset). Neither route has a validated
   rule, so no interval is reported; ``compute_relaxation_layer`` already
   routes RuntimeError to ``bca_ci_beta = (nan, nan)``.

2. ISSUE #123, THE SAME DEFECT ONE LAYER DOWN. On an identically-zero curve
   every direction is equally optimal, so the optimiser stops at the seed and
   reports success. Measured on ``linspace(0, 5, 64)``: ``success=True``, rate
   parameter = the seed 1.0 unchanged, and the bootstrap around it produced a
   BCa interval of width EXACTLY 0.0 -- perfect confidence as the failure mode
   of an uncertainty pipeline, with ZERO failed replicates. Guard 1 could
   never have caught it. ``fit_gls_ar1`` now refuses a curve whose variation
   is at or below the resolution of its own values (``ptp(y) <= eps*max|y|``,
   scale-invariant by construction, never absolute -- the #108/#111 lesson)
   and returns NaN parameters with ``success=False`` and a new ``degenerate``
   flag.

3. FOURTH INSTANCE, found while building the third and taken along because
   the third makes it reachable: ``estimate_neff_geyer`` ends in
   ``max(1.0, min(float(n), n_eff))``. Every comparison against NaN is false,
   so ``min`` keeps its first argument and a NaN residual series left the
   function as ``n_eff = n`` -- the most over-confident value in range,
   laundered out of a NaN by a clamp written to bound a number, not to decide
   whether there was one.

Discrimination proof (Tools/diskriminierung.py, spec c0c7af6047b9): 4/4
diskriminieren -- the resample refusal, the degenerate-curve refusal at both
its consequences, and the n_eff guard. Ledger diskriminierung_runs.jsonl.

Full suite (CI chain, incl. --cov-fail-under=90): 995 passed, exit 0,
coverage 95.09%; baseline before this round was 979 passed. ruff and mypy
clean on src/tests/benchmarks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aXYnUv5MTwQvcpb1QLtEb
The CI failure after the round-20 push was not a flaky test. Applicability
was decided by

    if not np.isfinite(tp_defect) or tp_defect > tp_rtol * max(fro, tiny):

which refuses a non-finite DEFECT but not a non-finite reference SCALE. For
the overflowing operator the defect is a finite sqrt(17) while ||L||_F is
inf, so the comparison is vacuously true and every operator is admitted.
The zero-mode band that follows is then drawn from a scale nobody measured,
and which modes it swallows is left to the LAPACK build: 4 of 4 locally,
2 of 4 on the runners. The 2-of-4 case walks past band_discriminates, which
only asks whether the band took EVERYTHING -- so that guard was never the
right place to catch this.

Refused at the root instead. Trace preservation *relative to the operator's
own scale* is not a statement that can be made about infinity.

This reverses a deliberate choice: the round-20 test asserted
applicable is True here and called it the criterion this module chose.
The band guard itself is unchanged and stays correct for the finite case --
the nilpotent twin still gets applicable=True and is still caught there.

The regression now asserts the REASON, not just the shared verdict, so
neither mechanism can rot unnoticed behind the other, and a new test pins
the finite-scale invariant directly rather than through a downstream verdict.

Evidence: 996 passed locally (was 1 failed, 982 passed on Linux CI);
mutation proof 2/2 discriminate -- removing the guard turns both the new
invariant test and the overflow regression red, restoring it turns them
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
Fallout from the round-20 degenerate-curve guard, caught by CI on 3.10/3.11.

``test_diagnose_accepts_valid_custom_t_grid`` left ``rho_initial`` to the
default. On those two runners the chosen state IS the steady state, so the
relaxation curve is identically zero -- ``varies by 0.000e+00 over a scale
of 0.000e+00``. The old fitter returned the optimiser's seed for such a
curve, so ``assert isfinite(beta_D)`` passed on a meaningless number and the
test looked green; on the other runners rounding noise kept the curve from
being exactly flat, which is why only two jobs went red.

So the test was a beneficiary of the very defect #123 removes. Fixed by
giving it a state that genuinely relaxes (|0><0| towards the maximally mixed
steady state) rather than by silencing the warning, and by asserting the
decay rate is positive -- a curve carrying no decay can no longer satisfy it.

Evidence: 996 passed locally; the file's own suite 25 passed, zero warnings
raised for this case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
…nd 22, part 1)

Findings 3, 4, 6 and 7 of the round-22 review. All four are the same shape:
a number that is not a measurement is used as though it were one.

* **4 --- `tp_rtol` was never validated.** `defect > tp_rtol * fro` is False
  for every operator when `tp_rtol` is NaN, and has an infinite right-hand
  side when it is inf. Measured before the guard:
  `certified_eigvals(diag([0,-1,-2,-3]), tp_rtol=nan)` reported
  `applicable=True, certified=True` for a trace defect of 3.0. Round 21
  closed exactly this shape one variable further along (the non-finite
  reference scale `fro`) and left the parameter beside it unchecked; the
  generalisation is the fix. Both `certified_eigvals` and `certified_eig`
  now validate `rtol` and `tp_rtol` the way `operator_zero_tolerance` and
  `spectral_zero_tolerance` already validated theirs.

* **6 --- an inapplicable certificate reported one zero mode.**
  `zero_mode_count` kept its dataclass default of 1 in both inapplicable
  branches, so `diag([1,2,3,4])` --- a spectrum with no zero eigenvalue ---
  came back claiming a stationary mode, and the dict is persisted as audit
  metadata. Now 0: nothing was certified as stationary.

* **3 --- the geometric mean underflowed.** `sqrt(lo * hi)` forms the
  product first, and that product scales as c^2 under a uniform rate
  rescale while the refinement itself is scale free. Below c ~ 1e-139 it
  underflows to 0, the applied tolerance becomes 0.0, and the numerical
  stationary residual survives the filter as a physical mode.
  `sqrt(lo) * sqrt(hi)` is the same number and cannot underflow.
  Measured: lo=1e-177, hi=1e-165 gave 0.0, now gives 1e-171.

* **7 --- an overflowing spectral radius produced a gapless spectrum.**
  |1.3e308 + 1.3e308j| is about 1.84e308 and does not fit a double, so
  `np.abs` returns inf although every component is finite and passes the
  finiteness gate. Measured before: tolerance inf, and D1/D3/D4 all 0.0 for
  a spectrum containing modes of order 1e308. After: tolerance 4.08e295,
  D1 5.0e307, D3 1.3e308, D4 8.0e307. The finite path is bit-identical; a
  tolerance that is still not finite is now refused rather than returned.

Evidence: 996 passed at baseline; 27 new tests in
tests/test_pr121_review_round22.py, 117 passed across the round-17..20,
certificate, anchor and issue-118 suites, 356 passed on the
scale/spectral/numerics/classification selection. ruff 0.15.20 clean,
mypy clean. Full suite and mutation proof follow at the end of the round.

Every new test carries a positive control, and findings 3 and 7 carry an
over-correction control (valid extreme input must still pass).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
Findings 1, 2, 5 and 8 of the round-22 review, plus the CHANGELOG entry for
all eight.

* **1 --- the sparse builder inherited none of the dense overflow repair.**
  Round 18 divided each diagonal entry before summing, in `build_liouvillian`
  only. `build_sparse_liouvillian` still called `H_sp.diagonal().sum()`, which
  overflows to inf for a finite H with large entries; `H_gauge` becomes NaN,
  `scale` becomes NaN, and `defect > EPS * NaN` is False. Measured before the
  fix on `[[1.3e308, 1e290], [0, 1.3e308]]`: dense RAISED, sparse ACCEPTED ---
  a Hermiticity defect of 1e290 admitted into a generator that is not GKSL.
  Both paths now share the overflow-safe gauge shift AND the refusal of a
  non-finite derived scale, and both now produce the identical error message.

* **2 --- the jackknife never asked whether its fits converged.** The round-20
  guard covers the bootstrap replicates only. `_jackknife` copied
  `fit_i.params` unconditionally, and a failed `least_squares` returns its
  unchanged starting value --- here `theta_hat` itself. The BCa acceleration is
  a third moment of exactly these estimates, so one non-fit moves the
  endpoints with no data behind it. Measured on 40 points, B=200, one
  injected failure: the amplitude interval narrows to 0.923x of its width
  while the rate interval moves 0.2 % the other way --- so the claim is that
  the interval SHIFTS, not that it uniformly narrows. `_jackknife` now raises
  RuntimeError, which `compute_relaxation_layer` already routes to
  `bca_ci_beta = (nan, nan)`, "fit uncertainty UNKNOWN".

* **5 --- the unresolved run was the one whose report would not serialise.**
  D1/D3 are withheld as NaN by design when a certificate is unresolved (D9
  likewise with the eigenvector certificate), `build_stability_report` copied
  them through as raw floats, and `dump_stability_report` writes with
  `allow_nan=False`. Measured before: `ValueError: Out of range float values
  are not JSON compliant: nan` --- on precisely the run whose audit record
  matters most. Non-finite values are now encoded as `null`, the placeholder
  already used for D8b/D10b and by `ZeroModeCertificate.as_dict`. `isfinite`
  rather than `isnan`, because +-inf raises in the same place.
  CHANGES PAYLOAD TYPES: consumers of `diagnostics["D1_gap"]` must accept
  None.

* **8 --- a missing gap was read as the gapless limit.** `_f5_reach` defaulted
  `gap` to 0.0, which is positive evidence for the F5 reach leg. With an
  unresolved certificate D1 is NaN, `_strip_unavailable` drops the key, and
  with `henrici_eta > 1` the classifier returned A10/F5 and the matrix
  reported the hypothesis SUPPORTED --- measured before the fix. The
  certificate floor downstream caps the VERDICT at UNDEFINED; it does not
  withdraw the class, the family or the matrix status, so a fabricated
  mechanism label survived the floor meant to contain it. `gap` is now a
  REQUIRED key of that condition: absent, the rung is UNEVALUABLE. A gap
  MEASURED as 0.0 still fires the documented gapless branch --- the repair
  separates "no gap" from "no measurement".

DELIBERATE REVERSAL, flagged rather than quietly absorbed: two tests in
`tests/test_hypothesis_matrix.py` pinned the OLD contract (`gap` optional,
missing gap => F5 SUPPORTED). Their invariant --- a defaulted key must not make
the matrix contradict the ladder --- is unchanged and now travels on
`gap_to_gns_ratio`, a key that genuinely has documented semantics without a
value. Two new tests pin the new required-key behaviour together with its
gapless positive control. `tests/test_anchors.py` was not touched and stays
green.

Evidence in this turn: 39 new tests in tests/test_pr121_review_round22.py;
86 passed in tests/test_hypothesis_matrix.py; 146 passed across round-22 +
hypothesis-matrix + anchors; 311 passed on the classification / report /
sparse / bootstrap / relaxation / anchors / manifest / export selection.
ruff 0.15.20 clean, mypy clean. Full suite and mutation proof follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
The round-22 mutation proof came back 7/9, and both gaps were in the tests,
not in the guards.

* `test_sparse_builder_rejects_what_the_dense_builder_rejects` died of
  `RuntimeWarning: overflow encountered in reduce` under the mutation --
  the repo runs pytest with `filterwarnings = ["error"]`, so the unrepaired
  arithmetic kills the test before it reaches its own assertion. Red by
  crash cannot distinguish "the gate refused" from "the gate accepted", and
  the proof scored it exactly that way: ROT DURCH ABSTURZ, no evidence.
  Warnings are now suppressed where the subject is the REFUSAL, and the
  absence of the overflow gets its own test where it is the subject.

* `test_non_finite_tp_rtol_is_refused_not_honoured` is parametrised over two
  APIs and three bad values, so the mutation produces six
  `Failed: DID NOT RAISE` lines. The tool accepts that death for a single
  test but not in its multi-line aggregation, which only recognises
  assertions -- so a valid proof was booked as ART-UNBESTIMMT. That is a
  defect in the measuring tool, reported rather than patched around: the
  parametrised test stays for breadth and an un-parametrised twin carrying
  the reviewer's example verbatim gives the proof a case it can read.

No production code changed in this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
`test_the_repaired_sparse_gate_does_not_overflow_on_the_way` detected the
reintroduced overflow perfectly well, but it detected it by DYING of the
`RuntimeWarning` that `filterwarnings = ["error"]` escalates -- and a test
whose death is a raised warning cannot be told apart from a test that
crashed. The mutation proof scored it "ROT DURCH ABSTURZ -- kein Beleg",
which was the correct verdict about the evidence even though the test was
right about the code.

The warnings are now recorded and asserted on, so the same detection
arrives as the test's own AssertionError. Same coverage, checkable death.

No production code changed in this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
…tor (round 23, finding 12)

Reviewer (chatgpt-codex-connector, thread on src/liouscope/numerics/linalg.py:269):
"When a non-trace-preserving operator is expressed in sufficiently small but
still normal rate units, both norms here underflow to zero and the certificate
incorrectly becomes applicable. For example, diag([0, -1e-200, -2e-200,
-3e-200]) yields (trace_defect, fro) == (0.0, 0.0) and both certificate APIs
report applicable=True, certified=True, whereas the same operator at 1e-150 is
correctly inapplicable."

Reproduced verbatim at e7b4150 before the change:
  scale=1e-150: (defect,fro)=(3e-150, 3.7416573867739415e-150) applicable=False
  scale=1e-200: (defect,fro)=(0.0, 0.0)                        applicable=True certified=True

Both norms are now recomputed on a copy scaled by the largest finite component
when the reference scale has been lost. The relative defect 3/sqrt(14) is then
reproduced at every scale from 1.0 down to 1e-300, so the verdict is a property
of the operator rather than of the rate unit it is written in.

The rescue is ONE-DIRECTIONAL by design: underflow is repaired, overflow is
not. ||L||_F = inf for the round-20 counterexample is the input to the round-21
refusal of a non-finite reference scale; its true value is about 1.4e308 and
therefore representable, so a scaled computation would readmit the operator and
reopen a hole CI needed five interpreter versions to close. Pinned by
test_the_overflow_direction_is_deliberately_not_rescued, which goes red on any
later "symmetric" tidy-up.

Baseline before this change: 1046 passed, exit 0 @ e7b4150 (Windows, .venv
Python 3.14.4, 484 s). Local gates after: ruff exit 0, mypy exit 0,
tests/test_pr121_review_round23.py 5 passed exit 0. Full-suite belegs-run
follows in the block below (~8 min).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
… (round 23, finding 13)

Reviewer (chatgpt-codex-connector, thread on src/liouscope/diagnostics/lep.py:73):
"When diagnose() receives an applicable but unresolved zero-mode certificate,
it still passes that certificate's candidate eigenvalues into lep_proximity;
this new tolerance calculation consequently publishes a finite D16 proximity
and candidate count from the same spectrum for which D1/D3/D4 are withheld. A
missing or ambiguous zero/slow mode can directly change the closest eigenvalue
pair, so the certificate floor on the final classification does not make this
diagnostic a valid measurement; propagate the unresolved state and report D16
as unavailable instead."

Reproduced verbatim at e7b4150 on the issue-#113 stiff fixture (rates
7.28e-6/3.67e-5/1.53e-5/1e8/1.42e-5):
  cert: applicable=True certified=True resolved=False zero_mode_count=9
  D1_gap=nan  D3=nan  D4=nan  has_complex_pairs=None
  D16 lep_proximity=0.0  candidates=51

0.0 is the coalescence limit -- the STRONGEST exceptional-point signal the
diagnostic can emit -- fabricated from modes below the eigensolver's backward
error. The withheld value must therefore be distinguishable from both measured
extremes (0.0 coalesced, inf no-pair), so NaN is used and pinned by a dedicated
test rather than left implicit.

compute_lep_layer gains spectral_resolved (default True); LepResult.
lep_candidate_count becomes int | None, in parity with has_complex_pairs.
diagnose() derives the flag from the same "applicable and not resolved"
predicate the spectral layer uses for D1/D3/D4. lep_proximity is still CALLED
before the withholding branch so the issue-#82 fail-closed refusal of
non-finite eigenvalues stays on the path -- closing one fail-open must not open
another. A missing certificate is not an unresolved one and keeps reporting.

Local gates: ruff exit 0, mypy exit 0 (mypy caught that
SpectralResult.zero_mode_certificate is Optional; handled explicitly),
tests/test_pr121_review_round23.py 10 passed exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
The discrimination spec exposed the gap before the ledger did. Two lines of the
finding-13 fix were defended by nothing:

* the WIRING in ``diagnose`` (``_spectrum_resolved``). ``compute_lep_layer``
  can withhold correctly and still publish D16 to every real caller if the
  certificate's verdict never reaches it. Every existing test drove the layer
  directly, so mutating that line killed no test.
* the PLACEMENT of the ``lep_proximity`` call before the withholding branch,
  which is what keeps the issue-#82 refusal of non-finite eigenvalues on the
  path. Moving the call behind the branch is the cheapest way to close one
  fail-open by opening another, and nothing would have noticed.

Both now have a test that fails by JUDGMENT, not by crash: an assertion on the
published D16 for the first, ``pytest.raises`` (Failed: DID NOT RAISE) for the
second.

Discrimination proof, 9/9 discriminate, exit 0, ledger
G:\Meine Ablage\Vero\Data\diskriminierung_runs.jsonl (spec_sha256 0a4396e6…).
tests/test_pr121_review_round23.py: 12 passed, exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
Self-check required by the standing rule "if you build a guard, run its own
failure class against your result first". It found two defects in the round-23
fix, neither of which any test would have caught.

1. FAIL-CLOSED REGRESSION ON VALID INPUT. The rescue divided by the largest
   component. That is COMPLEX division, and for a subnormal divisor NumPy forms
   an intermediate reciprocal that overflows: measured for
   diag([0, -1e-310, -2e-310, -3e-310]), ``L / m`` returned
   [nan, -inf, -inf, -inf] and both norms became NaN. The round-21 guard then
   refused the operator -- and a healthy GKSL generator at 1e-310 went with it,
   applicable True -> False. Repairing a fail-open installed a fail-closed
   defect in its place.
   Now scaled by an exact power of two (frexp/ldexp): the round trip changes no
   mantissa bit, and no reciprocal is formed. Measured after: the generator
   certifies at 1e-200, 1e-300, 1e-308, 1e-310 and 1e-315.

2. THE FINDING SURVIVED ITS OWN FIX, one line further down. The gate floors its
   reference scale at ``max(fro, tiny)``, and that constant is LARGER than a
   subnormal operator, so the relative test stops being relative. Measured with
   the norms already repaired: defect 3e-320 against a floor-derived bound of
   2.2e-318 -- "trace preserving" for a relative defect of 3/sqrt(14) = 0.80.
   The reviewer named 1e-200; the class does not stop there.
   The floor protected nothing: defect <= sqrt(d) * fro, so fro == 0 forces
   defect == 0 and ``0 > 0`` is False either way -- the exactly-zero generator
   stays applicable. Removed in BOTH ladders.
   Measured after: refused at 1e-300, 1e-310, 1e-315, 1e-320 and 5e-324, while
   the healthy generator still certifies at every one of them.

Local gates at this commit: ruff exit 0, mypy exit 0,
tests/test_pr121_review_round23.py 14 passed exit 0.
Full-suite belegs-run follows, ~8 min on this machine (baseline was 484 s).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
…round 23)

The floor removal went into BOTH deflation ladders; the regression exercised
only ``certified_eigvals``. A guard that half its call sites never run is a
guard sitting off half its path -- the same defect this branch already fixed
once today, so it does not get to recur silently.

test_the_repair_reaches_into_the_subnormal_range now asserts on both
``certified_eigvals`` and ``certified_eig``, which lets the mutation of each
ladder's gate be killed separately instead of one hiding behind the other.

Belegs-run before this change: 1060 passed, exit 0 @ 3cc0bbc, 465.95 s
(baseline 1046 passed @ e7b4150, 484.44 s; +14 = the round-23 tests).
ruff exit 0, round23 file 14 passed exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
The discrimination run reported ROT DURCH ABSTURZ:RuntimeWarning for
test_a_healthy_generator_survives_the_subnormal_repair. The mutation made
complex division overflow, numpy issued a RuntimeWarning, the suite's
filterwarnings=error turned it into a failure -- and the test never reached its
own assertion. A crash cannot distinguish detection from non-detection, so that
was not evidence that the guard is watched.

np.errstate(all="ignore") plus a warnings filter now let the NaN through to the
assertion, so the kill is the test's judgment about the value it saw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6q2mPSi12JaFqkcRYm1VL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants