Skip to content

Commit ecab4a5

Browse files
fix: Gram-consistent D2/D2b symmetrised gaps + 2026-07 audit hardening batch (#73)
* fix: Gram-consistent D2/D2b symmetrised gaps + audit hardening batch (property tests, CodeQL, repo-review fixes) Physics fixes (2026-07 audit, all anchor-preserving — 21/21 anchors unchanged): - alicki_adjoint: transpose the Kron rho factors so the pi-weighted adjoint is the true GNS-Gram adjoint G^-1 L G (G = rho.T (x) I, anchor B). Off the real-diagonal steady-state manifold the old construction returned unphysical NEGATIVE gns_gap (repro: Delta_s = -4.4 at Delta = 0.63). - kms_gap: use the KMS-Gram adjoint (new gram_adjoint helper) instead of mixing the GNS adjoint with a KMS similarity transform. - _real_gap_from_symmetric: deflate exactly one zero mode; additional Hermitian-part kernel vectors mean NO certified contraction (Delta_s = 0), filtering them over-certified the bound (Delta_s > Delta). Contraction bounds 0 <= Delta_s <= Delta verified on 40/40 random GKSL. - sparse.chi1: remove double square root (certificate was K^(1/4), docs say K^(1/2)); pinned against the dense petermann_factors oracle. - relaxation: failed bootstrap now yields (nan, nan) CI + RuntimeWarning instead of a silent zero-width interval (perfect-certainty failure mode). - Robustness guards: steady_state integer-dtype cast, v4_thermal beta*omega guard, seed bool rejection, engineered_target_jumps zero-vector guard. Quality/infra (best-practice research: SPEC 0/7/8, sp-repo-review, OpenSSF): - New Hypothesis property-based suite (gap scaling, unitary similarity, trace-distance axioms + CPTP contractivity, semigroup law, Choi gate). - New CodeQL SAST workflow (SHA-pinned, minimal permissions). - pre-commit config (ruff, zizmor, cffconvert, hygiene hooks). - pyproject strictness: pytest --strict-config/xfail_strict/filterwarnings=error, mypy warn_unreachable + extra error codes, build-requires cleanup. - CI concurrency auto-cancel; pypi.yml release QA gate (twine check --strict + check-wheel-contents); scorecard publish_results for the public repo. Docs: README layer table now uses the code-backed D-numbering; honest qualification of the D21-D23 schema slots; manifest/seed claims corrected. Semantics-changing classifier findings are tracked as issues #68-#72 for dedicated PRs per AGENTS.md working agreement 3 (anchors are sacred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XyiBg4VHjKnQ56kqJGFAN6 * fix(ci): allowlist QuTiP's matplotlib-absent import warning under filterwarnings=error The new fail-loud filterwarnings=["error"] gate turned QuTiP's import-time "matplotlib not found: Graphics will not work." UserWarning into a hard error on every CI leg (the matrix installs .[dev,qutip] without the figures extra; local runs had matplotlib installed, which is why this only surfaced in CI). Graphics are irrelevant to the cross-checks — ignore exactly this message, keep everything else erroring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XyiBg4VHjKnQ56kqJGFAN6 * fix(ci): match ResolventConvergenceWarning by message, not class path The class-based filterwarnings entry (ignore::liouscope.numerics.resolvent.ResolventConvergenceWarning) forces pytest to import liouscope at config time — BEFORE pytest-cov starts measuring. coverage then reports module-not-measured for the whole package and the total craters from ~94% to 74.55%, tripping --cov-fail-under=80 on every matrix leg. Matching the warning by its message prefix needs no import; verified locally: 403 passed, coverage 94.23%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XyiBg4VHjKnQ56kqJGFAN6 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 13f57fa commit ecab4a5

26 files changed

Lines changed: 764 additions & 87 deletions

.github/workflows/ci-qutip.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ on:
1818

1919
permissions: {}
2020

21+
# Auto-cancel superseded runs on the same ref; never cancel main-branch runs.
22+
concurrency:
23+
group: ${{ github.workflow }}-${{ github.ref }}
24+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
25+
2126
jobs:
2227
qutip-cross-check:
2328
runs-on: ubuntu-latest

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ on:
88

99
permissions: {}
1010

11+
# Auto-cancel superseded runs on the same ref (saves matrix minutes on
12+
# rapid-fire pushes); never cancel main-branch runs.
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
16+
1117
jobs:
1218
test:
1319
runs-on: ${{ matrix.os }}

.github/workflows/codeql.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# CodeQL — static application security testing (SAST) for the Python package.
2+
# Docs: https://docs.github.com/en/code-security/code-scanning
3+
#
4+
# Satisfies the OpenSSF Scorecard "SAST" check (ruff is a linter, not SAST).
5+
# Runs on PRs + main pushes + a weekly cron so newly added CodeQL queries
6+
# retroactively scan the existing code, not just new diffs.
7+
#
8+
# NOT branch-protection-required (Quality Workflow OS G6): it is path-relevant
9+
# to every PR, but keep it advisory until its signal/noise ratio on this
10+
# codebase is established.
11+
name: CodeQL
12+
13+
on:
14+
push:
15+
branches: ["main"]
16+
pull_request:
17+
branches: ["main"]
18+
schedule:
19+
- cron: "22 5 * * 1" # Mondays 05:22 UTC — offset from Scorecard/Dependabot
20+
21+
permissions: {}
22+
23+
jobs:
24+
analyze:
25+
name: CodeQL analyze (python)
26+
runs-on: ubuntu-latest
27+
permissions:
28+
security-events: write # upload CodeQL results to Code Scanning — required
29+
contents: read # checkout — required
30+
actions: read # workflow metadata for private-path resolution — required
31+
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
35+
with:
36+
persist-credentials: false
37+
38+
- name: Initialize CodeQL
39+
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
40+
with:
41+
languages: python
42+
# security-and-quality adds maintainability queries on top of the
43+
# default security suite; scope stays src/ + packaged code.
44+
queries: security-and-quality
45+
46+
- name: Perform CodeQL analysis
47+
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
48+
with:
49+
category: "/language:python"

.github/workflows/pypi.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ jobs:
3030
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
3131
with:
3232
python-version: "3.12"
33-
- name: Install build
33+
- name: Install build + QA tools
3434
run: |
3535
python -m pip install --upgrade pip
36-
pip install build
36+
pip install build twine check-wheel-contents
3737
- name: Build sdist + wheel
3838
run: python -m build
39+
- name: Release QA gate (fail-closed before upload)
40+
# twine check --strict: long-description renders on PyPI, metadata sane.
41+
# check-wheel-contents: no stray tests/.pyc/empty-wheel packaging bugs.
42+
run: |
43+
twine check --strict dist/*
44+
check-wheel-contents dist/*.whl
3945
- name: Publish via Trusted Publishing
4046
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
4147
with:

.github/workflows/scorecard.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
#
44
# Note: Scorecard requires read access to the GitHub GraphQL API. On private
55
# repos without GitHub Advanced Security this fails with "Resource not
6-
# accessible by integration". We skip the job on private repos until the
7-
# repo is made public or GHAS is enabled.
6+
# accessible by integration"; the `if:` guard keeps the job skipped there.
7+
# This repo is PUBLIC (since 2026-06), so the job runs and results are
8+
# published to the Scorecard REST API (badge-capable).
89
name: OpenSSF Scorecard
910

1011
on:
@@ -40,7 +41,9 @@ jobs:
4041
with:
4142
results_file: results.sarif
4243
results_format: sarif
43-
publish_results: false
44+
# Public repo: publish to the Scorecard REST API so the score is
45+
# badge-capable and externally auditable (uses the job's id-token).
46+
publish_results: true
4447

4548
- name: Upload artifact
4649
if: ${{ !cancelled() && hashFiles('results.sarif') != '' }}

.pre-commit-config.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# pre-commit — fast local hygiene gates (mirror of the cheap CI checks).
2+
# Install once: pip install pre-commit && pre-commit install
3+
# Run manually: pre-commit run --all-files
4+
#
5+
# Deliberately NOT included: a code formatter (would churn the whole history;
6+
# adopt ruff-format in a dedicated PR if ever), mypy (needs the full numpy
7+
# environment — CI covers it), pytest (too slow for a commit hook).
8+
repos:
9+
- repo: https://github.com/pre-commit/pre-commit-hooks
10+
rev: v5.0.0
11+
hooks:
12+
- id: check-added-large-files
13+
- id: check-case-conflict
14+
- id: check-merge-conflict
15+
- id: check-yaml
16+
- id: check-json
17+
- id: check-toml
18+
- id: end-of-file-fixer
19+
- id: trailing-whitespace
20+
args: [--markdown-linebreak-ext=md]
21+
- id: detect-private-key
22+
23+
- repo: https://github.com/astral-sh/ruff-pre-commit
24+
rev: v0.12.1
25+
hooks:
26+
- id: ruff-check
27+
args: [src, tests, benchmarks]
28+
pass_filenames: false
29+
30+
- repo: https://github.com/citation-file-format/cffconvert
31+
rev: b6045d78aac9e02b039703b030588d54d53262ac # 2.0.0
32+
hooks:
33+
- id: validate-cff
34+
35+
- repo: https://github.com/zizmorcore/zizmor-pre-commit
36+
rev: v1.26.1
37+
hooks:
38+
- id: zizmor
39+
args: [--persona=regular, --no-online-audits, .github/workflows]
40+
pass_filenames: false

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@ python examples/quickstart.py # smoke run
9191
4. **SHA-pin all GitHub Actions.** Welle G established the gold-standard:
9292
every action reference is `@<full-sha> # vX.Y.Z`. Dependabot is on a
9393
cooldown to avoid PR-spam.
94-
5. **Reproducibility.** Manifest seeds, library versions, lattice geometry
95-
are recorded automatically; do not bypass the manifest writer.
94+
5. **Reproducibility.** The run manifest records seed, framework/schema/
95+
taxonomy versions, Python/NumPy/SciPy versions, platform, solver path and a
96+
run-invariant `input_hash` automatically; do not bypass the manifest writer.
97+
(Lattice geometry / dissipator family are NOT yet manifest fields — do not
98+
claim them; see MANIFEST_SCHEMA.json for the authoritative field list.)
9699
6. **Reality-Anchor.** Prefer "anchors pass (verified <date>)" over
97100
"diagnostics implemented". No claims without code-belege.
98101
7. **Plain paths in code-blocks** for file references (no markdown links —

CHANGELOG.md

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,55 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77
## [Unreleased]
88

99
### Fixed
10+
- **D2/D2b symmetrised gaps (`numerics.adjoint`, `diagnostics.spectral`) are
11+
now genuine Gram-adjoint constructions** (2026-07 audit A1). Three coupled
12+
defects were fixed:
13+
- `alicki_adjoint` built the pi-weighted adjoint as
14+
`(rho^-1 (x) I) L (rho (x) I)` while the GNS similarity transform used the
15+
anchor-B Gram `G = rho.T (x) I`. The two coincide only for real-diagonal
16+
steady states; off that manifold the "symmetrised" generator was not
17+
G-Hermitian, its silent Hermitisation masked the defect, and `gns_gap()`
18+
returned unphysical **negative** values (repro: generic d=3 GKSL with
19+
steady-state coherences gave `Delta_s = -4.4` at `Delta = 0.63`). The Kron
20+
factors are now transposed — i.e. exactly `G^{-1} L G`. No-op for the
21+
real-diagonal anchor fixtures (all 21 anchors unchanged).
22+
- `kms_gap` mixed pictures: it symmetrised with the *GNS* adjoint but
23+
conjugated with the *KMS* Gram. It now uses the KMS-Gram adjoint via the
24+
new `numerics.adjoint.gram_adjoint(L, G)` helper.
25+
- Gap extraction (`_real_gap_from_symmetric`) now deflates exactly ONE
26+
steady-state zero mode instead of filtering *all* near-zero eigenvalues.
27+
A degenerate Hermitian-part kernel means there is **no certified
28+
exponential GNS contraction** (`Delta_s = 0`); the old filtering
29+
over-certified the bound (`Delta_s > Delta`, contradicting the
30+
Kadison-Schwarz contraction property). Consequence: driven pure dephasing
31+
with `rho_ss = I/2` now honestly reports `gns_gap = 0` (the sigma_z mode
32+
has zero instantaneous GNS decay rate); detailed-balance systems keep
33+
`Delta_s = Delta`. New contraction-bound regression tests
34+
(`0 <= Delta_s <= Delta`, `0 <= Delta_KMS <= Delta`) pin the fix on
35+
non-diagonal steady states (40/40 random systems verified).
36+
- **`sparse.chi1.chi1_lower_bound` returned `K^(1/4)` instead of the
37+
documented `K^(1/2)`** (2026-07 audit A4): the ratio
38+
`||r||*||l|| / |<l,r>|` is already the square root of the Petermann factor;
39+
the code took a second square root, quadratically weakening the
40+
ARPACK-reliability certificate. A regression test now pins the certificate
41+
against the dense `petermann_factors` oracle (the old test only asserted
42+
`chi > 0`).
43+
- **Silent zero-width confidence interval on bootstrap failure**
44+
(`diagnostics.relaxation.compute_relaxation_layer`, 2026-07 audit A7): a
45+
swallowed `parametric_bootstrap`/`bca_ci` exception used to keep the
46+
degenerate initialisation `(beta_D, beta_D)`, which the uncertainty layer
47+
read as `fit_uncertainty = 0.0`*perfect certainty* as the failure mode.
48+
The CI is now `(nan, nan)` plus a `RuntimeWarning`;
49+
`compute_uncertainty_layer` already maps that to `fit_uncertainty = nan`.
50+
- Robustness guards (2026-07 audit A10/A11): `core.lindblad.steady_state`
51+
casts integer/bool input instead of crashing in `np.finfo`;
52+
`examples.v4_thermal_two_level` rejects `beta*omega <= 0` (divide-by-zero);
53+
`io.seed.seed_everything` rejects `bool` seeds (silent 0/1 seeding);
54+
`core.jumps.engineered_target_jumps` rejects a zero target vector and its
55+
docstring now matches the two-operator return.
56+
- Docstring/comment drift fixed against implementations: D4 spread comment in
57+
`_types.py`, pseudospectrum grid location, D18 sensitivity ensemble, D7b
58+
fallback value, `diagnostics/__init__` layer count.
1059
- `liouscope.numerics.cptp`: fail-closed input validation now rejects
1160
non-finite generator/channel entries (`L_super`, `channel_super`) and invalid
1261
(negative / non-finite) tolerances *before* the matrix exponential, Choi
@@ -15,6 +64,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1564
the CP/TP verdict.
1665

1766
### Added
67+
- **Property-based test suite** (`tests/test_property_based.py`, Hypothesis):
68+
ground-truth-free invariants on randomly drawn GKSL systems — gap scaling
69+
`gap(c*L) = c*gap(L)`, spectrum invariance under unitary similarity,
70+
trace-distance axioms + CPTP contractivity, the GKSL semigroup law
71+
`Phi_{t1+t2} = Phi_{t2} Phi_{t1}`, and no-false-alarm coverage for the Choi
72+
CPTP gate. `derandomize=True` keeps CI deterministic (the `hypothesis` dev
73+
dependency was previously declared but unused).
74+
- **CodeQL SAST workflow** (`.github/workflows/codeql.yml`): weekly + PR/push
75+
`security-and-quality` Python analysis, SHA-pinned, minimal permissions
76+
(OpenSSF Scorecard SAST check; ruff is a linter, not SAST).
77+
- **pre-commit configuration** (`.pre-commit-config.yaml`): ruff-check,
78+
zizmor (regular persona), CITATION.cff validation (`cffconvert`) and
79+
standard hygiene hooks. No formatter (history churn) — CI remains the
80+
authoritative gate.
81+
- `numerics.adjoint.gram_adjoint(L_super, G)`: adjoint of the
82+
Heisenberg-picture generator w.r.t. an arbitrary Gram matrix (GNS and KMS
83+
are the two instantiations).
1884
- `jsonschema>=4.18` added to the `dev` extra so CI actually exercises the full
1985
`Draft202012Validator` structural-conformance path for run manifests. It was
2086
previously absent from the dev/CI environment, so `_compiled_validator()`
@@ -27,7 +93,42 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2793
unnormalised maximally entangled operator, *not* `eye(d**2)`) and its
2894
CP-boundary PSD-ness across `d in {2, 4, 8, 16}`.
2995

96+
### CI
97+
- `ci.yml`/`ci-qutip.yml`: `concurrency` groups auto-cancel superseded runs on
98+
non-main refs (sp-repo-review GH102).
99+
- `pypi.yml`: fail-closed release QA gate (`twine check --strict` +
100+
`check-wheel-contents`) between build and Trusted-Publishing upload.
101+
- `scorecard.yml`: `publish_results: true` now that the repo is public
102+
(badge-capable, externally auditable score); stale private-repo comment
103+
refreshed.
104+
- `pyproject.toml` strictness batch (sp-repo-review): pytest gains
105+
`--strict-config`, `xfail_strict`, `log_level` and
106+
`filterwarnings = ["error", ...]` (unexpected warnings — including
107+
deprecations from dependency drift — now fail CI; the documented
108+
`ResolventConvergenceWarning` lower-bound path is allowlisted); mypy gains
109+
`warn_unreachable` + `enable_error_code = [ignore-without-code,
110+
redundant-expr, truthy-bool]`; `wheel` removed from `build-system.requires`
111+
(setuptools adds it itself when needed); redundant ruff `target-version`
112+
dropped (inferred from `requires-python`).
113+
- `pypi.yml`: the Trusted-Publishing step now sets `print-hash: true` so each
114+
uploaded sdist/wheel's SHA-256 is logged for the release-evidence lock. No
115+
second attestation step is added — the PyPA action already uploads a PEP 740
116+
attestation by default under Trusted Publishing, and PyPI rejects duplicate
117+
predicates / more than two attestations per file. `docs/RELEASE_AUDIT_v0.5.0.md`
118+
§5 documents the evidence flow; the stale "private repo" comment was refreshed
119+
to "public".
120+
30121
### Documentation
122+
- README: the diagnostic layer table now uses the **code-backed D-numbering**
123+
(module docstrings / StabilityReport keys as source of truth) — the previous
124+
table circulated a second, contradictory numbering scheme. The "24
125+
diagnostics" claim is qualified honestly: D1-D20 (+D2b/D7b/D11b) and D24 are
126+
code-backed; D21-D23 are schema-defined but not yet implemented.
127+
- README/AGENTS.md reproducibility claims corrected to what the code does: the
128+
manifest does not (yet) record lattice geometry / dissipator family / full
129+
result graph; `seed_everything` does not control SciPy/BLAS threading.
130+
- README quickstart no longer suggests the dephased-qubit example classifies
131+
as "A1" (see the tracked Mpemba false-positive issue).
31132
- Synchronized `docs/CANON_STATUS.md` and `AGENTS.md` with the v0.5.0 runtime
32133
canon: public-repo status, Python 3.14 CI coverage, the dedicated QuTiP
33134
cross-check checks, and the `StabilityReport v2.1` additive projection.
@@ -43,15 +144,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
43144
and the old path is reduced to a pointer stub so existing links still resolve.
44145
Single numbered ADR going forward.
45146

46-
### CI
47-
- `pypi.yml`: the Trusted-Publishing step now sets `print-hash: true` so each
48-
uploaded sdist/wheel's SHA-256 is logged for the release-evidence lock. No
49-
second attestation step is added — the PyPA action already uploads a PEP 740
50-
attestation by default under Trusted Publishing, and PyPI rejects duplicate
51-
predicates / more than two attestations per file. `docs/RELEASE_AUDIT_v0.5.0.md`
52-
§5 documents the evidence flow; the stale "private repo" comment was refreshed
53-
to "public".
54-
55147
## [0.5.0] - 2026-06-25
56148

57149
Release cut: the Canon v0.5 diagnostics & contracts wave plus the cross-family

0 commit comments

Comments
 (0)