Skip to content

fix(#118): close the three review findings that survived PR #107 - #121

Merged
marcohost33-maker merged 71 commits into
mainfrom
pr107-fix
Sep 8, 2026
Merged

fix(#118): close the three review findings that survived PR #107#121
marcohost33-maker merged 71 commits into
mainfrom
pr107-fix

Conversation

@marcohost33-maker

@marcohost33-maker marcohost33-maker commented Aug 28, 2026

Copy link
Copy Markdown
Owner

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 bac5fe0d returns true, so merging this branch carries all sixteen. #118 is the
issue that records them; it is the issue this PR retires.

The band only works against main, and only through a merge COMMIT. This PR
targets main, so Closes #118 will fire. #107 is strictly contained here
(0 commits unique to it, measured git rev-list --left-right --count), which
means 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 4a8ae9c reachable
from main and 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 — and
at the time this work was finished, it was unclear whether the account could
earn a new one. #107 also sits at BEHIND under strict status checks, so it was
not 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:

  • the local suite is running in the repository's own .venv (Python 3.14) and
    had not finished;
  • the first attempt ran under the wrong interpreter — the system py launcher
    rather than the repository .venv — and produced 54 collection errors, all
    ModuleNotFoundError: No module named 'liouscope'. That was an environment
    fault, not a code fault, and is recorded here so it is not mistaken for one
    later;
  • CI on this pull request is the authoritative measurement. If it does not run,
    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.

  • F1 — near-degenerate modes sharing a reference pair (numerics/linalg.py).
    _injective_pairing assigns globally via linear_sum_assignment (injective by
    construction); :883-889 abstains whenever more than one reference entry is
    admissible, so the D11 tie (two assignments both costing 9.1739e-07) yields
    unresolved instead of a borrowed residual. Near-degenerate clusters take the
    invariant-subspace path (:602) or nothing.
  • F2 — unevaluable A12 fallback reaching the verdict (diagnostics/classification.py).
    _apply_unevaluable_winner_floor (:192-222) floors the verdict to
    UNDEFINED/EXPLORATION when the winning class is UNEVALUABLE; wired into
    classify_mechanism at :1306-1308, on the same matrix object the report carries.
  • F3 — unavailable precomputed inputs bypassing certification (_zhou.py:151-167).
    Non-finite caller-supplied gap/petermann_factor are refused before the
    recomputation branch can be skipped; returns unconverged with the supplied values
    preserved verbatim.
  • Two subsumed guards pinned at the contract seam (tests/test_pr121_review_round28.py).
    The reference-multiplicity check (linalg.py:660) and the sdim check (: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 the
    function's contract seam instead.
  • Fixture migration completed (tests/test_pr121_review_round27.py). fix(#126): fail closed when spectral evidence is missing #145 turned a
    missing spectral certificate from "do not floor" into "unexamined, withhold" and
    migrated two of three fixture files; the third is now aligned.

Test plan

  • pytest -q1147 passed, exit 0 (586.79s), .venv interpreter verified via
    import liouscope; print(liouscope.__file__)
  • pytest tests/test_anchors.py21 passed, exit 0 (sacred gate)
  • ruff check src tests benchmarks — exit 0
  • mypy src/liouscope — exit 0, 53 source files
  • All 8 required status checks green on this head — verified per job by
    runner_id and steps, not by conclusion: runner_id 1000026462–73,
    3–12 steps each, zero jobs with runner_id=0 (i.e. no quota-state
    pseudo-runs)
  • Retraction proofs in the canonical ledger Data/diskriminierung_runs.jsonl,
    all provenienz=VOLL, head_dirty=false:
    DK-20260907T225012 6 cases → 4 discriminating / 2 blind ·
    DK-20260908T100920 6 cases → 6 / 0 (F2 and F3, incl. a wiring ablation
    that calls the floor but discards its return value) ·
    DK-20260908T101549 7 cases → 7 / 0 (the two formerly blind guards, plus
    two positive controls)
  • Fixture-migration fix discriminates in both directions: with the line
    15 passed; line removed, exactly the two positive controls fail; restored,
    15 passed
  • All 37 review threads resolved (0 open)
  • CHANGELOG.md updated under [Unreleased]

Merge note: merge commit, not squash — squashing would orphan #107 and #115.

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

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

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

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

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

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

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

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

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

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

## Codex review follow-ups on #102 matrix

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

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

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

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

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

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

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

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

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

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

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

## Claim qualified, not broadened (P2)

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

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

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

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

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

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

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

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

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

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

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

## Compatibility claim corrected (P2)

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

Full suite 830 passed; ruff and mypy clean.

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

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

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

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

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

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

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

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

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

## NaN required evidence is unevaluable (P2)

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

## Residual limitation recorded, not asserted away

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

Full suite 844 passed; ruff and mypy clean.

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

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

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

## What the measurement shows

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

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

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

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

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

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

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

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

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

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

## Sixth Codex review, all four adopted

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

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

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

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

Full suite 849 passed; ruff and mypy clean.

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

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

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

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

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

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

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

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

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

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

Full suite 854 passed; ruff and mypy clean.

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

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

Full suite 855 passed; ruff and mypy clean.

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

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

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

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

Full suite 855 passed; ruff and mypy clean.

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

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

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

Full suite 856 passed; ruff and mypy clean.

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

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

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

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

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

## Mpemba abstains on unresolved spectra (P1)

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

## dgeev-real requires exact realness (P1)

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

## Hermiticity gate is gauge invariant (P1)

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

## Repair ladder is lazy (P2)

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

## Unresolved certificate floors the verdict (P2)

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

Full suite 920 passed; ruff and mypy clean.

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

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

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

All three findings reproduced before changing anything, then adopted.

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

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

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

## D24 recomputation is certified (P2)

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

## Conclusively refuted partial rungs are NOT_SUPPORTED (P2)

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

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

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

Both findings reproduced before changing anything, then adopted.

## Petermann factors consume the certified eigendecomposition (P1)

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

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

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

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

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

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

## certified_eig validates eigenvector residuals (P1)

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

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

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

## The ladder continues past ambiguous candidates (P2)

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

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

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

All four findings reproduced before changing anything, then adopted.

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

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

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

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

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

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

## Release-note correction (P2, documentation)

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/liouscope/_consts.py Dismissed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/spectral.py
Comment thread src/liouscope/diagnostics/mpemba.py Outdated
Comment thread src/liouscope/_zhou.py Outdated
Comment thread src/liouscope/diagnostics/nonnormality.py Outdated
Comment thread src/liouscope/fitting/gls.py
Comment thread src/liouscope/numerics/linalg.py Outdated
@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Evidence, now measured rather than quoted.

Local run

.venv/Scripts/python.exe -m pytest -q
947 passed in 848.38s (0:14:08)
exit 0

Interpreter: C:\Users\marco\dev\Liouscope\.venv\Scripts\python.exe, Python 3.14.
This matters: the first attempt used the system py launcher instead of the
repository .venv and produced 54 collection errors, every one of them
ModuleNotFoundError: No module named 'liouscope'. That was the wrong
environment, not a broken tree. Anyone reproducing this must use the repository
.venv.

CI on this pull request

All five workflows completed successfully:

Workflow Result
CI success
CI (QuTiP cross-checks) success
CodeQL success
Quality Contract success
ci-reusable-pilot success

What this settles beyond this pull request

The description opened with the question of whether this account is currently
assigned runners for public repositories. It is: these five runs are the
observation that was missing. Runs on other public repositories on 2026-08-25
and 2026-08-26 had already executed real jobs, but none of them was
push-triggered, so the question stayed open until now.

The practical consequence is that on public repositories a green rollup is
re-earnable, which was the assumption that kept this work on a local branch in
the first place. It does not extend to private repositories: red runs observed
there on 2026-08-26 and 2026-08-27 completed in 2–4 seconds with zero steps,
which is a start failure with no runner assigned rather than a substantive
failure.

Still open

This pull request is BEHIND main and needs updating before it can merge.
Issues #112, #113 and #117 remain deliberately untouched — see the description.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/nonnormality.py Outdated
Comment thread src/liouscope/_types.py Outdated
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/numerics/linalg.py Outdated
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/liouscope/diagnostics/classification.py
Comment thread src/liouscope/_zhou.py
marcohost33-maker and others added 2 commits September 7, 2026 21:54
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
@marcohost33-maker

Copy link
Copy Markdown
Owner Author

Status 2026-09-07 — three open threads answered, two of them repaired

All 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.

thread verdict outcome
_zhou.py:148 correct reproduced, repaired, retraction-proved
diagnostics/classification.py:1106 correct reproduced, repaired, retraction-proved
numerics/linalg.py:673 correct on the physics deliberately NOT repaired — modelling decision, repair cost measured

What changed (local commits, unpushed)

Branch fix/121-round25-threads, based on this PR's head bac5fe0d, two commits ending at b3c2d5d. It is a linear descendant of pr107-fix, so it fast-forwards.

  1. _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. A caller handing back an unresolved report passes two non-None NaNs; nan <= 0 is False, so the gap abstention was skipped too, and the record was published with converged=True and NaN bounds. Supplied values are now checked for finiteness before they may bypass certification.
  2. classify_mechanism published NOT_EXCLUDED for an A12 winner that its own evidence matrix marked UNEVALUABLE with an UNDEFINED claim floor. A floor now reads the matrix the report carries — the same object, built once and consumed twice — and caps the verdict at the winner's own floor.

Belegtlauf — CI command chain reproduced locally

Repository .venv, Python 3.14.4. Not the py launcher: under py this repo produces collection errors that look like a broken repository and are an environment fault.

ruff check src tests benchmarks              -> exit 0
mypy src/liouscope                           -> exit 0   (53 source files)
pytest --cov=liouscope --cov-fail-under=90   -> exit 0
                                                1118 passed, 529.23 s
                                                coverage 95.24 %

Collection at bac5fe0d excluding the new file: 1103. 1103 + 15 new = 1118, so nothing was displaced. tests/test_anchors.py untouched and green inside that run.

Retraction proof (the tests are non-vacuous and the guards are not unconditional): tests/test_pr121_review_round27.py against unpatched bac5fe0d8 failed, 7 passed; against the patched source — 15 passed. The 7 that pass either way are the positive controls and the pre-existing abstentions.

Closes #118 is now a real band

It was prose before (Closes the three review findings from #118), which GitHub does not parse. Verified after editing, via closingIssuesReferences: this PR now closes #118. #107's head 4a8ae9c is an ancestor of bac5fe0d (git merge-base --is-ancestor -> true) and #107 has 0 commits unique to it, so all sixteen findings of #118 are in this tree.

What Marco has to decide

  1. Push b3c2d5d. Two commits, unpushed by instruction. Nothing here is merged, rebased or force-pushed.
  2. numerics/linalg.py:673 — a real decision, now with both routes measured. The collision is reachable on a fixture the suite already carries. I implemented the fail-closed repair you would expect and measured its cost: cert.resolved goes True -> False, applied_tolerance moves from 4.4e-10 to 3.3e-06, and exactly one test fails — test_d11_fallback_scans_the_refined_zero_set, on its assert cert.resolved precondition, not on a value. So the choice is "borrowed evidence vs. conceding that this network does not resolve". It belongs in its own PR with the anchor movement stated. The probe was reverted; the tree is clean.
  3. Merge method is load-bearing. fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix #107 is strictly contained here. A merge commit keeps 4a8ae9c reachable from main and closes fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix #107 and PR #107: 16 offene Review-Befunde am aktuellen Stand — Sachfragen, nicht Nachpflege #118 automatically. Squash or rebase rewrites the SHA and leaves fix(#108): scale-relative zero-mode separation + feat(#102): hypothesis evidence matrix #107 open as a zombie.

Local measurement only. CI on GitHub not triggered — no push, no re-run, no workflow_dispatch. Nothing was resolved: the three threads carry replies, not resolutions.

Copy link
Copy Markdown
Owner Author

Merge gate — 2026-09-07

Ich habe diesen PR wieder auf Draft gesetzt. Das ist kein Urteil ueber die bereits gruenen Checks am aktuellen Head bac5fe0d; es trennt vielmehr CI-Evidenz von noch nicht integrierter Review-Evidenz.

Vor Ready for review muessen vier Bedingungen gleichzeitig erfuellt sein:

  1. Die lokal beschriebenen Reparaturen fuer _zhou.py (nicht-finite vorab gelieferte D24-Werte) und classification.py (UNEVALUABLE-Winner -> UNDEFINED) muessen auf einen neuen gepushten Head gelangen und dort reviewbar sein. Lokale 1118 passed sind wertvolle Entwicklungs-Evidenz, aber keine Evidenz fuer bac5fe0d.
  2. Der aktuelle P1 zur Eigenpaar-Zuordnung wird fail-closed entschieden: Residuen/Eigenvektor-Evidenz darf nicht von einem anderen Kandidaten geliehen werden. Die Zuordnung muss injektiv/eindeutig sein; wenn keine kollisionsfreie, hinreichend nahe Referenz existiert, bleibt der Kandidat unresolved. Dass dadurch die heutige D11-Fixture ihre Vorbedingung cert.resolved verliert, ist kein Grund, geliehene Evidenz zu behalten — dann ist die Vorbedingung der Fixture zu ersetzen bzw. eine neue positive Fixture zu bauen.
  3. Der neue Head braucht exact-head CI + die gezielten Regression-/Retraction-Proben. Ein gruener alter Head darf nicht stellvertretend fuer unpushed fixes stehen.
  4. Erst danach werden die drei aktuellen Threads anhand dieses gepushten Heads geschlossen. Der alte Exact-Degeneracy-Thread wurde heute nur als outdated Duplikat des allgemeineren Collision-P1 aufgeloest; der Defekt selbst ist damit ausdruecklich nicht geschlossen.

Begruendung fuer Punkt 2: certified_* ist eine Zertifizierungsschicht. Ein Residuum ist Evidenz fuer ein konkretes Eigenpaar, nicht fuer den numerisch naechsten Kandidaten schlechthin. Eine many-to-one-Zuordnung kann daher keinen zweiten Kandidaten zertifizieren; sie muss abstinent werden, nicht optimistisch. Das ist eine Evidenz-Identitaetsbedingung, keine frei waehlbare physikalische Konvention.

marcohost33-maker and others added 10 commits September 8, 2026 00:09
… 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
@marcohost33-maker
marcohost33-maker marked this pull request as ready for review September 8, 2026 14:32
@marcohost33-maker
marcohost33-maker merged commit 9dd0290 into main Sep 8, 2026
13 checks passed
@marcohost33-maker
marcohost33-maker deleted the pr107-fix branch September 8, 2026 14:32
marcohost33-maker added a commit that referenced this pull request Sep 11, 2026
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR #107: 16 offene Review-Befunde am aktuellen Stand — Sachfragen, nicht Nachpflege

3 participants