fix(#118): close the three review findings that survived PR #107 - #121
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>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3b73b4df1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Evidence, now measured rather than quoted. Local runInterpreter: CI on this pull requestAll five workflows completed successfully:
What this settles beyond this pull requestThe description opened with the question of whether this account is currently The practical consequence is that on public repositories a green rollup is Still openThis pull request is 🤖 Generated with Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98370aaa0b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
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>
…eaves open The round-26 repair splits eigenpairs by EXACT float equality, so the reported defect survives one step away from exact degeneracy: two merely close in-band candidates still both take argmin and can borrow the same reference eigenpair. Measured on a fixture this suite already carries -- the D11 network of tests/test_pr121_review_round17.py. The in-band candidates -3.3216107e-06 and -3.345505e-06 (relative separation 7.1e-03) both pair with reference index 8, and both clear the 10 per cent agreement guard, so the first is certified from the second's residual. The obvious fail-closed repair was applied and measured rather than reasoned about: on a collision keep the nearer candidate, certify neither of the others. It costs test_d11_fallback_scans_the_refined_zero_set on that same fixture -- the certificate drops to resolved=False with zero_mode_count=2, ambiguous_count=1, the refinement is abandoned wholesale, and a mode that WAS correctly rescued is lost. That is the identical regression the blanket bijection produces, reached by a narrower route. So this is a trade between two failure modes, not a repair with a free side, and it has anchor consequences. Recorded at the site instead of decided quietly. Documentation only: no behaviour change. Verified at e222406 + this note: ruff check clean, mypy src clean (53 files), tests/test_pr121_review_round17.py + round26 31 passed, tests/test_anchors.py 21 passed, all exit 0. Reverse-mutation run DK-20260904T073431-8149722dc989 on the CLEAN delivered head e222406, 5 of 5 discriminate -- the earlier ledger entry for this fix stood at 75922bb, which is not an ancestor of the shipped commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 05763c94ca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…d withdrawn Both open review threads on this PR are ZUTREFFEND. Comment-only change; the pairing logic is untouched. Thread 1 (exact degeneracy) is already repaired on this branch by the rank mechanism. Verified rather than assumed: removing it is caught by test_pr121_review_round26.py::test_borrowed_decomposition_matches_one_to_one. Probed per guard, and that mattered -- two of the round-26 tests are BLIND to the same mutation, both legitimately so (they drive the supplied-vector path, where the pairing block is never entered). One shared probe would have read as "the round-26 tests guard this"; three probes say which single one does. Thread 2 (near degeneracy) reproduced independently on the D11 fixture: candidates ev[3] = -3.3216107e-06 and ev[8] = -3.345505e-06 both pair with reference index 8 at a relative separation of 7.142e-03, both clear the 10 per cent agreement guard, and the first is certified from the second's residual. The existing note's numbers hold. One word of that note does not. It called the mode lost under the fail-closed repair "correctly rescued". The spectra do not support "correctly": the operator carries -3.345505e-06 exactly twice, ref[8] and ref[11], and both copies are claimed bit-for-bit by ev[8] and ev[11]. ev[3] has no counterpart left -- nearest free entry ref[3] = -4.239e-06 at relative distance 0.216, which the guard refuses. Its certification rests entirely on a residual measured for a different eigenvalue and already spoken for. That does not settle the decision, it sharpens it. Refusing ev[3] is fail-closed and defensible on the spectra, and it makes cert.resolved unreachable on this network -- measured, by applying a greedy one-to-one bijection and running the file: test_d11_fallback_scans_the_refined_zero_set fails with zero_mode_count=2, ambiguous_count=1, resolved=False, exactly the cost the note predicts. The test and the defect are coupled: the certificate resolves here only by way of the borrowed residual. So the trade is not "borrowed evidence vs. losing a good mode" but "borrowed evidence vs. conceding that this network does not resolve". Recorded, not decided -- it moves an anchor, and no test was touched to make either side look better. ruff check src tests benchmarks exit 0 mypy src/liouscope exit 0 pytest round26 + round17 31 passed, exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bac5fe0d21
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Both findings share one shape: a correct guard that the control flow can reach the arithmetic without passing through. _zhou.compute_zhou_predictor skipped its recomputation branch -- and with it the spectral certificate guard -- as soon as BOTH gap and petermann_factor were supplied. The natural caller hands back an UNRESOLVED report (gap=report.spectral.gap, petermann_factor=report.nonnorm.petermann_max) and both fields are then NaN, this library's unavailable-value sentinel. NaN is not None, so the branch was skipped; nan <= 0 is False, so the gap abstention was skipped too, and the record returned converged=True with NaN mixing-time bounds. Supplied values are now checked for finiteness before they are allowed to bypass certification; the unconverged record honours them verbatim so a manifest reader sees WHICH value made the predictor abstain. classification.classify_mechanism falls through to A12 when a rung is UNEVALUABLE, because an unevaluable rung cannot fire. The evidence matrix said so honestly (A12 UNEVALUABLE, claim floor UNDEFINED) while the top-level verdict published NOT_EXCLUDED: two fields of one report contradicting each other. A floor now reads the matrix that the report carries -- the same object, so the two cannot drift -- and caps the verdict at UNDEFINED when the WINNER is unevaluable. Stated over the winner generally rather than over A12: a fired rung is SUPPORTED by construction, so it is a no-op everywhere else. Measured, retraction-proved (tests/test_pr121_review_round27.py): 15/15 pass with the fix; on bac5fe0 8 fail / 7 pass, the 7 being the positive controls and the pre-existing abstentions -- so the tests are non-vacuous AND neither guard abstains unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
It imported its result-dataclass builders from tests/test_classification, the only cross-test-module import in the suite: two files that are meant to fail independently would have failed together. The builders are ~40 lines of literals; duplicating them is cheaper than the coupling. Also drops three now-unused noqa directives that RUF100 rejected -- ruff runs BEFORE pytest in ci.yml, so an unfixed lint would have kept this file from ever executing in CI. Measured: ruff check src tests benchmarks exit 0; mypy src/liouscope exit 0 (53 files); round-27 15/15; targeted regression across test_classification / test_zhou / test_classifier_semantics_debt / test_reserved_slots / test_failclosed_hardening / test_anchors 135/135. Retraction proof unchanged against bac5fe0: 8 failed, 7 passed. Full-suite belegtlauf follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
Status 2026-09-07 — three open threads answered, two of them repairedAll three findings that were genuinely open are now judged at the code, not triaged. Thread counts measured by GraphQL, not estimated: 37 threads, 33 resolved, 1 open-but-outdated, 3 genuinely open — all three now answered.
What changed (local commits, unpushed)Branch
Belegtlauf — CI command chain reproduced locallyRepository Collection at Retraction proof (the tests are non-vacuous and the guards are not unconditional):
|
Merge gate — 2026-09-07Ich habe diesen PR wieder auf Draft gesetzt. Das ist kein Urteil ueber die bereits gruenen Checks am aktuellen Head Vor
Begruendung fuer Punkt 2: |
… its neighbour
The adjudicated repair of the eigenpair-assignment collision. Which reference
eigenPAIR may serve as evidence for an in-band candidate was decided by an
independent nearest-value lookup per candidate, and a lookup is not a map: two
candidates that are merely CLOSE both took the same reference index, and the
second was certified from a residual measured for the first. For a
certification layer that is not a choosable convention but a violation of
evidence identity.
Three cases, three answers:
* separated and non-degenerate -> index identity where the decomposition is
the caller's own, and a GLOBALLY INJECTIVE assignment
(scipy.optimize.linear_sum_assignment) where it is borrowed -- never a
greedy nearest-neighbour lookup;
* near-degenerate cluster -> certify the INVARIANT SUBSPACE from the
operator's own Schur basis. LAPACK's caveat is the whole point: a clustered
subspace can be well conditioned while its individual eigenvectors are not
determined at all, so forcing individual eigenvectors there is the wrong
object. Block bound res/sigma_min(P^H Q), which is the cosine of the largest
principal angle between the left and right subspaces and reduces to the
scalar |y^H x| at k = 1, so the two paths agree about a lone mode;
* not separable -> unresolved. Nothing certified, no borrowed residual.
Two things this measurement corrected in the draft, recorded because both were
wrong for the same reason -- a global optimum is not the same as a determined
one:
1. linear_sum_assignment ALONE does not fix it. On the D11 network's
dgeev-real route the minimum-cost assignment is EXACTLY degenerate:
{ev[3]->ref[8], ev[11]->ref[3]} and {ev[3]->ref[3], ev[11]->ref[11]} both
cost 9.1739e-07, because ev[11] matches ref[8] to the last bit. The
arbitrary winner re-created the borrowed residual. A separated candidate is
therefore certified only when EXACTLY ONE reference eigenvalue is
admissible under the agreement guard.
2. The subspace path must not be a fallback after the individual path
declines. As a fallback it never ran on D11, for the reason above. Inside a
cluster the individual assignment is undetermined, not merely imprecise.
Measured on D11 (tests/test_pr121_review_round17.py): certified nonzero modes
go from {3, 8} on dgeev-real and {8, 11} on zgeev to {} on BOTH routes, and
cert.resolved goes True -> False. That network becomes a negative control, as
adjudicated: conceding that it does not resolve is the honest verdict when the
alternative is a borrowed residual.
ruff exit 0; mypy exit 0 (53 files). Two tests fail by construction on this
commit and are rebuilt in the next: the D11 precondition, and the round-26
borrowed test whose sla stub predates the Schur path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
…at proves it The adjudication named this consequence in advance: a fixture that forces ``cert.resolved`` as a PRECONDITION must not hold the certification logic hostage to borrowed evidence. So the stiff D11 network changes sides. * ``test_the_stiff_d11_network_is_a_negative_control`` (new) pins it at both ends. Not resolving is asserted, and so is the stronger statement that NEITHER solver route certifies anything -- because a ladder that merely switched from dgeev-real to zgeev would satisfy the first assertion while the borrowed residual survived on the route it left. Measured: an earlier draft of this repair did exactly that. * ``test_d11_fallback_scans_the_refined_zero_set`` keeps its assertion, at the same seam, on a new separable generator of the same topology. Measured: solver dgeev-real, resolved=True, zero_mode_count=1, ambiguous_count=0, tolerance refined 3.156e-05 -> 2.64e-09, rescued pair at 3.134e-05. The margins are deliberate -- the rescued pair sits 33x above the ambiguity split and the only uncertified in-band mode is the stationary one at 2.2e-13, four decades below it, so the verdict does not turn on the third digit of a BLAS result. That pair is exactly degenerate, so the positive path runs through the new invariant-subspace certificate. * ``test_borrowed_decomposition_matches_one_to_one`` asked which arm of a tie the search took. On an exactly degenerate pair that question has no answer, which is why round 28 stops asking it: the cluster is certified from the operator's own Schur basis and the borrowed vectors are not consulted. The test now asserts the stronger property -- corrupting them cannot move the verdict -- with the untouched decomposition as comparison and the fast and stationary modes as negative control. Measured: round-17 20 passed; round-24 + round-26 27 passed; ruff exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
…it refuses Twelve assertions on the three regimes of ``certified_nonzero_modes``, each refusal paired with a positive control on the same fixture so a certificate that had simply stopped certifying could not pass the file. The discrimination proof found a blind test in this very file, which is the reason it is worth recording. The first draft built its near-degenerate cluster at ``S = 1e-14`` with separations of ``1e-3`` relative -- 1e-17 absolute, while the eigenvalue error on these operators is 4.4e-16. The structure the fixture claimed to test did not survive the eigensolver: the test refused, but out of numerical noise rather than out of the gate it was aimed at, and ablating that gate left it green. Every fixture now runs at ``S = 2e-13`` with separations of 5 per cent, 22x above the measured error, and the gate discriminates. A tool written against blind refusals containing a blind refusal is the expected failure, not a surprising one; it is why the ablation is run rather than reasoned about. Discrimination (Data/diskriminierung_runs.jsonl), six cells, three-valued: D1 unique admissible reference (separated candidate) KILLED D2 cluster routing, no individual path for members KILLED D3 subspace separation factor KILLED (blind before) D4 reference-multiplicity agreement SURVIVED D5 Schur reordering must select exactly k SURVIVED POSITIVE CONTROL: certification bound itself disabled KILLED D4 and D5 are defence in depth, and which guard catches them was measured rather than assumed: with D4 ablated the unmatchable fixture stops at the injective candidate-to-Schur-eigenvalue agreement instead; with D3 ablated the crowded fixture certifies, and ablating D5 as well changes nothing, so no current fixture produces a ball that selects the wrong count. Neither is claimed to be load-bearing. Measured: round-28 12 passed; ruff exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
…ng a filename Both conflicts were decisions, not formatting, and neither side was discarded. ``src/liouscope/_zhou.py`` -- the round-25/27 finding was repaired independently on both branches within hours of each other, and the two gates are behaviourally identical: validate a caller-supplied gap/Petermann value for finiteness before it may bypass certification, and abstain with the value preserved verbatim. The PUSHED implementation is kept, because rewriting a reviewed commit to install an equivalent one buys nothing. Equivalence was measured, not asserted: the fifteen assertions of tests/test_pr121_review_round27.py -- written against the other gate, with a twelve-case parametrisation covering NaN, +/-inf, mixed None, the pre-existing non-positive abstention and two positive controls -- all pass against this one. The duplicate ``_unconverged`` helper and its ``math`` import go with it; the part of its comment the pushed version did not carry (the concrete caller path through an unresolved report, and why +inf is refused) is folded in rather than lost. ``tests/test_pr121_review_round28.py`` -- an add/add collision of two unrelated reviews that happened to pick the same name. The incoming file probes the supplied-D24 availability boundary; the local one reviews the eigenpair assignment collision. Disjoint subjects, so both sets are kept and the incoming one is renamed to test_pr121_review_round27_supplied_d24.py -- the round-27 label being the accurate one, as its own source comment says. Its overlap with the existing round-27 file is documented in its docstring and is asymmetric: all six of its sentinel cases are subsumed there and asserted more strictly, while its exact fast-path window (log(1000), log(2000)) is not covered anywhere else. Kept whole rather than trimmed to the unique test: two files written independently should be able to fail independently, and deleting reviewed assertions to save eight test-seconds is the wrong trade. Merge base bac5fe0. The incoming branch carries none of this side's other work -- the classification UNEVALUABLE floor, the round-27 tests, or the round-28 eigenpair-assignment repair -- so this is a genuine union, not a fast-forward. ruff exit 0; mypy exit 0 (53 files); round-27 + supplied-D24 + round-28 35 passed. Full-suite belegtlauf follows, ~5 min. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU6zGAKqZt8Vh2KN3mMeYk
…ound blind The round-28 repair was measured with the discrimination tool at HEAD 9e60143 (ledger DK-20260907T225012-a908bed7de33): 4 of 6 ablations discriminated, two came back BLIND -- the reference-multiplicity agreement and the sdim check inside `_certify_invariant_subspace` could both be deleted without a single test noticing. The reason is structural, not an oversight in the round-28 tests. Reached through `certified_nonzero_modes`, `ref` IS the operator's own `sla.eig` spectrum, so a fixture that violates either guard is refused first by the injective agreement check downstream. Neither guard can be the sole cause on that path. They are therefore pinned where they are the sole cause: at the function's own contract boundary, with `ref`, `L_c` and `cluster_values` supplied independently -- which is exactly the disagreement the guards exist for. The operators are diagonal, so the residual is ~0 and the bound cannot be what refuses; a `False` there can only come from the guard under test. A shared positive control certifies the identical fixture with a matching reference, so a certificate broken into refusing everything would not pass this file. Retraction proof follows in the same block. Test count 12 -> 15. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6LJzwSLfR25NzLgjpqC1K
…ing-certificate-failclosed fix(#126): fail closed when spectral evidence is missing
…sitive controls PR #145 (issue #126) deliberately dropped a documented compatibility promise: the old code stated that the spectral-certificate evidence key is "absent for synthetic results that carry no certificate, so every pre-existing test and serialised result is untouched". #126 replaces that with "missing => unexamined => withhold", which is the right call -- an unexamined spectrum must not yield a publication-level class. The migration added "zero_mode_certificate": {"applicable": True, "certified": True, "resolved": True} to the fixtures in tests/test_failclosed_hardening.py and tests/test_classification.py, but not to tests/test_pr121_review_round27.py. That file's _spectral() helper carries no certificate at all, so after the merge of #145 into pr107-fix the new spectral floor fired inside two tests whose whole purpose is to show that the *A12 unevaluable* floor does NOT fire: FAILED test_a_decided_a12_fallback_keeps_its_verdict FAILED test_a_firing_rung_is_unaffected_by_the_floor AssertionError: assert 'UNDEFINED' != 'UNDEFINED' Both are positive controls. They did exactly what a positive control is for: they caught a second, independently correct fail-closed guard firing where the first one's contract says it must not. Without them the change would have travelled through #121 into main unremarked. Fix is the same single line the other two fixtures already received. Tests that exercise the spectral floor itself keep passing their own certificate via **kw, so this does not weaken any gate. Evidence: with the line tests/test_pr121_review_round27.py 15 passed line removed the two controls FAIL, and only those two restored 15 passed full suite 1147 passed, exit 0 (586.79s), venv interpreter verified anchors 21 passed, exit 0 ruff check exit 0 mypy src exit 0, 53 source files No CHANGELOG entry: test-fixture migration only, no behaviour change and no public surface touched. The behaviour change itself belongs to #145. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6LJzwSLfR25NzLgjpqC1K
… reconciled per hunk 28 conflict hunks in 12 files, resolved one by one (merge, not rebase: a rebase would rewrite ~27 reviewed commits and need a force-push). - Hermiticity gate (lindblad.py, sparse/build.py): PR #127's generator- relative tolerance kept; with no dissipation it is main's gauge-fixed gate. The half-scale restatement is kept instead of main's refusal of a non-finite scale, because #127 round 20 pins an exactly Hermitian diag(1.7e308, -1.7e308, 1.7e308) as accepted. - overflow_safe_mean_real: the expected overflow of the direct sum no longer warns. Four main tests (PR #121 rounds 19/22) died of that RuntimeWarning under filterwarnings=error once the branches met. - Zero-mode applicability (linalg.py): main's inline conditions (non-finite fro, componentwise #130, no tiny floor) plus #127's derived cutoff sqrt(d)*bound as an additional refusal term; tiny floor removed from the helper for the round-23 reason. - relaxation/gls/bootstrap/_types: both sides' fields kept; k counts the CAR(1) theta (#127), non-succeeded fits are not selectable (#121), the log-space likelihood (#135) carries the CAR(1)/AR(1) Jacobian (#127). - classification/spectral/hypothesis tests: the same finding fixed on both sides; main's text kept, #127's extra matrix test kept alongside. Targeted set 353 passed; full suite follows as the evidence run (~11 min). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186oBqQsooNFqonGmyBd9Mp
Closes #118.
That line is a keyword band, not a summary: this branch closes the three review
findings from #118 that survived PR #107, and #107's own thirteen are already in
this tree -- verified, not assumed,
git merge-base --is-ancestor 4a8ae9c bac5fe0dreturns true, so merging this branch carries all sixteen. #118 is theissue that records them; it is the issue this PR retires.
The band only works against
main, and only through a merge COMMIT. This PRtargets
main, soCloses #118will fire. #107 is strictly contained here(0 commits unique to it, measured
git rev-list --left-right --count), whichmeans a squash or a rebase merge would rewrite its head SHA, leave #107 open as
a zombie, and take #118 with it. A real merge commit keeps
4a8ae9creachablefrom
mainand closes both.Why this is a separate branch, not a push to #107
PR #107 carries a complete green rollup of 20 successful checks, earned on
2026-08-15. Pushing this work onto its head branch
(
claude/liouscope-repo-analysis-xgztfd) would have replaced that rollup — andat the time this work was finished, it was unclear whether the account could
earn a new one. #107 also sits at
BEHINDunder strict status checks, so it wasnot regularly mergeable in any case.
This branch therefore carries the fix under its own name. #107 keeps its
evidence untouched; verified before and after the push, still 20× SUCCESS.
What changed
Commit
f3b73b4— the three findings from #118 that were still open after#107 was reviewed. With this, all 16 review findings recorded in #118 are
closed.
Evidence status — read this before trusting any number
This description deliberately contains no test counts. The figures that
circulated for the 2026-08-27 run were not re-measured when this pull request
was opened, and a number that is quoted rather than observed is a claim, not
evidence.
At the time of opening:
.venv(Python 3.14) andhad not finished;
pylauncherrather than the repository
.venv— and produced 54 collection errors, allModuleNotFoundError: No module named 'liouscope'. That was an environmentfault, not a code fault, and is recorded here so it is not mistaken for one
later;
that itself is the finding: whether this account is currently assigned runners
for public repositories is an open question at the time of writing. Runs
observed on other public repositories on 2026-08-25 and 2026-08-26 executed
real jobs; no push-triggered run has occurred on a public repository since
2026-08-24, so the question is not settled by observation yet.
Local results will be added as a comment once the run completes, labelled with
the interpreter used.
Review focus
The zero-mode threshold changed in the 2026-08-27 session. Issues #112, #113 and
#117 all depend on that threshold and are deliberately not addressed here —
they are waiting on an independent cross-family review of the construction
itself, because building further on a construction that may yet be refuted would
be the more expensive mistake.
🤖 Generated with Claude Code
Summary
Closes the three review findings from #118 that survived PR #107, plus the two
guards a retraction probe found blind, plus one fixture migration that #145 left
incomplete.
numerics/linalg.py)._injective_pairingassigns globally vialinear_sum_assignment(injective byconstruction);
:883-889abstains whenever more than one reference entry isadmissible, so the D11 tie (two assignments both costing 9.1739e-07) yields
unresolvedinstead of a borrowed residual. Near-degenerate clusters take theinvariant-subspace path (
:602) or nothing.diagnostics/classification.py)._apply_unevaluable_winner_floor(:192-222) floors the verdict toUNDEFINED/EXPLORATIONwhen the winning class isUNEVALUABLE; wired intoclassify_mechanismat:1306-1308, on the same matrix object the report carries._zhou.py:151-167).Non-finite caller-supplied
gap/petermann_factorare refused before therecomputation branch can be skipped; returns unconverged with the supplied values
preserved verbatim.
tests/test_pr121_review_round28.py).The reference-multiplicity check (
linalg.py:660) and thesdimcheck (:683)can never be the sole cause of failure on the public path, because the downstream
injective agreement (
:694-698) catches the same fixtures first. Pinned via thefunction's contract seam instead.
tests/test_pr121_review_round27.py). fix(#126): fail closed when spectral evidence is missing #145 turned amissing spectral certificate from "do not floor" into "unexamined, withhold" and
migrated two of three fixture files; the third is now aligned.
Test plan
pytest -q— 1147 passed, exit 0 (586.79s),.venvinterpreter verified viaimport liouscope; print(liouscope.__file__)pytest tests/test_anchors.py— 21 passed, exit 0 (sacred gate)ruff check src tests benchmarks— exit 0mypy src/liouscope— exit 0, 53 source filesrunner_idandsteps, not byconclusion:runner_id1000026462–73,3–12 steps each, zero jobs with
runner_id=0(i.e. no quota-statepseudo-runs)
Data/diskriminierung_runs.jsonl,all
provenienz=VOLL,head_dirty=false:DK-20260907T2250126 cases → 4 discriminating / 2 blind ·DK-20260908T1009206 cases → 6 / 0 (F2 and F3, incl. a wiring ablationthat calls the floor but discards its return value) ·
DK-20260908T1015497 cases → 7 / 0 (the two formerly blind guards, plustwo positive controls)
15 passed; line removed, exactly the two positive controls fail; restored,
15 passed
CHANGELOG.mdupdated under[Unreleased]Merge note: merge commit, not squash — squashing would orphan #107 and #115.