ci: exact-head verification for #134 - #138
Closed
marcohost33-maker wants to merge 49 commits into
Closed
Conversation
…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>
…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
# Conflicts: # CHANGELOG.md
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.