From 67e56da1984565e3b92f88d25e284f046bfd8a57 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 21:24:56 +0200 Subject: [PATCH 01/15] Reorient public README and citation for 0.2.11 candidate --- CITATION.cff | 8 +- README.md | 353 +++++++++---------- examples/README.md | 68 ++-- tests/documentation/test_docs_generation.py | 13 +- tests/unit/test_gate_b_release_surface.py | 16 +- tests/unit/test_pp05_release_metadata.py | 22 +- tests/unit/test_publication_metadata_gate.py | 5 +- 7 files changed, 234 insertions(+), 251 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 7727b77e..feb17c5b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,10 +1,8 @@ cff-version: 1.2.0 -message: "If you use QF Solver, please cite the published software release." +message: "For the evolving project, use the project concept DOI. Version 0.2.11 is a release candidate and has no release date or version DOI yet." type: software title: "QF Solver" -version: "0.2.10" -date-released: 2026-10-03 -doi: "10.5281/zenodo.23106744" +version: "0.2.11" license: "Apache-2.0" authors: - family-names: "Farinazzo" @@ -12,5 +10,5 @@ authors: repository-code: "https://github.com/emptiesvoid-cloud/QF_solver" url: "https://emptiesvoid-cloud.github.io/QF_solver/" -abstract: "Open-source white-box finite-element solver for verifiable structural mechanics and dynamics." +abstract: "Open-source Python finite-element solver for structural mechanics and dynamics, designed for inspectable formulations, engineering-method development, and reproducible numerical verification." diff --git a/README.md b/README.md index 4a530aaf..99352c82 100644 --- a/README.md +++ b/README.md @@ -6,62 +6,90 @@ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-2f80ed.svg)](https://emptiesvoid-cloud.github.io/QF_solver/) [![CI](https://github.com/emptiesvoid-cloud/QF_solver/actions/workflows/quality.yml/badge.svg)](https://github.com/emptiesvoid-cloud/QF_solver/actions/workflows/quality.yml) -QF Solver is a Python finite-element solver for structural mechanics and -dynamics. It provides inspectable formulations, a public CLI and Python API, -and reproducible verification evidence. Linear analyses form the established -base; selected nonlinear material, geometric, contact and continuation routes -have their own, narrower evidence scopes. PETSc/MPI workflows are optional. +QF Solver is an open-source Python finite-element solver for structural +mechanics and dynamics, designed for inspectable formulations, +engineering-method development and reproducible numerical verification. It +offers linear, dynamic and selected nonlinear analysis routes through a +Python API and command-line interface. Tests, benchmarks, evidence records and +explicit scope limits help users inspect what a result does—and does not—show. QF Solver is intended for engineering-method development, research and -reproducible studies. An implemented feature or a converged example is not +reproducible studies. An implemented route or a converged example is not automatically qualified or physically validated. The project is not certified and is not a general-purpose replacement for industrial FEA software. -**Published release:** 0.2.10. **0.2.11 candidate:** not yet published; it -adds experimental serial gyroscopic modal and Campbell analysis for a narrowly -bounded BEAM2/disk model. See [What's New in 0.2.11](docs/whats-new/0.2.11.md) -for its evidence and limitations. The candidate does not change the current -installation or citation instructions below. +On this page: [Capabilities](#capabilities) · [How it works](#how-qf-solver-works) · +[Quick start](#quick-start) · [Analysis families](#analysis-families) · +[Rotating dynamics](#rotating-dynamics) · [Verification](#verification-and-maturity) · +[Limitations](#limitations) · [Current release](#current-release-and-citation). -On this page: [Purpose](#why-qf-solver) · [First calculation](#quick-start) · -[Capabilities](#capabilities) · [Nonlinear mechanics](#nonlinear-mechanics) · -[Evidence](#verification-and-maturity) · [Limits](#limitations) · -[Release and citation](#release-and-citation) · [Documentation](#documentation). - -## Why QF Solver? - -- **Inspect the method:** Python-native model and result APIs, visible - formulations, mesh checks, residuals and reaction diagnostics. -- **Start small:** maintained JSON examples cover solids, shells, statics and - dynamics; the standard package does not require PETSc, MPI or HDF5. -- **Choose a route deliberately:** elements, materials, analyses, backends and - maturity are documented separately, with links to controlling evidence. -- **Reproduce a study:** retain inputs, environment, diagnostics and the exact - software version. Large-model workflows and V&V campaigns have their own - recorded boundaries. - -## Installation +## Capabilities -Python 3.10 or newer is required. Install the published package: +The capabilities below are cumulative, but maturity belongs to a specific +element, analysis, material, configuration and evidence record—not to the +solver as a whole. Use the [capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) +and [element map](https://emptiesvoid-cloud.github.io/QF_solver/elements/) +before relying on a particular combination. -```bash -python -m pip install "qf-solver==0.2.10" -qf-solver --version +| Domain | Current capabilities | Evidence status and boundary | +| --- | --- | --- | +| Elements | TET4, TET10, HEX8, HEX20, WEDGE6, BEAM2, MITC3 and MITC4 routes | Family support and maturity vary by analysis and formulation; see the element-analysis records. | +| Linear statics | Structural solves, reactions and selected stress/result fields | Bounded element, material, constraint and load combinations; not every combination is qualified. | +| Modal and dynamics | Classical modal analysis, Newmark transient response and harmonic response | Route-specific mass, damping, timestep, element-family and mixed-model limits apply. | +| Stability | Linear buckling | Does not establish postbuckling or general nonlinear stability behavior. | +| Material nonlinearity | Selected small-strain J2 plasticity routes | Bounded material/element evidence; this is not general finite-strain plasticity. | +| Geometric nonlinearity | Selected Total-Lagrangian and corotational routes | Formulation and element scopes are narrow; corotational J2 assumes small local strains. | +| Contact and constraints | RBE2-style constraints, frictionless contact, and bounded frictional stick/slip evidence | Contact search, sliding, mesh sensitivity and current-source requalification remain limited. | +| Nonlinear solution | Newton methods, line search, adaptive increments, cutback/retry, and selected continuation/arc-length routes | Shared infrastructure does not imply convergence or qualification for arbitrary models. | +| Larger models | Structured-TET4 workflows and selected PETSc/MPI linear-static cases | Optional dependencies and recorded configurations only; no general HPC or nonlinear distributed claim. | +| Rotating dynamics | Experimental disk-gyroscopic `rotating_modal` and Campbell analysis in the 0.2.11 candidate | Linear BEAM2 shaft with centered rigid axisymmetric disks; serial dense QEP and explicit scope limits. | +| I/O and interfaces | JSON models, public `qf_solver` Python namespace, CLI, bounded `.inp` import and optional mixed HDF5 results | Import/export formats and optional backends are not general-purpose interchange guarantees. | +| Verification and validation | Analytical checks, regression tests, controlled benchmarks, reproducible evidence and selected Code_Aster correlations | Numerical verification, external solver correlation and physical validation are distinct claims. | + +Status terms such as `QUALIFIED_BOUNDED`, `EXPERIMENTAL_BOUNDED`, +`EXPERIMENTAL` and `NOT_VALIDATED` refer only to the scope documented by the +controlling record. The [published 0.2.8 registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) +remains the authority for its historical element-analysis decisions; later +evidence does not silently rewrite those decisions. + +## How QF Solver works + +The public API and CLI validate a model, route it to an analysis-specific +driver, assemble the required contributions and return results with +route-specific diagnostics. Not every analysis assembles or uses every +contribution shown here. + +```text +Model / input + ↓ +Public qf_solver API or CLI + ↓ +AnalysisRouter + ↓ +Analysis driver + ↓ +Assembly: elements · materials · loads · constraints · contact · rotating terms + ↓ +Linear / nonlinear / eigenvalue solver + ↓ +Results · diagnostics · provenance ``` -The unpinned command `python -m pip install qf-solver` selects the latest -version available on PyPI. For reproducible source access, use the immutable -[`v0.2.10` tag](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10), -not an unqualified clone of `main`. Optional `mesh`, `hdf5`, `large`, -`hpc` and `docs` extras are described in the -[installation guide](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/installation/). -Native MPI/PETSc/SLEPc availability remains environment-dependent. +Linear, dynamic, nonlinear and rotating analyses have distinct contracts and +solver routes. SciPy provides the standard numerical stack; selected large +linear workflows can use optional PETSc/MPI components. Optional packages do +not expand the validated scope by themselves. See the [architecture](https://emptiesvoid-cloud.github.io/QF_solver/architecture/) +and [API stability](https://emptiesvoid-cloud.github.io/QF_solver/reference/api_stability/) +pages for implementation boundaries and compatibility status. ## Quick start -From a repository checkout containing the maintained example: +The latest published PyPI release is 0.2.10. From a repository checkout with +the maintained example files, install that release and run a small linear +static case: ```bash +python -m pip install "qf-solver==0.2.10" qf-solver check-mesh --input examples/tet4_static.json qf-solver solve --input examples/tet4_static.json --output results/tet4.json ``` @@ -79,167 +107,116 @@ result = solve_model(model) save_result(result, "results/tet4.json") ``` -Inspect the result and its diagnostics before relying on it; output fields and -their interpretation depend on the selected route. The -[first-calculation guide](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/quickstart/) -explains the workflow, and the -[example index](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/examples/README.md) -groups additional inputs. New integrations should use `qf_solver`; `solveur` -and the legacy launchers remain compatibility paths in 0.2.x. - -## Capabilities +Inspect the result fields, residuals, reactions and route-specific diagnostics +before relying on an output. The [first-calculation guide](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/quickstart/) +explains the workflow. New integrations should use `qf_solver`; `solveur` and +the legacy launchers remain 0.2.x compatibility paths. -The table is an orientation, not a substitute for an element-analysis -decision. Check the -[capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) -and the applicable record before choosing a mesh, material or backend. +## Analysis families -| Area | What is available | Scope to check | -| --- | --- | --- | -| Elements | TET4, TET10, HEX8, HEX20 and WEDGE6 solids; BEAM2 and MITC3/MITC4 routes | Analysis-specific family and formulation records; WEDGE15 is unsupported and PYRAMID5 is internal/research-only. | -| Linear statics | Small-strain structural solves, including selected mixed-family, multi-material and MPC workflows | `QUALIFIED_BOUNDED` only for recorded combinations; mixed workflows have separate decisions. | -| Dynamics and stability | Modal, linear Newmark transient, harmonic response and linear buckling routes | Mass, damping, timestep, family and mixed-route maturity vary; linear buckling does not establish postbuckling. | -| Materials | Linear elastic and selected small-strain J2 plasticity routes | Small-strain J2 has bounded element-analysis decisions; other constitutive paths require their own evidence. | -| Nonlinear mechanics | Selected geometric, coupled material/geometric, corotational J2, contact and continuation routes | Evidence differs by route; see [below](#nonlinear-mechanics). | -| Rotating dynamics (0.2.11 candidate) | Experimental `rotating_modal` and Campbell sweep | Linear straight BEAM2 shaft with centered rigid axisymmetric disks; serial dense QEP; the high-frequency pair remains ambiguous at 100 rad/s. Not general rotordynamics or critical-speed prediction. | -| Inputs and results | JSON model workflow, bounded Abaqus/CalculiX `.inp` import, optional mixed HDF5 results | `.inp` and HDF5 paths are provisional/bounded, not general format or parallel-storage guarantees. | -| Larger models | Structured-TET4 workloads and selected PETSc/MPI linear-static cases | Recorded hardware and rank configurations only; mixed distributed runtime remains `NOT_VALIDATED`. | - -The published -[0.2.8 consolidated registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) -remains authoritative for its 46 element-analysis decisions: 32 -`QUALIFIED_BOUNDED`, 14 `EXPERIMENTAL`, 0 `NOT_QUALIFIED`. The 0.2.10 -development-cycle records add separate evidence; they do not silently -reclassify those historical decisions. For example: - -| Route | Published status | Boundary | +| Analysis | Orientation | Documentation | | --- | --- | --- | -| WEDGE6 static | `QUALIFIED_BOUNDED` | Declared Gmsh Prism 6 static scope. | -| WEDGE6 modal | `QUALIFIED_BOUNDED` | Homogeneous consistent-mass route and first three modes. | -| Frictionless penalty contact | `EXPERIMENTAL_BOUNDED` | Node-to-triangle, bounded small-sliding cases. | -| Mixed distributed PETSc/MPI | `NOT_VALIDATED` | No validated generic mixed-distributed runtime claim. | - -## Nonlinear mechanics - -Version 0.2.10 adds shared residual/tangent assembly, Newton iteration and -diagnostics, trial/accepted-state transactions, commit/rollback and selected -checkpoint/restart paths. Full Newton, line search, adaptive increments, -cutback/retry and a specialized arc-length route support recorded cases. Shared -infrastructure does **not** qualify every combination of element, material, -geometry, contact and backend; some contact recovery and arc-length machinery -remains route-specific. - -- **Small-strain J2:** use the bounded decisions for the recorded element and - analysis combinations. Corotational J2 permits large rotations under a - small-local-strain assumption; formal bounded acceptance is HEX8-only. - TET10/HEX20 extensions have separate limited evidence. This is not general - multiplicative finite-strain plasticity. -- **Geometric nonlinearity:** selected static, serial TET4/HEX8 - Total-Lagrangian St. Venant–Kirchhoff cases passed a - `GO_WITH_LIMITATIONS` audit. That audit did not promote public maturity or - establish general geometric nonlinearity. -- **Coupled routes:** Owner-accepted bounded static material/geometric - evidence exists for selected TET4/TET10/HEX8/HEX20 cases. It does not extend - to friction, dynamics or distributed execution. -- **Contact:** frictionless penalty contact remains experimental and bounded. - Frictional stick/slip has narrow prior Owner-accepted serial evidence, but - current-source formal requalification is not established. Mesh sensitivity - and search/sliding limits remain. -- **Robustness:** state transactions, retry, restart and continuation apply - only where their route records say so. Arc-length evidence does not establish - general bifurcation or postbuckling capability. - -The [nonlinear mechanics overview](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/nonlinear-overview/) -explains the architecture. A -[small nonlinear example](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/nonlinear-example/) -demonstrates a selected TET4 route and its engineering-profile warning; -convergence of that example is not a general maturity claim. +| Linear static | Small-strain structural equilibrium and reactions | [Analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) · [elements](https://emptiesvoid-cloud.github.io/QF_solver/elements/) | +| Modal | Classical structural eigenvalue analysis | [Analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) · [capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) | +| Transient and harmonic | Newmark time integration and linear frequency response | [Analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) | +| Buckling | Linearized eigenvalue buckling | [Analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) · [limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) | +| Nonlinear mechanics | Material, geometric, continuation and contact routes | [Nonlinear overview](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/nonlinear-overview/) | +| Rotating modal | Experimental disk-gyroscopic eigenanalysis | [Model and limits](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/rotating-modal/) | +| Campbell | Experimental speed sweep and explicit modal tracking | [Tracking and limits](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/campbell/) | + +The [examples index](examples/README.md) groups maintained inputs by the +mechanical problem they illustrate. It also identifies controlled verification +fixtures and intentionally invalid inputs. + +## Rotating dynamics + +The 0.2.11 candidate adds **experimental serial gyroscopic modal analysis for +linear BEAM2 structures carrying centered rigid axisymmetric disks**, plus +experimental Campbell diagrams using explicit complex-mode tracking. This is +a bounded extension to the solver, not a general rotating-machinery solver. + +The initial model is a straight circular-isotropic BEAM2 shaft with centered +rigid axisymmetric disks, constant prescribed signed spin, small perturbations, +zero damping and no centrifugal prestress. The route uses a dense serial +quadratic eigenvalue solve. Distributed shaft gyroscopy, speed-dependent +bearings, centrifugal stress stiffening, unbalance response, rotor/stator +contact, nonlinear rotors, PETSc/SLEPc and MPI are outside this scope. + +At 100 rad/s, the high-frequency pair in the bounded Campbell verification +remains ambiguous. The tracker preserves that gap instead of forcing branch +continuity. GYRO-06 provides **internal mesh-convergence evidence**, not +independent physical validation. Campbell frequencies are not a forced +response, amplitude, operational-danger or validated critical-speed +prediction. Both `rotating_modal` and Campbell remain `EXPERIMENTAL`. ## Verification and maturity -QF Solver separates an implemented path, an executed test, a verified -invariant, external numerical correlation, an Owner-accepted maturity -decision, and physical validation. These are different kinds of evidence. -The [V&V and maturity guide](https://emptiesvoid-cloud.github.io/QF_solver/verification/evidence-and-maturity/) -defines the status labels; the -[0.2.10 verification summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_10/) -links the newer bounded records. - -Code_Aster 18.1 comparisons provide same-mesh **linear-static solver -correlation** for recorded TET4, HEX8, TET10 and HEX20 cases. They are not -physical validation or general nonlinear correlation. Internal independent -recomputation of selected observables is not a second global FEM/Newton solve. - -Historical structured-TET4 large-model evidence includes recorded ~1.029M, -~3M, ~5.01264M and bounded ~10M DOF observations. The two complete 5M Silver replays -were recorded on the declared eight-rank environment. Separate two-rank -PETSc/MPI evidence covers selected linear-static cases with replicated input -and root-side assembly. No claim of GPU, general HPC, hardware-independent -scaling, mixed nonlinear support or general distributed assembly is made. -No claim of certification or universal physical validation is made. +QF Solver distinguishes implementation, a passing test, numerical +verification, external solver correlation, maturity decisions and physical +validation. These are separate kinds of evidence. The [V&V and maturity guide](https://emptiesvoid-cloud.github.io/QF_solver/verification/evidence-and-maturity/) +explains the labels; the [0.2.11 verification summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_11/) +links the candidate gyro and Campbell records. + +Selected Code_Aster 18.1 results provide same-mesh **linear-static solver +correlation** for declared cases and comparable observables. This is not +experimental physical validation or general nonlinear correlation. Internal +independent recomputation of selected observables is also not a second global +FEM/Newton solve. Large-model and PETSc/MPI evidence is tied to specific +workloads and environments; it does not establish general scalability or +distributed nonlinear support. ## Limitations -- General finite-strain plasticity, nonlinear transient dynamics, universal - postbuckling, finite-sliding/self-contact and impact/contact dynamics are - outside the demonstrated scope. -- Higher-order nonlinear evidence is narrower than linear element support; - frictional contact remains mesh-sensitive and is not generally requalified - on the current source. -- Mixed Newmark/harmonic workflows, MITC4 modal, HEX8-SRI, the `.inp` subset - and mixed HDF5 results have their own experimental or provisional bounds. -- Optional PETSc/MPI workflows do not establish general scalability, - nonlinear distributed solving or a validated mixed-distributed runtime. - -Read the [technical limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) -and the controlling capability record for the chosen route. - -## Release and citation - -| Item | Status | -| --- | --- | -| Release line | `0.2.10` | -| 0.2.11 candidate | Experimental rotating modal and Campbell routes; not yet published | -| Source tag | [`v0.2.10`](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10) | -| PyPI distribution | [`qf-solver==0.2.10`](https://pypi.org/project/qf-solver/0.2.10/) | -| GitHub Release | [QF Solver 0.2.10](https://github.com/emptiesvoid-cloud/QF_solver/releases/tag/v0.2.10) | -| Version DOI | [`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744) | -| Project concept DOI | [`10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) | - -QF Solver 0.2.10 is the first published release after 0.2.8. The intervening -`v0.2.9` tag is a source snapshot, not a PyPI, GitHub Release or Zenodo -version. For changes since the last published release, read -[What's New in 0.2.10](https://emptiesvoid-cloud.github.io/QF_solver/whats-new/0.2.10/) -or the [changelog](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/CHANGELOG.md). - -For reproducibility, cite the exact version DOI; use the concept DOI when -referring to the evolving project. The machine-readable record is -[`CITATION.cff`](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/CITATION.cff). -It records the latest published 0.2.10 release; no 0.2.11 version DOI is -claimed while that candidate remains unpublished. The audited distribution -scope is the selected wheel and sdist. The whole-repository G03 archive gate -remains **FAIL** and WP14 remains on -**HOLD**. Automatic GitHub source archives—and the full-repository ZIP while -it remains attached to the Zenodo record—are **not** cleared distribution -artifacts. A DOI resolving does not change that boundary. - -## Documentation +- General multiplicative finite-strain plasticity, nonlinear transient + dynamics, universal postbuckling, finite-sliding/self-contact and + impact/contact dynamics are outside the demonstrated scope. +- Corotational J2 permits large rotations only under a small-local-strain + assumption. Higher-order nonlinear routes are narrower than linear element + support. +- Frictional contact remains bounded and mesh-sensitive; general updated + search, finite sliding and self-contact are not claimed. +- Optional PETSc/MPI workflows do not establish general HPC scaling, nonlinear + distributed solving or a validated mixed-distributed runtime. +- Rotating modal and Campbell are experimental and restricted to the model + described above; the 100 rad/s high-frequency branch ambiguity remains + visible. +- QF Solver is not certified and has no universal physical-validation claim. + +See the detailed [technical limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) +and the controlling evidence record for the chosen analysis. + +## Current release and citation + +The latest **published** release is `0.2.10` ([PyPI](https://pypi.org/project/qf-solver/0.2.10/), +[GitHub Release](https://github.com/emptiesvoid-cloud/QF_solver/releases/tag/v0.2.10), +source tag [`v0.2.10`](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10)). +Version 0.2.11 is a **candidate, not yet published**. Its cumulative additions +and bounded evidence are summarized in [What's New in 0.2.11](docs/whats-new/0.2.11.md); +the [changelog](CHANGELOG.md) retains the full project history. + +For reproducibility, cite the exact published version DOI +[`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744). Use the +project concept DOI [`10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) +when citing the evolving project rather than a specific release. The candidate +[`CITATION.cff`](CITATION.cff) identifies version 0.2.11 but intentionally has +no release date or version DOI until publication. The whole-repository G03 +archive gate remains **FAIL**, the full repository archive is not cleared, and +WP14 remains **HOLD_NOT_PROMOTED**; selected distribution checks do not change +those states. + +## Documentation and project - [Documentation home](https://emptiesvoid-cloud.github.io/QF_solver/) -- [Elements](https://emptiesvoid-cloud.github.io/QF_solver/elements/) and +- [Capabilities](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/), + [elements](https://emptiesvoid-cloud.github.io/QF_solver/elements/) and [analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) -- [Capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) - and [known limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) - [Public API contract](https://emptiesvoid-cloud.github.io/QF_solver/reference/qf_solver_api/) and [API stability](https://emptiesvoid-cloud.github.io/QF_solver/reference/api_stability/) - [Benchmarks](https://emptiesvoid-cloud.github.io/QF_solver/benchmarks/) and [V&V evidence](https://emptiesvoid-cloud.github.io/QF_solver/verification/evidence-and-maturity/) +- [Contributing](CONTRIBUTING.md) · [code license](LICENSE) · + [documentation/examples license](LICENSE-DOCS) -Development guidance is in -[`CONTRIBUTING.md`](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/CONTRIBUTING.md). -Code is licensed under -[Apache-2.0](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/LICENSE); -documentation and original examples are under -[CC BY 4.0](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/LICENSE-DOCS). -Third-party terms are listed in -[`THIRD_PARTY_LICENSES.md`](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/THIRD_PARTY_LICENSES.md). +The solver code is licensed under Apache-2.0; documentation and original +examples are under CC BY 4.0. Third-party terms are listed in +[`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md). diff --git a/examples/README.md b/examples/README.md index 447e6a62..5dfbdce1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,52 +1,50 @@ # QF Solver examples -These JSON inputs illustrate specific QF Solver routes. Start with a small -linear case, inspect its mesh and result diagnostics, then consult the -[capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) -before interpreting a more advanced result. An executable example is not a -general qualification or physical-validation claim. +These maintained inputs illustrate solver routes and mechanical problems. +They are organized by capability rather than release number. An executable +input is not, by itself, a qualification or a physical-validation claim; check +the [capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) +and the applicable evidence before generalizing a result. -## Start here +## Start with a linear static case From the repository root after installing QF Solver: ```bash qf-solver check-mesh --input examples/tet4_static.json qf-solver solve --input examples/tet4_static.json --output results/tet4.json +qf-solver inspect --input examples/tet4_static.json --markdown results/tet4_audit.md ``` The [first-calculation guide](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/quickstart/) -explains the CLI and public Python API. For a small, bounded nonlinear case, -follow the separate -[Total-Lagrangian TET4 example](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/nonlinear-example/); +covers the CLI and public Python API. For a small nonlinear illustration, +follow the separate [Total-Lagrangian TET4 example](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/nonlinear-example/); its engineering-profile `WARNING` is part of the documented outcome. -## Find an input +## Find an example by problem -| Goal | Example inputs | Boundary | +| Problem family | Example inputs | Scope notes | | --- | --- | --- | -| Linear solid statics | `tet4_static.json`, `tet10_static.json`, `tet4_compression.json`, `tet4_body_force.json`, `tet4_pressure.json` | Check element, load and output scope in the registry. | -| Material and geometric routes | `tet4_elastoplastic_static.json`, `tet4_nonlinear_static.json`, `tet4_geometric_nonlinear_static.json`, `hex8_g06_j2.json` | These inputs do not qualify arbitrary J2, coupled or finite-strain cases. | -| Modal, transient and harmonic response | `tet4_modal_unit.json`, `tet4_transient_dynamic.json`, `tet4_dynamic_sdof_free_vibration.json`, `tet4_harmonic_sdof_response.json` | Linear dynamic assumptions and route-specific mass/damping bounds apply. | -| Beams and shells | `beam2_cantilever.json`, `mitc3_shell_static.json`, `mitc4_shell_static.json`, `mitc4_modal_cantilever.json` | Shell/beam evidence and maturity differ by analysis. | -| Constraints and contact | `rbe2_rigid_arm.json`, `frictionless_contact_plane.json`, `frictional_contact_plane.json` | Contact is bounded; the frictional current-source route is not generally requalified. | -| Input rejection | `invalid_inverted_tet4.json` | Intentionally invalid: use it to inspect validation diagnostics, not to solve. | - -Other JSON files in this directory cover orthotropy, laminate response, -tabulated loads, additional dynamics and recorded G06 cases. The -[elements](https://emptiesvoid-cloud.github.io/QF_solver/elements/), -[analyses](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) and -[known-limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) -pages explain how to choose among them. +| Linear solid statics | `tet4_static.json`, `tet10_static.json`, `tet4_compression.json`, `tet4_body_force.json`, `tet4_pressure.json`, `tet4_orthotropic_static.json`, `tet10_orthotropic_static.json` | Check the element, material, loading and result scope in the capability records. | +| Modal analysis | `tet4_modal_unit.json`, `tet4_orthotropic_modal.json`, `mitc3_modal_cantilever.json`, `mitc4_modal_cantilever.json` | Family and mass-formulation maturity vary; the MITC4 modal historical issue remains documented. | +| Transient and harmonic dynamics | `tet4_transient_dynamic.json`, `tet4_dynamic_free_vibration.json`, `tet4_dynamic_sdof_free_vibration.json`, `tet4_dynamic_tabulated_load.json`, `tet4_harmonic_response.json`, `tet4_harmonic_sdof_response.json`, `mitc3_newmark_cantilever.json`, `mitc3_laminate_harmonic.json`, `mitc4_newmark_cantilever.json`, `mitc4_harmonic_cantilever.json` | Linear dynamics, timestep, mass, damping and mixed-family limitations apply. | +| Beams, shells and laminates | `beam2_cantilever.json`, `mitc3_shell_static.json`, `mitc3_laminate_harmonic.json`, `mitc4_shell_static.json`, `mitc4_laminate_static.json`, `mitc4_dynamic_multicomponent.json` | Element routes do not have identical analysis maturity. | +| Material and geometric nonlinearity | `tet4_elastoplastic_static.json`, `tet4_nonlinear_static.json`, `tet4_geometric_nonlinear_static.json`, `hex8_g06_j2.json`, `hex20_g06_j2.json` | These cases do not establish arbitrary J2, coupled, finite-strain or high-order nonlinear behavior. | +| Constraints and contact | `rbe2_rigid_arm.json`, `frictionless_contact_plane.json`, `frictionless_contact_surface.json`, `frictional_contact_plane.json` | Contact is bounded; frictional current-source formal requalification and general finite sliding are not claimed. | +| Linear buckling | `tet4_linear_buckling.json` | Linearized buckling is not postbuckling analysis. | +| Rotating modal and Campbell | See the [rotating-modal guide](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/rotating-modal/), [Campbell guide](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/campbell/) and [0.2.11 verification summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_11/). | Experimental bounded BEAM2/disk route. The repository currently has no curated standalone JSON input for this API workflow. | +| Controlled verification fixtures | `tet4_g06_analytic.json`, `tet10_g06_analytic.json`, `hex8_g06_analytic.json`, `hex20_g06_analytic.json` | Read the associated contract and provenance; a fixture is not a general user benchmark. | +| Input rejection | `invalid_inverted_tet4.json` | Intentionally invalid; use it to inspect mesh-validation diagnostics, not to solve. | + +Other inputs cover orthotropy, laminate response, springs, tabulated loads and +additional controlled cases. The [elements map](https://emptiesvoid-cloud.github.io/QF_solver/elements/), +[analysis index](https://emptiesvoid-cloud.github.io/QF_solver/analyses/) and +[technical limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) +explain how to select and interpret a route. ## Inspect a result -```bash -qf-solver inspect --input examples/tet4_static.json --markdown results/tet4_audit.md -qf-solver solve --input examples/tet4_static.json --output results/tet4.json -``` - -The public Python workflow uses `qf_solver`: +The public Python workflow uses the `qf_solver` namespace: ```python from qf_solver import check_mesh, load_model, solve_model @@ -59,15 +57,15 @@ result = solve_model(model) print(result.to_dict()["status"]) ``` -Inspect the residual, reactions and route-specific diagnostics as well as the -solve status. The integration suite exercises a maintained subset of these -inputs; it does not make every example an accepted qualification case: +Inspect residuals, reactions and route-specific diagnostics in addition to +the solve status. The integration suite exercises a maintained subset of +these inputs; it does not qualify every example or every possible model: ```bash python -m pytest tests/integration/test_examples.py ``` -Larger documentation models are generated by the project's controlled docs +Larger documentation models are generated by the controlled documentation workflow rather than maintained as a second hand-edited example collection. Their manifests and applicable evidence are linked from the -[documentation](https://emptiesvoid-cloud.github.io/QF_solver/). +[documentation site](https://emptiesvoid-cloud.github.io/QF_solver/). diff --git a/tests/documentation/test_docs_generation.py b/tests/documentation/test_docs_generation.py index ce5f3191..1c381f28 100644 --- a/tests/documentation/test_docs_generation.py +++ b/tests/documentation/test_docs_generation.py @@ -76,9 +76,10 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> comparison_snapshot.replace(">", "").split() ) - assert "| Release line | `0.2.10` |" in readme + assert "latest **published** release is `0.2.10`" in readme + assert "0.2.11 is a **candidate, not yet published**" in readme assert "`0.2.8` — release candidate; not published" not in readme - assert "Source tag" in readme and "v0.2.10" in readme + assert "source tag" in readme and "v0.2.10" in readme assert "QF Solver 0.2.10 is the current published release" in normalized_roadmap assert "Version 0.2.9 was a development/source snapshot" in normalized_roadmap assert "QF Solver 0.2.10 architecture" in normalized_architecture @@ -88,7 +89,7 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> assert "0.2.8 is the current development candidate" not in roadmap assert "10.5281/zenodo.23106744" in readme assert "10.5281/zenodo.23106744" in index - assert "Version DOI" in readme + assert "version DOI" in readme assert "Current release" in normalized_index assert "current published package is 0.2.10" in normalized_comparisons assert ( @@ -96,8 +97,10 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> in normalized_comparison_snapshot ) assert "not a current capability or maturity record" in normalized_comparison_snapshot - assert 'version: "0.2.10"' in (ROOT / "CITATION.cff").read_text(encoding="utf-8") - assert 'doi: "10.5281/zenodo.23106744"' in (ROOT / "CITATION.cff").read_text(encoding="utf-8") + citation = (ROOT / "CITATION.cff").read_text(encoding="utf-8") + assert 'version: "0.2.11"' in citation + assert not any(line.startswith("date-released:") for line in citation.splitlines()) + assert not any(line.startswith("doi:") for line in citation.splitlines()) def test_tetra_boundary_faces_remove_shared_face() -> None: diff --git a/tests/unit/test_gate_b_release_surface.py b/tests/unit/test_gate_b_release_surface.py index 8bd66862..73e81238 100644 --- a/tests/unit/test_gate_b_release_surface.py +++ b/tests/unit/test_gate_b_release_surface.py @@ -78,13 +78,14 @@ def test_version_registry_and_public_docs_are_aligned() -> None: assert project["version"] == __version__ == "0.2.11" assert project["optional-dependencies"]["hdf5"] == ["h5py>=3.10"] - # CITATION.cff identifies the latest published release until 0.2.11 is - # actually published; the candidate uses the existing project concept DOI. + # The candidate citation identifies 0.2.11, but has no release-only date + # or DOI until publication. The project concept DOI remains in project URLs. citation = (ROOT / "CITATION.cff").read_text(encoding="utf-8") - assert 'version: "0.2.10"' in citation - assert 'doi: "10.5281/zenodo.23106744"' in citation + assert 'version: "0.2.11"' in citation + assert not any(line.startswith("date-released:") for line in citation.splitlines()) + assert not any(line.startswith("doi:") for line in citation.splitlines()) assert project["urls"]["DOI"] == "https://doi.org/10.5281/zenodo.22697897" - assert "0.2.11 candidate" in (ROOT / "README.md").read_text(encoding="utf-8") + assert "0.2.11 is a **candidate, not yet published**" in (ROOT / "README.md").read_text(encoding="utf-8") registry = json.loads((ROOT / "qualification/0_2_8/consolidated_registry.json").read_text(encoding="utf-8")) assert registry["combination_registry"]["state_counts"] == { "QUALIFIED_BOUNDED": 32, @@ -93,5 +94,6 @@ def test_version_registry_and_public_docs_are_aligned() -> None: } readme = (ROOT / "README.md").read_text(encoding="utf-8") assert "qualification/0_2_8/consolidated_registry.json" in readme - assert "Mixed distributed PETSc/MPI" in readme and "`NOT_VALIDATED`" in readme - assert "WEDGE6 static | `QUALIFIED_BOUNDED`" in readme + assert "no general HPC or nonlinear distributed claim" in readme + assert "`NOT_VALIDATED`" in readme + assert "WEDGE6 static | `QUALIFIED_BOUNDED`" in (ROOT / "docs/capabilities/index.md").read_text(encoding="utf-8") diff --git a/tests/unit/test_pp05_release_metadata.py b/tests/unit/test_pp05_release_metadata.py index 38242131..bd73d4f3 100644 --- a/tests/unit/test_pp05_release_metadata.py +++ b/tests/unit/test_pp05_release_metadata.py @@ -54,15 +54,20 @@ def test_security_policy_identifies_current_and_older_release_channels() -> None assert "best-effort basis" in security -def test_release_candidate_and_latest_published_citation_are_distinct() -> None: +def test_release_candidate_citation_has_no_unpublished_release_metadata() -> None: project = tomllib.loads(_text("pyproject.toml"))["project"] runtime = _text("src/solveur/version.py") citation = _text("CITATION.cff") assert project["version"] == "0.2.11" assert '__version__ = "0.2.11"' in runtime - # Citation metadata remains attached to the latest actually published release. - assert 'version: "0.2.10"' in citation - assert 'doi: "10.5281/zenodo.23106744"' in citation + # The candidate is identified, but release-only metadata stays absent until + # the version DOI and publication date actually exist. + assert 'title: "QF Solver"' in citation + assert 'version: "0.2.11"' in citation + assert 'license: "Apache-2.0"' in citation + assert 'repository-code: "https://github.com/emptiesvoid-cloud/QF_solver"' in citation + assert not any(line.startswith("date-released:") for line in citation.splitlines()) + assert not any(line.startswith("doi:") for line in citation.splitlines()) assert "NOT_PUBLISHED_YET" not in citation assert "NOT_AVAILABLE_YET" not in citation @@ -80,13 +85,10 @@ def test_release_candidate_and_latest_published_citation_are_distinct() -> None: assert "0.2.8" in _text("README.md") assert "0.2.8" in _text("CHANGELOG.md") - assert "0.2.11 candidate" in _text("README.md") + assert "0.2.11 is a **candidate, not yet published**" in _text("README.md") assert "10.5281/zenodo.23106744" in _text("README.md") assert "10.5281/zenodo.22697897" in _text("README.md") - assert ( - "`v0.2.9` tag is a source snapshot, not a PyPI, GitHub Release or Zenodo" - in _text("README.md") - ) + assert "latest **published** release is `0.2.10`" in _text("README.md") def test_public_release_policy_classifies_reviewed_evidence_and_exclusions() -> None: @@ -110,7 +112,7 @@ def test_release_registry_and_public_boundaries_remain_unchanged() -> None: "EXPERIMENTAL": 14, "NOT_QUALIFIED": 0, } - assert "Mixed distributed PETSc/MPI" in _text("README.md") + assert "no general HPC or nonlinear distributed claim" in _text("README.md") assert "`NOT_VALIDATED`" in _text("README.md") assert "0.2.10" in _text("CONTRIBUTING.md") assert "haven't had the time to put everything on GitHub" not in _text("CONTRIBUTING.md") diff --git a/tests/unit/test_publication_metadata_gate.py b/tests/unit/test_publication_metadata_gate.py index 54fe226c..74bbc394 100644 --- a/tests/unit/test_publication_metadata_gate.py +++ b/tests/unit/test_publication_metadata_gate.py @@ -14,9 +14,12 @@ def test_unreleased_candidate_is_not_mistaken_for_a_published_tag() -> None: failures = check_publication_metadata(ROOT, "v0.2.11") - assert "CITATION.cff does not identify this version" in failures + assert "CITATION.cff does not identify this version" not in failures + assert "CITATION.cff needs a release date" in failures + assert "CITATION.cff needs an assigned version DOI" in failures assert "CHANGELOG.md has no released entry for this version" in failures assert "README.md release line does not identify this version" in failures + assert "README.md still denies publication of this version" in failures def test_metadata_gate_rejects_nonstable_or_mismatched_tag() -> None: From 4b184d5cc45a2305de2e3f9b82ffec57d0e75f8f Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 21:28:03 +0200 Subject: [PATCH 02/15] Freeze 0.2.11 public-surface candidate build contract --- ...wp07_candidate_build_contract_67e56da.json | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 qualification/0_2_11/wp07_candidate_build_contract_67e56da.json diff --git a/qualification/0_2_11/wp07_candidate_build_contract_67e56da.json b/qualification/0_2_11/wp07_candidate_build_contract_67e56da.json new file mode 100644 index 00000000..8efb288b --- /dev/null +++ b/qualification/0_2_11/wp07_candidate_build_contract_67e56da.json @@ -0,0 +1,176 @@ +{ + "schema_version": 1, + "contract_id": "QF-0211-WP07-SELECTED-PACKAGE-CANDIDATE-67E56DA-01", + "status": "FROZEN_CANDIDATE_BUILD", + "package_version": "0.2.11", + "source_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "source_date_epoch": 1791305555, + "integration": { + "strategy": "The public-surface correction is a new candidate commit on a dedicated branch from the integrated WP07 candidate; it is not merged to main.", + "main_sha_at_candidate": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_candidate_sha": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_candidate_tree_sha": "b4e08a09d98216029099810177d8851b366bd1d6", + "expected_tree_sha": "b30d11f13da6656c8023760acb493fc92a12c5b3", + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "historical_tag_unchanged": true, + "tag_is_not_created_by_this_contract": true + }, + "change_scope": { + "changed_paths_from_previous_candidate": [ + "CITATION.cff", + "README.md", + "examples/README.md", + "tests/documentation/test_docs_generation.py", + "tests/unit/test_gate_b_release_surface.py", + "tests/unit/test_pp05_release_metadata.py", + "tests/unit/test_publication_metadata_gate.py" + ], + "numerical_source_changed": false, + "release_tag_or_artifacts_changed": false, + "historical_records_or_maturities_changed": false + }, + "selection": { + "include_exact": [ + "CHANGELOG.md", + "CITATION.cff", + "LICENSE", + "LICENSE-DOCS", + "MANIFEST.in", + "NOTICE", + "README.md", + "THIRD_PARTY_LICENSES.md", + "pyproject.toml", + "qualification/benchmarks.json", + "qualification/campaign.json", + "qualification/demonstrations.json", + "qualification/element_analysis_matrix.json", + "qualification/formulas.json", + "qualification/requirements.json", + "qualification/release_vv_0_2_1.json", + "qualification/technical_content_coverage.json", + "qualification/0_2_7/capability_registry_v2.json", + "qualification/0_2_8/consolidated_registry.json", + "qualification/0_2_8/wp10_hex8_sri_owner_gate_final.json", + "qualification/0_2_8/wp13_02d_mixed_dynamics_delivery.json", + "qualification/0_2_8/wp13_03d_mixed_mpc_owner_delivery.json", + "qualification/0_2_8/wp13_04d_multimaterial_owner_delivery.json", + "qualification/0_2_8/wp13_05_inp_import_capability_record.json", + "qualification/0_2_8/wp13_05c_registry_reconciliation_erratum.json", + "qualification/0_2_8/wp13_06d_mixed_hdf5_capability_record.json", + "qualification/0_2_8/wp13_07d_contact_capability_record.json", + "requirements/baseline-standard.txt", + "requirements/baseline-large-linux.txt" + ], + "include_prefixes": [ + "examples/", + "src/qf_solver/", + "src/solveur/" + ], + "exclude_exact": [ + "examples/README.md", + "src/solveur/verification/j2_multifamily_performance.py" + ], + "forbid_prefixes": [ + "qualification/0_2_9/", + "docs/", + "scripts/", + "tests/" + ] + }, + "documentation_selection": { + "include_prefixes": ["docs/"], + "exclude_prefixes": ["docs/verification/0_2_9/"], + "suffixes": [ + ".cff", ".cfg", ".csv", ".htm", ".html", ".ini", ".json", ".md", + ".pdf", ".ps1", ".py", ".rst", ".sh", ".tex", ".toml", ".txt", + ".xml", ".yaml", ".yml" + ] + }, + "documentation_publication_policy": { + "config_path": ".github/pages/mkdocs.yml", + "config_sha256": "87f17d2a57b75ffb62b5412c5d72de5cc843595dc2e7c6252a01a752c6f0af8c", + "excluded_source_prefix": "docs/verification/0_2_9/", + "mkdocs_exclude_pattern": "verification/0_2_9/**" + }, + "tool_bindings": { + "scripts/audit_public_release.py": "cb1493a8a49d3f1ee0be89efc37050dc87a8c5165dbfb6de6fdd17b3bbeaf79c", + "scripts/audit_release_archive.py": "b67613d0eb5e1b305aba2bb9181d12529eda7859db00b840a71746bc39c52a37", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/plan_public_package.py": "d1a734cb4a11d53abfa102507f531f9369bc70797adf23075dd5b03c10f8e1f8", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/review_vocabulary.py": "28af00eab2a09fdcb94bf3c4682230dcaaca9b519aeb0867d96b67d4589ad3aa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c" + }, + "scope_scan_tool_sha256": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "expected_preflight_counts": { + "selected_package_files": 562, + "served_document_files": 753, + "docs_archival_exclusion_files": 208, + "findings_per_surface": 0 + }, + "candidate_execution_profile": { + "source_materialization": "Selected regular Git blobs from source_sha only; no working-tree source bytes.", + "build_command": "python scripts/verify_public_package.py --root . --contract qualification/0_2_11/wp07_candidate_build_contract_67e56da.json --output ", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "reproducibility": "Use two independent fresh output directories; compare exact wheel bytes and policy-normalized sdist bytes. Never reuse prior candidate outputs.", + "archive_checks": [ + "regular files and safe portable paths only", + "selected source byte equality", + "reject undeclared payloads, aliases, symlinks and unexpected files", + "verify package version and distribution metadata", + "twine check on wheel and normalized sdist" + ], + "installed_probes": [ + "separate no-index/no-deps install of the exact wheel", + "verify installed source hashes and import origins outside and inside checkout with safe-path mode", + "qf-solver, solveur-ef and mitc4-solver version/help probes", + "static, modal, rotating_modal and Campbell smoke solves using only the installed wheel" + ], + "evidence_output": "Write candidate outputs only to new external directories; preserve source mapping, commands, environment, archive checks, installed probes and hashes." + }, + "candidate_status": { + "latest_published_version": "0.2.10", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "release_date_0_2_11": "NOT_SET", + "citation_cff": "Identifies candidate version 0.2.11; no release date or version DOI is claimed before publication.", + "whole_repository_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14": "HOLD_NOT_PROMOTED", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_NOT_INDEPENDENT_PHYSICAL_VALIDATION" + }, + "package_build_allowed": true, + "publication_allowed": false, + "structural_solves_allowed": false, + "numeric_threshold_changes_allowed": false, + "whole_repository_gate_waivers_allowed": false, + "release_permissions": { + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "whole_repository_archive_publication_allowed": false + }, + "limitations": [ + "This contract authorizes selected candidate package build and bounded audits only; it does not authorize a tag, GitHub Release, PyPI upload, Zenodo deposit, ledger update or maturity promotion.", + "The package selection does not scan or clear the full repository or Git history; G03 remains FAIL_PRESERVED and the complete repository archive remains uncleared.", + "WP14 remains HOLD_NOT_PROMOTED; successful targeted CI checks do not change that maturity decision.", + "rotating_modal and Campbell remain EXPERIMENTAL for the bounded serial dense BEAM2 shaft and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and its gap is preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are not assigned or published." + ] +} From e6d6a1ffd76bf21e31f055f8a43fe61164917600 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 21:36:26 +0200 Subject: [PATCH 03/15] Bind deterministic sdist normalization for 0.2.11 candidate --- ...7_candidate_build_contract_67e56da_r2.json | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json diff --git a/qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json b/qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json new file mode 100644 index 00000000..23257150 --- /dev/null +++ b/qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json @@ -0,0 +1,158 @@ +{ + "schema_version": 1, + "contract_id": "QF-0211-WP07-SELECTED-PACKAGE-CANDIDATE-67E56DA-02", + "status": "FROZEN_CANDIDATE_BUILD", + "package_version": "0.2.11", + "source_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "source_date_epoch": 1791305555, + "integration": { + "strategy": "The public-surface correction is a new candidate commit on a dedicated branch from the integrated WP07 candidate; it is not merged to main.", + "main_sha_at_candidate": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_candidate_sha": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_candidate_tree_sha": "b4e08a09d98216029099810177d8851b366bd1d6", + "expected_tree_sha": "b30d11f13da6656c8023760acb493fc92a12c5b3", + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "historical_tag_unchanged": true, + "tag_is_not_created_by_this_contract": true + }, + "change_scope": { + "changed_paths_from_previous_candidate": [ + "CITATION.cff", + "README.md", + "examples/README.md", + "tests/documentation/test_docs_generation.py", + "tests/unit/test_gate_b_release_surface.py", + "tests/unit/test_pp05_release_metadata.py", + "tests/unit/test_publication_metadata_gate.py" + ], + "numerical_source_changed": false, + "release_tag_or_artifacts_changed": false, + "historical_records_or_maturities_changed": false + }, + "selection": { + "include_exact": [ + "CHANGELOG.md", "CITATION.cff", "LICENSE", "LICENSE-DOCS", "MANIFEST.in", + "NOTICE", "README.md", "THIRD_PARTY_LICENSES.md", "pyproject.toml", + "qualification/benchmarks.json", "qualification/campaign.json", + "qualification/demonstrations.json", "qualification/element_analysis_matrix.json", + "qualification/formulas.json", "qualification/requirements.json", + "qualification/release_vv_0_2_1.json", "qualification/technical_content_coverage.json", + "qualification/0_2_7/capability_registry_v2.json", + "qualification/0_2_8/consolidated_registry.json", + "qualification/0_2_8/wp10_hex8_sri_owner_gate_final.json", + "qualification/0_2_8/wp13_02d_mixed_dynamics_delivery.json", + "qualification/0_2_8/wp13_03d_mixed_mpc_owner_delivery.json", + "qualification/0_2_8/wp13_04d_multimaterial_owner_delivery.json", + "qualification/0_2_8/wp13_05_inp_import_capability_record.json", + "qualification/0_2_8/wp13_05c_registry_reconciliation_erratum.json", + "qualification/0_2_8/wp13_06d_mixed_hdf5_capability_record.json", + "qualification/0_2_8/wp13_07d_contact_capability_record.json", + "requirements/baseline-standard.txt", "requirements/baseline-large-linux.txt" + ], + "include_prefixes": ["examples/", "src/qf_solver/", "src/solveur/"], + "exclude_exact": ["examples/README.md", "src/solveur/verification/j2_multifamily_performance.py"], + "forbid_prefixes": ["qualification/0_2_9/", "docs/", "scripts/", "tests/"] + }, + "documentation_selection": { + "include_prefixes": ["docs/"], + "exclude_prefixes": ["docs/verification/0_2_9/"], + "suffixes": [ + ".cff", ".cfg", ".csv", ".htm", ".html", ".ini", ".json", ".md", + ".pdf", ".ps1", ".py", ".rst", ".sh", ".tex", ".toml", ".txt", + ".xml", ".yaml", ".yml" + ] + }, + "documentation_publication_policy": { + "config_path": ".github/pages/mkdocs.yml", + "config_sha256": "87f17d2a57b75ffb62b5412c5d72de5cc843595dc2e7c6252a01a752c6f0af8c", + "excluded_source_prefix": "docs/verification/0_2_9/", + "mkdocs_exclude_pattern": "verification/0_2_9/**" + }, + "tool_bindings": { + "scripts/audit_public_release.py": "cb1493a8a49d3f1ee0be89efc37050dc87a8c5165dbfb6de6fdd17b3bbeaf79c", + "scripts/audit_release_archive.py": "b67613d0eb5e1b305aba2bb9181d12529eda7859db00b840a71746bc39c52a37", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/plan_public_package.py": "d1a734cb4a11d53abfa102507f531f9369bc70797adf23075dd5b03c10f8e1f8", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/review_vocabulary.py": "28af00eab2a09fdcb94bf3c4682230dcaaca9b519aeb0867d96b67d4589ad3aa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c" + }, + "scope_scan_tool_sha256": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "sdist_normalization": { + "tool_path": "scripts/normalize_public_sdist.py", + "tool_sha256": "1c3594c61b46af5601fda7993644aa613fb1523edb4709526b0ee35a01213d2c", + "source_date_epoch": 1791305555, + "command_contract": "Set SOURCE_DATE_EPOCH to this exact value for each build; normalize each fresh raw sdist with this frozen script into a new path; compare normalized bytes, not raw sdist gzip metadata." + }, + "expected_preflight_counts": { + "selected_package_files": 562, + "served_document_files": 753, + "docs_archival_exclusion_files": 208, + "findings_per_surface": 0 + }, + "candidate_execution_profile": { + "source_materialization": "Selected regular Git blobs from source_sha only; no working-tree source bytes.", + "build_command": "Set SOURCE_DATE_EPOCH=1791305555; python scripts/verify_public_package.py --root . --contract qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json --output ", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "reproducibility": "Two independent fresh output directories; compare exact wheel bytes and separately normalized sdist bytes. No existing output reuse.", + "archive_checks": [ + "regular files and safe portable paths only", + "selected source byte equality", + "reject undeclared payloads, aliases, symlinks and unexpected files", + "verify package version and distribution metadata", + "twine check on the exact wheel and normalized sdist" + ], + "installed_probes": [ + "separate no-index/no-deps install of exact wheel", + "verify installed source hashes and import origins outside and inside checkout with safe-path mode", + "qf-solver, solveur-ef and mitc4-solver version/help probes", + "static, modal, rotating_modal and Campbell smoke solves using only the installed wheel" + ], + "evidence_output": "New external output directories only; preserve source mapping, command records, environment, archive checks, installed probes and hashes." + }, + "candidate_status": { + "latest_published_version": "0.2.10", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "release_date_0_2_11": "NOT_SET", + "citation_cff": "Identifies candidate version 0.2.11; no release date or version DOI is claimed before publication.", + "whole_repository_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14": "HOLD_NOT_PROMOTED", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_NOT_INDEPENDENT_PHYSICAL_VALIDATION" + }, + "package_build_allowed": true, + "publication_allowed": false, + "structural_solves_allowed": false, + "numeric_threshold_changes_allowed": false, + "whole_repository_gate_waivers_allowed": false, + "release_permissions": { + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "whole_repository_archive_publication_allowed": false + }, + "limitations": [ + "This contract authorizes selected candidate package build and bounded audits only; it does not authorize a tag, GitHub Release, PyPI upload, Zenodo deposit, ledger update or maturity promotion.", + "The package selection does not scan or clear the full repository or Git history; G03 remains FAIL_PRESERVED and the complete repository archive remains uncleared.", + "WP14 remains HOLD_NOT_PROMOTED; successful targeted CI checks do not change that maturity decision.", + "rotating_modal and Campbell remain EXPERIMENTAL for the bounded serial dense BEAM2 shaft and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and its gap is preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are not assigned or published." + ] +} From d2251cdb331e1beac5e9ba122019111c7e550e02 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 22:20:33 +0200 Subject: [PATCH 04/15] Align public claim test with cumulative README --- tests/unit/test_f3_public_claims.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_f3_public_claims.py b/tests/unit/test_f3_public_claims.py index 97bf5dac..c1423eb0 100644 --- a/tests/unit/test_f3_public_claims.py +++ b/tests/unit/test_f3_public_claims.py @@ -82,11 +82,12 @@ def test_active_boundaries_match_registry_and_release_truth() -> None: assert state["global_accounting"]["level_up_2"] == "50/50 CLOSED" root_readme = (ROOT / "README.md").read_text(encoding="utf-8") - assert "WEDGE6 static | `QUALIFIED_BOUNDED`" in root_readme + capabilities_index = (ROOT / "docs/capabilities/index.md").read_text(encoding="utf-8") + assert "WEDGE6 static | `QUALIFIED_BOUNDED`" in capabilities_index assert "qualification/0_2_8/consolidated_registry.json" in root_readme - assert "No claim of GPU, general HPC" in root_readme - assert "two complete 5M Silver replays" in root_readme - assert "No claim of certification" in root_readme + assert "no general HPC or nonlinear distributed claim" in root_readme + assert "two complete 5M Silver replays" not in root_readme + assert "project is not certified" in root_readme interfaces = (ROOT / "docs/reference/interfaces.md").read_text(encoding="utf-8") assert "PYTHONPATH=src" in interfaces From b07816d6f9f3f9421776304874ad0a43aa5fda79 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 22:23:56 +0200 Subject: [PATCH 05/15] Freeze audited 0.2.11 source candidate d2251cdb --- ...p07_candidate_build_contract_d2251cdb.json | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json diff --git a/qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json b/qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json new file mode 100644 index 00000000..6d3c3fda --- /dev/null +++ b/qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json @@ -0,0 +1,150 @@ +{ + "schema_version": 1, + "contract_id": "QF-0211-WP07-SELECTED-PACKAGE-CANDIDATE-D2251CDB-03", + "status": "FROZEN_CANDIDATE_BUILD", + "package_version": "0.2.11", + "source_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "source_date_epoch": 1791305555, + "integration": { + "strategy": "A single stale assertion in the F3 public-claims test was aligned with the existing capability index after the README became cumulative; source remains on the dedicated candidate branch and is not merged to main.", + "main_sha_at_candidate": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_candidate_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "previous_candidate_tree_sha": "b30d11f13da6656c8023760acb493fc92a12c5b3", + "expected_tree_sha": "2357162743edd8dbcb6a95963d32f9073a25b791", + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "historical_tag_unchanged": true, + "tag_is_not_created_by_this_contract": true + }, + "change_scope": { + "changed_paths_from_previous_candidate": ["tests/unit/test_f3_public_claims.py"], + "numerical_source_changed": false, + "release_tag_or_artifacts_changed": false, + "historical_records_or_maturities_changed": false + }, + "selection": { + "include_exact": [ + "CHANGELOG.md", "CITATION.cff", "LICENSE", "LICENSE-DOCS", "MANIFEST.in", + "NOTICE", "README.md", "THIRD_PARTY_LICENSES.md", "pyproject.toml", + "qualification/benchmarks.json", "qualification/campaign.json", + "qualification/demonstrations.json", "qualification/element_analysis_matrix.json", + "qualification/formulas.json", "qualification/requirements.json", + "qualification/release_vv_0_2_1.json", "qualification/technical_content_coverage.json", + "qualification/0_2_7/capability_registry_v2.json", + "qualification/0_2_8/consolidated_registry.json", + "qualification/0_2_8/wp10_hex8_sri_owner_gate_final.json", + "qualification/0_2_8/wp13_02d_mixed_dynamics_delivery.json", + "qualification/0_2_8/wp13_03d_mixed_mpc_owner_delivery.json", + "qualification/0_2_8/wp13_04d_multimaterial_owner_delivery.json", + "qualification/0_2_8/wp13_05_inp_import_capability_record.json", + "qualification/0_2_8/wp13_05c_registry_reconciliation_erratum.json", + "qualification/0_2_8/wp13_06d_mixed_hdf5_capability_record.json", + "qualification/0_2_8/wp13_07d_contact_capability_record.json", + "requirements/baseline-standard.txt", "requirements/baseline-large-linux.txt" + ], + "include_prefixes": ["examples/", "src/qf_solver/", "src/solveur/"], + "exclude_exact": ["examples/README.md", "src/solveur/verification/j2_multifamily_performance.py"], + "forbid_prefixes": ["qualification/0_2_9/", "docs/", "scripts/", "tests/"] + }, + "documentation_selection": { + "include_prefixes": ["docs/"], + "exclude_prefixes": ["docs/verification/0_2_9/"], + "suffixes": [ + ".cff", ".cfg", ".csv", ".htm", ".html", ".ini", ".json", ".md", + ".pdf", ".ps1", ".py", ".rst", ".sh", ".tex", ".toml", ".txt", + ".xml", ".yaml", ".yml" + ] + }, + "documentation_publication_policy": { + "config_path": ".github/pages/mkdocs.yml", + "config_sha256": "87f17d2a57b75ffb62b5412c5d72de5cc843595dc2e7c6252a01a752c6f0af8c", + "excluded_source_prefix": "docs/verification/0_2_9/", + "mkdocs_exclude_pattern": "verification/0_2_9/**" + }, + "tool_bindings": { + "scripts/audit_public_release.py": "cb1493a8a49d3f1ee0be89efc37050dc87a8c5165dbfb6de6fdd17b3bbeaf79c", + "scripts/audit_release_archive.py": "b67613d0eb5e1b305aba2bb9181d12529eda7859db00b840a71746bc39c52a37", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/plan_public_package.py": "d1a734cb4a11d53abfa102507f531f9369bc70797adf23075dd5b03c10f8e1f8", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/review_vocabulary.py": "28af00eab2a09fdcb94bf3c4682230dcaaca9b519aeb0867d96b67d4589ad3aa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c" + }, + "scope_scan_tool_sha256": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "sdist_normalization": { + "tool_path": "scripts/normalize_public_sdist.py", + "tool_sha256": "1c3594c61b46af5601fda7993644aa613fb1523edb4709526b0ee35a01213d2c", + "source_date_epoch": 1791305555, + "command_contract": "Set SOURCE_DATE_EPOCH to this exact value for each build; normalize each fresh raw sdist with this frozen script into a new path; compare normalized bytes, not raw sdist gzip metadata." + }, + "expected_preflight_counts": { + "selected_package_files": 562, + "served_document_files": 753, + "docs_archival_exclusion_files": 208, + "findings_per_surface": 0 + }, + "candidate_execution_profile": { + "source_materialization": "Selected regular Git blobs from source_sha only; no working-tree source bytes.", + "build_command": "Set SOURCE_DATE_EPOCH=1791305555; python scripts/verify_public_package.py --root . --contract qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json --output ", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "reproducibility": "Two independent fresh output directories; compare exact wheel bytes and separately normalized sdist bytes. No existing output reuse.", + "archive_checks": [ + "regular files and safe portable paths only", + "selected source byte equality", + "reject undeclared payloads, aliases, symlinks and unexpected files", + "verify package version and distribution metadata", + "twine check on the exact wheel and normalized sdist" + ], + "installed_probes": [ + "separate no-index/no-deps install of exact wheel", + "verify installed source hashes and import origins outside and inside checkout with safe-path mode", + "qf-solver, solveur-ef and mitc4-solver version/help probes", + "static, modal, rotating_modal and Campbell smoke solves using only the installed wheel" + ], + "evidence_output": "New external output directories only; preserve source mapping, command records, environment, archive checks, installed probes and hashes." + }, + "candidate_status": { + "latest_published_version": "0.2.10", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "release_date_0_2_11": "NOT_SET", + "citation_cff": "Identifies candidate version 0.2.11; no release date or version DOI is claimed before publication.", + "whole_repository_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14": "HOLD_NOT_PROMOTED", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_NOT_INDEPENDENT_PHYSICAL_VALIDATION" + }, + "package_build_allowed": true, + "publication_allowed": false, + "structural_solves_allowed": false, + "numeric_threshold_changes_allowed": false, + "whole_repository_gate_waivers_allowed": false, + "release_permissions": { + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "whole_repository_archive_publication_allowed": false + }, + "limitations": [ + "This contract authorizes selected candidate package build and bounded audits only; it does not authorize a tag, GitHub Release, PyPI upload, Zenodo deposit, ledger update or maturity promotion.", + "The package selection does not scan or clear the full repository or Git history; G03 remains FAIL_PRESERVED and the complete repository archive remains uncleared.", + "WP14 remains HOLD_NOT_PROMOTED; successful targeted CI checks do not change that maturity decision.", + "rotating_modal and Campbell remain EXPERIMENTAL for the bounded serial dense BEAM2 shaft and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and its gap is preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are not assigned or published." + ] +} From 0d3b0c9a29b6282954f243abff923a5e28f8c43f Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 00:04:42 +0200 Subject: [PATCH 06/15] Record QF Solver 0.2.11 public surface candidate audit --- ...blic_surface_candidate_audit_d2251cdb.json | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json diff --git a/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json b/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json new file mode 100644 index 00000000..53029a4e --- /dev/null +++ b/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json @@ -0,0 +1,168 @@ +{ + "schema_version": 1, + "record_id": "QF-0211-WP07-PUBLIC-SURFACE-CANDIDATE-AUDIT-D2251CDB-2026-10-07", + "status": "READY_FOR_OWNER_RELEASE_AUTHORIZATION", + "candidate": { + "package_version": "0.2.11", + "package_source_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "package_source_tree_sha": "2357162743edd8dbcb6a95963d32f9073a25b791", + "branch": "codex/v0.2.11-public-readme-citation", + "governance_parent_sha": "b07816d6f9f3f9421776304874ad0a43aa5fda79", + "baseline_main_sha": "fcf1dd12e176ff206d811910b4b1459043e93fa6", + "previous_public_surface_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "previous_public_surface_tree_sha": "b30d11f13da6656c8023760acb493fc92a12c5b3", + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "tag_created": false + }, + "source_change_review": { + "changed_paths_since_previous_public_surface_candidate": [ + "tests/unit/test_f3_public_claims.py" + ], + "change_reason": "Corrected one obsolete test assertion so the test checks detailed capability maturity in the capability index instead of requiring the cumulative README to duplicate the full matrix; updated README claim expectations to its solver-oriented content.", + "production_or_numerical_source_changed": false, + "formulations_or_thresholds_changed": false, + "maturity_or_historical_records_changed": false, + "v0_2_10_tag_or_artifacts_changed": false, + "earlier_governance_contracts_in_candidate_tree": [ + "qualification/0_2_11/wp07_candidate_build_contract_67e56da.json", + "qualification/0_2_11/wp07_candidate_build_contract_67e56da_r2.json" + ], + "earlier_governance_contracts_selected_for_distribution": false + }, + "ci": { + "quality_run_id": 37525861754, + "quality_run_url": "https://github.com/emptiesvoid-cloud/QF_solver/actions/runs/37525861754", + "quality_head_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "quality_conclusion": "SUCCESS", + "quality_jobs": { + "ubuntu_python_3_10": "PASS", + "ubuntu_python_3_13": "PASS", + "windows_python_3_10": "PASS", + "windows_python_3_13": "PASS", + "wp14_recovered_evidence_targeted": "PASS", + "documentation_evidence": "PASS", + "full_engineering_campaign": "PASS" + }, + "documentation_run_id": 37525862337, + "documentation_run_url": "https://github.com/emptiesvoid-cloud/QF_solver/actions/runs/37525862337", + "documentation_head_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "documentation_conclusion": "SUCCESS", + "superseded_quality_run": { + "run_id": 37518910380, + "head_sha": "67e56da1984565e3b92f88d25e284f046bfd8a57", + "conclusion": "FAIL", + "classification": "STALE_TEST_EXPECTATION", + "failed_test": "tests/unit/test_f3_public_claims.py::test_active_boundaries_match_registry_and_release_truth", + "resolution_commit": "d2251cdb331e1beac5e9ba122019111c7e550e02" + } + }, + "selected_scope_audit": { + "build_contract_path": "qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json", + "build_contract_sha256": "152f74eabdb9e5b28f5955a7bc4af7a4857399eacbe95564c087a276b29337e4", + "status": "PASS_BOUNDED_PUBLIC_SURFACES", + "source_materialization": "Selected regular Git blobs from package_source_sha; no working-tree source bytes.", + "selected_package_file_count": 562, + "selected_package_findings": 0, + "served_document_file_count": 753, + "served_document_findings": 0, + "historical_document_files_excluded": 208, + "whole_repository_scope_scanned": false + }, + "build_and_artifacts": { + "build_policy": "Two fresh independent output directories from Git materialized source, pinned environment and SOURCE_DATE_EPOCH=1791305555; normalized sdist comparison uses the frozen normalizer bound in the candidate build contract.", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "wheel": { + "filename": "qf_solver-0.2.11-py3-none-any.whl", + "sha256": "f956a9d4b40ade45959b921ff8c964d4c21c84cc24d4a2f0257e7714862654e0", + "size_bytes": 1551331, + "independent_builds_byte_identical": true + }, + "sdist": { + "filename": "qf_solver-0.2.11.tar.gz", + "sha256": "cfc841bd70165ce844fec258118cedb3c3fa42aed56c33462b850cbcbfb09057", + "size_bytes": 1151431, + "file_count": 570, + "raw_build_a_sha256": "8cd4ee59b6e784d320169f531725b3c7fea01bc65419f1a90da0861cfc61c585", + "raw_build_b_sha256": "c2203034cee8757a293d30dcf7b6ae57ec6f133ed89e0e98ba4d54d91550bf1a", + "normalized_builds_byte_identical": true, + "normalization_policy": "Compare normalized archive bytes; raw gzip metadata differed as explicitly anticipated by the frozen contract." + }, + "manifest": { + "filename": "qf_solver-0.2.11-d2251cdb-SHA256SUMS.txt", + "sha256": "b37336a2f47d67ded799a4a54f47105dd5cc5c0636ff6510e0acdc399691f635", + "size_bytes": 192, + "entries_match_artifacts": true + }, + "twine_check": "PASS", + "archive_content_and_path_audit": "PASS", + "installed_package_probes": { + "isolated_no_index_no_deps_install": "PASS", + "imports_outside_checkout_and_safe_path": "PASS", + "console_scripts_and_version_help": "PASS", + "static_smoke": "PASS", + "modal_smoke": "PASS", + "rotating_modal_smoke": "PASS_EXPERIMENTAL", + "campbell_smoke": "PASS_EXPERIMENTAL", + "developer_only_verify_all_from_installed_wheel": "EXPECTED_FAIL_CLOSED_NOT_A_DISTRIBUTION_PROBE" + } + }, + "local_validation": { + "focused_corrected_claim_test": "PASS", + "relevant_documentation_release_api_package_tests": "132 passed, 6 skipped", + "ruff_modified_python_tests": "PASS", + "mkdocs_strict": "PASS", + "all_final_candidate_ci_is_authoritative_for_sha": true + }, + "release_gates": { + "QF0211_G01": "PASS_CARRIED_FORWARD_FROM_APPROVED_WP02", + "QF0211_G02": "PASS_CARRIED_FORWARD_FROM_WP01_BASELINE_AND_WP02", + "QF0211_G03": "PASS_CARRIED_FORWARD_FROM_WP04", + "QF0211_G04": "PASS_CARRIED_FORWARD_FROM_WP05", + "QF0211_G05": "PASS_CARRIED_FORWARD_FROM_WP05", + "QF0211_G06": "PASS_CARRIED_FORWARD_FROM_WP06", + "QF0211_G07": "PASS", + "QF0211_G08": "PASS" + }, + "public_claims_and_preserved_limits": { + "readme_orientation": "Cumulative solver overview; not primarily a 0.2.11 release-delta page.", + "citation_version": "0.2.11 candidate", + "citation_release_date": "NOT_SET", + "version_doi": "NOT_ASSIGNED_NOT_CLAIMED", + "concept_doi": "10.5281/zenodo.22697897", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_high_frequency_pair": "AMBIGUITY_PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY_NOT_INDEPENDENT_PHYSICAL_VALIDATION", + "historical_G03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "historical_WP14": "HOLD_NOT_PROMOTED", + "historical_failures_reclassified": false, + "maturity_promotions": "NONE" + }, + "authorization_and_publication": { + "ready_for_owner_release_authorization": true, + "owner_release_authorization": "PENDING", + "publication_allowed": false, + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "version_doi_or_release_date_may_be_claimed": false, + "limitations": [ + "This is a candidate audit and does not authorize a tag, merge, GitHub Release, PyPI upload, Zenodo deposit, DOI activation or ledger update.", + "The full repository archive remains outside the selected distribution audit; historical G03 remains FAIL_PRESERVED.", + "Historical WP14 remains HOLD_NOT_PROMOTED; the targeted CI job passing does not change this status.", + "The 0.2.11 version DOI and release date remain unset until an authorized publication sequence confirms them." + ] + } +} From 1396028abc8e4e5814a887c3df81792d9d05820c Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 00:05:56 +0200 Subject: [PATCH 07/15] Clarify WP07 governance-only candidate records --- ...wp07_public_surface_candidate_audit_d2251cdb.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json b/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json index 53029a4e..5c246de6 100644 --- a/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json +++ b/qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json @@ -20,6 +20,18 @@ "changed_paths_since_previous_public_surface_candidate": [ "tests/unit/test_f3_public_claims.py" ], + "governance_only_changes_since_package_source_sha": [ + { + "path": "qualification/0_2_11/wp07_candidate_build_contract_d2251cdb.json", + "introduced_by": "b07816d6f9f3f9421776304874ad0a43aa5fda79", + "included_in_selected_package": false + }, + { + "path": "qualification/0_2_11/wp07_public_surface_candidate_audit_d2251cdb.json", + "introduced_by": "this governance audit commit", + "included_in_selected_package": false + } + ], "change_reason": "Corrected one obsolete test assertion so the test checks detailed capability maturity in the capability index instead of requiring the cumulative README to duplicate the full matrix; updated README claim expectations to its solver-oriented content.", "production_or_numerical_source_changed": false, "formulations_or_thresholds_changed": false, From 5c34a5cae20a8147501d6a31ff47c97c36bcc5a3 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 08:14:56 +0200 Subject: [PATCH 08/15] docs: finalize 0.2.11 public surface --- .github/pages/mkdocs.yml | 4 +- CITATION.cff | 2 +- README.md | 387 ++++++++++++++++-- docs/analyses/index.md | 11 +- docs/architecture.md | 41 +- docs/capabilities/index.md | 20 +- docs/comparisons/index.md | 17 +- docs/demonstrations/index.md | 14 +- docs/document_registry.json | 4 +- docs/elements/index.md | 12 +- docs/etat/capacites.md | 11 +- docs/etat/limites.md | 19 +- docs/examples/index.md | 12 +- docs/generated/document_registry.md | 4 +- docs/generated/status.md | 8 +- docs/getting-started/installation.md | 30 +- docs/getting-started/nonlinear-example.md | 2 +- docs/getting-started/quickstart.md | 10 +- docs/getting-started/when-to-use-qf-solver.md | 19 +- docs/index.md | 75 ++-- docs/mechanics/campbell.md | 4 +- docs/mechanics/nonlinear-overview.md | 4 +- docs/mechanics/rotating-modal.md | 6 +- docs/reference/api_stability.md | 12 +- docs/reference/feuille_de_route.md | 18 +- docs/reference/open_source.md | 8 +- docs/reference/qf_solver_api.md | 24 +- docs/reference/registre_documentaire.md | 2 +- docs/solveurs/index.md | 13 +- docs/verification/0_2_11/README.md | 23 +- docs/verification/evidence-and-maturity.md | 29 +- docs/whats-new/0.2.11.md | 31 +- scripts/docs_publication.py | 6 +- tests/documentation/test_docs_generation.py | 84 +++- tests/unit/test_f3_public_claims.py | 2 +- tests/unit/test_gate_b_release_surface.py | 4 +- tests/unit/test_pp04_public_documentation.py | 15 +- tests/unit/test_pp05_release_metadata.py | 4 +- tests/unit/test_publication_metadata_gate.py | 4 +- 39 files changed, 755 insertions(+), 240 deletions(-) diff --git a/.github/pages/mkdocs.yml b/.github/pages/mkdocs.yml index 221bf426..9ccc29ab 100644 --- a/.github/pages/mkdocs.yml +++ b/.github/pages/mkdocs.yml @@ -51,7 +51,7 @@ nav: - Verification & Validation: - V&V and Maturity: verification/evidence-and-maturity.md - 0.2.10 Verification Summary: verification/0_2_10/README.md - - 0.2.11 Candidate Verification: verification/0_2_11/README.md + - 0.2.11 Verification Summary: verification/0_2_11/README.md - Published 0.2.8 Verification: verification/0_2_8/README.md - Benchmarks: benchmarks/index.md - Historical Evidence: @@ -67,7 +67,7 @@ nav: - QF Solver vs scikit-fem: comparisons/qf-vs-scikit-fem.md - QF Solver vs SfePy: comparisons/qf-vs-sfepy.md - Project: - - What's New 0.2.11 (candidate): whats-new/0.2.11.md + - What's New 0.2.11: whats-new/0.2.11.md - What's New 0.2.10: whats-new/0.2.10.md - What's New 0.2.8: whats-new/0.2.8.md - Roadmap: reference/feuille_de_route.md diff --git a/CITATION.cff b/CITATION.cff index feb17c5b..abcfdde9 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,5 +1,5 @@ cff-version: 1.2.0 -message: "For the evolving project, use the project concept DOI. Version 0.2.11 is a release candidate and has no release date or version DOI yet." +message: "For the evolving project, use concept DOI 10.5281/zenodo.22697897. For exact-version reproducibility, use a published version DOI when assigned. This metadata identifies version 0.2.11 without asserting a release date or version DOI." type: software title: "QF Solver" version: "0.2.11" diff --git a/README.md b/README.md index 99352c82..11e64d99 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,44 @@ mechanics and dynamics, designed for inspectable formulations, engineering-method development and reproducible numerical verification. It offers linear, dynamic and selected nonlinear analysis routes through a Python API and command-line interface. Tests, benchmarks, evidence records and -explicit scope limits help users inspect what a result does—and does not—show. +explicit scope limits help users understand both what a result shows and what +it does not show. QF Solver is intended for engineering-method development, research and reproducible studies. An implemented route or a converged example is not automatically qualified or physically validated. The project is not certified and is not a general-purpose replacement for industrial FEA software. -On this page: [Capabilities](#capabilities) · [How it works](#how-qf-solver-works) · -[Quick start](#quick-start) · [Analysis families](#analysis-families) · -[Rotating dynamics](#rotating-dynamics) · [Verification](#verification-and-maturity) · -[Limitations](#limitations) · [Current release](#current-release-and-citation). +On this page: [Capabilities](#capabilities) · [Modeling](#elements-materials-and-modeling) · +[How it works](#how-qf-solver-works) · [Installation](#installation) · +[Quick start](#quick-start) · [Analyses](#analysis-families) · +[Nonlinear mechanics](#nonlinear-mechanics) · [Rotating dynamics](#rotating-dynamics) · +[Results](#results-inspection-and-export) · [Larger models](#larger-models-and-performance-context) · +[Verification](#verification-and-maturity) · [Limitations](#limitations) · +[Current release](#current-release-and-citation). + +## What QF Solver is for + +Use QF Solver when you need to inspect how a structural finite-element model +is assembled and solved, develop a mechanical method, compare formulations, +or reproduce a numerical study from explicit inputs and evidence. Typical +starting points include solid stress/displacement problems, beam and shell +response, laminate studies, vibration, and selected nonlinear static cases. +The rotating-modal and Campbell routes add a separate experimental use case; +they do not replace the solver's existing structural capabilities. + +The same model workflow is available from Python and the command line. Mesh +checks, assembly audits, solver diagnostics and result exports are part of the +tool, rather than just benchmark infrastructure. The code is intended to be +read and extended, but internal modules are not all stable public API. + +For model selection, read [when to use QF Solver](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/when-to-use-qf-solver/) +and the scope of the specific route you intend to use. ## Capabilities The capabilities below are cumulative, but maturity belongs to a specific -element, analysis, material, configuration and evidence record—not to the +element, analysis, material, configuration and evidence record, not to the solver as a whole. Use the [capability index](https://emptiesvoid-cloud.github.io/QF_solver/capabilities/) and [element map](https://emptiesvoid-cloud.github.io/QF_solver/elements/) before relying on a particular combination. @@ -34,6 +56,8 @@ before relying on a particular combination. | Domain | Current capabilities | Evidence status and boundary | | --- | --- | --- | | Elements | TET4, TET10, HEX8, HEX20, WEDGE6, BEAM2, MITC3 and MITC4 routes | Family support and maturity vary by analysis and formulation; see the element-analysis records. | +| Materials and composites | Isotropic elasticity, oriented orthotropic solids, layered shells and first-ply indicators | Material orientations, element combinations and laminate analyses have separate scopes; failure indicators are experimental, not progressive damage. | +| Model composition | Discrete masses/inertias, linear springs, MPC, RBE2/RBE3 and selected conforming mixed-solid workflows | Individual entities do not establish qualification for arbitrary assemblies or interfaces. | | Linear statics | Structural solves, reactions and selected stress/result fields | Bounded element, material, constraint and load combinations; not every combination is qualified. | | Modal and dynamics | Classical modal analysis, Newmark transient response and harmonic response | Route-specific mass, damping, timestep, element-family and mixed-model limits apply. | | Stability | Linear buckling | Does not establish postbuckling or general nonlinear stability behavior. | @@ -42,8 +66,9 @@ before relying on a particular combination. | Contact and constraints | RBE2-style constraints, frictionless contact, and bounded frictional stick/slip evidence | Contact search, sliding, mesh sensitivity and current-source requalification remain limited. | | Nonlinear solution | Newton methods, line search, adaptive increments, cutback/retry, and selected continuation/arc-length routes | Shared infrastructure does not imply convergence or qualification for arbitrary models. | | Larger models | Structured-TET4 workflows and selected PETSc/MPI linear-static cases | Optional dependencies and recorded configurations only; no general HPC or nonlinear distributed claim. | -| Rotating dynamics | Experimental disk-gyroscopic `rotating_modal` and Campbell analysis in the 0.2.11 candidate | Linear BEAM2 shaft with centered rigid axisymmetric disks; serial dense QEP and explicit scope limits. | -| I/O and interfaces | JSON models, public `qf_solver` Python namespace, CLI, bounded `.inp` import and optional mixed HDF5 results | Import/export formats and optional backends are not general-purpose interchange guarantees. | +| Rotating dynamics | Experimental disk-gyroscopic `rotating_modal` and Campbell analysis in 0.2.11 | Linear BEAM2 shaft with centered rigid axisymmetric disks; serial dense QEP and explicit scope limits. | +| I/O and interfaces | JSON models, public `qf_solver` Python namespace, CLI, Gmsh mesh import, bounded `.inp` import, JSON/CSV/VTU exports and optional mixed HDF5 results | Import/export formats and optional backends are not general-purpose interchange guarantees. | +| Inspection | Mesh/DOF checks, white-box audits, residuals, equilibrium checks and evidence bundles | Diagnostics describe the selected route and verification profile; they do not certify a model. | | Verification and validation | Analytical checks, regression tests, controlled benchmarks, reproducible evidence and selected Code_Aster correlations | Numerical verification, external solver correlation and physical validation are distinct claims. | Status terms such as `QUALIFIED_BOUNDED`, `EXPERIMENTAL_BOUNDED`, @@ -52,6 +77,74 @@ controlling record. The [published 0.2.8 registry](https://github.com/emptiesvoi remains the authority for its historical element-analysis decisions; later evidence does not silently rewrite those decisions. +## Elements, materials and modeling + +### Element families + +| Family | Modeling role | Important boundary | +| --- | --- | --- | +| TET4 / TET10 | Four-node and ten-node tetrahedral solids | Linear and selected material-nonlinear routes; higher-order support does not imply every geometric/contact coupling is available. | +| HEX8 / HEX20 | Eight-node and twenty-node hexahedral solids | Formulation, integration and nonlinear scope are route-specific. HEX8-SRI is a separate experimental capability, not a general HEX8R/B-bar/hourglass-control claim. | +| WEDGE6 | Six-node prism solid | Recorded bounded static and homogeneous consistent-mass modal scopes; these do not transfer automatically to other analyses. | +| BEAM2 | Two-node structural beams with axial, bending, shear and torsional response | Selected straight-beam static and dynamic evidence. The disk-gyroscopic route imposes additional straight-shaft/circular-section restrictions. | +| MITC3 / MITC4 | Triangular and quadrilateral shell routes for membrane, bending and transverse shear response | Isotropic and selected laminate workflows; planar, curved, modal and transient scopes must be checked separately. | +| Discrete entities | Concentrated mass and rotary inertia, linear springs and rigid disks | These are distinct from finite elements; their compatibility and mass ownership must be checked for the chosen analysis. | + +WEDGE15 is unsupported. PYRAMID5 remains internal/research-only and is not a +supported public element. See the [element map](https://emptiesvoid-cloud.github.io/QF_solver/elements/) +for formulations and evidence rather than assuming a Cartesian product of +all elements, materials and analyses. + +### Elastic materials and composites + +- **Isotropic elasticity:** solid, beam and shell material definitions, with + density and section/thickness data where the analysis requires them. +- **Oriented orthotropic solids:** TET4/TET10 routes with explicit material + axes and nine-constant 3D elasticity. Homogenized composite definitions + retain material provenance; selected cylindrical-tangent orientations are + also implemented. This is not a general arbitrary fiber-field model. +- **Laminated shells:** orthotropic plies, ply angles and thicknesses, + classical laminate `A/B/D` matrices, transverse shear terms, resultants + and ply-level stresses. Selected MITC4 static scopes have bounded internal + engineering acceptance; MITC3 and dynamic/curved extensions have their own + development and review records, not blanket laminate qualification. +- **First-ply indicators:** maximum stress, maximum strain, Tsai-Hill and + Tsai-Wu indices and reserve factors where material allowables are supplied. + These indicators remain experimental. They do not degrade stiffness or + simulate progressive failure, delamination or cohesive interfaces. + +Implementation, runtime maturity labels and historical bounded acceptance +are separate. In particular, composite output is not a certification or a +general composite-design allowable. Read the [composite overview](https://emptiesvoid-cloud.github.io/QF_solver/composites/), +[orthotropic solid specification](https://emptiesvoid-cloud.github.io/QF_solver/composites/solides_orthotropes/) +and [first-ply criteria](https://emptiesvoid-cloud.github.io/QF_solver/composites/criteres_rupture/) +before interpreting those results. Plasticity is described under +[nonlinear mechanics](#nonlinear-mechanics). + +### Loads, constraints and mixed models + +The model can define nodal forces and moments on available DOFs, gravity, +body forces, pressure and surface traction, beam line loads and shell edge +tractions. Linear dynamic routes include documented time-dependent/tabulated +and harmonic loading. Load integration and local/global-axis conventions are +family-specific; input availability does not establish follower-load or +nonlinear-dynamics support. + +Fixed DOFs, linear multi-point constraints, RBE2 rigid relations, weighted +RBE3 relations, springs to ground or between nodes, and concentrated +mass/inertia definitions are available within their route contracts. Named +DOFs and local frames matter, particularly where shells/beams introduce +rotations. RBE3 availability is not a general connection qualification, and +constraint/discrete entities can cause experimental runtime classifications. + +Separate bounded records cover connected, conforming, shared-node +TET4/WEDGE6/HEX8 static and modal workflows, plus selected translational-MPC +and multi-material cases. The recorded mixed Newmark/harmonic workflows +remain `EXPERIMENTAL_BOUNDED` and serial. These records do not establish +arbitrary mixed meshes, hanging-node/nonconforming interfaces, or distributed +mixed solving. The [0.2.8 workflow summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_8/) +links the separate mixed-model decisions. + ## How QF Solver works The public API and CLI validate a model, route it to an analysis-specific @@ -82,14 +175,61 @@ not expand the validated scope by themselves. See the [architecture](https://emp and [API stability](https://emptiesvoid-cloud.github.io/QF_solver/reference/api_stability/) pages for implementation boundaries and compatibility status. +### Numerical methods and backends + +An analysis type, a numerical method and an assembly contribution are +different choices. Combining element contributions into `K` and `M` does not +select a solver algorithm; adding disk gyroscopy supplies a separate `G` only +for the rotating route. + +| Problem | Available method family | Selection boundary | +| --- | --- | --- | +| Sparse linear system | Direct sparse solve, CG, MINRES, GMRES and BiCGSTAB; explicit or `auto` selection | Matrix symmetry/definiteness, residuals, preconditioner compatibility and resource estimates matter. Available SciPy preconditioners include Jacobi and ILU where compatible. | +| Classical modal | Generalized real symmetric `eigh`, sparse `eigsh`/Lanczos and LOBPCG routes | Mass formulation, mode count and reduction contract matter; dense conversion is bounded. These are not gyroscopic QEP methods. | +| Linear transient / harmonic | Newmark time integration and direct complex frequency-domain solves | Timestep, frequency sampling, damping and load definitions belong to the model contract. | +| Nonlinear static | Newton, modified Newton, line search and specialized continuation | Method availability and state handling remain route-dependent. | +| Rotating modal | Dense generalized QEP with `scipy.linalg.eig(A, B)` | Complex spectrum and original-QEP residual checks; serial experimental scope only. | +| Campbell | Complex-MAC global assignment over single-speed rotating solves | Tracking is separate from the eigensolver and retains unresolved ambiguity. | + +Use `qf-solver methods` or the public `list_methods()` function to inspect +analysis-method choices. Residual checks and diagnostics remain necessary +even when an algorithm reports convergence. Optional PETSc/MPI and SLEPc +integration does not imply that every analysis accepts those backends; see +[solver/backend notes](https://emptiesvoid-cloud.github.io/QF_solver/solveurs/). + +## Installation + +Python 3.10 or newer is required. The standard installation uses NumPy, +SciPy and Matplotlib and does not require Gmsh, HDF5, MPI, PETSc or SLEPc. +The recorded CI matrix covers Linux and Windows with Python 3.10 and 3.13; +it is not a claim that every Python/OS/backend combination has been tested. + +```bash +python -m pip install "qf-solver==0.2.11" +qf-solver --version +``` + +| Optional extra | Purpose | Boundary | +| --- | --- | --- | +| `mesh` | Gmsh mesh tooling | Requires its runtime and a supported import/setup contract. | +| `hdf5` | Family-aware mixed HDF5 result storage | Opt-in, provisional storage; not a general restart or parallel-HDF5 contract. | +| `large` | HDF5 plus MPI/PETSc bindings for large-model workflows | Native runtimes and the recorded workload scope are still required. | +| `hpc` | MPI/PETSc/SLEPc bindings | Not general distributed or rotating-QEP support. | +| `docs` | MkDocs and controlled documentation tooling | For building documentation, not running standard analyses. | + +For example, `python -m pip install "qf-solver[hdf5]==0.2.11"` adds the HDF5 +dependency. Native MPI/PETSc/SLEPc environments are platform-dependent; +installing an extra does not make those runtimes universally available. +Missing optional runtimes are reported rather than treated as successful +backend evidence. See the [installation guide](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/installation/) +for environment setup. + ## Quick start -The latest published PyPI release is 0.2.10. From a repository checkout with -the maintained example files, install that release and run a small linear -static case: +The current release is 0.2.11. With the maintained example inputs available +from a matching source checkout, run a small linear static case: ```bash -python -m pip install "qf-solver==0.2.10" qf-solver check-mesh --input examples/tet4_static.json qf-solver solve --input examples/tet4_static.json --output results/tet4.json ``` @@ -112,6 +252,22 @@ before relying on an output. The [first-calculation guide](https://emptiesvoid-c explains the workflow. New integrations should use `qf_solver`; `solveur` and the legacy launchers remain 0.2.x compatibility paths. +### Choose a maintained starting case + +| Interest | Example input or guide | +| --- | --- | +| Solid statics and loads | [`tet4_static.json`](examples/tet4_static.json), [`tet4_pressure.json`](examples/tet4_pressure.json), [`tet4_body_force.json`](examples/tet4_body_force.json) | +| Beam or shell response | [`beam2_cantilever.json`](examples/beam2_cantilever.json), [`mitc4_shell_static.json`](examples/mitc4_shell_static.json) | +| Orthotropy or laminates | [`tet4_orthotropic_static.json`](examples/tet4_orthotropic_static.json), [`mitc4_laminate_static.json`](examples/mitc4_laminate_static.json) | +| Vibration and dynamics | [`tet4_modal_unit.json`](examples/tet4_modal_unit.json), [`tet4_dynamic_tabulated_load.json`](examples/tet4_dynamic_tabulated_load.json), [`tet4_harmonic_response.json`](examples/tet4_harmonic_response.json) | +| Material/geometric nonlinearity | [`tet4_elastoplastic_static.json`](examples/tet4_elastoplastic_static.json), [small nonlinear tutorial](https://emptiesvoid-cloud.github.io/QF_solver/getting-started/nonlinear-example/) | +| Connections and contact | [`rbe2_rigid_arm.json`](examples/rbe2_rigid_arm.json), [`frictionless_contact_surface.json`](examples/frictionless_contact_surface.json) | +| Disk gyroscopy and speed sweeps | [Rotating-modal guide](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/rotating-modal/) and [Campbell guide](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/campbell/); see their structured verification fixtures rather than assuming a standalone example JSON is shipped. | + +These inputs illustrate a workflow, not a qualification for all similar +models. The nonlinear tutorial's engineering-profile `WARNING` is an +expected scope warning, not something to hide after numerical convergence. + ## Analysis families | Analysis | Orientation | Documentation | @@ -128,9 +284,43 @@ The [examples index](examples/README.md) groups maintained inputs by the mechanical problem they illustrate. It also identifies controlled verification fixtures and intentionally invalid inputs. +## Nonlinear mechanics + +Nonlinear statics uses residual/tangent assembly, Newton iteration, +convergence diagnostics and trial/accepted-state transactions. Selected +routes share a composite assembly interface for material, geometric and +contact contributions. This common core is an architectural foundation, not +a qualification of every possible combination. + +| Route | What is available | Evidence and limit | +| --- | --- | --- | +| Small-strain J2 | Von Mises radial-return plasticity with linear isotropic hardening and integration-point history | Bounded decisions for recorded TET4/TET10/HEX8/HEX20 element-analysis combinations; no general finite-strain plasticity. | +| Total-Lagrangian geometry | Selected St. Venant–Kirchhoff static formulations | Selected serial TET4/HEX8 audit records `GO_WITH_LIMITATIONS`, without maturity promotion; not a general high-order/contact/dynamic route. | +| Corotational J2 | Large rotations with small local material strains | Formal bounded acceptance is the HEX8 scope; not multiplicative finite-strain plasticity or blanket higher-order acceptance. | +| Material + geometry | Selected coupled static workflows across TET4/TET10/HEX8/HEX20 | Owner-accepted bounded evidence, not general frictional, dynamic or distributed coupling. | +| Normal contact | Penalty node-to-triangle contact and selected contact-state/recovery paths | Frictionless capability remains experimental and bounded, with small-sliding/search limits. | +| Tangential contact | Selected frictional stick/slip and recovery evidence | Narrow serial prior acceptance; mesh sensitivity and missing current-source formal requalification remain visible. | + +Path-dependent history is evaluated in a **trial state**, then **committed** +only after acceptance. Rejected increments can **roll back** before a cutback +or retry. Selected versioned checkpoint/restart routes and state digests +support replay; they do not imply universal restart for contact, distributed +or nonlinear transient models. + +Full Newton, modified Newton, line search, stagnation classification, +numerical-residual-floor handling and adaptive increments are available where +their route supports them. Specialized arc-length evidence remains +experimental and bounded; it does not establish general bifurcation or +postbuckling tracking. A numerically converged increment is not by itself a +physically valid or qualified result. + +Read the [mechanics overview](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/nonlinear-overview/) +for how the core works and the [0.2.10 V&V summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_10/) +for the separate material, geometric, coupled and contact evidence. + ## Rotating dynamics -The 0.2.11 candidate adds **experimental serial gyroscopic modal analysis for +QF Solver 0.2.11 provides **experimental serial gyroscopic modal analysis for linear BEAM2 structures carrying centered rigid axisymmetric disks**, plus experimental Campbell diagrams using explicit complex-mode tracking. This is a bounded extension to the solver, not a general rotating-machinery solver. @@ -149,13 +339,114 @@ independent physical validation. Campbell frequencies are not a forced response, amplitude, operational-danger or validated critical-speed prediction. Both `rotating_modal` and Campbell remain `EXPERIMENTAL`. +The single-speed equation is `M q̈ + Ω G q̇ + K q = 0`, with signed `Ω` +in rad/s and a fixed global axis. `G` is the unit-speed disk contribution; +speed is applied once. The result retains complex eigenvalues and modes, +growth rates, mass normalization, original-QEP residuals and the raw +spectrum. It does not silently convert complex modes to real ones. + +Campbell supplies an explicit ordered speed sweep over the same model, +mass-weighted complex MAC, global one-to-one association, degenerate-subspace +handling, branch lineage and polarization where meaningful. Its plotting +layer cannot decide or repair tracks. The optional `1×` line denotes +`|Ω|/(2π)`; a crossing is only a frequency coincidence within this linear +modal model, not a response-amplitude or operational critical-speed result. + +The dense backend was characterized up to **1,000 physical DOFs** using +synthetic matrix pencils in a recorded environment. That measured bound is +not a practical-size or timing guarantee for every rotor; cost grows steeply +and larger domains were not characterized. See the +[WP05 measurements](qualification/0_2_11/wp05_gyroscopic_report.md#dense-backend-characterization), +[rotating-modal contract](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/rotating-modal/) +and [Campbell contract](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/campbell/). + +## Results, inspection and export + +Outputs are analysis-specific. Depending on the route, they include +displacements and rotations, reactions, stresses/strains and invariants, +modal frequencies and shapes, transient histories, harmonic complex response, +nonlinear material states, or rotating/Campbell complex spectra and tracks. +Do not assume that a field available in one result is available in every +other analysis. + +For solids, recovery can include integration-point, element and nodal fields, +principal values and von Mises stress. For shells/laminates, outputs include +membrane, bending and shear resultants, face/section values and ply stresses +in the declared axes. Interface ply stresses are not artificially averaged +across a discontinuity. Nodal averaging is not a superconvergent stress +recovery or a global error estimator; see [result conventions](https://emptiesvoid-cloud.github.io/QF_solver/conventions_resultats/). + +| Tool | Purpose | Scope | +| --- | --- | --- | +| `check_mesh` / `qf-solver check-mesh` | Geometry, connectivity and boundary-condition checks | Inspect `PASS`, `WARNING` and `FAIL`, not just whether parsing succeeded. | +| `inspect_model` / `qf-solver inspect` | White-box model, DOF, matrix and consistency audit | `summary`, `diagnostic` and `values` detail levels; full-value dumps can be large. | +| `assess_result` | Non-raising run/qualification summary | Numerical status and verification-profile acceptance are separate. | +| `save_result` | Structured JSON results | Available fields and complex serialization follow the result contract. | +| `save_result_csv` / `save_result_vtu` | Result tables and visualization fields | Export support is result/family-specific, not lossless export of every analysis; VTU can be viewed in a compatible visualization tool. | +| `save_audit_markdown` | Readable model/result audit | Compact reports retain warnings/failures; detail level does not change the solve. | +| `save_evidence` / `verify_evidence` | Evidence bundle and fingerprint verification | File integrity does not automatically establish scientific qualification. | +| Mixed HDF5 APIs | Family-aware storage and selective result reads | Opt-in/provisional; no general parallel-HDF5 or restart claim. | + +For a supported static example, a CLI export workflow is: + +```bash +qf-solver solve --input examples/tet4_static.json --output results/tet4.json \ + --csv-dir results/tet4_csv --vtu results/tet4.vtu --audit-md results/tet4_audit.md +``` + +Use [audit detail modes](https://emptiesvoid-cloud.github.io/QF_solver/reference/audit_detail_modes/) +and the [public API contract](https://emptiesvoid-cloud.github.io/QF_solver/reference/qf_solver_api/) +to choose the outputs you need. The `quick`, `engineering`, `strict` and +`qualification` profiles are acceptance/evidence policies, not different +physical formulations. Tightening a profile does not promote maturity. + +### Input and mesh interchange + +Strict JSON is the standard model interchange. It defines nodes, elements, +materials, constraints, loads, analysis settings, units and the verification +profile. Mechanical quantities must be dimensionally consistent; declaring +units does not mean arbitrary quantities are converted automatically. + +Gmsh MSH 4.1 import uses physical groups and a companion setup to assign +materials, boundary conditions and loads for supported cell families. The +mesh alone is not a complete analysis definition. Coordinate scaling and +orientation repair, where supported, must be explicit and reported. +The [Gmsh guide](https://emptiesvoid-cloud.github.io/QF_solver/reference/import_gmsh/) +describes the workflow; later mixed-family evidence has separate contracts. +The provisional `.inp` reader covers a bounded Abaqus/CalculiX subset, not +general deck or solver compatibility. Large-model HDF5/NPZ storage is a +separate workflow from standard JSON inputs and mixed HDF5 result storage. + +## Larger models and performance context + +Large-model tooling includes structured TET4 generation, large-model +inspection/readiness, chunked assembly or matrix-free paths, benchmark +reports, selected PETSc preconditioner comparisons and chunked +postprocessing. These are specialized workflows, not a promise that every +model supported by the small serial API can run through them. + +Historical evidence records structured-TET4 workloads at approximately +1.029M, 3M, 5.01264M and 10.125M DOFs. Their source, solver settings, rank +count, hardware and acceptance limits are part of the evidence. They do not +establish a hardware-independent speed or memory guarantee, nor general +strong/weak scaling. The +[historical large-model summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_7/) +and [bounded limitations](https://emptiesvoid-cloud.github.io/QF_solver/etat/limites/) +provide the workload context. + +Separate two-rank PETSc/MPI evidence covers selected linear-static cases +with replicated input and root-side assembly. That is not distributed +assembly or general nonlinear/dynamic/contact MPI support. Generic mixed +distributed runtime remains `NOT_VALIDATED`. No GPU claim is made, and the +rotating QEP is dense serial rather than part of these large-model routes. + ## Verification and maturity QF Solver distinguishes implementation, a passing test, numerical verification, external solver correlation, maturity decisions and physical validation. These are separate kinds of evidence. The [V&V and maturity guide](https://emptiesvoid-cloud.github.io/QF_solver/verification/evidence-and-maturity/) explains the labels; the [0.2.11 verification summary](https://emptiesvoid-cloud.github.io/QF_solver/verification/0_2_11/) -links the candidate gyro and Campbell records. +links the gyro and Campbell records. Selected Code_Aster 18.1 results provide same-mesh **linear-static solver correlation** for declared cases and comparable observables. This is not @@ -165,6 +456,31 @@ FEM/Newton solve. Large-model and PETSc/MPI evidence is tied to specific workloads and environments; it does not establish general scalability or distributed nonlinear support. +Verification covers analytical element/material identities, equilibrium and +energy checks, regression cases, mesh/time-grid convergence within declared +studies, solver residuals and selected external comparisons. Historical +composite/solid studies also contain bounded CalculiX correlations where the +formulations and observables are comparable; these must not be generalized +to every family or route. + +New execution records bind scientific inputs, referenced file bytes, +resolved configuration, oracle, tolerance policy, code and relevant +environment to an execution identity. Safe resume checks identity and +artifact integrity; structured expected failures distinguish an intended +rejection from an unrelated exception. Evidence availability remains +separate from a numerical `PASS`: some historical payloads are local-only, +reconstructed, optional or missing. Historical schemas and decisions are +preserved rather than rewritten by newer tooling. + +For method-development work, the public surface also exposes benchmark and +demonstration catalogs, evidence verification and controlled V&V/campaign +tools. These advanced interfaces are generally provisional. See +[benchmarks](https://emptiesvoid-cloud.github.io/QF_solver/benchmarks/), +[demonstration API](https://emptiesvoid-cloud.github.io/QF_solver/reference/demonstrations_api/) +and the [public export inventory](https://emptiesvoid-cloud.github.io/QF_solver/reference/qf_solver_api/) +for their contracts. A benchmark `PASS` is never an automatic qualification +decision. + ## Limitations - General multiplicative finite-strain plasticity, nonlinear transient @@ -177,6 +493,15 @@ distributed nonlinear support. search, finite sliding and self-contact are not claimed. - Optional PETSc/MPI workflows do not establish general HPC scaling, nonlinear distributed solving or a validated mixed-distributed runtime. +- Composite first-ply indicators are experimental, with no progressive + damage/delamination model or universal design-validation claim. Laminate + and orthotropic acceptance applies only to the recorded scopes. +- MITC4 modal historical issues, discrete Newmark replays and experimental + mixed dynamics retain their separate records; a later retest does not + automatically promote a family. HEX8-SRI remains separately bounded; + WEDGE15 is unsupported and PYRAMID5 is internal/research-only. +- Importers, result recovery and export schemas have explicit subsets; + neither a valid input nor a readable visualization proves model accuracy. - Rotating modal and Campbell are experimental and restricted to the model described above; the 100 rad/s high-frequency branch ambiguity remains visible. @@ -187,22 +512,21 @@ and the controlling evidence record for the chosen analysis. ## Current release and citation -The latest **published** release is `0.2.10` ([PyPI](https://pypi.org/project/qf-solver/0.2.10/), -[GitHub Release](https://github.com/emptiesvoid-cloud/QF_solver/releases/tag/v0.2.10), -source tag [`v0.2.10`](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10)). -Version 0.2.11 is a **candidate, not yet published**. Its cumulative additions -and bounded evidence are summarized in [What's New in 0.2.11](docs/whats-new/0.2.11.md); -the [changelog](CHANGELOG.md) retains the full project history. - -For reproducibility, cite the exact published version DOI -[`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744). Use the -project concept DOI [`10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) -when citing the evolving project rather than a specific release. The candidate -[`CITATION.cff`](CITATION.cff) identifies version 0.2.11 but intentionally has -no release date or version DOI until publication. The whole-repository G03 -archive gate remains **FAIL**, the full repository archive is not cleared, and -WP14 remains **HOLD_NOT_PROMOTED**; selected distribution checks do not change -those states. +The current QF Solver release is `0.2.11`; it follows the published `0.2.10` +release. The [What's New in 0.2.11](docs/whats-new/0.2.11.md) page summarizes +the changes, while the [changelog](CHANGELOG.md) retains the full project +history. + +Use a version DOI for exact-release reproducibility when one is assigned. Use +the version DOI for the preceding release, `10.5281/zenodo.23106744`, when +citing 0.2.10. Use the project concept DOI +[`10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) to cite +QF Solver as a project. The previous release used source tag +[`v0.2.10`](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10). +[`CITATION.cff`](CITATION.cff) provides machine-readable citation metadata. +The whole-repository G03 archive gate remains **FAIL**, the full repository +archive is not cleared, and WP14 remains **HOLD_NOT_PROMOTED**. Selected +distribution checks do not change those states. ## Documentation and project @@ -214,6 +538,9 @@ those states. and [API stability](https://emptiesvoid-cloud.github.io/QF_solver/reference/api_stability/) - [Benchmarks](https://emptiesvoid-cloud.github.io/QF_solver/benchmarks/) and [V&V evidence](https://emptiesvoid-cloud.github.io/QF_solver/verification/evidence-and-maturity/) +- [Composite mechanics](https://emptiesvoid-cloud.github.io/QF_solver/composites/), + [nonlinear mechanics](https://emptiesvoid-cloud.github.io/QF_solver/mechanics/nonlinear-overview/) + and [solver/backend notes](https://emptiesvoid-cloud.github.io/QF_solver/solveurs/) - [Contributing](CONTRIBUTING.md) · [code license](LICENSE) · [documentation/examples license](LICENSE-DOCS) diff --git a/docs/analyses/index.md b/docs/analyses/index.md index ca9020d0..483ce162 100644 --- a/docs/analyses/index.md +++ b/docs/analyses/index.md @@ -9,8 +9,8 @@ approver: "" # Analyses -This page maps published QF Solver 0.2.10 routes and separately identifies -the experimental 0.2.11 release candidates. Candidate rows are not part of +This page maps the cumulative QF Solver 0.2.11 analysis surface. The +experimental rotating routes have their own evidence and are not part of the immutable 0.2.10 tag or distribution. The [consolidated 0.2.8 registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) remains authoritative for its 46 element-analysis decisions. Development-cycle @@ -23,8 +23,8 @@ evidence is linked separately and does not rewrite that registry. | Newmark transient | Route-dependent; mixed route `EXPERIMENTAL_BOUNDED` | Linear dynamics only; recorded time-step, mass and damping assumptions apply. | | Harmonic | Route-dependent; mixed route `EXPERIMENTAL_BOUNDED` | Recorded linear frequency-domain cases only. | | Linear buckling | Route-dependent | Bounded first-factor cases; not postbuckling or bifurcation analysis. | -| Rotating modal (0.2.11 candidate) | `EXPERIMENTAL` | One signed speed per solve; serial dense QEP; straight circular-isotropic BEAM2 shaft with centered rigid axisymmetric disks, undamped and unprestressed. See [scope and limitations](../mechanics/rotating-modal.md). | -| Campbell sweep / modal tracking (0.2.11 candidate) | `EXPERIMENTAL` | Orchestrates explicit ordered speeds through the bounded `rotating_modal` route; complex MAC, global assignment, subspace-aware degeneracy and visible ambiguity. No forced-response or general rotordynamics claim; see [Campbell limits](../mechanics/campbell.md). | +| Rotating modal | `EXPERIMENTAL` | One signed speed per solve; serial dense QEP; straight circular-isotropic BEAM2 shaft with centered rigid axisymmetric disks, undamped and unprestressed. See [scope and limitations](../mechanics/rotating-modal.md). | +| Campbell sweep / modal tracking | `EXPERIMENTAL` | Orchestrates explicit ordered speeds through the bounded `rotating_modal` route; complex MAC, global assignment, subspace-aware degeneracy and visible ambiguity. No forced-response or general rotordynamics claim; see [Campbell limits](../mechanics/campbell.md). | | Small-strain J2 | `QUALIFIED_BOUNDED` | The exact element-analysis combinations in the registry. | | Corotational J2 | `QUALIFIED_BOUNDED` within accepted scope | HEX8 only; large rotations with small local strains, not general finite-strain plasticity. | | Total-Lagrangian geometric nonlinear | Audited `GO_WITH_LIMITATIONS`; maturity not promoted | Selected StVK static serial TET4/HEX8 cases. | @@ -37,5 +37,6 @@ evidence is linked separately and does not rewrite that registry. For element-family boundaries use the [elements map](../elements/index.md). For nonlinear mechanics see the [mechanics overview](../mechanics/nonlinear-overview.md) and [capability matrix](../capabilities/index.md). The current -[V&V summary](../verification/0_2_10/README.md) distinguishes implementation, +[0.2.11 V&V summary](../verification/0_2_11/README.md) and separate +[0.2.10 evidence](../verification/0_2_10/README.md) distinguish implementation, verification, bounded acceptance, and physical validation. diff --git a/docs/architecture.md b/docs/architecture.md index 36a8158e..f246e7d3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,16 +2,16 @@ doc_id: DOC-ARCH-001 revision: 3.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # QF Solver architecture -This page describes the QF Solver 0.2.10 architecture. Architecture diagrams describe implementation +This page describes the QF Solver 0.2.11 architecture. Architecture diagrams describe implementation boundaries; they do not establish capability maturity. See the -[capability index](capabilities/index.md) and [0.2.10 V&V summary](verification/0_2_10/README.md) +[capability index](capabilities/index.md) and [0.2.11 V&V summary](verification/0_2_11/README.md) for evidence and scope. ## Public entry points and routing @@ -40,6 +40,34 @@ The CLI validates and translates input, calls the API, and writes result artifacts. It is not the location of finite-element formulations. Core calculation modules do not own command-line parsing or output serialization. +Rotating modal and Campbell are additional analysis routes, not alternative +implementations of classical modal analysis. They use the existing public +model workflow with separate input and result contracts. + +## Rotating analysis flow + +```text +campbell request + explicit ordered speeds + -> fixed model / disk / tracking policy + -> rotating_modal at each signed speed + -> fail-closed BEAM2/disk input validation + -> structural K/M + disk mass + unit-speed G + -> common fixed-DOF reduction + -> scaled dense generalized QEP + -> RotatingModalResult (raw complex spectrum + residuals) + -> modal_tracking (complex MAC / global assignment / subspaces) + -> CampbellResult (lineage / scores / ambiguity / provenance) + -> post/campbell (projection only; cannot assign tracks) +``` + +The responsibilities are implemented separately in +`solveur.core.analyses.rotating_modal`, `qep`, `modal_tracking`, `campbell` +and `solveur.post.campbell`. Campbell varies speed, not model properties or +`K/M/G`. Gyroscopic assembly is not activated for legacy analyses. These +internal modules are not stable public API. Both rotating capabilities +remain `EXPERIMENTAL`, serial and bounded; PETSc/SLEPc/MPI, distributed +shaft gyroscopy and speed-dependent physics are not enabled by this graph. + ## Nonlinear analysis flow Several bounded nonlinear routes share a driver foundation, Newton engine, @@ -129,8 +157,11 @@ documentation and exposes an uncommitted tree as such. The `qualification` profile has stricter source and page-status requirements. Generated measurements are not manually transcribed as new qualification decisions. The MkDocs site intentionally excludes the detailed `verification/0_2_9` -engineering archive from public navigation; the concise 0.2.10 summary is -served separately. +engineering archive from public navigation; concise 0.2.10 and 0.2.11 summaries +are served separately. Prospective execution identities, content hashes, +safe resume, structured expected failures and evidence availability are +documented in the [0.2.11 provenance summary](verification/0_2_11/README.md). +Numerical results and maturity decisions remain separate. ## Known architectural debt diff --git a/docs/capabilities/index.md b/docs/capabilities/index.md index 155fdc7d..9ff62250 100644 --- a/docs/capabilities/index.md +++ b/docs/capabilities/index.md @@ -9,12 +9,12 @@ approver: "" # QF Solver capability index -**Current release:** `0.2.10` / `v0.2.10`
-**0.2.11 candidate:** experimental rotating modal and Campbell routes; not yet published. +**Current release documentation:** `0.2.11`
+**Rotating modal and Campbell:** `EXPERIMENTAL`, within their declared scope. -The published capability rows describe QF Solver 0.2.10 without changing -previous maturity decisions; separately labeled 0.2.11 rows describe only -unreleased candidate evidence. In particular, source implementation and a +The capability rows describe the cumulative QF Solver 0.2.11 surface without +changing previous maturity decisions. New experimental routes have separate +contracts and evidence. In particular, source implementation and a successful test do not by themselves establish qualification. ## How to read the statuses @@ -25,7 +25,7 @@ general qualified capability. “Owner-accepted bounded evidence” is reported as such and is not silently relabeled `QUALIFIED_BOUNDED`. “Audited; maturity not promoted” means a technical audit exists but no maturity promotion was recorded. The 46-case registry remains the authority for its original 0.2.8 -scope and counts; this 0.2.10 index does not rewrite it. +scope and counts; this current index does not rewrite it. ## Capability matrix @@ -37,8 +37,8 @@ scope and counts; this 0.2.10 index does not rewrite it. | Dynamics | WEDGE6 modal | `QUALIFIED_BOUNDED` — [registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) | Homogeneous isotropic consistent-mass route; first three modes in the declared refinement scope. | | Dynamics | Newmark | Route-dependent / mixed route experimental — [registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) | Linear transient scope only; timestep, damping and mass assumptions apply. | | Dynamics | Harmonic | Route-dependent / mixed route experimental — [registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) | Recorded linear frequency-domain cases; no general nonlinear transient claim. | -| Rotating dynamics | `rotating_modal` (candidate) | `EXPERIMENTAL` — [WP05 contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/wp05_gyroscopic_contract.json) | Serial dense QEP; straight circular-isotropic BEAM2 shaft, centered axisymmetric disks, constant signed spin, undamped/unprestressed small perturbations only. | -| Rotating dynamics | Campbell / modal tracking (candidate) | `EXPERIMENTAL` — [WP06 contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/wp06_campbell_contract.json) | Explicit speed sweep over the WP05 model; complex MAC and ambiguity-preserving tracks. High-frequency pair ambiguous at 100 rad/s; no forced response or validated critical-speed prediction. | +| Rotating dynamics | `rotating_modal` | `EXPERIMENTAL` — [WP05 contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/wp05_gyroscopic_contract.json) | Serial dense QEP; straight circular-isotropic BEAM2 shaft, centered axisymmetric disks, constant signed spin, undamped/unprestressed small perturbations only. | +| Rotating dynamics | Campbell / modal tracking | `EXPERIMENTAL` — [WP06 contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/wp06_campbell_contract.json) | Explicit speed sweep over the WP05 model; complex MAC and ambiguity-preserving tracks. High-frequency pair ambiguous at 100 rad/s; no forced response or validated critical-speed prediction. | | Material NL | Small-strain J2 | `QUALIFIED_BOUNDED` — [registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) | Recorded homogeneous constitutive cases and element combinations only. | | Geometric NL | Total-Lagrangian StVK | Audited `GO_WITH_LIMITATIONS`; maturity not promoted — [audit](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_9/wp04f/wp04_final_closure_audit.json) | Selected static serial TET4/HEX8 cases within explicit deformation/formulation bounds; excludes contact, dynamics, MPI/PETSc and high-order routes. | | Coupled NL | Material + geometry | Owner-accepted bounded evidence — [WP10 decision](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_9/wp10_owner_acceptance.json) | Selected static cases across recorded families; not frictional contact, dynamics, Code_Aster correlation or MPI. | @@ -55,13 +55,13 @@ scope and counts; this 0.2.10 index does not rewrite it. For the nonlinear architecture see the [mechanics overview](../mechanics/nonlinear-overview.md). For concrete route restrictions see [known limitations](../etat/limites.md). For published 0.2.10 evidence see the [0.2.10 V&V summary](../verification/0_2_10/README.md). -For candidate GYRO-01–06 evidence see the [0.2.11 V&V summary](../verification/0_2_11/README.md). +For GYRO-01–06 evidence see the [0.2.11 V&V summary](../verification/0_2_11/README.md). ## Registry boundary The consolidated 0.2.8 registry contains 46 element-analysis combinations: 32 `QUALIFIED_BOUNDED`, 14 `EXPERIMENTAL`, and 0 `NOT_QUALIFIED`. These are -historical, scoped counts—not a 0.2.10 score and not a count of every separate +historical, scoped counts, not a current score or a count of every separate workflow above. Mixed workflows, mechanics evidence, and external correlation remain separate records. WP14 is on HOLD while the whole-repository G03 scan is failed; the selected-package audit does not waive or clear that gate. diff --git a/docs/comparisons/index.md b/docs/comparisons/index.md index 2e84cf8d..d96b3301 100644 --- a/docs/comparisons/index.md +++ b/docs/comparisons/index.md @@ -2,7 +2,7 @@ doc_id: DOC-SOLVER-COMP-000 revision: 1.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -17,12 +17,12 @@ This section compares QF Solver with several open-source finite-element tools. > **HISTORICAL SNAPSHOT NOTICE.** The detailed comparison pages retain their > explicitly labeled 0.2.7 analysis snapshots and have not been refreshed for -> the 0.2.10 release. They are not current capability or maturity records; -> use the 0.2.10 index and V&V summary for current scope. +> subsequent releases. They are not current capability or maturity records; +> use the current capability index and V&V summary for current scope. -The current published package is 0.2.10. The detailed comparison articles +The current documentation baseline is 0.2.11. The detailed comparison articles retain their explicit 0.2.7 snapshots. For current maturity and -limitations, use the 0.2.10 sources linked below; in particular, mixed +limitations, use the sources linked below; in particular, mixed distributed PETSc/MPI remains `NOT_VALIDATED`. The objective is not to claim that QF Solver is universally better. Each @@ -58,7 +58,7 @@ QF Solver deliberately distinguishes between: - implemented capabilities; - tested capabilities; - verified capabilities; -- externally validated capabilities; +- external numerical correlation (distinct from physical validation); - qualified capabilities; - experimental capabilities. @@ -101,8 +101,9 @@ The authoritative QF Solver capability information is available in: - [Elements](../elements/index.md) - [Analyses](../analyses/index.md) -- [0.2.10 capability index](../capabilities/index.md) -- [0.2.10 V&V summary](../verification/0_2_10/README.md) +- [Current capability index](../capabilities/index.md) +- [0.2.11 V&V summary](../verification/0_2_11/README.md) +- [Inherited 0.2.10 evidence](../verification/0_2_10/README.md) - [Known limitations](../etat/limites.md) - [Published QF Solver 0.2.8 verification](../verification/0_2_8/README.md) - [Historical QF Solver 0.2.7 verification](../verification/0_2_7/README.md) diff --git a/docs/demonstrations/index.md b/docs/demonstrations/index.md index dcf2af92..a7bd726e 100644 --- a/docs/demonstrations/index.md +++ b/docs/demonstrations/index.md @@ -2,7 +2,7 @@ doc_id: DOC-DEMO-000 revision: 1.0 status: controlled -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -15,7 +15,7 @@ not establish universal element or solver qualification. ## Public examples -The maintained JSON examples are in [`examples/`](https://github.com/emptiesvoid-cloud/QF_solver/tree/v0.2.10/examples). The +The maintained JSON examples are in [`examples/`](https://github.com/emptiesvoid-cloud/QF_solver/tree/main/examples). Choose examples from the same source revision as the solver. The shortest path is the [TET4 static quickstart](../getting-started/quickstart.md). The public Python API is available through `qf_solver`. @@ -27,7 +27,13 @@ mesh checks together. A plausible displacement alone is not sufficient. The published 0.2.8 boundaries remain in the [historical verification summary](../verification/0_2_8/README.md). For -0.2.10, consult the [capability index](../capabilities/index.md), -[V&V summary](../verification/0_2_10/README.md), and the bounded +the cumulative 0.2.11 solver, consult the [capability index](../capabilities/index.md), +[V&V summary](../verification/0_2_11/README.md), the inherited +[0.2.10 evidence](../verification/0_2_10/README.md), and the bounded [nonlinear example](../getting-started/nonlinear-example.md). Large-model examples are described in [Large models](../solveurs/grand_modele.md). + +Experimental [rotating-modal](../mechanics/rotating-modal.md) and +[Campbell](../mechanics/campbell.md) demonstrations retain their bounded +BEAM2/disk scope. GYRO-06 is internal mesh-convergence evidence, not physical +validation, and the high-frequency ambiguity at 100 rad/s is preserved. diff --git a/docs/document_registry.json b/docs/document_registry.json index 5eb75931..5ad34926 100644 --- a/docs/document_registry.json +++ b/docs/document_registry.json @@ -11,7 +11,7 @@ { "id": "DOC-STATE-001", "path": "index.md", - "title": "QF Solver 0.2.10", + "title": "QF Solver", "status": "controlled_candidate", "requirements": [ "REQ-AUD-001", @@ -115,7 +115,7 @@ { "id": "DOC-VV-PUBLIC-0211-001", "path": "verification/0_2_11/README.md", - "title": "QF Solver 0.2.11 candidate verification summary", + "title": "QF Solver 0.2.11 verification summary", "status": "controlled_candidate", "requirements": [], "examples": [], diff --git a/docs/elements/index.md b/docs/elements/index.md index 34c909f7..7c366b25 100644 --- a/docs/elements/index.md +++ b/docs/elements/index.md @@ -2,7 +2,7 @@ doc_id: DOC-ELEM-000 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -12,7 +12,7 @@ approver: "" Element support and maturity are separate. The published 0.2.8 consolidated registry remains authoritative for its 46 recorded element-analysis combinations; 0.2.10 nonlinear evidence and its limitations are reported -separately. +separately and remain relevant in 0.2.11. | Element family | 0.2.8 bounded scope | 0.2.10 nonlinear evidence / limitation | | --- | --- | --- | @@ -29,6 +29,14 @@ HEX8-SRI is a separate experimental capability; it is not a blanket claim for HEX8R, B-bar, or hourglass-control formulations. Higher-order families may have narrower nonlinear evidence than their linear registry coverage. +The 0.2.11 rotating extension uses only a straight, collinear BEAM2 shaft +with circular isotropic sections and centered rigid axisymmetric disks. +It is `EXPERIMENTAL`; legacy beam or shell support does not extend that +scope to shells, solid rotor meshes, eccentric disks or distributed shaft +gyroscopy. Each disk owns its mass and gyroscopic term; duplicate generic +disk mass at the same node is rejected. See [rotating modal](../mechanics/rotating-modal.md) +and [Campbell](../mechanics/campbell.md). + Before selecting an element, check geometry quality, orientation, expected deformation, loading, material model, and required output. Refinement cannot repair an invalid Jacobian or an unsupported kinematic/constitutive route. diff --git a/docs/etat/capacites.md b/docs/etat/capacites.md index 7afd80c8..9c5cec67 100644 --- a/docs/etat/capacites.md +++ b/docs/etat/capacites.md @@ -2,14 +2,14 @@ doc_id: DOC-STATE-002 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # Capabilities and maturity -This page is an orientation to the 0.2.10 release, not a new qualification +This page is an orientation to the cumulative 0.2.11 solver, not a new qualification decision. Maturity applies only to a named scope; implementation or execution alone does not promote it. @@ -26,13 +26,16 @@ scope; implementation or execution alone does not promote it. | PETSc/MPI | Owner-accepted bounded evidence | Two-rank linear static with replicated input/root-side assembly; no scaling claim. | | Code_Aster | Owner-accepted bounded external correlation | Same-mesh linear-static observables for selected families; not physical validation. | | Mixed PETSc/MPI general workflow | `NOT_VALIDATED` | Architecture evidence does not establish generic distributed execution. | +| Gyroscopic modal analysis | `EXPERIMENTAL` | Straight circular BEAM2 shafts with centered rigid axisymmetric disks; dense serial QEP, no distributed shaft gyro. | +| Campbell diagrams | `EXPERIMENTAL` | Explicit complex-mode tracking; the high-frequency pair at 100 rad/s remains ambiguous. GYRO-06 is internal mesh-convergence evidence, not independent physical validation. | `QUALIFIED_BOUNDED`, `EXPERIMENTAL_BOUNDED`, Owner-accepted evidence, and an audit result are distinct labels; this summary preserves the source decision language. The 46-row historical registry remains unchanged: 32 `QUALIFIED_BOUNDED`, 14 `EXPERIMENTAL`, and 0 `NOT_QUALIFIED` for its -specified 0.2.8 element-analysis combinations. It is not a 0.2.10 score. +specified 0.2.8 element-analysis combinations. It is not a current-version score. Read the [capability matrix](../capabilities/index.md), [analysis map](../analyses/index.md), [known limitations](limites.md), and -[0.2.10 V&V summary](../verification/0_2_10/README.md) together. +[0.2.11 V&V summary](../verification/0_2_11/README.md) and inherited +[0.2.10 evidence](../verification/0_2_10/README.md) together. diff --git a/docs/etat/limites.md b/docs/etat/limites.md index 3c4d2f7f..adf6ba76 100644 --- a/docs/etat/limites.md +++ b/docs/etat/limites.md @@ -9,8 +9,8 @@ approver: "" # Known limitations -QF Solver 0.2.10 remains the current published release; 0.2.11 is an -unreleased candidate. Every result is bounded by its element family, +These limitations apply to the cumulative QF Solver 0.2.11 surface. +Every result is bounded by its element family, formulation, mesh, loads, boundary conditions, constitutive model, solver, and evidence decision. The limitations below are technical boundaries, not a complete list of input checks. @@ -39,9 +39,9 @@ complete list of input checks. named in their records. Do not infer frictional-contact, distributed, or nonlinear-dynamics restart support from shared transaction code. -## Rotating modal and Campbell candidate +## Rotating modal and Campbell -- The 0.2.11 candidate is experimental and restricted to serial dense gyroscopic +- The rotating capability is experimental and restricted to serial dense gyroscopic modal analysis of straight, collinear, circular-isotropic BEAM2 shafts with centered rigid axisymmetric disks, constant signed spin, a fixed global axis, and an undamped, unprestressed small-perturbation model. @@ -51,12 +51,12 @@ complete list of input checks. - At 100 rad/s, the high-frequency pair remains ambiguous. The tracker keeps the gap; it must not force continuity or interpolate across the ambiguity. - GYRO-06 is internal mesh-convergence evidence, not independent physical - validation. Both new analysis maturities remain `EXPERIMENTAL`; candidate + validation. Both analysis maturities remain `EXPERIMENTAL`; numerical PASS does not itself qualify the capability. - Distributed shaft gyros, variable speed, speed-dependent stiffness/mass, general damping, bearings, centrifugal stiffening, contact/rubbing, nonlinear rotors, forced unbalance response, PETSc/SLEPc and MPI are outside - this candidate scope. + this scope. ## Contact @@ -101,12 +101,13 @@ complete list of input checks. solve. A test pass is not itself a maturity decision. - No certification, Abaqus equivalence, industrial-grade guarantee, or universal physical validation is claimed. -- The selected wheel and sdist are audited against their prospective release - contract. That package-scoped result does not clear the full-repository G03 +- Selected wheel/sdist audits are bound to their exact source and release + contract. A package-scoped result does not clear the full-repository G03 archive gate or the automatic GitHub source archives. G03 remains FAIL and WP14 remains on HOLD. For exact published element-analysis boundaries, see the [0.2.8 consolidated registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json). -For release evidence see the [0.2.10 V&V summary](../verification/0_2_10/README.md) +For evidence see the [0.2.11 V&V summary](../verification/0_2_11/README.md), +the separate [0.2.10 records](../verification/0_2_10/README.md) and the [capability index](../capabilities/index.md). diff --git a/docs/examples/index.md b/docs/examples/index.md index 3f7a8209..09544041 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -2,7 +2,7 @@ doc_id: DOC-EXAMPLES-000 revision: 1.0 status: controlled -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -11,13 +11,19 @@ approver: "" The maintained examples live in the repository `examples/` directory and use the public `qf_solver` API or the `qf-solver` CLI. Start with -[`tet4_static.json`](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.10/examples/tet4_static.json) and the +[`tet4_static.json`](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/examples/tet4_static.json) and the [first-calculation guide](../getting-started/quickstart.md). Examples demonstrate a concrete workflow; they do not qualify every possible combination of element, load, material and solver. -For 0.2.10, see the bounded +Use examples from the same source revision as the installed solver. For a +bounded nonlinear workflow, see the [nonlinear example](../getting-started/nonlinear-example.md). It is a smoke test for one research-profile Total-Lagrangian TET4 route, not a general qualification claim. + +The [rotating-modal guide](../mechanics/rotating-modal.md) and +[Campbell guide](../mechanics/campbell.md) cover the experimental disk-gyroscopic +API. Their scope is not general rotordynamics; see the explicit limitations +and ambiguity diagnostics before interpreting results. diff --git a/docs/generated/document_registry.md b/docs/generated/document_registry.md index e4ae3b9f..7e26e0ad 100644 --- a/docs/generated/document_registry.md +++ b/docs/generated/document_registry.md @@ -1,6 +1,6 @@ | ID | Title | Status | Requirements | Examples | Tests | Source | | --- | --- | --- | --- | --- | --- | --- | -| DOC-STATE-001 | QF Solver 0.2.10 | controlled_candidate | REQ-AUD-001, REQ-CMP-001 | 1 | 1 | index.md | +| DOC-STATE-001 | QF Solver | controlled_candidate | REQ-AUD-001, REQ-CMP-001 | 1 | 1 | index.md | | DOC-VNV-CODEASTER-CAMPAIGN-2026-08-14 | Campagne de correlation Code_Aster du 2026-08-14 | ready_for_owner_review | REQ-CMP-003, REQ-AUD-001 | 1 | 3 | verification/code_aster_correlation_campaign_2026-08-14.md | | DOC-WHATS-NEW-0210-001 | What's New in QF Solver 0.2.10 | controlled_candidate | | 0 | 1 | whats-new/0.2.10.md | | DOC-WHATS-NEW-0211-001 | What's New in QF Solver 0.2.11 | controlled_candidate | | 0 | 1 | whats-new/0.2.11.md | @@ -9,7 +9,7 @@ | DOC-MECH-CAMPBELL-WP06 | Experimental Campbell diagrams | controlled_candidate | | 0 | 2 | mechanics/campbell.md | | DOC-START-NONLINEAR-001 | Nonlinear example | controlled_candidate | | 1 | 1 | getting-started/nonlinear-example.md | | DOC-VV-PUBLIC-0210-001 | QF Solver 0.2.10 verification summary | controlled_release | | 0 | 1 | verification/0_2_10/README.md | -| DOC-VV-PUBLIC-0211-001 | QF Solver 0.2.11 candidate verification summary | controlled_candidate | | 0 | 1 | verification/0_2_11/README.md | +| DOC-VV-PUBLIC-0211-001 | QF Solver 0.2.11 verification summary | controlled_candidate | | 0 | 1 | verification/0_2_11/README.md | | DOC-STATE-002 | Capabilities and maturity | controlled_candidate | REQ-CMP-001, REQ-REL-001 | 0 | 0 | etat/capacites.md | | DOC-STATE-003 | Known limitations | controlled_candidate | REQ-CMP-002, REQ-REL-001 | 0 | 0 | etat/limites.md | | DOC-CAPABILITY-028-001 | QF Solver capability index | controlled_candidate | | 0 | 0 | capabilities/index.md | diff --git a/docs/generated/status.md b/docs/generated/status.md index 759ebac3..58f7bab3 100644 --- a/docs/generated/status.md +++ b/docs/generated/status.md @@ -1,11 +1,11 @@
-

Project version

0.2.10release status and citation identifiers are maintained in README and CITATION.cff
-

Evidence status

BOUNDED0.2.8 registry remains historical authority; 0.2.10 routes are separately scoped; WP14 HOLD and G03 FAIL
+

Project version

0.2.11release status and citation identifiers are maintained in README and CITATION.cff
+

Evidence status

ROUTE-SPECIFIC0.2.8 registry remains historical authority; inherited nonlinear evidence and 0.2.11 experimental rotating routes are separately scoped; WP14 HOLD_NOT_PROMOTED and G03 FAIL_PRESERVED

Distribution scope

SELECTEDwheel and sdist only; whole-repository source archives are not cleared
The 0.2.8 published registry remains bounded at its declared historical -boundaries. The full-suite test inventory is recorded by the final Gate-E evidence; this generated overview deliberately does not hard-code a collection count. The 0.2.10 evidence and limitations are summarized separately; this status panel does not assert that the complete repository archive passed G03. +boundaries. The full-suite test inventory is recorded by the final Gate-E evidence; this generated overview deliberately does not hard-code a collection count. Inherited 0.2.10 evidence and experimental 0.2.11 evidence have separate decisions; this status panel does not assert publication or whole-repository G03 clearance. | Scope | Maturity | Public boundary | | --- | --- | --- | @@ -19,3 +19,5 @@ boundaries. The full-suite test inventory is recorded by the final Gate-E eviden | WEDGE6 static | qualified bounded | Approved documented static scope | | PETSc/MPI linear static | Owner-accepted bounded | Two-rank cases with replicated input/root-side assembly; no scaling claim | | Mixed distributed PETSc/MPI runtime (including nonlinear routes) | not validated | No general nonlinear distributed evidence | +| Gyroscopic modal analysis | experimental | Straight circular BEAM2 shafts with centered rigid axisymmetric disks; dense serial QEP only | +| Campbell diagrams | experimental | Explicit complex-mode tracking; high-frequency ambiguity at 100 rad/s preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation | diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index a06861a9..64385a35 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,28 +2,28 @@ doc_id: DOC-START-PUB-001 revision: 1.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # Installation -The selected QF Solver `0.2.10` wheel and sdist are the audited PyPI release -artifacts. The tagged release workflow publishes them only after its CI and -source-bound package gates pass. QF Solver 0.2.10 is the first public release -after `0.2.8`; the intervening `v0.2.9` tag is a source snapshot, not a package -release. See [What's New in 0.2.10](../whats-new/0.2.10.md) for the release +This guide applies to QF Solver `0.2.11`. Release availability is +authoritative on [PyPI](https://pypi.org/project/qf-solver/) and +[GitHub Releases](https://github.com/emptiesvoid-cloud/QF_solver/releases). +Only the exact selected wheel/sdist bytes bound to a successful release audit +are audited distribution artifacts; this page is not an upload confirmation. +See [What's New in 0.2.11](../whats-new/0.2.11.md) for the release scope and [the capability index](../capabilities/index.md) for maturity boundaries. ## User installation -After the tagged release workflow completes the PyPI upload, install the exact -version from the package index: +Install the exact version from the package index after its release upload: ```bash -python -m pip install "qf-solver==0.2.10" +python -m pip install "qf-solver==0.2.11" qf-solver --version ``` @@ -32,13 +32,14 @@ release selector. Select the immutable release tag explicitly when reproducibility matters: ```bash -git clone --branch v0.2.10 --single-branch https://github.com/emptiesvoid-cloud/QF_solver.git +git clone --branch v0.2.11 --single-branch https://github.com/emptiesvoid-cloud/QF_solver.git cd QF_solver python -m pip install . qf-solver --version ``` -The default branch is not a promise that it contains a specific release. +The tag command applies once that release tag exists. The default branch is +not a promise that it contains a specific release. The published core package requires Python 3.10 or newer. The recommended public import is: @@ -99,7 +100,7 @@ commands. ## Distribution traceability data -The published 0.2.10 wheel and source distribution include the selected +The distribution selection retains the lightweight traceability set: the consolidated 46-record element-analysis registry and the HEX8-SRI, mixed-dynamics, mixed-MPC, mixed-multimaterial, `.inp`, mixed-HDF5 and contact owner/delivery records. Historical 0.2.7 capability metadata is @@ -108,5 +109,6 @@ retained separately. Raw NPZ/HDF5 arrays, full campaign output, caches, debug artifacts and temporary files are intentionally excluded from package data. They remain repository evidence and are not required for importing or using the base -package. The selected wheel and sdist are the audited distribution artifacts; -this does not clear the complete repository archive. +package. Exact source selection, contents and artifact hashes are controlled +by the matching release contract, not inferred from a default-branch build. +Auditing a selected wheel/sdist does not clear the complete repository archive. diff --git a/docs/getting-started/nonlinear-example.md b/docs/getting-started/nonlinear-example.md index 4989af60..b3038237 100644 --- a/docs/getting-started/nonlinear-example.md +++ b/docs/getting-started/nonlinear-example.md @@ -2,7 +2,7 @@ title: Nonlinear example doc_id: DOC-START-NONLINEAR-001 revision: 1.0 -applicable_version: 0.2.10 +applicable_version: 0.2.11 status: controlled_candidate reviewer: "" approver: "" diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 2c2428b9..abeba8d3 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -2,7 +2,7 @@ doc_id: DOC-START-PUB-002 revision: 1.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -28,7 +28,9 @@ using a result. from qf_solver import check_mesh, load_model, save_result, solve_model model = load_model("examples/tet4_static.json") -check_mesh(model) +report = check_mesh(model) +if report.status == "FAIL": + raise RuntimeError(report.errors) result = solve_model(model) save_result(result, "results/tet4.json") ``` @@ -42,8 +44,8 @@ surface. Confirm that the element, analysis, material, loading, boundary conditions and solver backend fall within a matching evidence scope. The linked 0.2.8 consolidated registry remains the authority for its 46 published -element-analysis records; the [0.2.10 capability index](../capabilities/index.md) -adds source-cycle evidence without rewriting those historical decisions. For +element-analysis records; the [current capability index](../capabilities/index.md) +adds separate later evidence without rewriting those historical decisions. For mixed workflows or separate capabilities, read the controlling record. Review the [known limitations](../etat/limites.md) and retain the input, configuration and result files with the calculation record. diff --git a/docs/getting-started/when-to-use-qf-solver.md b/docs/getting-started/when-to-use-qf-solver.md index 589471db..61c6ee98 100644 --- a/docs/getting-started/when-to-use-qf-solver.md +++ b/docs/getting-started/when-to-use-qf-solver.md @@ -2,7 +2,7 @@ doc_id: DOC-SOLVER-GUIDE-001 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -11,9 +11,9 @@ approver: "" QF Solver is an open-source Python finite-element solver for structural mechanics and dynamics. It emphasizes inspectable formulations, explicit -solver diagnostics and traceable verification evidence. The 0.2.10 release is -the first public release after 0.2.8. Its pages describe bounded evidence from -the development cycle, not a blanket maturity upgrade. +solver diagnostics and traceable verification evidence. This 0.2.11 guide +describes the cumulative solver, with route-specific evidence rather than +a blanket maturity claim. QF Solver can suit controlled analysis, solver development, computational mechanics research and reproducible FEM studies when the exact route appears @@ -35,10 +35,13 @@ industrial systems. | Arc-length / postbuckling | Bounded experimental continuation evidence | No formal limit-point, bifurcation or general structural postbuckling claim. | | PETSc/MPI | Route-specific | Bounded recorded linear-static executions only; no general distributed nonlinear claim. | | General mixed distributed PETSc/MPI | `NOT_VALIDATED` | Historical runtime physical-balance and partition gates failed. | +| Gyroscopic modal analysis | `EXPERIMENTAL` | Straight circular BEAM2 shafts with centered rigid axisymmetric disks; dense serial QEP only. | +| Campbell diagrams | `EXPERIMENTAL` | Explicit speed sweeps and complex-mode tracking within that disk model; ambiguity is retained, including the high-frequency pair at 100 rad/s. | The internal roadmap score is not a public capability score. Read the -[0.2.10 V&V summary](../verification/0_2_10/README.md) for the evidence -classes and provenance behind these labels. +[0.2.11 V&V summary](../verification/0_2_11/README.md) and the inherited +[0.2.10 evidence](../verification/0_2_10/README.md) for the records behind +these labels. ## Good-fit workflows @@ -51,6 +54,8 @@ QF Solver is a reasonable candidate when you need: - a controlled experiment with the nonlinear driver, state transaction or continuation policies; - numerical diagnostics and reproducible input/result artifacts; +- exploratory disk-gyroscopic modes or Campbell diagrams within the + [rotating-modal scope](../mechanics/rotating-modal.md); - a solver whose implementation, verification and maturity decisions are kept distinct. @@ -117,6 +122,8 @@ is: - robust frictional updated-search or finite-sliding contact, self-contact, impact or contact dynamics; - general nonlinear transient dynamics; +- general rotordynamics, distributed shaft gyroscopy, unbalance response, + rotor/stator contact or operational critical-speed validation; - general nonlinear MPI/PETSc, universal HPC scaling or GPU acceleration; - unrestricted mixed-element behavior, certified workflows or universal physical validation. diff --git a/docs/index.md b/docs/index.md index b9b36693..d69b3b67 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,36 +2,32 @@ doc_id: DOC-STATE-001 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- -# QF Solver 0.2.10 +# QF Solver -**Current release:** [`0.2.10`](https://github.com/emptiesvoid-cloud/QF_solver/releases/tag/v0.2.10)
-**Next candidate:** `0.2.11` — not yet published
-**Selected PyPI distribution:** `qf-solver==0.2.10`
-**Version DOI:** [`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744)
+**Current release documentation:** `0.2.11`
**Project concept DOI:** [`10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) QF Solver is an inspectable Python finite-element solver for structural -mechanics and dynamics. Version 0.2.10 is the first public release after -0.2.8 and documents the nonlinear-mechanics development from the intervening -source cycle. The 0.2.9 source snapshot was not a public package release. +mechanics and dynamics, intended for engineering-method development, +research and reproducible FEM studies. It combines solid, beam and shell +formulations, linear and dynamic analyses, selected nonlinear routes, and +experimental disk-gyroscopic modal analysis and Campbell diagrams. Claims remain bounded by the exact element, formulation, material, loading, -mesh and solver route in their controlling record. The selected wheel and -sdist were audited; the full-repository G03 archive gate remains failed. +mesh and solver route in their controlling record. Distribution audits are +bound to exact source and artifact bytes; the full-repository G03 archive gate remains failed. GitHub-generated source archives are not cleared distribution artifacts. -See the [0.2.10 V&V summary](verification/0_2_10/README.md). -The 0.2.11 candidate adds an experimental, bounded gyroscopic modal route and -Campbell tracking; it is not yet the current release. See its -[candidate V&V summary](verification/0_2_11/README.md). +See the [0.2.11 V&V summary](verification/0_2_11/README.md) and the separate +[0.2.10 evidence](verification/0_2_10/README.md). ## Start here -1. [Install the latest published package](getting-started/installation.md). +1. [Install QF Solver](getting-started/installation.md). 2. [Check whether QF Solver fits your problem](getting-started/when-to-use-qf-solver.md). 3. Run the [linear first calculation](getting-started/quickstart.md). 4. Try the separate [bounded nonlinear example](getting-started/nonlinear-example.md). @@ -39,49 +35,56 @@ Campbell tracking; it is not yet the current release. See its and [analysis routes](analyses/index.md). 6. Read [known limitations](etat/limites.md) before relying on a result. -## What changed since 0.2.8 +## Explore the solver -The main documented development is a more common nonlinear execution -architecture: selected routes share a Newton engine, assembly protocol, -accepted-state transactions and robustness diagnostics. Evidence also covers -bounded Total-Lagrangian StVK checks, bounded J2/contact routes, selected -multi-family comparisons and a same-mesh Code_Aster correlation campaign. -Those are different evidence classes and do not imply general nonlinear -qualification, physical validation or unrestricted PETSc/MPI support. +- [Elements](elements/index.md) and [analyses](analyses/index.md): solid, + beam, shell, static, modal and linear dynamic workflows. +- [Composites](composites/index.md): orthotropy, laminates, ply-level output + and experimental first-ply indicators. +- [Nonlinear mechanics](mechanics/nonlinear-overview.md): selected J2, + geometric/contact routes, Newton iteration and transactional state. +- [Rotating modal](mechanics/rotating-modal.md) and [Campbell](mechanics/campbell.md): + experimental serial BEAM2/disk scope with complex modes and explicit tracking. +- [Solvers and backends](solveurs/index.md), [model audits](reference/audit_detail_modes.md) + and [public API](reference/qf_solver_api.md): calculation and inspection tools. -Read [What's New in 0.2.10](whats-new/0.2.10.md) for the published release -and [What's New in 0.2.11](whats-new/0.2.11.md) for the unreleased candidate. -The 0.2.10 page gives the concise comparison with published 0.2.8. The -[nonlinear mechanics overview](mechanics/nonlinear-overview.md) explains its -implementation, scope and boundaries. +Read [What's New in 0.2.11](whats-new/0.2.11.md) for changes in this version. +The historical [0.2.10 release notes](whats-new/0.2.10.md) describe the first +public release after 0.2.8; the intervening 0.2.9 tag is a source snapshot, +not a public package release. ## Verification and maturity The [capability index](capabilities/index.md) provides route-specific status and evidence links. The [V&V and maturity model](verification/evidence-and-maturity.md) explains the distinction between testing, verification, external correlation, -qualification and physical validation. The [0.2.10 verification summary](verification/0_2_10/README.md) -states the evidence provenance and open release limitations. The +qualification and physical validation. The [0.2.11 verification summary](verification/0_2_11/README.md) +states the current evidence provenance and limitations. The separate +[0.2.10 summary](verification/0_2_10/README.md), [0.2.8 verification summary](verification/0_2_8/README.md) and [0.2.7 summary](verification/0_2_7/README.md) remain historical evidence. No certification, universal physical validation, industrial equivalence or general-purpose nonlinear-solver claim is made. The internal 95/100 roadmap -score is not a public quality rating. WP14 remains `HOLD`; passing a bounded +score is not a public quality rating. WP14 remains `HOLD_NOT_PROMOTED`; passing a bounded package scan or CI job does not clear the complete repository archive. ## Release and citation -The selected wheel and sdist are the audited Python distribution artifacts. +Only wheel/sdist bytes bound to the matching selected-release contract can +be represented as audited Python distribution artifacts. The full-repository G03 archive gate remains failed; automatic GitHub source archives and a complete repository archive are not represented as cleared. -For reproducibility, cite the exact release DOI -[`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744). Use +For reproducibility, cite a published exact-version DOI when one is assigned. +The DOI [`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744) +identifies the preceding 0.2.10 release, not 0.2.11. Use [concept DOI `10.5281/zenodo.22697897`](https://doi.org/10.5281/zenodo.22697897) to cite the project across versions. The machine-readable [`CITATION.cff`](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/CITATION.cff) -records the 0.2.10 release citation. +identifies version 0.2.11 without asserting a publication date or version DOI. +Release availability is authoritative on [PyPI](https://pypi.org/project/qf-solver/) +and [GitHub Releases](https://github.com/emptiesvoid-cloud/QF_solver/releases). See [Release history](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/CHANGELOG.md), [API stability](reference/api_stability.md) and the [public roadmap](reference/feuille_de_route.md) for more context. diff --git a/docs/mechanics/campbell.md b/docs/mechanics/campbell.md index e6986b31..22e134e5 100644 --- a/docs/mechanics/campbell.md +++ b/docs/mechanics/campbell.md @@ -10,8 +10,8 @@ approver: "" # Experimental Campbell diagrams -This is an unreleased 0.2.11 candidate capability. The latest published -release remains 0.2.10, and Campbell maturity remains `EXPERIMENTAL`. +This capability is part of QF Solver 0.2.11. Campbell maturity remains +`EXPERIMENTAL`, independently of package publication. The `campbell` route orchestrates a set of single-speed `rotating_modal` analyses and associates their complex modes. It is experimental and limited to diff --git a/docs/mechanics/nonlinear-overview.md b/docs/mechanics/nonlinear-overview.md index 56463ba8..30fb99fb 100644 --- a/docs/mechanics/nonlinear-overview.md +++ b/docs/mechanics/nonlinear-overview.md @@ -2,7 +2,7 @@ title: Nonlinear mechanics overview doc_id: DOC-MECH-NONLINEAR-001 revision: 1.0 -applicable_version: 0.2.10 +applicable_version: 0.2.11 status: controlled_candidate reviewer: "" approver: "" @@ -10,7 +10,7 @@ approver: "" # Nonlinear mechanics overview -This page explains the current candidate architecture. It is a mechanics +This page explains the nonlinear architecture retained in QF Solver 0.2.11. It is a mechanics guide, not a verification report or a qualification decision. The [0.2.10 V&V summary](../verification/0_2_10/README.md) explains what evidence exists and what it does not establish. diff --git a/docs/mechanics/rotating-modal.md b/docs/mechanics/rotating-modal.md index 13d7305c..32ad8e0e 100644 --- a/docs/mechanics/rotating-modal.md +++ b/docs/mechanics/rotating-modal.md @@ -10,9 +10,9 @@ approver: "" # Experimental rotating modal analysis -This route belongs to the unreleased 0.2.11 candidate; the latest published -package remains 0.2.10. Its experimental maturity and bounded scope do not -change the published release contract. +This experimental route is part of QF Solver 0.2.11. Its bounded scope and +maturity are distinct from package publication and do not change the +immutable 0.2.10 release contract. This page documents the bounded `rotating_modal` capability introduced for QF Solver 0.2.11. It describes the numerical route; it does not assign a diff --git a/docs/reference/api_stability.md b/docs/reference/api_stability.md index 3153b1da..00de8bc4 100644 --- a/docs/reference/api_stability.md +++ b/docs/reference/api_stability.md @@ -2,14 +2,14 @@ doc_id: DOC-REF-API-002 revision: 1.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # API stability -This page describes the public API surface of the 0.2.10 release. +This page describes the public API surface of QF Solver 0.2.11. ## Public namespace @@ -33,7 +33,7 @@ The `solveur` namespace remains a compatibility facade for existing 0.2.x applications. The recommended CLI is `qf-solver`; legacy entry points remain available during the 0.2.x compatibility period. The current compatibility plan schedules removal of the legacy `solveur-ef` launcher for 0.3.0; this is -a plan, not a 0.2.10 release event. +a plan, not a 0.2.11 removal. ## Compatibility boundary @@ -46,11 +46,11 @@ and separate mixed-workflow/capability records define those boundaries. `qf_solver.read_inp(path)` remains `PROVISIONAL`; it covers only the documented bounded Abaqus/CalculiX subset and is not a full-format compatibility claim. -The 0.2.11 release candidate contains `rotating_modal` and `campbell` JSON +Version 0.2.11 contains `rotating_modal` and `campbell` JSON routes with `EXPERIMENTAL` maturity. They are not part of the published 0.2.10 API contract. Their complex result schemas are provisional -and must not be treated as stable for downstream integrations before a -separate release contract says otherwise. See the [rotating-modal +and must not be treated as stable for downstream integrations. Package +publication does not promote numerical maturity or schema stability. See the [rotating-modal scope](../mechanics/rotating-modal.md) and [Campbell scope](../mechanics/campbell.md). The family-aware mixed HDF5 functions are also `PROVISIONAL` and diff --git a/docs/reference/feuille_de_route.md b/docs/reference/feuille_de_route.md index 6927b0ca..4ecdfff9 100644 --- a/docs/reference/feuille_de_route.md +++ b/docs/reference/feuille_de_route.md @@ -9,7 +9,7 @@ approver: "" # Public roadmap -QF Solver 0.2.10 is the current published release. Version 0.2.9 was a +QF Solver 0.2.11 is the current documentation baseline. Version 0.2.9 was a development/source snapshot, not a PyPI package, GitHub Release, or version DOI. This roadmap is a product-level orientation; it is not a release gate, tag, publication decision, or promise that an unqualified route is ready for @@ -17,12 +17,12 @@ production. ## Current direction -The 0.2.11 candidate is in release preparation. WP01–WP06 add prospective V&V -consolidation and experimental gyroscopic modal/Campbell routes within a -bounded BEAM2-and-disk scope; neither route is published or promoted beyond -`EXPERIMENTAL`. The [candidate verification summary](../verification/0_2_11/README.md) -records its evidence and limitations. The latest published release remains -0.2.10. +Version 0.2.11 includes prospective V&V consolidation and experimental +gyroscopic modal/Campbell routes within a bounded BEAM2-and-disk scope. +Neither route is promoted beyond `EXPERIMENTAL`. The +[verification summary](../verification/0_2_11/README.md) records its evidence +and limitations. Package publication is governed by separate exact-source +release gates and is not inferred from this roadmap. Follow-up work will extend evidence and reproducibility without generalizing the existing bounded nonlinear routes or changing the linear @@ -31,7 +31,9 @@ Newton and residual/tangent infrastructure, transactional state management, bounded geometric/J2/contact routes, and continuation diagnostics. The [capability index](../capabilities/index.md) and [0.2.10 V&V summary](../verification/0_2_10/README.md) state what the -evidence accepts—and what it excludes. +evidence accepts and what it excludes. The +[0.2.11 summary](../verification/0_2_11/README.md) adds the experimental +rotating evidence without rewriting those earlier decisions. ## Follow-up themes diff --git a/docs/reference/open_source.md b/docs/reference/open_source.md index a9f9f8eb..58daac07 100644 --- a/docs/reference/open_source.md +++ b/docs/reference/open_source.md @@ -2,7 +2,7 @@ doc_id: DOC-REF-OSS-001 revision: 1.1 status: controlled -applicable_version: 0.2.8 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -26,10 +26,14 @@ terms described in CONTRIBUTING.md. ## Release boundary -The published 0.2.8 release scope is explicitly bounded. Experimental, +The cumulative 0.2.11 solver retains explicitly bounded scopes. Experimental, research-only, not-validated and non-comparable routes remain labeled as such in the capabilities and known-limitations pages. Historical audit records keep the version and status that were true when they were produced. This page does not imply certification or automatic publication. + +Selected distribution audits apply only to their recorded source and exact +artifact bytes. They do not clear full-repository archives: historical G03 +remains `FAIL_PRESERVED`, and WP14 remains `HOLD_NOT_PROMOTED`. diff --git a/docs/reference/qf_solver_api.md b/docs/reference/qf_solver_api.md index c6c7eb4d..4a5d5b8e 100644 --- a/docs/reference/qf_solver_api.md +++ b/docs/reference/qf_solver_api.md @@ -2,14 +2,14 @@ doc_id: DOC-REF-API-003 revision: 1.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # `qf_solver` public API contract -This page documents the public `qf_solver` API for release 0.2.10. +This page documents the public `qf_solver` API for release 0.2.11. This page is the concise contract for the Python surface exported by `qf_solver`. New integrations should use this namespace. Implementation @@ -126,7 +126,7 @@ return annotation is the documented return type where one is available. - `QualificationGateError(message: str, *, result: object | None = None, summary: dict[str, Any] | None = None) -> None` — verification-profile rejection; `STABLE`. - `RunVerdict(*values)` — stable run-verdict enum; `STABLE`. - `ConstraintTerm(node: int, dof: str, coefficient: float) -> None` — one linear-constraint term; `STABLE`. -- `FiniteElementModel(nodes: np.ndarray, elements: list[ElementDefinition], materials: dict[str, dict[str, Any]], fixed_dofs: list[BoundaryCondition] = , loads: list[NodalLoad] = , distributed_loads: list[DistributedLoad] = , springs: list[SpringDefinition] = , concentrated_masses: list[ConcentratedMass | RotatingDisk] = , multipoint_constraints: list[LinearConstraint] = , rbe2: list[Rbe2Definition] = , rbe3: list[Rbe3Definition] = , contacts: list[FrictionlessContact] = , analysis: AnalysisSettings = , schema_version: int = 1, units: dict[str, str] = , verification_profile: str = 'engineering') -> None` — in-memory model; the `RotatingDisk` alternative is candidate-only and experimental; `STABLE` container API. +- `FiniteElementModel(nodes: np.ndarray, elements: list[ElementDefinition], materials: dict[str, dict[str, Any]], fixed_dofs: list[BoundaryCondition] = , loads: list[NodalLoad] = , distributed_loads: list[DistributedLoad] = , springs: list[SpringDefinition] = , concentrated_masses: list[ConcentratedMass | RotatingDisk] = , multipoint_constraints: list[LinearConstraint] = , rbe2: list[Rbe2Definition] = , rbe3: list[Rbe3Definition] = , contacts: list[FrictionlessContact] = , analysis: AnalysisSettings = , schema_version: int = 1, units: dict[str, str] = , verification_profile: str = 'engineering') -> None` — in-memory model; the `RotatingDisk` alternative is restricted to the experimental rotating scope; `STABLE` container API, not a promotion of disk gyroscopy. - `LinearConstraint(terms: tuple[ConstraintTerm, ...], value: float = 0.0, name: str = '') -> None` — linear multi-point constraint; `STABLE`. - `Rbe2Definition(master: int, slaves: tuple[int, ...], tie_rotations: bool = False, name: str = '') -> None` — rigid-body RBE2 definition; `STABLE`. - `Rbe3Definition(reference: int, independents: tuple[tuple[int, float], ...], dofs: tuple[str, ...] = ('UX', 'UY', 'UZ', 'RX', 'RY', 'RZ'), mode: str = 'rigid_body_projection', name: str = '') -> None` — weighted RBE3 definition; `STABLE`. @@ -147,20 +147,20 @@ return annotation is the documented return type where one is available. - `solve_model(model: FiniteElementModel, *, enforce_policy: bool = True) -> object` — solve through the public router; `STABLE`. - `verify_evidence(path: str | Path) -> EvidenceVerificationReport` — verify evidence fingerprints; `STABLE`. -### 0.2.11 release candidate: `rotating_modal` +### Experimental `rotating_modal` -The 0.2.11 candidate adds an unreleased JSON analysis route consumed through the existing +Version 0.2.11 adds a JSON analysis route consumed through the existing `load_model`/`solve_model` API. It returns a distinct complex `RotatingModalResult`, but neither that result schema nor the numerical -capability is declared stable or included in the published 0.2.10 contract. +capability is declared stable or included in the immutable 0.2.10 contract. Its maturity remains `EXPERIMENTAL`; see the [bounded rotating-modal scope](../mechanics/rotating-modal.md). Do not infer Campbell diagrams or general rotordynamics support from the single-speed route; Campbell orchestration -is a separate experimental candidate documented below. +is a separate experimental route documented below. -### 0.2.11 release candidate: `campbell` +### Experimental `campbell` -The candidate adds an experimental JSON `campbell` route, +Version 0.2.11 adds an experimental JSON `campbell` route, consumed through the same `load_model`/`solve_model` workflow. The caller must provide finite, strictly increasing `spin_speeds_rad_s`, an explicit global axis/frame, a mode count, and the frozen WP05 QEP/WP06 tracking policy IDs. @@ -168,7 +168,7 @@ It returns a provisional `CampbellResult` with branch lineage and explicit ambiguity/gaps. Its plot helper is a projection only and cannot assign tracks. This route is not part of the published 0.2.10 contract, is not a general rotordynamics capability, and does not predict forced-response amplitudes or -operational critical speeds. See the [candidate Campbell scope](../mechanics/campbell.md). +operational critical speeds. See the [Campbell scope](../mechanics/campbell.md). ### Provisional symbols @@ -238,6 +238,6 @@ For the overview and compatibility boundary, see the [0.2.8 consolidated registry](https://github.com/emptiesvoid-cloud/QF_solver/blob/v0.2.8/qualification/0_2_8/consolidated_registry.json) and linked mixed-workflow/capability records. -The 0.2.10 cross-registry public orientation is maintained by the -[0.2.10 capability index](../capabilities/index.md); the linked 0.2.8 +The current cross-registry public orientation is maintained by the +[capability index](../capabilities/index.md); the linked 0.2.8 registry remains the authority for its original published decisions. diff --git a/docs/reference/registre_documentaire.md b/docs/reference/registre_documentaire.md index 8456d58b..2784bfcb 100644 --- a/docs/reference/registre_documentaire.md +++ b/docs/reference/registre_documentaire.md @@ -2,7 +2,7 @@ doc_id: DOC-REF-002 revision: 1.0 status: controlled -applicable_version: 0.2.8 +applicable_version: 0.2.11 reviewer: "" approver: "" --- diff --git a/docs/solveurs/index.md b/docs/solveurs/index.md index 9dc38967..155ae542 100644 --- a/docs/solveurs/index.md +++ b/docs/solveurs/index.md @@ -2,7 +2,7 @@ doc_id: DOC-SOL-000 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- @@ -12,12 +12,14 @@ approver: "" The route, element, formulation and evidence boundary determine which solver path is supported. Method names are not convergence guarantees; inspect the residual, conditioning, warnings and final diagnostics for every calculation. -This page describes the bounded solver and backend scope of release 0.2.10. +This page describes the bounded solver and backend scope of release 0.2.11. | Analysis / backend | Status | Scope | | --- | --- | --- | | Linear static | `QUALIFIED_BOUNDED` | Direct and iterative sparse routes only within the recorded element-analysis matrix. | | Modal | Route-dependent | Sparse eigenvalue routes for recorded bounded cases; mass and family scope vary. | +| Rotating modal | `EXPERIMENTAL` | Serial dense generalized QEP with `scipy.linalg.eig(A, B)`; explicit complex spectrum, mass normalization and original-polynomial residuals. Not interchangeable with classical `eigh`/`eigsh`. | +| Campbell | `EXPERIMENTAL` | Multiple bounded rotating solves plus complex-MAC global assignment and subspace-aware tracking. Unresolved matches remain ambiguous, including the high-frequency pair at 100 rad/s. | | Newmark / harmonic | Route-dependent; selected mixed routes `EXPERIMENTAL_BOUNDED` | Linear analysis and documented timestep/frequency/damping limits only. | | Linear buckling | Route-dependent | Bounded first-factor cases; no postbuckling or bifurcation claim. | | Nonlinear Newton | Bounded route evidence | Shared Newton/assembly/state infrastructure in selected material/geometric routes; not a general nonlinear solver claim. | @@ -37,6 +39,13 @@ TET4 large-model observations apply only to their exact configurations and environment. The standard installation uses SciPy and does not require these external runtimes. +The rotating QEP does not use these optional backends. Its initial method is +`dense_qep`; Campbell uses `complex_mac_hungarian` for tracking, not a new +physical solver. The dense characterization reached 1,000 physical DOFs on +synthetic matrix pencils in the recorded environment, without extrapolation +or a universal capacity promise. See [rotating modal](../mechanics/rotating-modal.md) +and [Campbell](../mechanics/campbell.md) for the full scope. + ## Public API New applications should import from `qf_solver` and use the `qf-solver` CLI. diff --git a/docs/verification/0_2_11/README.md b/docs/verification/0_2_11/README.md index aa3e7f34..dbf4422c 100644 --- a/docs/verification/0_2_11/README.md +++ b/docs/verification/0_2_11/README.md @@ -1,6 +1,6 @@ --- doc_id: DOC-VV-PUBLIC-0211-001 -title: QF Solver 0.2.11 candidate verification summary +title: QF Solver 0.2.11 verification summary revision: 1.0 status: controlled_candidate applicable_version: 0.2.11 @@ -8,11 +8,12 @@ reviewer: "" approver: "" --- -# QF Solver 0.2.11 candidate verification summary +# QF Solver 0.2.11 verification summary -**Status:** prepublication candidate evidence. QF Solver 0.2.10 remains the -latest published version. This summary reports the WP01–WP06 evidence without -turning a numerical result into an automatic maturity decision. +This summary reports the recorded WP01–WP06 evidence for QF Solver 0.2.11. +It does not turn a numerical result into an automatic maturity decision or +confirm a package upload. The original source identities, measurements and +decisions remain in their structured records. ## Scope and provenance @@ -33,7 +34,7 @@ and [contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualific | GYRO-03 | Independent disk-oscillator analytical oracle over signed speeds | PASS; oracle does not use the production gyroscopic assembly. | | GYRO-04 | Zero-speed degeneracy and signed-speed spectral splitting | PASS within the frozen WP05 scope and tolerances. | | GYRO-05 | Analytical Campbell branches, phase/order invariance, degeneracy lineage and a controlled ambiguous case | PASS; ambiguity is reported rather than connected by force. | -| GYRO-06 | 4/8/16/32-element BEAM2 shaft-and-disk mesh sequence at 0, 100 and 250 rad/s | PASS internal convergence evidence only; the high-frequency pair remains ambiguous at 100 rad/s. | +| GYRO-06 | 4/8/16/32-element BEAM2 shaft-and-disk mesh sequence at 0, 100 and 250 rad/s | PASS internal mesh-convergence evidence only; the high-frequency pair remains ambiguous at 100 rad/s. | The detailed machine-readable records are [GYRO-05](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/gyro_05_campbell_analytical.json) and [GYRO-06](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/gyro_06_beam2_convergence.json). @@ -45,8 +46,8 @@ For GYRO-06, the maximum relative frequency delta between the two finest meshes (16 and 32 elements) was `0.00011432899`, against the predeclared `0.005` limit. The largest recorded QEP relative residual was `1.61735001e-9`, against `1e-8`. These comparisons establish internal -discretization-convergence evidence for the documented model; they are not an -independent physical validation or a general rotordynamics qualification. +mesh-convergence evidence for the documented model, not independent physical +validation or a general rotordynamics qualification. ## Maturity and visible limitations @@ -70,9 +71,11 @@ outside scope. WP01–WP06 provide their recorded baseline, architecture, V&V, historical retest, gyro and Campbell evidence. The final QF0211-G07/G08 candidate gates and exact-source release audit are recorded separately during WP07. The -published 0.2.10 tag and artifacts are not modified by this candidate. +published 0.2.10 tag and artifacts are not modified by this version or by +edits to the current public documentation. Each release audit applies only +to its recorded source and artifact bytes. Historical 0.2.10 G03 remains `FAIL_PRESERVED`; the complete repository archive is not cleared. Historical WP14 remains `HOLD_NOT_PROMOTED`. These statuses are distinct from the QF0211 work-package gates and are not changed -by the candidate's passing results. +by the newer passing results. diff --git a/docs/verification/evidence-and-maturity.md b/docs/verification/evidence-and-maturity.md index 5dec3b67..53d4df32 100644 --- a/docs/verification/evidence-and-maturity.md +++ b/docs/verification/evidence-and-maturity.md @@ -2,17 +2,17 @@ doc_id: DOC-VV-MATURITY-028-001 revision: 2.0 status: controlled_candidate -applicable_version: 0.2.10 +applicable_version: 0.2.11 reviewer: "" approver: "" --- # Verification, validation, evidence and maturity -**Current release:** QF Solver `0.2.10` / `v0.2.10`
-**Version DOI:** [`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744) +**Current release documentation:** QF Solver `0.2.11`
+**Previous version DOI (0.2.10 only):** [`10.5281/zenodo.23106744`](https://doi.org/10.5281/zenodo.23106744) -The 0.2.10 release accumulates development made since the published 0.2.8 +The cumulative solver retains the 0.2.10 development made since the published 0.2.8 baseline, including the development-only 0.2.9 cycle. The [0.2.10 V&V summary](0_2_10/README.md) describes the bounded records; the [capability index](../capabilities/index.md) is the public navigation to @@ -21,11 +21,15 @@ current scopes. The 46-combination is historical authority for those published element-analysis decisions and is not silently rewritten here. -The 0.2.11 release candidate adds experimental gyroscopic modal and Campbell +QF Solver 0.2.11 adds experimental gyroscopic modal and Campbell evidence. Those results, their availability and their maturity decisions are -summarized separately in the [0.2.11 candidate V&V page](0_2_11/README.md); +summarized separately in the [0.2.11 V&V page](0_2_11/README.md); they are not part of the published 0.2.10 record. +Publication availability is established by PyPI and GitHub Releases, not by +this page. The 0.2.11 citation metadata does not assert a publication date or +an assigned version DOI. + ## Four distinct claims 1. **Implementation:** a code path exists. @@ -62,10 +66,10 @@ preserved; they are not rewritten to match a later desired outcome. The 0.2.8 registry counts remain 32 `QUALIFIED_BOUNDED`, 14 `EXPERIMENTAL`, and 0 `NOT_QUALIFIED` across 46 specified combinations. These counts are not a -0.2.10 quality score and exclude separate mixed workflows and capability +current-version quality score and exclude separate mixed workflows and capability records. -## 0.2.10 scope boundaries +## Inherited structural scope boundaries - Small-strain J2 and the 0.2.8 linear registry retain their original bounded scopes. @@ -92,3 +96,12 @@ The detailed development-cycle records remain in [`qualification/0_2_9`](https://github.com/emptiesvoid-cloud/QF_solver/tree/main/qualification/0_2_9). They are engineering evidence, not user-level replacements for the concise scope statements above. + +## Experimental rotating scope + +Gyroscopic modal analysis is limited to linear straight circular BEAM2 shafts +with centered rigid axisymmetric disks and a dense serial QEP. Campbell +diagrams orchestrate this route and use explicit complex-mode tracking. +The high-frequency pair at 100 rad/s remains ambiguous; the tracker does not +force continuity. GYRO-06 supplies internal mesh-convergence evidence, not +independent physical validation. Both capabilities remain `EXPERIMENTAL`. diff --git a/docs/whats-new/0.2.11.md b/docs/whats-new/0.2.11.md index a4d36037..162c5f94 100644 --- a/docs/whats-new/0.2.11.md +++ b/docs/whats-new/0.2.11.md @@ -10,17 +10,18 @@ approver: "" # What's New in QF Solver 0.2.11 -**Release candidate — not published.** The latest published release remains -QF Solver 0.2.10. This page describes the proposed 0.2.11 delta and its -evidence; it is not a publication notice. +This page summarizes changes from QF Solver 0.2.10 to 0.2.11. For the +cumulative solver capabilities, use the [capability index](../capabilities/index.md). +Publication availability is authoritative on PyPI and GitHub Releases, not +inferred from this documentation page. ## Overview -The 0.2.11 candidate builds on the existing structural mechanics, dynamics, +Version 0.2.11 builds on the existing structural mechanics, dynamics, nonlinear routes and reproducible V&V system. Its main new capability is a first experimental gyroscopic modal route for a tightly bounded shaft-and-disk model, followed by an experimental Campbell speed sweep and complex modal -tracking. The candidate does not broaden the maturity of the pre-existing +tracking. This release does not broaden the maturity of the pre-existing 0.2.10 capabilities. ## Gyroscopic modal analysis @@ -36,7 +37,7 @@ in the result. This is not a general rotating-machinery solver. Distributed shaft gyros, variable speed, speed-dependent stiffness or bearings, centrifugal stiffening, forced unbalance response, contact, nonlinear rotors, and PETSc/SLEPc/MPI are -outside the candidate scope. +outside this scope. ## Campbell sweep and tracking @@ -58,7 +59,7 @@ response, operational danger, or instability. ## Verification evidence -The candidate carries six bounded gyro checks. GYRO-01 checks zero-speed +The verification evidence contains six bounded gyro checks. GYRO-01 checks zero-speed recovery against the classic modal route; GYRO-02 checks the local disk mass, gyroscopic matrix, frame covariance, signed-speed convention and ownership; GYRO-03 uses an independent analytical disk-oscillator oracle; and GYRO-04 @@ -77,15 +78,17 @@ See the [0.2.11 V&V summary](../verification/0_2_11/README.md) and the [frozen Campbell contract](https://github.com/emptiesvoid-cloud/QF_solver/blob/main/qualification/0_2_11/wp06_campbell_contract.json) for provenance, thresholds, evidence availability and the full boundaries. -## Compatibility and release status +## Compatibility and citation -The candidate keeps the existing `qf_solver` namespace and 0.2.x compatibility +Version 0.2.11 keeps the existing `qf_solver` namespace and 0.2.x compatibility surface. The new routes and their complex result contracts are experimental -and provisional, not stable downstream API commitments. Until 0.2.11 is -actually published, install the latest published package (`qf-solver==0.2.10`) -and cite its version DOI in `CITATION.cff`; the project concept DOI remains -available for version-independent citation. No 0.2.11 version DOI is claimed -here. +and provisional, not stable downstream API commitments. See the +[installation guide](../getting-started/installation.md) for exact-version +installation and availability checks. Use the concept DOI +`10.5281/zenodo.22697897` for the evolving project; use a published version DOI +for exact-release citation when one is assigned. The preceding version DOI +`10.5281/zenodo.23106744` identifies 0.2.10 only. No publication date or +0.2.11 version DOI is asserted here. Historical G03 remains `FAIL_PRESERVED`, the whole-repository archive is not cleared, and WP14 remains `HOLD_NOT_PROMOTED`. diff --git a/scripts/docs_publication.py b/scripts/docs_publication.py index 9e6042e5..f2accec8 100644 --- a/scripts/docs_publication.py +++ b/scripts/docs_publication.py @@ -335,12 +335,12 @@ def _status(self, campaign: dict[str, Any]) -> int: panels = f"""

Project version

{version}release status and citation identifiers are maintained in README and CITATION.cff
-

Evidence status

BOUNDED0.2.8 registry remains historical authority; 0.2.10 routes are separately scoped; WP14 HOLD and G03 FAIL
+

Evidence status

ROUTE-SPECIFIC0.2.8 registry remains historical authority; inherited nonlinear evidence and 0.2.11 experimental rotating routes are separately scoped; WP14 HOLD_NOT_PROMOTED and G03 FAIL_PRESERVED

Distribution scope

SELECTEDwheel and sdist only; whole-repository source archives are not cleared
The 0.2.8 published registry remains bounded at its declared historical -boundaries. The full-suite test inventory is recorded by the final Gate-E evidence; this generated overview deliberately does not hard-code a collection count. The 0.2.10 evidence and limitations are summarized separately; this status panel does not assert that the complete repository archive passed G03. +boundaries. The full-suite test inventory is recorded by the final Gate-E evidence; this generated overview deliberately does not hard-code a collection count. Inherited 0.2.10 evidence and experimental 0.2.11 evidence have separate decisions; this status panel does not assert publication or whole-repository G03 clearance. | Scope | Maturity | Public boundary | | --- | --- | --- | @@ -354,6 +354,8 @@ def _status(self, campaign: dict[str, Any]) -> int: | WEDGE6 static | qualified bounded | Approved documented static scope | | PETSc/MPI linear static | Owner-accepted bounded | Two-rank cases with replicated input/root-side assembly; no scaling claim | | Mixed distributed PETSc/MPI runtime (including nonlinear routes) | not validated | No general nonlinear distributed evidence | +| Gyroscopic modal analysis | experimental | Straight circular BEAM2 shafts with centered rigid axisymmetric disks; dense serial QEP only | +| Campbell diagrams | experimental | Explicit complex-mode tracking; high-frequency ambiguity at 100 rad/s preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation | """.strip() (self.generated / "status.md").write_text(panels + "\n", encoding="utf-8") write_markdown_table( diff --git a/tests/documentation/test_docs_generation.py b/tests/documentation/test_docs_generation.py index 1c381f28..4c7163bb 100644 --- a/tests/documentation/test_docs_generation.py +++ b/tests/documentation/test_docs_generation.py @@ -76,14 +76,14 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> comparison_snapshot.replace(">", "").split() ) - assert "latest **published** release is `0.2.10`" in readme - assert "0.2.11 is a **candidate, not yet published**" in readme + assert "The current QF Solver release is `0.2.11`" in readme + assert "0.2.11 is a **candidate, not yet published**" not in readme assert "`0.2.8` — release candidate; not published" not in readme assert "source tag" in readme and "v0.2.10" in readme - assert "QF Solver 0.2.10 is the current published release" in normalized_roadmap + assert "QF Solver 0.2.11 is the current documentation baseline" in normalized_roadmap assert "Version 0.2.9 was a development/source snapshot" in normalized_roadmap - assert "QF Solver 0.2.10 architecture" in normalized_architecture - assert "published 0.2.8 release scope" in open_source + assert "QF Solver 0.2.11 architecture" in normalized_architecture + assert "cumulative 0.2.11 solver retains explicitly bounded scopes" in open_source assert "first public release after 0.2.8" in normalized_index assert "0.2.10" in index assert "0.2.8 is the current development candidate" not in roadmap @@ -91,7 +91,7 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> assert "10.5281/zenodo.23106744" in index assert "version DOI" in readme assert "Current release" in normalized_index - assert "current published package is 0.2.10" in normalized_comparisons + assert "current documentation baseline is 0.2.11" in normalized_comparisons assert ( "not been refreshed against the 0.2.10 release" in normalized_comparison_snapshot @@ -103,6 +103,78 @@ def test_public_release_status_copy_distinguishes_release_and_tagged_source() -> assert not any(line.startswith("doi:") for line in citation.splitlines()) +@pytest.mark.parametrize( + "relative", + ( + "index.md", + "architecture.md", + "capabilities/index.md", + "analyses/index.md", + "elements/index.md", + "etat/capacites.md", + "etat/limites.md", + "getting-started/installation.md", + "getting-started/quickstart.md", + "getting-started/nonlinear-example.md", + "getting-started/when-to-use-qf-solver.md", + "mechanics/nonlinear-overview.md", + "mechanics/rotating-modal.md", + "mechanics/campbell.md", + "reference/qf_solver_api.md", + "reference/api_stability.md", + "solveurs/index.md", + "verification/evidence-and-maturity.md", + "verification/0_2_11/README.md", + "whats-new/0.2.11.md", + ), +) +def test_current_public_surfaces_use_consistent_version_without_promotion(relative: str) -> None: + content = (DOCS / relative).read_text(encoding="utf-8") + assert "applicable_version: 0.2.11" in content + normalized = " ".join(content.split()).lower() + for stale in ( + "current release:** qf solver `0.2.10`", + "0.2.11 release candidate", + "0.2.11 candidate v&v", + "future 0.2.11", + "upcoming 0.2.11", + ): + assert stale not in normalized + assert "0.2.12" not in content + + +@pytest.mark.parametrize( + "relative", + ( + "mechanics/campbell.md", + "verification/0_2_11/README.md", + "whats-new/0.2.11.md", + "verification/evidence-and-maturity.md", + ), +) +def test_rotating_public_evidence_retains_limits(relative: str) -> None: + content = (DOCS / relative).read_text(encoding="utf-8") + normalized = " ".join(content.split()) + assert "EXPERIMENTAL" in content + assert "100 rad/s" in normalized + assert "ambigu" in normalized.lower() + assert "internal mesh-convergence evidence" in normalized + assert "not independent physical validation" in normalized + + +def test_generated_status_matches_its_template_without_running_campaign( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "docs" / "generated").mkdir(parents=True) + monkeypatch.setattr("scripts.docs_publication.collect_test_count", lambda _root: 0) + publisher = DocumentationPublisher( + tmp_path, profile="quick", records=[], scales={}, source_state={"revision": "uncommitted"} + ) + publisher._status({}) + generated = (tmp_path / "docs" / "generated" / "status.md").read_text(encoding="utf-8") + assert generated == (DOCS / "generated" / "status.md").read_text(encoding="utf-8") + + def test_tetra_boundary_faces_remove_shared_face() -> None: faces = tetra_boundary_faces([(0, 1, 2, 3), (0, 2, 1, 4)]) assert faces.shape == (6, 3) diff --git a/tests/unit/test_f3_public_claims.py b/tests/unit/test_f3_public_claims.py index c1423eb0..93195de5 100644 --- a/tests/unit/test_f3_public_claims.py +++ b/tests/unit/test_f3_public_claims.py @@ -111,7 +111,7 @@ def test_active_lu2_views_do_not_present_old_accounting_as_current() -> None: roadmap = (ROOT / "docs/reference/feuille_de_route.md").read_text(encoding="utf-8") normalized_roadmap = " ".join(roadmap.split()) - assert "QF Solver 0.2.10 is the current published release" in normalized_roadmap + assert "QF Solver 0.2.11 is the current documentation baseline" in normalized_roadmap assert ( "Version 0.2.9 was a development/source snapshot, not a PyPI package, " "GitHub Release, or version DOI." diff --git a/tests/unit/test_gate_b_release_surface.py b/tests/unit/test_gate_b_release_surface.py index 73e81238..d46e0476 100644 --- a/tests/unit/test_gate_b_release_surface.py +++ b/tests/unit/test_gate_b_release_surface.py @@ -85,7 +85,9 @@ def test_version_registry_and_public_docs_are_aligned() -> None: assert not any(line.startswith("date-released:") for line in citation.splitlines()) assert not any(line.startswith("doi:") for line in citation.splitlines()) assert project["urls"]["DOI"] == "https://doi.org/10.5281/zenodo.22697897" - assert "0.2.11 is a **candidate, not yet published**" in (ROOT / "README.md").read_text(encoding="utf-8") + readme = (ROOT / "README.md").read_text(encoding="utf-8") + assert "The current QF Solver release is `0.2.11`" in readme + assert "0.2.11 is a **candidate, not yet published**" not in readme registry = json.loads((ROOT / "qualification/0_2_8/consolidated_registry.json").read_text(encoding="utf-8")) assert registry["combination_registry"]["state_counts"] == { "QUALIFIED_BOUNDED": 32, diff --git a/tests/unit/test_pp04_public_documentation.py b/tests/unit/test_pp04_public_documentation.py index 6d72d912..2debeeb3 100644 --- a/tests/unit/test_pp04_public_documentation.py +++ b/tests/unit/test_pp04_public_documentation.py @@ -75,7 +75,7 @@ def test_current_document_metadata_and_registry_are_coherent() -> None: entries = {entry["id"]: entry for entry in registry["documents"]} expected = { - "DOC-STATE-001": ("index.md", "QF Solver 0.2.10"), + "DOC-STATE-001": ("index.md", "QF Solver"), "DOC-ARCH-001": ("architecture.md", "QF Solver architecture"), "DOC-REF-002": ("reference/registre_documentaire.md", "Documentation registry"), } @@ -86,14 +86,14 @@ def test_current_document_metadata_and_registry_are_coherent() -> None: assert (DOCS / path).is_file() for path in ("index.md", "architecture.md"): - assert "applicable_version: 0.2.10" in _front_matter(DOCS / path) - assert "applicable_version: 0.2.8" in _front_matter(DOCS / "reference" / "registre_documentaire.md") + assert "applicable_version: 0.2.11" in _front_matter(DOCS / path) + assert "applicable_version: 0.2.11" in _front_matter(DOCS / "reference" / "registre_documentaire.md") - assert re.search(r"^# QF Solver 0\.2\.10$", _text(DOCS / "index.md"), re.MULTILINE) + assert re.search(r"^# QF Solver$", _text(DOCS / "index.md"), re.MULTILINE) assert re.search(r"^# QF Solver architecture$", _text(DOCS / "architecture.md"), re.MULTILINE) assert re.search(r"^# Documentation registry$", _text(DOCS / "reference" / "registre_documentaire.md"), re.MULTILINE) generated = _text(DOCS / "generated" / "document_registry.md") - assert "| DOC-STATE-001 | QF Solver 0.2.10 |" in generated + assert "| DOC-STATE-001 | QF Solver |" in generated assert "| DOC-ARCH-001 | QF Solver architecture |" in generated assert "| DOC-REF-002 | Documentation registry |" in generated @@ -103,12 +103,15 @@ def test_generated_status_avoids_fragile_test_count() -> None: assert "Test inventory" not in status assert re.search(r"full-suite test\s+inventory is recorded by the final Gate-E evidence", status) assert "0.2.8-development" not in status - assert ">0.2.10<" in status + assert ">0.2.11<" in status assert "

Project version

" in status assert "whole-repository source archives are not cleared" in status assert "G03 FAIL" in status assert "Mixed distributed PETSc/MPI" in status assert "not validated" in status + assert "Gyroscopic modal analysis" in status and "Campbell diagrams" in status + assert "100 rad/s preserved" in status + assert "not independent physical validation" in status def test_current_public_pages_use_canonical_maturity_vocabulary() -> None: diff --git a/tests/unit/test_pp05_release_metadata.py b/tests/unit/test_pp05_release_metadata.py index bd73d4f3..90d3de62 100644 --- a/tests/unit/test_pp05_release_metadata.py +++ b/tests/unit/test_pp05_release_metadata.py @@ -85,10 +85,10 @@ def test_release_candidate_citation_has_no_unpublished_release_metadata() -> Non assert "0.2.8" in _text("README.md") assert "0.2.8" in _text("CHANGELOG.md") - assert "0.2.11 is a **candidate, not yet published**" in _text("README.md") + assert "The current QF Solver release is `0.2.11`" in _text("README.md") assert "10.5281/zenodo.23106744" in _text("README.md") assert "10.5281/zenodo.22697897" in _text("README.md") - assert "latest **published** release is `0.2.10`" in _text("README.md") + assert "current QF Solver release is `0.2.11`" in _text("README.md") def test_public_release_policy_classifies_reviewed_evidence_and_exclusions() -> None: diff --git a/tests/unit/test_publication_metadata_gate.py b/tests/unit/test_publication_metadata_gate.py index 74bbc394..36b6ba30 100644 --- a/tests/unit/test_publication_metadata_gate.py +++ b/tests/unit/test_publication_metadata_gate.py @@ -19,7 +19,9 @@ def test_unreleased_candidate_is_not_mistaken_for_a_published_tag() -> None: assert "CITATION.cff needs an assigned version DOI" in failures assert "CHANGELOG.md has no released entry for this version" in failures assert "README.md release line does not identify this version" in failures - assert "README.md still denies publication of this version" in failures + # Final-state documentation is not proof of publication. The missing + # release date, assigned DOI and released changelog keep this gate closed. + assert "README.md still denies publication of this version" not in failures def test_metadata_gate_rejects_nonstable_or_mismatched_tag() -> None: From e93094eb2a74c630f45892e81e5ad0afc4e039b7 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 08:18:55 +0200 Subject: [PATCH 09/15] governance: freeze 0.2.11 candidate build contract --- ...p07_candidate_build_contract_5c34a5ca.json | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 qualification/0_2_11/wp07_candidate_build_contract_5c34a5ca.json diff --git a/qualification/0_2_11/wp07_candidate_build_contract_5c34a5ca.json b/qualification/0_2_11/wp07_candidate_build_contract_5c34a5ca.json new file mode 100644 index 00000000..399b4616 --- /dev/null +++ b/qualification/0_2_11/wp07_candidate_build_contract_5c34a5ca.json @@ -0,0 +1,154 @@ +{ + "schema_version": 1, + "contract_id": "QF-0211-WP07-SELECTED-PACKAGE-CANDIDATE-5C34A5CA-01", + "status": "FROZEN_CANDIDATE_BUILD", + "package_version": "0.2.11", + "source_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "source_date_epoch": 1791305555, + "integration": { + "strategy": "Dedicated 0.2.11 candidate branch; the public-surface editorial commit contains the 39 explicitly accepted documentation, navigation, citation, registry, generator and assertion-only test changes.", + "previous_package_candidate_sha": "d2251cdb331e1beac5e9ba122019111c7e550e02", + "public_surface_commit_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "public_surface_files_committed": 39, + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "historical_tag_unchanged": true, + "tag_is_not_created_by_this_contract": true + }, + "change_scope": { + "numerical_source_changed": false, + "scientific_test_changed": false, + "qualification_records_changed_in_public_surface_commit": false, + "qualification_thresholds_changed": false, + "scientific_inputs_changed": false, + "qualification_contract_changed": false, + "package_metadata_changed_in_public_surface_commit": false, + "changelog_changed_in_public_surface_commit": false, + "historical_records_or_maturities_changed": false + }, + "selection": { + "include_exact": [ + "CHANGELOG.md", "CITATION.cff", "LICENSE", "LICENSE-DOCS", "MANIFEST.in", + "NOTICE", "README.md", "THIRD_PARTY_LICENSES.md", "pyproject.toml", + "qualification/benchmarks.json", "qualification/campaign.json", + "qualification/demonstrations.json", "qualification/element_analysis_matrix.json", + "qualification/formulas.json", "qualification/requirements.json", + "qualification/release_vv_0_2_1.json", "qualification/technical_content_coverage.json", + "qualification/0_2_7/capability_registry_v2.json", + "qualification/0_2_8/consolidated_registry.json", + "qualification/0_2_8/wp10_hex8_sri_owner_gate_final.json", + "qualification/0_2_8/wp13_02d_mixed_dynamics_delivery.json", + "qualification/0_2_8/wp13_03d_mixed_mpc_owner_delivery.json", + "qualification/0_2_8/wp13_04d_multimaterial_owner_delivery.json", + "qualification/0_2_8/wp13_05_inp_import_capability_record.json", + "qualification/0_2_8/wp13_05c_registry_reconciliation_erratum.json", + "qualification/0_2_8/wp13_06d_mixed_hdf5_capability_record.json", + "qualification/0_2_8/wp13_07d_contact_capability_record.json", + "requirements/baseline-standard.txt", "requirements/baseline-large-linux.txt" + ], + "include_prefixes": ["examples/", "src/qf_solver/", "src/solveur/"], + "exclude_exact": ["examples/README.md", "src/solveur/verification/j2_multifamily_performance.py"], + "forbid_prefixes": ["qualification/0_2_9/", "docs/", "scripts/", "tests/"] + }, + "documentation_selection": { + "include_prefixes": ["docs/"], + "exclude_prefixes": ["docs/verification/0_2_9/"], + "suffixes": [ + ".cff", ".cfg", ".csv", ".htm", ".html", ".ini", ".json", ".md", + ".pdf", ".ps1", ".py", ".rst", ".sh", ".tex", ".toml", ".txt", + ".xml", ".yaml", ".yml" + ] + }, + "documentation_publication_policy": { + "config_path": ".github/pages/mkdocs.yml", + "config_sha256": "3a0ebd36a87ffe53ff3ab1aea3b67dce8f2ab7343877a2a130bb46f32a6b6fcd", + "excluded_source_prefix": "docs/verification/0_2_9/", + "mkdocs_exclude_pattern": "verification/0_2_9/**" + }, + "tool_bindings": { + "scripts/audit_public_release.py": "cb1493a8a49d3f1ee0be89efc37050dc87a8c5165dbfb6de6fdd17b3bbeaf79c", + "scripts/audit_release_archive.py": "b67613d0eb5e1b305aba2bb9181d12529eda7859db00b840a71746bc39c52a37", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/plan_public_package.py": "d1a734cb4a11d53abfa102507f531f9369bc70797adf23075dd5b03c10f8e1f8", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/review_vocabulary.py": "28af00eab2a09fdcb94bf3c4682230dcaaca9b519aeb0867d96b67d4589ad3aa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c" + }, + "scope_scan_tool_sha256": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "sdist_normalization": { + "tool_path": "scripts/normalize_public_sdist.py", + "tool_sha256": "c8d969bfb0ac4a368379a95c6b71658bd60be326e0f53ed4b77f620eb2dcf8c1", + "source_date_epoch": 1791305555, + "command_contract": "Set SOURCE_DATE_EPOCH to this exact value for each build; normalize each fresh raw sdist with this source-bound script into a new path; compare normalized bytes, not raw gzip metadata." + }, + "expected_preflight_counts": { + "selected_package_files": 562, + "served_document_files": 753, + "docs_archival_exclusion_files": 208, + "findings_per_surface": 0 + }, + "candidate_execution_profile": { + "source_materialization": "Selected regular Git blobs from source_sha only; no working-tree source bytes.", + "build_command": "Set SOURCE_DATE_EPOCH=1791305555; python scripts/verify_public_package.py --root . --contract qualification/0_2_11/wp07_candidate_build_contract_5c34a5ca.json --output ", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "reproducibility": "Two independent fresh external output directories; compare exact wheel bytes and separately normalized sdist bytes. No prior candidate artifact reuse.", + "archive_checks": [ + "regular files and safe portable paths only", + "selected source byte equality", + "reject undeclared payloads, aliases, symlinks and unexpected files", + "verify package version and distribution metadata", + "twine check on the exact wheel and normalized sdist" + ], + "installed_probes": [ + "separate no-index/no-deps install of exact wheel", + "verify installed source hashes and import origins outside and inside checkout with safe-path mode", + "qf-solver, solveur-ef and mitc4-solver version/help probes", + "static, modal, rotating_modal and Campbell smoke solves using only the installed wheel" + ], + "evidence_output": "New external output directories only; preserve source mapping, command records, environment, archive checks, installed probes and hashes." + }, + "candidate_status": { + "latest_published_version": "0.2.10", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "release_date_0_2_11": "NOT_SET", + "citation_cff": "Identifies candidate version 0.2.11 without asserting a release date or version DOI.", + "whole_repository_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14": "HOLD_NOT_PROMOTED", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_NOT_INDEPENDENT_PHYSICAL_VALIDATION" + }, + "package_build_allowed": true, + "publication_allowed": false, + "structural_solves_allowed": false, + "numeric_threshold_changes_allowed": false, + "whole_repository_gate_waivers_allowed": false, + "release_permissions": { + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "whole_repository_archive_publication_allowed": false + }, + "limitations": [ + "This contract authorizes selected candidate build and bounded scans only. It does not authorize a tag, GitHub Release, PyPI upload, Zenodo deposit, DOI assignment, ledger update or maturity promotion.", + "The package selection does not scan or clear the full repository or Git history; G03 remains FAIL_PRESERVED and the whole-repository archive remains uncleared.", + "WP14 remains HOLD_NOT_PROMOTED; successful targeted checks do not change that decision.", + "rotating_modal and Campbell remain EXPERIMENTAL for the bounded serial dense BEAM2 shaft and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and that ambiguity is preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are not assigned or published." + ] +} From afd2efa469a18e66581720ff4801f50de7d29eb4 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 08:56:41 +0200 Subject: [PATCH 10/15] test: align packaging surface with 0.2.11 --- tests/unit/test_pp06_packaging_surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_pp06_packaging_surface.py b/tests/unit/test_pp06_packaging_surface.py index 05458c97..ba625310 100644 --- a/tests/unit/test_pp06_packaging_surface.py +++ b/tests/unit/test_pp06_packaging_surface.py @@ -41,7 +41,7 @@ def test_installation_document_matches_extras_and_runtime_boundaries() -> None: for extra in ("mesh", "hdf5", "large", "hpc", "docs"): assert f'qf-solver[{extra}]' in installation assert "python -m mkdocs build --strict -f .github/pages/mkdocs.yml" in installation - assert "--branch v0.2.10" in installation + assert "--branch v0.2.11" in installation assert "default branch" in installation assert "Linux" in installation and "native Windows" in installation assert "NOT_VALIDATED" in installation From 1176612212c970579cc43c6695aa4af238a905c4 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 08:58:26 +0200 Subject: [PATCH 11/15] governance: freeze candidate build for afd2efa4 --- ...p07_candidate_build_contract_afd2efa4.json | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json diff --git a/qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json b/qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json new file mode 100644 index 00000000..03139e16 --- /dev/null +++ b/qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json @@ -0,0 +1,158 @@ +{ + "schema_version": 1, + "contract_id": "QF-0211-WP07-SELECTED-PACKAGE-CANDIDATE-AFD2EFA4-01", + "status": "FROZEN_CANDIDATE_BUILD", + "package_version": "0.2.11", + "source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "source_date_epoch": 1791305555, + "integration": { + "strategy": "Dedicated 0.2.11 candidate branch; the 39-file public-surface commit is followed by one assertion-only correction aligning the packaging documentation test with the published-candidate installation instructions.", + "previous_package_candidate_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "public_surface_commit_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "test_correction_commit_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "public_surface_files_committed": 39, + "correction_files_committed": 1, + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1", + "historical_tag_unchanged": true, + "tag_is_not_created_by_this_contract": true + }, + "change_scope": { + "numerical_source_changed": false, + "scientific_test_changed": false, + "qualification_records_changed_in_public_surface_or_correction_commits": false, + "qualification_thresholds_changed": false, + "scientific_inputs_changed": false, + "qualification_contract_changed": false, + "public_api_changed": false, + "documentation_meaning_changed": false, + "package_metadata_changed_in_public_surface_or_correction_commits": false, + "changelog_changed_in_public_surface_or_correction_commits": false, + "historical_records_or_maturities_changed": false + }, + "selection": { + "include_exact": [ + "CHANGELOG.md", "CITATION.cff", "LICENSE", "LICENSE-DOCS", "MANIFEST.in", + "NOTICE", "README.md", "THIRD_PARTY_LICENSES.md", "pyproject.toml", + "qualification/benchmarks.json", "qualification/campaign.json", + "qualification/demonstrations.json", "qualification/element_analysis_matrix.json", + "qualification/formulas.json", "qualification/requirements.json", + "qualification/release_vv_0_2_1.json", "qualification/technical_content_coverage.json", + "qualification/0_2_7/capability_registry_v2.json", + "qualification/0_2_8/consolidated_registry.json", + "qualification/0_2_8/wp10_hex8_sri_owner_gate_final.json", + "qualification/0_2_8/wp13_02d_mixed_dynamics_delivery.json", + "qualification/0_2_8/wp13_03d_mixed_mpc_owner_delivery.json", + "qualification/0_2_8/wp13_04d_multimaterial_owner_delivery.json", + "qualification/0_2_8/wp13_05_inp_import_capability_record.json", + "qualification/0_2_8/wp13_05c_registry_reconciliation_erratum.json", + "qualification/0_2_8/wp13_06d_mixed_hdf5_capability_record.json", + "qualification/0_2_8/wp13_07d_contact_capability_record.json", + "requirements/baseline-standard.txt", "requirements/baseline-large-linux.txt" + ], + "include_prefixes": ["examples/", "src/qf_solver/", "src/solveur/"], + "exclude_exact": ["examples/README.md", "src/solveur/verification/j2_multifamily_performance.py"], + "forbid_prefixes": ["qualification/0_2_9/", "docs/", "scripts/", "tests/"] + }, + "documentation_selection": { + "include_prefixes": ["docs/"], + "exclude_prefixes": ["docs/verification/0_2_9/"], + "suffixes": [ + ".cff", ".cfg", ".csv", ".htm", ".html", ".ini", ".json", ".md", + ".pdf", ".ps1", ".py", ".rst", ".sh", ".tex", ".toml", ".txt", + ".xml", ".yaml", ".yml" + ] + }, + "documentation_publication_policy": { + "config_path": ".github/pages/mkdocs.yml", + "config_sha256": "3a0ebd36a87ffe53ff3ab1aea3b67dce8f2ab7343877a2a130bb46f32a6b6fcd", + "excluded_source_prefix": "docs/verification/0_2_9/", + "mkdocs_exclude_pattern": "verification/0_2_9/**" + }, + "tool_bindings": { + "scripts/audit_public_release.py": "cb1493a8a49d3f1ee0be89efc37050dc87a8c5165dbfb6de6fdd17b3bbeaf79c", + "scripts/audit_release_archive.py": "b67613d0eb5e1b305aba2bb9181d12529eda7859db00b840a71746bc39c52a37", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/plan_public_package.py": "d1a734cb4a11d53abfa102507f531f9369bc70797adf23075dd5b03c10f8e1f8", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/review_vocabulary.py": "28af00eab2a09fdcb94bf3c4682230dcaaca9b519aeb0867d96b67d4589ad3aa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c" + }, + "scope_scan_tool_sha256": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "sdist_normalization": { + "tool_path": "scripts/normalize_public_sdist.py", + "tool_sha256": "c8d969bfb0ac4a368379a95c6b71658bd60be326e0f53ed4b77f620eb2dcf8c1", + "source_date_epoch": 1791305555, + "command_contract": "Set SOURCE_DATE_EPOCH to this exact value for each build; normalize each fresh raw sdist with this source-bound script into a new path; compare normalized bytes, not raw gzip metadata." + }, + "expected_preflight_counts": { + "selected_package_files": 562, + "served_document_files": 753, + "docs_archival_exclusion_files": 208, + "findings_per_surface": 0 + }, + "candidate_execution_profile": { + "source_materialization": "Selected regular Git blobs from source_sha only; no working-tree source bytes.", + "build_command": "Set SOURCE_DATE_EPOCH=1791305555; python scripts/verify_public_package.py --root . --contract qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json --output ", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "reproducibility": "Two independent fresh external output directories; compare exact wheel bytes and separately normalized sdist bytes. No prior candidate artifact reuse.", + "archive_checks": [ + "regular files and safe portable paths only", + "selected source byte equality", + "reject undeclared payloads, aliases, symlinks and unexpected files", + "verify package version and distribution metadata", + "twine check on the exact wheel and normalized sdist" + ], + "installed_probes": [ + "separate no-index/no-deps install of exact wheel", + "verify installed source hashes and import origins outside and inside checkout with safe-path mode", + "qf-solver, solveur-ef and mitc4-solver version/help probes", + "static, modal, rotating_modal and Campbell smoke solves using only the installed wheel" + ], + "evidence_output": "New external output directories only; preserve source mapping, command records, environment, archive checks, installed probes and hashes." + }, + "candidate_status": { + "latest_published_version": "0.2.10", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "release_date_0_2_11": "NOT_SET", + "citation_cff": "Identifies candidate version 0.2.11 without asserting a release date or version DOI.", + "whole_repository_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14": "HOLD_NOT_PROMOTED", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_NOT_INDEPENDENT_PHYSICAL_VALIDATION" + }, + "package_build_allowed": true, + "publication_allowed": false, + "structural_solves_allowed": false, + "numeric_threshold_changes_allowed": false, + "whole_repository_gate_waivers_allowed": false, + "release_permissions": { + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "whole_repository_archive_publication_allowed": false + }, + "limitations": [ + "This contract authorizes selected candidate build and bounded scans only. It does not authorize a tag, GitHub Release, PyPI upload, Zenodo deposit, DOI assignment, ledger update or maturity promotion.", + "The package selection does not scan or clear the full repository or Git history; G03 remains FAIL_PRESERVED and the whole-repository archive remains uncleared.", + "WP14 remains HOLD_NOT_PROMOTED; successful targeted checks do not change that decision.", + "rotating_modal and Campbell remain EXPERIMENTAL for the bounded serial dense BEAM2 shaft and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and that ambiguity is preserved; GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are not assigned or published." + ] +} From 5c64c51a74375aef35db00c33c40861044d892cc Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 10:55:22 +0200 Subject: [PATCH 12/15] governance: record final 0.2.11 candidate audit --- .../wp07_final_candidate_audit_afd2efa4.json | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json diff --git a/qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json b/qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json new file mode 100644 index 00000000..80b866e8 --- /dev/null +++ b/qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json @@ -0,0 +1,193 @@ +{ + "schema_version": 1, + "record_id": "QF-0211-WP07-FINAL-CANDIDATE-AUDIT-AFD2EFA4-2026-10-07", + "record_type": "PROSPECTIVE_RELEASE_CANDIDATE_AUDIT", + "status": "READY_FOR_OWNER_RELEASE_AUTHORIZATION", + "candidate": { + "package_version": "0.2.11", + "package_source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "package_source_tree_sha": "8b135547a9dcde4420c18eea7a39565a1ff446e8", + "branch": "codex/v0.2.11-public-readme-citation", + "pre_audit_governance_head_sha": "1176612212c970579cc43c6695aa4af238a905c4", + "previous_package_candidate_sha": "5c34a5cae20a8147501d6a31ff47c97c36bcc5a3", + "expected_tag": "v0.2.11", + "expected_tag_target_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "tag_created": false, + "v0_2_10_tag_target_sha": "e535ff63464ddd7d76c2898df25470350b5154f1" + }, + "source_change_review": { + "correction_commit_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "correction_commit_parent_sha": "e93094eb2a74c630f45892e81e5ad0afc4e039b7", + "correction_commit_paths": [ + "tests/unit/test_pp06_packaging_surface.py" + ], + "correction_commit_change": "Updated the explicit installation-branch assertion from v0.2.10 to v0.2.11; the version-specific packaging/documentation check remains strict.", + "root_cause": "OBSOLETE_DOCUMENTATION_ASSERTION", + "numerical_defect": false, + "packaging_defect": false, + "numerical_source_changed": false, + "scientific_test_changed": false, + "qualification_input_changed": false, + "scientific_qualification_contract_changed": false, + "qualification_threshold_changed": false, + "public_api_changed": false, + "documentation_changed_by_correction": false, + "maturity_or_historical_record_changed": false, + "governance_records_excluded_from_selected_package": [ + "qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json", + "qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json" + ], + "historical_governance_contracts_rewritten": false + }, + "build_contract": { + "path": "qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json", + "sha256": "3aaea04c62f2c622a143fdb039c43fa2128ddadc4a11c32e9105e1c17570158e", + "status": "FROZEN_CANDIDATE_BUILD", + "source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "expected_tag_target_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "source_date_epoch": 1791305555, + "source_materialization": "Selected regular Git blobs from the exact package_source_sha; no working-tree source bytes.", + "selected_package_file_count": 562, + "served_document_file_count": 753, + "historical_document_files_excluded": 208, + "expected_findings_per_surface": 0 + }, + "ci": { + "quality_run_id": 37584391386, + "quality_run_url": "https://github.com/emptiesvoid-cloud/QF_solver/actions/runs/37584391386", + "quality_head_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "quality_conclusion": "SUCCESS", + "quality_jobs": { + "ubuntu_python_3_10": "PASS", + "ubuntu_python_3_13": "PASS", + "windows_python_3_10": "PASS", + "windows_python_3_13": "PASS", + "wp14_recovered_evidence_targeted": "PASS", + "documentation_evidence": "PASS", + "full_engineering_campaign": "PASS" + }, + "documentation_run_id": 37584391488, + "documentation_run_url": "https://github.com/emptiesvoid-cloud/QF_solver/actions/runs/37584391488", + "documentation_head_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "documentation_conclusion": "SUCCESS", + "ci_sha_exact_match": true, + "non_blocking_runner_warnings": [ + "GitHub Actions warns that actions targeting Node.js 20 are being run on Node.js 24.", + "GitHub Actions warns of the planned ubuntu-latest image migration." + ] + }, + "focused_validation": { + "targeted_test_command": "python -m pytest tests/unit/test_pp06_packaging_surface.py tests/unit/test_gate_b_release_surface.py tests/unit/test_pp05_release_metadata.py tests/unit/test_f3_public_claims.py tests/unit/test_pp04_public_documentation.py tests/unit/test_publication_metadata_gate.py tests/documentation/test_docs_generation.py", + "targeted_test_result": "92 passed, 2 skipped", + "ruff_modified_python_tests": "PASS", + "documentation_tests": "PASS", + "mkdocs_strict": "PASS", + "release_surface_and_metadata_tests": "PASS" + }, + "selected_scope_audit": { + "status": "PASS_BOUNDED_PUBLIC_SURFACES", + "source_materialization": "Exact selected Git blobs from afd2efa469a18e66581720ff4801f50de7d29eb4.", + "selected_package_files": 562, + "selected_package_findings": 0, + "served_document_files": 753, + "served_document_findings": 0, + "historical_document_files_excluded": 208, + "whole_repository_scanned": false, + "whole_repository_g03": "FAIL_PRESERVED" + }, + "build_and_artifacts": { + "build_policy": "Two independent builds from fresh external output directories using the frozen Git-blob materialization, pinned build environment and SOURCE_DATE_EPOCH. Compare wheel bytes directly and compare sdist bytes after only the bound normalizer.", + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0", + "source_date_epoch": 1791305555 + }, + "wheel": { + "filename": "qf_solver-0.2.11-py3-none-any.whl", + "sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8", + "size_bytes": 1559189, + "independent_builds_byte_identical": true, + "archive_content_and_path_audit": "PASS" + }, + "sdist": { + "filename": "qf_solver-0.2.11.tar.gz", + "sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e", + "size_bytes": 1177522, + "file_count": 570, + "raw_build_a_sha256": "4b8aa4e0d31b300f55a7d83bc64034f686e8d96a9e4a30a2615d8210a965a167", + "raw_build_b_sha256": "9a5dace6b64320ce64bfadd293ee7c1b2a6dfff874595ce9e647b31c45a8a978", + "raw_builds_byte_identical": false, + "raw_difference": "Gzip metadata only, handled by the pre-bound sdist normalizer.", + "normalized_builds_byte_identical": true, + "normalization_policy": "The source-bound scripts/normalize_public_sdist.py and frozen epoch were used; normalized payload preservation was checked." + }, + "manifest": { + "filename": "qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt", + "sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364", + "size_bytes": 190, + "entries_match_exact_artifacts": true + }, + "twine_check": "PASS", + "installed_package_probes": { + "isolated_no_index_no_deps_install": "PASS", + "imports_outside_checkout_and_safe_path": "PASS", + "installed_import_origins": "PASS", + "version_and_cli_probes": "PASS", + "static_smoke": "PASS", + "modal_smoke": "PASS", + "rotating_modal_smoke": "PASS_EXPERIMENTAL", + "campbell_smoke": "PASS_EXPERIMENTAL", + "developer_only_verify_all_from_installed_wheel": "EXPECTED_FAIL_CLOSED_NOT_A_DISTRIBUTION_PROBE" + } + }, + "release_gates": { + "QF0211_G01": "INHERITED_PASS", + "QF0211_G02": "INHERITED_PASS", + "QF0211_G03": "INHERITED_PASS", + "QF0211_G04": "INHERITED_PASS", + "QF0211_G05": "INHERITED_PASS", + "QF0211_G06": "INHERITED_PASS", + "inheritance_basis": "No numerical source, scientific test, scientific input, or scientific qualification contract changed in the assertion-only correction.", + "QF0211_G07": "PASS", + "QF0211_G08": "PASS", + "final_release_contract": "PASS", + "ready_for_owner_release_authorization": true + }, + "citation_and_public_claims": { + "readme_current_release": "0.2.11", + "citation_version": "0.2.11", + "version_doi_0_2_11": "NOT_ASSIGNED_NOT_CLAIMED", + "concept_doi": "10.5281/zenodo.22697897", + "version_doi_0_2_10": "10.5281/zenodo.23106744", + "rotating_modal_maturity": "EXPERIMENTAL", + "campbell_maturity": "EXPERIMENTAL", + "campbell_100_rad_s_high_frequency_pair": "AMBIGUITY_PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY_NOT_INDEPENDENT_PHYSICAL_VALIDATION", + "historical_g03": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "historical_wp14": "HOLD_NOT_PROMOTED", + "historical_failures_reclassified": false, + "maturity_promotions": "NONE" + }, + "authorization_and_publication": { + "owner_release_authorization": "PENDING", + "publication_allowed": false, + "tag_creation_allowed": false, + "github_release_allowed": false, + "pypi_upload_allowed": false, + "zenodo_deposit_allowed": false, + "ledger_update_allowed": false, + "version_doi_or_release_date_may_be_claimed": false, + "limitations": [ + "This audit establishes readiness for Owner authorization only; it does not authorize a tag or any publication.", + "The full repository archive remains outside selected distribution clearance. Historical G03 remains FAIL_PRESERVED and whole_repository_archive_cleared remains false.", + "WP14 remains HOLD_NOT_PROMOTED despite its targeted CI job passing.", + "Rotating modal and Campbell remain EXPERIMENTAL within their bounded serial BEAM2 and centered rigid axisymmetric disk scope.", + "At 100 rad/s the high-frequency pair remains ambiguous and the tracker preserves that ambiguity. GYRO-06 is internal mesh-convergence evidence, not independent physical validation.", + "The 0.2.11 version DOI and release date are unassigned pending an authorized publication sequence." + ] + } +} From f87e423765a67612cce8ebeb6e1c8932a21d0481 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 12:18:43 +0200 Subject: [PATCH 13/15] release: add fail-closed 0.2.11 governance --- .github/workflows/publish-pypi.yml | 14 +- PUBLIC_RELEASE_POLICY.md | 61 ++- ...er_publication_authorization_afd2efa4.json | 102 ++++ .../qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt | 2 + scripts/check_publication_metadata.py | 465 ++++++++++++++++-- scripts/public_release_scope.py | 24 + .../verify_authorized_release_artifacts.py | 231 ++++++--- .../unit/test_authorized_release_artifacts.py | 296 +++++++---- tests/unit/test_publication_metadata_gate.py | 331 +++++++++++-- 9 files changed, 1258 insertions(+), 268 deletions(-) create mode 100644 qualification/0_2_11/owner_publication_authorization_afd2efa4.json create mode 100644 qualification/0_2_11/qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt create mode 100644 scripts/public_release_scope.py diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 8f9b4f83..072d4149 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -66,14 +66,26 @@ jobs: contract = json.load(open(sys.argv[1], encoding="utf-8")) if contract.get("status") != "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE": raise SystemExit("The release contract is not Owner-authorized.") + if contract.get("publication_scope") != "SELECTED_DISTRIBUTION": + raise SystemExit("Only an explicit selected-distribution contract may use this workflow.") + if contract.get("package_version") != sys.argv[2][1:]: + raise SystemExit("The release contract version differs from the requested tag.") if contract.get("release_tag") != sys.argv[2] or contract.get("source_sha") != sys.argv[3]: raise SystemExit("The requested tag differs from the exact audited package source.") if contract.get("g03_status") != "FAIL_PRESERVED" or contract.get("whole_repository_archive_cleared") is not False: raise SystemExit("G03 must remain failed and the complete repository archive uncleared.") if contract.get("wp14_status") != "HOLD_NOT_PROMOTED": raise SystemExit("This release workflow does not authorize a WP14 status change.") + authorization = contract.get("authorization", {}) + if authorization.get("whole_repository_archive_publication_allowed") is not False: + raise SystemExit("Selected distribution authorization must not permit a whole-repository archive.") + if authorization.get("selected_package_publication_allowed") is not True: + raise SystemExit("The selected distribution is not explicitly authorized.") PY - python scripts/check_publication_metadata.py --root tagged-source --tag "$RELEASE_TAG" + python scripts/check_publication_metadata.py \ + --root tagged-source \ + --tag "$RELEASE_TAG" \ + --phase PRE_PUBLICATION quality: name: Tagged release quality gate diff --git a/PUBLIC_RELEASE_POLICY.md b/PUBLIC_RELEASE_POLICY.md index f012d6ae..9f09877d 100644 --- a/PUBLIC_RELEASE_POLICY.md +++ b/PUBLIC_RELEASE_POLICY.md @@ -54,7 +54,7 @@ do not rely on deleting a file in a later commit. ## Release Gate -Before promoting a full repository source archive or GitHub Release, run: +Before promoting a **whole-repository source archive** as a cleared distribution, run: ```powershell python .\scripts\audit_public_release.py --output .\public_release_audit.json @@ -73,11 +73,36 @@ is also excluded: only selected, reviewed V&V packages may be copied into a future public release deliberately. These rules are safeguards, not substitutes for review. -A source tag used solely to identify a separately audited PyPI candidate does -not pass this whole-repository gate by implication. GitHub may still offer an -automatic source archive for the tag; it must not be described or uploaded as -a cleared distribution while this gate fails. The separate PyPI decision and -its frozen selected-content checks are specified below. +### Selected distribution versus whole-repository archive + +`SELECTED_DISTRIBUTION_CLEARANCE` and +`WHOLE_REPOSITORY_ARCHIVE_CLEARANCE` are separate decisions. A prospective, +Owner-authorized selected-release contract may authorize only its exact wheel, +sdist and checksum manifest when the selected-source/documentation audit, +package checks, artifact hashes and required release gates pass. This bounded +decision does not change a historical whole-repository G03 failure and does +not clear or authorize a complete repository archive. + +A GitHub Release page may carry those exact selected assets when a separate +Owner authorization and the selected-release contract permit it. This is a +channel-specific authorization, independent of the PyPI decision. GitHub's +automatically generated `Source code (zip)` and `Source code (tar.gz)` assets +are platform-generated repository archives, not selected distribution assets. +When G03 is not cleared, they must not be called audited, qualified or cleared, +and they must not be uploaded to Zenodo as release artifacts. Authorization +for one publication channel does not implicitly authorize another channel. + +A Zenodo version record for a selected distribution may contain only the exact +files named in its selected-release contract. It must not import or include a +full repository snapshot, GitHub-generated source archive or unselected +qualification archive unless whole-repository archive clearance is separately +granted. + +A source tag used solely to identify a separately audited selected candidate +does not pass this whole-repository gate by implication. GitHub may still offer +an automatic source archive for the tag; it must not be described or uploaded +as a cleared distribution while this gate fails. Selected binary distribution +checks are independent and do not alter the whole-repository gate. `audit_release_archive.py` uses worktree attributes by default to verify the next prospective archive. Immediately before tagging, run it again with @@ -188,9 +213,21 @@ alone is not. The selected package must be built from the contract's exact tagged Git blobs, not from the rest of the engineering checkout. Publish only those audited -wheel/sdist bytes. A PyPI-only decision never clears a failing whole-repository -source/archive/history audit and never authorizes a GitHub Release, an upload -of the repository archive to Zenodo, or a WP14 status change. Do not move an -existing tag to make corrected metadata appear retroactively in its source. -If the immutable tag's public metadata is inconsistent, prepare a new version -and tag after the corrections and a new frozen audit. +wheel/sdist bytes. A selected PyPI decision never clears a failing +whole-repository source/archive/history audit, authorizes an unselected +repository archive for Zenodo, or changes WP14. A GitHub Release requires its +own explicit Owner permission in the exact selected-release contract. Do not +move an existing tag to make corrected metadata appear retroactively in its +source. If immutable tagged metadata is inconsistent, stop and follow a +prospective, explicitly authorized correction sequence. + +### Publication metadata phases + +Before Zenodo publication, a frozen candidate may identify its version and +project concept DOI while leaving `date-released` absent and its version DOI +absent. +The prepublication metadata gate requires this state and rejects a reserved or +invented version DOI. After publication, a separate postpublication check +requires the factual release date, the resolving version DOI, matching public +citation metadata, and verified selected-asset hashes. A missing version DOI +is valid only in `PRE_PUBLICATION`; it fails the `POST_PUBLICATION` gate. diff --git a/qualification/0_2_11/owner_publication_authorization_afd2efa4.json b/qualification/0_2_11/owner_publication_authorization_afd2efa4.json new file mode 100644 index 00000000..09aba06b --- /dev/null +++ b/qualification/0_2_11/owner_publication_authorization_afd2efa4.json @@ -0,0 +1,102 @@ +{ + "schema_version": 1, + "record_id": "QF-0211-OWNER-SELECTED-PUBLICATION-AUTHORIZATION-AFD2EFA4-2026-10-07", + "record_type": "PROSPECTIVE_OWNER_SELECTED_DISTRIBUTION_AUTHORIZATION", + "decision": "AUTHORIZE_SELECTED_PACKAGE_RELEASE", + "decision_authority": "Repository Owner", + "decision_date": "2026-10-07", + "decision_basis": "Explicit Owner authorization in the release-governance task, conditional on all fail-closed checks passing.", + "status": "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "version": "0.2.11", + "authorized_source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "authorized_tag": "v0.2.11", + "authorized_tag_target_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "authorized_artifacts": { + "wheel": { + "filename": "qf_solver-0.2.11-py3-none-any.whl", + "bytes": 1559189, + "sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8" + }, + "sdist": { + "filename": "qf_solver-0.2.11.tar.gz", + "bytes": 1177522, + "sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e" + }, + "manifest": { + "filename": "qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt", + "bytes": 190, + "sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364" + } + }, + "authorized_wheel_sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8", + "authorized_sdist_sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e", + "authorized_manifest_sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364", + "publication_scope": "SELECTED_DISTRIBUTION", + "release_gates": { + "QF0211_G01": "INHERITED_PASS", + "QF0211_G02": "INHERITED_PASS", + "QF0211_G03": "INHERITED_PASS", + "QF0211_G04": "INHERITED_PASS", + "QF0211_G05": "INHERITED_PASS", + "QF0211_G06": "INHERITED_PASS", + "QF0211_G07": "PASS", + "QF0211_G08": "PASS" + }, + "maturities": { + "rotating_modal": "EXPERIMENTAL", + "campbell": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY" + }, + "publication_phase": { + "current_phase": "PRE_PUBLICATION", + "version_doi": "NOT_ASSIGNED", + "release_date": "NOT_SET", + "concept_doi": "10.5281/zenodo.22697897" + }, + "authorization": { + "selected_package_publication_allowed": true, + "tag_creation_allowed": true, + "pypi_publication_allowed": true, + "github_release_allowed": true, + "zenodo_selected_artifact_publication_allowed": true, + "whole_repository_archive_cleared": false, + "whole_repository_archive_publication_allowed": false, + "ledger_update_allowed": false, + "wp14_promotion_allowed": false, + "numeric_maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "certification_or_universal_validation_claims_allowed": false + }, + "selected_package_publication_allowed": true, + "tag_creation_allowed": true, + "pypi_publication_allowed": true, + "github_release_allowed": true, + "zenodo_selected_artifact_publication_allowed": true, + "whole_repository_archive_cleared": false, + "whole_repository_archive_publication_allowed": false, + "ledger_update_allowed": false, + "wp14_promotion_allowed": false, + "numeric_maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "certification_or_universal_validation_claims_allowed": false, + "g03_status": "FAIL_PRESERVED", + "wp14_status": "HOLD_NOT_PROMOTED", + "preserved_gates": { + "g03_status": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "wp14_status": "HOLD_NOT_PROMOTED" + }, + "zenodo_selected_files": [ + "qf_solver-0.2.11-py3-none-any.whl", + "qf_solver-0.2.11.tar.gz", + "qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt" + ], + "restrictions": [ + "This authorization is limited to the exact selected distribution files and source SHA above.", + "It does not authorize or clear any whole-repository archive, including GitHub-generated source ZIP/TAR files.", + "Historical G03 remains FAIL_PRESERVED, whole_repository_archive_cleared remains false, and historical WP14 remains HOLD_NOT_PROMOTED.", + "No numerical maturity promotion, historical result reclassification, ledger update, or certification claim is authorized.", + "The 0.2.11 version DOI and release date are not claimed before actual Zenodo publication." + ] +} diff --git a/qualification/0_2_11/qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt b/qualification/0_2_11/qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt new file mode 100644 index 00000000..cf62d574 --- /dev/null +++ b/qualification/0_2_11/qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt @@ -0,0 +1,2 @@ +bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8 qf_solver-0.2.11-py3-none-any.whl +bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e qf_solver-0.2.11.tar.gz diff --git a/scripts/check_publication_metadata.py b/scripts/check_publication_metadata.py index bc32312b..7e9da892 100644 --- a/scripts/check_publication_metadata.py +++ b/scripts/check_publication_metadata.py @@ -1,10 +1,12 @@ -"""Fail closed if tagged PyPI release metadata disagrees with its source tag.""" +"""Validate release-facing metadata in explicit pre- or post-publication phase.""" from __future__ import annotations import argparse +import json import re import sys +from datetime import date from pathlib import Path try: @@ -13,6 +15,9 @@ import tomli as tomllib +CONCEPT_DOI = "10.5281/zenodo.22697897" +VERSION_RE = re.compile(r"\d+\.\d+\.\d+\Z") +DOI_RE = re.compile(r"10\.5281/zenodo\.(\d+)\Z") PUBLIC_PAGES = ( "README.md", "CHANGELOG.md", @@ -24,71 +29,422 @@ "SUPPORT.md", "SECURITY.md", ) +ROTATING_PAGES = ( + "docs/whats-new/0.2.11.md", + "docs/verification/0_2_11/README.md", +) +FORBIDDEN_PUBLIC_CLAIMS = ( + re.compile(r"\bqualified\s+rotordynamics\b", re.IGNORECASE), + re.compile(r"\bvalidated\s+general\s+critical[- ]speed(?:\s+prediction)?\b", re.IGNORECASE), + re.compile(r"\bindustrial\s+(?:rotor|rotordynamics)\s+solver\b", re.IGNORECASE), + re.compile(r"\bgeneral\s+rotordynamics\s+(?:solver|support)\b", re.IGNORECASE), + re.compile( + r"\b(?:whole|full)[- ]repository(?:\s+source)?\s+archive\s+(?:is\s+)?(?:cleared|audited|qualified)\b", + re.IGNORECASE, + ), +) + + +def _field(text: str, name: str) -> str | None: + match = re.search(rf"(?m)^{re.escape(name)}:\s*(.*?)\s*$", text) + if match is None: + return None + value = match.group(1).strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"\"", "'"}: + value = value[1:-1] + return value + + +def _released_versions(changelog: str) -> list[str]: + return re.findall(r"(?mi)^##\s+(\d+\.\d+\.\d+)\s+-\s+Released\b", changelog) + + +def _require(condition: bool, message: str, failures: list[str]) -> None: + if not condition: + failures.append(message) + + +def _phase_inputs( + phase: str, version_doi: str | None, release_date: str | None, failures: list[str] +) -> tuple[str | None, str | None]: + phase = phase.upper() + if phase not in {"PRE_PUBLICATION", "POST_PUBLICATION"}: + failures.append("publication phase must be PRE_PUBLICATION or POST_PUBLICATION") + return None, None + if phase == "PRE_PUBLICATION": + _require(version_doi is None, "PRE_PUBLICATION must not claim a version DOI", failures) + _require(release_date is None, "PRE_PUBLICATION must not claim a release date", failures) + return None, None + + valid_doi = version_doi is not None and DOI_RE.fullmatch(version_doi) is not None + _require(valid_doi, "POST_PUBLICATION needs a captured Zenodo version DOI", failures) + valid_date = False + if release_date is not None: + try: + valid_date = date.fromisoformat(release_date).isoformat() == release_date + except ValueError: + valid_date = False + _require(valid_date, "POST_PUBLICATION needs the factual ISO release date", failures) + if version_doi == CONCEPT_DOI: + failures.append("the project concept DOI cannot be used as a version DOI") + return version_doi, release_date -def check_publication_metadata(root: Path, tag: str) -> list[str]: - """Check release-facing metadata; this is not a content or mechanics audit.""" +def _check_citation( + citation: str, + version: str, + phase: str, + version_doi: str | None, + release_date: str | None, + failures: list[str], +) -> None: + _require(_field(citation, "version") == version, "CITATION.cff version differs from the release", failures) + _require(CONCEPT_DOI in citation, "CITATION.cff must preserve the project concept DOI", failures) + citation_doi = _field(citation, "doi") + citation_date = _field(citation, "date-released") + if phase == "PRE_PUBLICATION": + _require(citation_doi is None, "PRE_PUBLICATION CITATION.cff must not assert a version DOI", failures) + _require(citation_date is None, "PRE_PUBLICATION CITATION.cff must not assert a release date", failures) + _require( + "without asserting a release date or version doi" in citation.lower(), + "PRE_PUBLICATION CITATION.cff must state that release metadata is not yet asserted", + failures, + ) + return + + _require(citation_doi == version_doi, "CITATION.cff DOI differs from the captured version DOI", failures) + _require(citation_date == release_date, "CITATION.cff date differs from the factual release date", failures) + + +def _check_current_publication_claims(contents: dict[str, str], version: str, tag: str, phase: str, + failures: list[str]) -> None: + readme = contents["README.md"] + changelog = contents["CHANGELOG.md"] + index = contents["docs/index.md"] + install = " ".join(contents["docs/getting-started/installation.md"].split()) + capabilities = contents["docs/capabilities/index.md"] + roadmap = " ".join(contents["docs/reference/feuille_de_route.md"].split()) + + _require( + re.search(rf"(?i)current QF Solver release is\s+`{re.escape(version)}`", readme) is not None, + "README.md must identify the requested current release semantically", + failures, + ) + _require( + re.search(rf"(?mi)^\*\*Current release documentation:\*\*\s*`{re.escape(version)}`", index) + is not None, + "docs/index.md must identify the current release documentation version", + failures, + ) + _require( + f"This guide applies to QF Solver `{version}`" in install, + "installation guide must identify the applicable package version", + failures, + ) + _require( + re.search(rf"(?mi)^\*\*Current release documentation:\*\*\s*`{re.escape(version)}`", capabilities) + is not None, + "capability index must identify the current release documentation version", + failures, + ) + _require( + re.search( + rf"(?i)(?:version\s+|qf solver\s+){re.escape(version)}\s+is the current documentation baseline", + roadmap, + ) + is not None, + "public roadmap must identify the current documentation baseline", + failures, + ) + + if phase == "PRE_PUBLICATION": + heading = re.search(rf"(?mi)^##\s+{re.escape(version)}\s+-\s+([^\n]+)", changelog) + _require(heading is not None, "CHANGELOG.md must contain this version's entry", failures) + if heading is not None: + _require( + "candidate" in heading.group(1).lower() and "not published" in heading.group(1).lower(), + "PRE_PUBLICATION changelog entry must remain explicitly unpublished", + failures, + ) + released = _released_versions(changelog) + _require(bool(released), "CHANGELOG.md must retain an explicit latest published entry", failures) + latest_published = released[0] if released else None + if latest_published is not None: + _require( + f"QF Solver `{latest_published}` (`v{latest_published}`) is the current published release" + in contents["CONTRIBUTING.md"], + "CONTRIBUTING.md must distinguish the latest published release from the candidate", + failures, + ) + _require( + f"currently `{latest_published}`" in contents["SUPPORT.md"], + "SUPPORT.md must retain the actual latest published release during prepublication", + failures, + ) + _require( + f"Current release: `{latest_published}` / `v{latest_published}`" in contents["SECURITY.md"], + "SECURITY.md must retain the actual latest published release during prepublication", + failures, + ) + else: + _require( + re.search(rf"(?mi)^##\s+{re.escape(version)}\s+-\s+Released\b", changelog) is not None, + "POST_PUBLICATION changelog must mark this version released", + failures, + ) + _require( + f"selected QF Solver `{version}` wheel and sdist are the audited PyPI release artifacts" in install, + "POST_PUBLICATION installation guide must identify the audited release artifacts", + failures, + ) + _require( + f"QF Solver {version} is the current published release" in roadmap, + "POST_PUBLICATION roadmap must identify the current published release", + failures, + ) + _require( + f"QF Solver `{version}` (`{tag}`) is the current published release" in contents["CONTRIBUTING.md"], + "POST_PUBLICATION CONTRIBUTING.md must identify the current published release", + failures, + ) + _require( + f"currently `{version}`" in contents["SUPPORT.md"], + "POST_PUBLICATION SUPPORT.md must identify the current published release", + failures, + ) + _require( + f"Current release: `{version}` / `{tag}`" in contents["SECURITY.md"], + "POST_PUBLICATION SECURITY.md must identify the current published release", + failures, + ) + _require( + not re.search(r"(?i)without asserting (?:a )?publication date or version DOI", readme + index), + "POST_PUBLICATION public pages must not retain prepublication citation wording", + failures, + ) + + +def _check_bounded_claims(contents: dict[str, str], failures: list[str]) -> None: + combined = "\n".join(contents.values()) + for pattern in FORBIDDEN_PUBLIC_CLAIMS: + _require(pattern.search(combined) is None, f"prohibited public claim detected: {pattern.pattern}", failures) + + readme = " ".join(contents["README.md"].lower().split()) + whats_new = " ".join(contents.get("docs/whats-new/0.2.11.md", "").lower().split()) + summary = " ".join(contents.get("docs/verification/0_2_11/README.md", "").lower().split()) + capabilities = contents["docs/capabilities/index.md"].lower() + _require("rotating_modal" in readme and "experimental" in readme, "README must retain experimental rotating-modal status", failures) + _require("campbell" in readme and "experimental" in readme, "README must retain experimental Campbell status", failures) + _require("100 rad/s" in readme and "ambig" in readme, "README must retain the 100 rad/s ambiguity", failures) + _require("internal mesh-convergence evidence" in readme, "README must limit GYRO-06 to internal mesh convergence", failures) + _require("not independent physical validation" in readme, + "README must state that internal convergence is not physical validation", failures) + _require("100 rad/s" in whats_new and "ambiguous" in whats_new, "What's New must preserve the 100 rad/s ambiguity", failures) + _require("internal mesh-convergence evidence" in whats_new, "What's New must identify GYRO-06 as internal convergence evidence", failures) + _require("not independent physical validation" in whats_new, + "What's New must state that GYRO-06 is not physical validation", failures) + _require("experimental" in capabilities and "rotating_modal" in capabilities and "campbell" in capabilities, + "capability index must retain experimental rotating-analysis maturity", failures) + _require("fail_preserved" in summary and "hold_not_promoted" in summary, + "V&V summary must preserve historical G03 and WP14 statuses", failures) + _require(re.search(r"(?i)(?:whole|full|complete)[ -]repository archive is not cleared", summary) is not None, + "V&V summary must state that the whole-repository archive is not cleared", failures) + + +def _artifact_hashes(items: object) -> dict[str, str] | None: + if not isinstance(items, list): + return None + result: dict[str, str] = {} + for item in items: + if not isinstance(item, dict): + return None + filename, sha256 = item.get("filename"), item.get("sha256") + if ( + not isinstance(filename, str) + or not isinstance(sha256, str) + or not re.fullmatch(r"[0-9a-f]{64}", sha256) + or filename in result + ): + return None + result[filename] = sha256 + return result + + +def _check_postpublication_record( + record_path: Path | None, + contract_path: Path | None, + version: str, + tag: str, + version_doi: str | None, + release_date: str | None, + failures: list[str], +) -> None: + if record_path is None or contract_path is None: + failures.append("POST_PUBLICATION requires a captured publication record and selected-release contract") + return + try: + record = json.loads(record_path.read_text(encoding="utf-8")) + contract = json.loads(contract_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + failures.append(f"POST_PUBLICATION evidence is unreadable: {type(exc).__name__}") + return + + _require(record.get("schema_version") == 1, "POST_PUBLICATION record schema is unsupported", failures) + _require(record.get("status") == "PUBLICATION_VERIFIED", "POST_PUBLICATION record is not verified", failures) + _require(record.get("publication_phase") == "POST_PUBLICATION", "publication record phase is not POST_PUBLICATION", failures) + _require(record.get("version") == version, "publication record version differs from the release", failures) + _require(record.get("tag") == tag, "publication record tag differs from the release", failures) + _require(record.get("version_doi") == version_doi, "publication record DOI differs from the captured DOI", failures) + _require(record.get("release_date") == release_date, "publication record date differs from the factual release date", failures) + _require(record.get("concept_doi") == CONCEPT_DOI, "publication record must preserve the project concept DOI", failures) + _require(record.get("tag_exists") is True, "POST_PUBLICATION must confirm the immutable tag exists", failures) + _require(record.get("main_contains_tag_target") is True, "POST_PUBLICATION must confirm the tag target is in main history", failures) + + expected_source = contract.get("source_sha") + _require( + contract.get("status") == "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE" + and contract.get("package_version") == version + and contract.get("release_tag") == tag + and contract.get("tag_target_sha") == expected_source + and contract.get("publication_scope") == "SELECTED_DISTRIBUTION" + and record.get("source_sha") == expected_source, + "publication record and selected-release contract do not bind the same exact source", + failures, + ) + authorization = contract.get("authorization", {}) + _require( + isinstance(authorization, dict) + and authorization.get("selected_package_publication_allowed") is True + and authorization.get("github_release_allowed") is True + and authorization.get("pypi_publication_allowed") is True + and authorization.get("zenodo_selected_artifact_publication_allowed") is True + and authorization.get("whole_repository_archive_cleared") is False + and authorization.get("whole_repository_archive_publication_allowed") is False, + "postpublication contract does not preserve selected-only authorization", + failures, + ) + _require( + contract.get("g03_status") == "FAIL_PRESERVED" + and contract.get("whole_repository_archive_cleared") is False + and contract.get("wp14_status") == "HOLD_NOT_PROMOTED", + "postpublication contract must preserve G03, whole-repository and WP14 statuses", + failures, + ) + + contract_artifacts = contract.get("audited_artifacts") + manifest = contract.get("sha256_manifest") + if not isinstance(contract_artifacts, dict) or not isinstance(manifest, dict): + failures.append("selected-release contract has no exact artifact bindings") + return + expected_all = { + contract_artifacts.get(kind, {}).get("filename"): contract_artifacts.get(kind, {}).get("sha256") + for kind in ("wheel", "sdist") + } + expected_all[manifest.get("filename")] = manifest.get("sha256") + if any( + not isinstance(name, str) or not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest) + for name, digest in expected_all.items() + ): + failures.append("selected-release contract artifact bindings are malformed") + return + expected_pypi = {name: digest for name, digest in expected_all.items() if name.endswith((".whl", ".tar.gz"))} + + tag_record = record.get("tag_verification", {}) + _require( + isinstance(tag_record, dict) + and tag_record.get("target_sha") == expected_source + and tag_record.get("annotated") is True, + "POST_PUBLICATION tag target does not match the audited source", + failures, + ) + github = record.get("github_release", {}) + _require( + isinstance(github, dict) + and github.get("published") is True + and github.get("tag") == tag + and _artifact_hashes(github.get("selected_assets")) == expected_all, + "GitHub Release or its selected asset hashes do not match the contract", + failures, + ) + pypi = record.get("pypi", {}) + _require( + isinstance(pypi, dict) + and pypi.get("published") is True + and pypi.get("version") == version + and pypi.get("install_probe") == "PASS" + and _artifact_hashes(pypi.get("files")) == expected_pypi, + "PyPI publication, install probe or selected file hashes do not match the contract", + failures, + ) + zenodo = record.get("zenodo", {}) + _require( + isinstance(zenodo, dict) + and zenodo.get("published") is True + and zenodo.get("doi_resolves") is True + and zenodo.get("version_doi") == version_doi + and zenodo.get("concept_doi") == CONCEPT_DOI + and zenodo.get("release_date") == release_date + and zenodo.get("whole_repository_archive_present") is False + and _artifact_hashes(zenodo.get("files")) == expected_all, + "Zenodo record, DOI, selected files or archive boundary do not match the contract", + failures, + ) + + +def check_publication_metadata( + root: Path, + tag: str, + phase: str = "PRE_PUBLICATION", + version_doi: str | None = None, + release_date: str | None = None, + publication_record: Path | None = None, + release_contract: Path | None = None, +) -> list[str]: + """Validate release metadata against the explicitly selected publication phase.""" failures: list[str] = [] if not re.fullmatch(r"v\d+\.\d+\.\d+", tag): return ["release tag must be a stable vMAJOR.MINOR.PATCH tag"] version = tag[1:] + if not VERSION_RE.fullmatch(version): + return ["release version must be a stable MAJOR.MINOR.PATCH version"] + phase = phase.upper() + version_doi, release_date = _phase_inputs(phase, version_doi, release_date, failures) try: project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"] except (OSError, KeyError, ValueError) as exc: return [f"project metadata is unreadable: {type(exc).__name__}"] - if project.get("version") != version: - failures.append("pyproject.toml version differs from release tag") + _require(project.get("version") == version, "pyproject.toml version differs from release tag", failures) + required_paths = (*PUBLIC_PAGES, "CITATION.cff") contents: dict[str, str] = {} - for relative in (*PUBLIC_PAGES, "CITATION.cff"): + for relative in required_paths: try: contents[relative] = (root / relative).read_text(encoding="utf-8") except OSError: failures.append(f"missing public metadata: {relative}") - if failures: + if version == "0.2.11": + for relative in ROTATING_PAGES: + try: + contents[relative] = (root / relative).read_text(encoding="utf-8") + except OSError: + failures.append(f"missing bounded rotating-analysis evidence page: {relative}") + if any(path not in contents for path in required_paths): return failures citation = contents["CITATION.cff"] - if not re.search(rf'(?m)^version:\s*["\']?{re.escape(version)}["\']?\s*$', citation): - failures.append("CITATION.cff does not identify this version") - if not re.search(r'(?m)^date-released:\s*\d{4}-\d{2}-\d{2}\s*$', citation): - failures.append("CITATION.cff needs a release date") - doi = re.search(r'(?m)^doi:\s*["\']?(10\.5281/zenodo\.\d+)["\']?\s*$', citation) - if doi is None or doi.group(1) == "10.5281/zenodo.22697898": - failures.append("CITATION.cff needs an assigned version DOI") - - readme = contents["README.md"] - if f"| Release line | `{version}` |" not in readme: - failures.append("README.md release line does not identify this version") - if f"## {version} - Released" not in contents["CHANGELOG.md"]: - failures.append("CHANGELOG.md has no released entry for this version") - if f"**Current release:** [`{version}`]" not in contents["docs/index.md"]: - failures.append("docs/index.md still advertises a different current release") - installation = " ".join(contents["docs/getting-started/installation.md"].split()) - if f"selected QF Solver `{version}` wheel and sdist are the audited PyPI release artifacts" not in installation: - failures.append("installation guide does not identify the selected artifacts for this version") - if f"**Current release:** `{version}` / `{tag}`" not in contents["docs/capabilities/index.md"]: - failures.append("capability index still advertises a different release") - if f"QF Solver {version} is the current published release." not in contents["docs/reference/feuille_de_route.md"]: - failures.append("public roadmap still advertises a different release") - if f"QF Solver `{version}` (`{tag}`)" not in contents["CONTRIBUTING.md"]: - failures.append("contribution guide still advertises a different release") - if f"currently `{version}`" not in contents["SUPPORT.md"]: - failures.append("support guide still advertises a different release") - if f"Current release: `{version}` / `{tag}`" not in contents["SECURITY.md"]: - failures.append("security policy still advertises a different release") - - for relative, body in contents.items(): - denial_before_version = re.search( - rf"(?is)(?:no|not|pas de|non)\s+[^.\n]{{0,90}}{re.escape(version)}[^.\n]{{0,90}}(?:publish|publi)", - body, - ) - denial_after_version = re.search( - rf"(?is){re.escape(version)}[^.]{{0,160}}\b(?:not\s+(?:yet\s+)?(?:a\s+)?(?:tagged|published|publishable)|no\s+[^.]{{0,30}}(?:publication|version\s+DOI))\b", - body, + _check_citation(citation, version, phase, version_doi, release_date, failures) + _check_current_publication_claims(contents, version, tag, phase, failures) + if version == "0.2.11": + _check_bounded_claims(contents, failures) + if phase == "POST_PUBLICATION": + _check_postpublication_record( + publication_record, release_contract, version, tag, version_doi, release_date, failures ) - if denial_before_version or denial_after_version: - failures.append(f"{relative} still denies publication of this version") + _require( + phase in {"PRE_PUBLICATION", "POST_PUBLICATION"}, + "publication phase is invalid", + failures, + ) return failures @@ -96,13 +452,26 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--tag", required=True) + parser.add_argument("--phase", choices=("PRE_PUBLICATION", "POST_PUBLICATION"), required=True) + parser.add_argument("--version-doi") + parser.add_argument("--release-date") + parser.add_argument("--publication-record", type=Path) + parser.add_argument("--release-contract", type=Path) args = parser.parse_args() - failures = check_publication_metadata(args.root, args.tag) + failures = check_publication_metadata( + args.root, + args.tag, + args.phase, + version_doi=args.version_doi, + release_date=args.release_date, + publication_record=args.publication_record, + release_contract=args.release_contract, + ) for failure in failures: print(f"PUBLICATION METADATA FAIL: {failure}", file=sys.stderr) if failures: return 1 - print(f"PUBLICATION METADATA: PASS for {args.tag}") + print(f"PUBLICATION METADATA: PASS for {args.tag} in {args.phase}") return 0 diff --git a/scripts/public_release_scope.py b/scripts/public_release_scope.py new file mode 100644 index 00000000..2938a938 --- /dev/null +++ b/scripts/public_release_scope.py @@ -0,0 +1,24 @@ +"""Fail-closed policy for selected distributions versus whole-repository archives.""" + +from __future__ import annotations + +from typing import Literal + + +def publication_scope_decision( + scope: str, + g03_status: str, + whole_repository_archive_cleared: bool, +) -> Literal["ALLOWED", "DENIED"]: + """Decide one explicitly named scope without conflating its clearance gates.""" + if scope == "SELECTED_DISTRIBUTION": + if g03_status == "FAIL_PRESERVED" and whole_repository_archive_cleared is False: + return "ALLOWED" + if g03_status == "PASS" and whole_repository_archive_cleared is True: + return "ALLOWED" + return "DENIED" + if scope == "WHOLE_REPOSITORY_ARCHIVE": + if g03_status == "PASS" and whole_repository_archive_cleared is True: + return "ALLOWED" + return "DENIED" + return "DENIED" diff --git a/scripts/verify_authorized_release_artifacts.py b/scripts/verify_authorized_release_artifacts.py index 88282544..eb6e799d 100644 --- a/scripts/verify_authorized_release_artifacts.py +++ b/scripts/verify_authorized_release_artifacts.py @@ -4,14 +4,16 @@ import argparse import hashlib +import importlib import importlib.metadata import json import os +import re import shutil import subprocess import sys from pathlib import Path -from typing import Any +from typing import Any, Callable, cast if __package__: from scripts.git_tools import git_run @@ -34,36 +36,18 @@ write_record, ) +_scope_policy = cast( + Callable[[str, str, bool], str], + getattr( + importlib.import_module( + "scripts.public_release_scope" if __package__ else "public_release_scope" + ), + "publication_scope_decision", + ), +) -OWNER_REAUTHORIZATION_PATH = "qualification/0_2_10/owner_publication_reauthorization_e535ff.json" -OWNER_REAUTHORIZATION_COMMIT = "a2eb6ac4b091725bd05393fb3b1115f836953e81" -OWNER_REAUTHORIZATION_SHA256 = "485e7c2f612b470be4443e36e1ec46ec43346828338212093c58f63039cb4572" -OWNER_REAUTHORIZATION_ID = "QF-0210-OWNER-PUBLICATION-REAUTHORIZATION-E535FF63464D" -AUTHORIZED_SOURCE_SHA = "e535ff63464ddd7d76c2898df25470350b5154f1" -AUTHORIZED_TAG = "v0.2.10" -AUTHORIZED_ARTIFACTS = { - "wheel": { - "filename": "qf_solver-0.2.10-py3-none-any.whl", - "bytes": 1509431, - "sha256": "61641c97860cb25fb4c7a01e5c7ab724dc58616b97802cea2b9926fd10912454", - }, - "sdist": { - "filename": "qf_solver-0.2.10.tar.gz", - "bytes": 1114201, - "sha256": "82e54d0d5d05c1ff224e1af72f90d0bbd84e13e93f7cc5f881ccd16242e4f3f3", - }, - "manifest": { - "filename": "qf_solver-0.2.10-e535ff-SHA256SUMS.txt", - "bytes": 190, - "sha256": "a8d7e13cd3bf5292b9b941975d569512fed9c4985a9f9d7cd8a5c9efe9af6287", - }, -} -SUPERSEDED_OWNER_RECORD = { - "path": "qualification/0_2_10/owner_publication_reauthorization_b453265d.json", - "commit_sha": "e350ef870dfa5182a4e25c79c07ce04b974d5568", - "sha256": "8d684c17345f827ea70ab647c2e38cd4b3b711e0e8925207c09881f3582b20a7", - "historical_record_preserved": True, -} + +SUPPORTED_RELEASE_CONTRACT_SCHEMA_VERSIONS = {1, 2} def _relative_path(value: str) -> str: @@ -84,11 +68,14 @@ def _committed_bytes(root: Path, head: str, relative: str) -> bytes: return committed -def _check_tool_bindings(root: Path, head: str, bindings: Any) -> None: +def _check_tool_bindings(root: Path, commit: str, bindings: Any) -> None: if not isinstance(bindings, dict) or not bindings: raise ValueError("The release contract must bind its release verification tools.") for relative, expected in bindings.items(): - observed = hashlib.sha256(_committed_bytes(root, head, relative)).hexdigest() + path = _relative_path(relative) + if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{64}", expected): + raise ValueError(f"Release tool binding is malformed: {relative}") + observed = hashlib.sha256(git_run(["show", f"{commit}:{path}"], cwd=root, check=True).stdout).hexdigest() if observed != expected: raise ValueError(f"Release tool binding mismatch: {relative}") @@ -164,37 +151,76 @@ def verify_artifact_set(dist: Path, contract: dict[str, Any]) -> dict[str, Any]: def validate_owner_artifact_scope(contract: dict[str, Any], owner_record: dict[str, Any]) -> None: - """Require a prospective release to stay inside the Owner's exact source and byte authorization.""" + """Require the contract and prospective Owner record to agree on exact release identity and bytes.""" + schema_version = contract.get("schema_version") + if schema_version not in SUPPORTED_RELEASE_CONTRACT_SCHEMA_VERSIONS: + raise ValueError("Unknown selected-release contract schema; refusing implicit fallback.") + + version = contract.get("package_version") + if not isinstance(version, str) or not re.fullmatch(r"\d+\.\d+\.\d+", version): + raise ValueError("The selected-release contract has no stable package version.") + if contract.get("status") != "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE": + raise ValueError("The selected-release contract is not explicitly Owner-authorized.") + expected_decisions = { + f"AUTHORIZE_V{version.replace('.', '_')}_PUBLICATION", + "AUTHORIZE_SELECTED_PACKAGE_RELEASE", + } approved_source = owner_record.get("authorized_source_sha") if ( - owner_record.get("record_id") != OWNER_REAUTHORIZATION_ID - or owner_record.get("decision") != "AUTHORIZE_V0_2_10_PUBLICATION" - or owner_record.get("version") != "0.2.10" - or approved_source != AUTHORIZED_SOURCE_SHA - or contract.get("source_sha") != AUTHORIZED_SOURCE_SHA + owner_record.get("decision") not in expected_decisions + or owner_record.get("version") != version + or owner_record.get("status") not in { + "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "OWNER_REAUTHORIZED_LATER_AUDITED_RELEASE_CANDIDATE", + } + or not isinstance(approved_source, str) + or not re.fullmatch(r"[0-9a-f]{40}", approved_source) + or contract.get("source_sha") != approved_source or contract.get("tag_target_sha") != approved_source or owner_record.get("authorized_tag_target_sha") != approved_source - or owner_record.get("authorized_tag") != AUTHORIZED_TAG - or contract.get("release_tag") != AUTHORIZED_TAG + or owner_record.get("authorized_tag") != f"v{version}" + or contract.get("release_tag") != f"v{version}" ): raise ValueError("The candidate source differs from the exact source accepted by the Owner.") artifacts = contract.get("audited_artifacts") approved_artifacts = owner_record.get("authorized_artifacts") - if not isinstance(artifacts, dict) or not isinstance(approved_artifacts, dict): - raise ValueError("The prospective release does not bind the Owner-authorized package bytes.") - for kind in ("wheel", "sdist", "manifest"): + if not isinstance(artifacts, dict) or set(artifacts) != {"wheel", "sdist"}: + raise ValueError("The selected-release contract must bind exactly one wheel and one sdist.") + if not isinstance(approved_artifacts, dict) or set(approved_artifacts) != {"wheel", "sdist", "manifest"}: + raise ValueError("The Owner record must bind exactly the selected wheel, sdist and manifest.") + manifest = contract.get("sha256_manifest") + if not isinstance(manifest, dict): + raise ValueError("The selected-release contract has no checksum manifest binding.") + candidate_bindings = {"wheel": artifacts["wheel"], "sdist": artifacts["sdist"], "manifest": manifest} + for kind, candidate in candidate_bindings.items(): approved = approved_artifacts.get(kind) - candidate = contract.get("sha256_manifest") if kind == "manifest" else artifacts.get(kind) - if not isinstance(approved, dict) or not isinstance(candidate, dict) or approved != AUTHORIZED_ARTIFACTS[kind]: - raise ValueError(f"The Owner authorization lacks the exact {kind} artifact binding.") + if not isinstance(approved, dict) or set(approved) != {"filename", "bytes", "sha256"}: + raise ValueError(f"The Owner authorization lacks a complete {kind} artifact binding.") + if not isinstance(candidate, dict) or set(candidate) != {"filename", "bytes", "sha256"}: + raise ValueError(f"The selected-release contract has a malformed {kind} binding.") + candidate_size = candidate.get("bytes") if ( - candidate.get("filename") != approved.get("filename") - or candidate.get("bytes") != approved.get("bytes") - or candidate.get("sha256") != approved.get("sha256") + not isinstance(candidate.get("filename"), str) + or not isinstance(candidate_size, int) + or isinstance(candidate_size, bool) + or candidate_size <= 0 + or not isinstance(candidate.get("sha256"), str) + or not re.fullmatch(r"[0-9a-f]{64}", candidate["sha256"]) ): + raise ValueError(f"The selected-release contract has an invalid {kind} binding.") + if candidate != approved: raise ValueError(f"The candidate {kind} bytes differ from the exact artifact accepted by the Owner.") + if ( + not artifacts["wheel"]["filename"].startswith(f"qf_solver-{version}-") + or not artifacts["wheel"]["filename"].endswith(".whl") + or artifacts["sdist"]["filename"] != f"qf_solver-{version}.tar.gz" + or not manifest["filename"].startswith(f"qf_solver-{version}-") + or not manifest["filename"].endswith("-SHA256SUMS.txt") + ): + raise ValueError("Selected artifact filenames do not identify the authorized package version.") + required_authorization = { "selected_package_publication_allowed": True, "tag_creation_allowed": True, @@ -246,6 +272,10 @@ def validate_owner_artifact_scope(contract: dict[str, Any], owner_record: dict[s raise ValueError("The Owner record changes the preserved release boundary.") if owner_record.get("whole_repository_archive_publication_allowed") is not False: raise ValueError("The Owner record permits whole-repository archive publication despite G03.") + if any(contract.get(key) != expected for key, expected in required_preserved.items() if key != "numeric_maturity_promotion_allowed" and key != "historical_result_reclassification_allowed" and key != "ledger_update_allowed"): + raise ValueError("The release contract does not preserve G03 and WP14 archive boundaries.") + if contract.get("whole_repository_archive_publication_allowed") is not False: + raise ValueError("The release contract permits a whole-repository archive despite G03.") expected_gate_summary = { "g03_status": "FAIL_PRESERVED", "whole_repository_archive_cleared": False, @@ -254,23 +284,78 @@ def validate_owner_artifact_scope(contract: dict[str, Any], owner_record: dict[s if owner_record.get("preserved_gates") != expected_gate_summary: raise ValueError("The Owner record's preserved gate summary is inconsistent.") + scope = contract.get("publication_scope") + if schema_version == 1 and scope is None: + # Explicit adapter for the unchanged 0.2.10 contract shape. + scope = "SELECTED_DISTRIBUTION" + if scope != "SELECTED_DISTRIBUTION": + raise ValueError("This verifier authorizes only an explicit selected-distribution scope.") + if schema_version == 2 and owner_record.get("publication_scope") != scope: + raise ValueError("The Owner record and release contract must name the same selected scope.") + g03_status = contract.get("g03_status") + archive_cleared = contract.get("whole_repository_archive_cleared") + if not isinstance(scope, str) or not isinstance(g03_status, str) or not isinstance(archive_cleared, bool): + raise ValueError("The selected-release scope and preserved G03 state are malformed.") + if _scope_policy(scope, g03_status, archive_cleared) != "ALLOWED": + raise ValueError("The selected-release policy does not clear the declared publication scope.") + + release_gates = contract.get("release_gates") + owner_gates = owner_record.get("release_gates") + if schema_version == 2 or release_gates is not None or owner_gates is not None: + if not isinstance(release_gates, dict) or not isinstance(owner_gates, dict) or release_gates != owner_gates: + raise ValueError("Owner and release-contract gate decisions differ.") + required = {f"QF0211_G0{number}" for number in range(1, 9)} + if set(release_gates) != required: + raise ValueError("A 0.2.11 release contract must bind exactly QF0211 gates G01 through G08.") + if any(release_gates[f"QF0211_G0{number}"] not in {"PASS", "INHERITED_PASS"} for number in range(1, 9)): + raise ValueError("A required QF0211 release gate is not passing.") + if version == "0.2.11": + expected_maturity = { + "rotating_modal": "EXPERIMENTAL", + "campbell": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY", + } + if contract.get("maturities") != expected_maturity or owner_record.get("maturities") != expected_maturity: + raise ValueError("The 0.2.11 contract must preserve the bounded experimental maturity and evidence claims.") + expected_phase = { + "current_phase": "PRE_PUBLICATION", + "version_doi": "NOT_ASSIGNED", + "release_date": "NOT_SET", + "concept_doi": "10.5281/zenodo.22697897", + } + if ( + contract.get("publication_phase") != expected_phase + or owner_record.get("publication_phase") != expected_phase + ): + raise ValueError("The 0.2.11 authorization must explicitly remain in the no-DOI prepublication phase.") + def _validate_superseded_owner_record( root: Path, head: str, owner_record: dict[str, Any], contract: dict[str, Any], ) -> None: previous = owner_record.get("superseded_record") previous_contract = contract.get("superseded_owner_authorization") + if previous is None and previous_contract is None: + if owner_record.get("supersedes_for_release") is not None: + raise ValueError("Owner record names a superseded decision without binding its exact record.") + return if not isinstance(previous, dict) or not isinstance(previous_contract, dict): - raise ValueError("The prospective reauthorization must identify the preserved historical Owner record.") + raise ValueError("Superseded Owner decisions must be bound on both records or neither.") path = _relative_path(previous.get("path", "")) - if previous != SUPERSEDED_OWNER_RECORD or previous != previous_contract: + if previous != previous_contract: raise ValueError("The superseded Owner record reference differs from the prospective contract.") commit = previous.get("commit_sha") if ( - owner_record.get("supersedes_for_release") != "QF-0210-OWNER-PUBLICATION-REAUTHORIZATION-B453265D" + not isinstance(commit, str) + or not re.fullmatch(r"[0-9a-f]{40}", commit) + or not isinstance(previous.get("sha256"), str) + or not re.fullmatch(r"[0-9a-f]{64}", previous["sha256"]) + or previous.get("historical_record_preserved") is not True + or not isinstance(owner_record.get("supersedes_for_release"), str) or git_run(["merge-base", "--is-ancestor", commit, contract["source_sha"]], cwd=root).returncode ): - raise ValueError("The reauthorization does not supersede the exact historical decision for this release.") + raise ValueError("The reauthorization does not bind an exact prior decision in source history.") historical_bytes = git_run(["show", f"{commit}:{path}"], cwd=root, check=True).stdout current_bytes = _committed_bytes(root, head, path) if ( @@ -303,6 +388,9 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> relative_contract = contract_path.relative_to(root).as_posix() relative_candidate = candidate_path.relative_to(root).as_posix() release_contract = json.loads(_committed_bytes(root, head, relative_contract)) + schema_version = release_contract.get("schema_version") + if schema_version not in SUPPORTED_RELEASE_CONTRACT_SCHEMA_VERSIONS: + raise ValueError("Unknown selected-release contract schema; refusing implicit fallback.") source_contract = release_contract.get("source_contract", {}) if source_contract.get("path") != relative_candidate: raise ValueError("The supplied frozen package contract path differs from the authorized release contract.") @@ -337,12 +425,13 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> owner_path = _relative_path(owner.get("path", "")) owner_commit = owner.get("commit_sha") if ( - owner_path != OWNER_REAUTHORIZATION_PATH - or owner_commit != OWNER_REAUTHORIZATION_COMMIT - or owner.get("sha256") != OWNER_REAUTHORIZATION_SHA256 - or owner.get("record_id") != OWNER_REAUTHORIZATION_ID + not isinstance(owner_commit, str) + or not re.fullmatch(r"[0-9a-f]{40}", owner_commit) + or not isinstance(owner.get("sha256"), str) + or not re.fullmatch(r"[0-9a-f]{64}", owner["sha256"]) + or not isinstance(owner.get("record_id"), str) ): - raise ValueError("The release contract does not bind the exact prospective Owner reauthorization.") + raise ValueError("The release contract does not bind an exact prospective Owner authorization.") owner_bytes = _committed_bytes(root, head, owner_path) owner_record = json.loads(owner_bytes) recorded_owner_bytes = git_run(["show", f"{owner_commit}:{owner_path}"], cwd=root, check=True).stdout @@ -356,9 +445,12 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> if ( owner.get("record_id") != owner_record.get("record_id") or owner.get("supersedes_for_release") != owner_record.get("supersedes_for_release") - or owner_record.get("status") != "OWNER_REAUTHORIZED_LATER_AUDITED_RELEASE_CANDIDATE" + or owner_record.get("status") not in { + "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "OWNER_REAUTHORIZED_LATER_AUDITED_RELEASE_CANDIDATE", + } ): - raise ValueError("The contract does not identify the exact prospective Owner reauthorization.") + raise ValueError("The contract does not identify the exact prospective Owner authorization.") _validate_superseded_owner_record(root, head, owner_record, release_contract) validate_owner_artifact_scope(release_contract, owner_record) resolution = release_contract.get("authorization_resolution", {}) @@ -371,7 +463,10 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> "candidate_manifest_sha256": release_contract["sha256_manifest"]["sha256"], } if ( - resolution.get("status") != "RESOLVED_BY_OWNER_REAUTHORIZATION" + resolution.get("status") not in { + "RESOLVED_BY_OWNER_REAUTHORIZATION", + "RESOLVED_BY_EXACT_OWNER_AUTHORIZATION", + } or resolution.get("owner_approved_source_sha") != source_sha or resolution.get("candidate_source_sha") != source_sha or any(resolution.get(key) != value for key, value in resolution_hashes.items()) @@ -402,7 +497,19 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> raise ValueError("G03 and WP14 must remain explicitly uncleared and unpromoted.") bindings = release_contract.get("tool_bindings") - _check_tool_bindings(root, head, bindings) + if schema_version == 1: + # Version 1 was frozen with its tools in the last commit that changed + # that historical contract. Verify those historical Git blobs rather + # than silently comparing them with today's release tooling. + frozen_contract_commit = git_run( + ["log", "-1", "--format=%H", head, "--", relative_contract], + cwd=root, + check=True, + text=True, + ).stdout.strip() + _check_tool_bindings(root, frozen_contract_commit, bindings) + else: + _check_tool_bindings(root, head, bindings) manifest_path = _relative_path(release_contract.get("manifest_repository_path", "")) manifest_bytes = _committed_bytes(root, head, manifest_path) manifest_binding = release_contract["sha256_manifest"] diff --git a/tests/unit/test_authorized_release_artifacts.py b/tests/unit/test_authorized_release_artifacts.py index 24f12be5..14d017e2 100644 --- a/tests/unit/test_authorized_release_artifacts.py +++ b/tests/unit/test_authorized_release_artifacts.py @@ -1,160 +1,254 @@ -"""Release-asset verification accepts only exact Owner-authorized selected bytes.""" +"""Selected release validation is contract-driven and fails closed.""" from __future__ import annotations import hashlib +import json from pathlib import Path import pytest -from scripts.verify_authorized_release_artifacts import verify_artifact_set, validate_owner_artifact_scope +from scripts.verify_authorized_release_artifacts import ( + validate_owner_artifact_scope, + verify_artifact_set, +) -def _sha(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() +ROOT = Path(__file__).resolve().parents[2] +AUTHORIZATION = { + "selected_package_publication_allowed": True, + "tag_creation_allowed": True, + "pypi_publication_allowed": True, + "github_release_allowed": True, + "zenodo_selected_artifact_publication_allowed": True, + "whole_repository_archive_cleared": False, + "whole_repository_archive_publication_allowed": False, + "ledger_update_allowed": False, + "wp14_promotion_allowed": False, + "numeric_maturity_promotion_allowed": False, + "historical_result_reclassification_allowed": False, + "certification_or_universal_validation_claims_allowed": False, +} -def _fixture(tmp_path: Path) -> tuple[Path, dict[str, object]]: - tmp_path.mkdir(parents=True, exist_ok=True) +def _sha(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest() + + +def _fixture(tmp_path: Path, version: str = "0.2.11", schema_version: int = 2): dist = tmp_path / "dist" - dist.mkdir() - wheel = b"audited wheel bytes" - sdist = b"audited source bytes" - wheel_name = "qf_solver-0.2.10-py3-none-any.whl" - sdist_name = "qf_solver-0.2.10.tar.gz" - manifest_name = "qf_solver-0.2.10-SHA256SUMS.txt" + dist.mkdir(parents=True, exist_ok=True) + source = ("a" if version == "0.2.10" else "b") * 40 + wheel_name = f"qf_solver-{version}-py3-none-any.whl" + sdist_name = f"qf_solver-{version}.tar.gz" + manifest_name = f"qf_solver-{version}-test-SHA256SUMS.txt" + wheel = b"exact selected wheel fixture" + sdist = b"exact selected source fixture" (dist / wheel_name).write_bytes(wheel) (dist / sdist_name).write_bytes(sdist) manifest = f"{_sha(wheel)} {wheel_name}\n{_sha(sdist)} {sdist_name}\n".encode("ascii") (dist / manifest_name).write_bytes(manifest) - contract: dict[str, object] = { - "audited_artifacts": { - "wheel": {"filename": wheel_name, "bytes": len(wheel), "sha256": _sha(wheel)}, - "sdist": {"filename": sdist_name, "bytes": len(sdist), "sha256": _sha(sdist)}, - }, - "sha256_manifest": { - "filename": manifest_name, "bytes": len(manifest), "sha256": _sha(manifest), - }, + artifacts = { + "wheel": {"filename": wheel_name, "bytes": len(wheel), "sha256": _sha(wheel)}, + "sdist": {"filename": sdist_name, "bytes": len(sdist), "sha256": _sha(sdist)}, } - return dist, contract - - -def test_only_exact_packages_and_checksum_manifest_are_accepted(tmp_path: Path) -> None: - dist, contract = _fixture(tmp_path) - result = verify_artifact_set(dist, contract) - assert result["status"] == "PASS_EXACT_RELEASE_ASSETS" - assert len(result["files"]) == 3 - - -def test_changed_package_bytes_and_manifest_are_rejected(tmp_path: Path) -> None: - dist, contract = _fixture(tmp_path) - (dist / "qf_solver-0.2.10.tar.gz").write_bytes(b"changed bytes") - with pytest.raises(ValueError, match="differs from the authorized contract"): - verify_artifact_set(dist, contract) - - dist, contract = _fixture(tmp_path / "second") - (dist / "qf_solver-0.2.10-SHA256SUMS.txt").write_text("0" * 64, encoding="ascii") - with pytest.raises(ValueError, match="differs from the authorized contract"): - verify_artifact_set(dist, contract) - - -def test_extra_assets_are_not_silently_published(tmp_path: Path) -> None: - dist, contract = _fixture(tmp_path) - (dist / "repository-source.zip").write_bytes(b"uncleared source archive") - with pytest.raises(ValueError, match="only the two selected packages"): - verify_artifact_set(dist, contract) - - -def test_owner_authorization_pins_exact_source_and_package_hashes() -> None: - source = "e535ff63464ddd7d76c2898df25470350b5154f1" - contract: dict[str, object] = { + manifest_binding = {"filename": manifest_name, "bytes": len(manifest), "sha256": _sha(manifest)} + record_id = f"QF-TEST-OWNER-{version}" + gates = {f"QF0211_G0{number}": "PASS" if number > 6 else "INHERITED_PASS" for number in range(1, 9)} + contract = { + "schema_version": schema_version, + "status": "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "package_version": version, + "release_tag": f"v{version}", "source_sha": source, "tag_target_sha": source, - "release_tag": "v0.2.10", - "audited_artifacts": { - "wheel": {"filename": "qf_solver-0.2.10-py3-none-any.whl", "bytes": 1509431, "sha256": "61641c97860cb25fb4c7a01e5c7ab724dc58616b97802cea2b9926fd10912454"}, - "sdist": {"filename": "qf_solver-0.2.10.tar.gz", "bytes": 1114201, "sha256": "82e54d0d5d05c1ff224e1af72f90d0bbd84e13e93f7cc5f881ccd16242e4f3f3"}, - }, - "sha256_manifest": {"filename": "qf_solver-0.2.10-e535ff-SHA256SUMS.txt", "bytes": 190, "sha256": "a8d7e13cd3bf5292b9b941975d569512fed9c4985a9f9d7cd8a5c9efe9af6287"}, + "publication_scope": "SELECTED_DISTRIBUTION" if schema_version == 2 else None, + "audited_artifacts": artifacts, + "sha256_manifest": manifest_binding, + "g03_status": "FAIL_PRESERVED", + "whole_repository_archive_cleared": False, + "whole_repository_archive_publication_allowed": False, + "wp14_status": "HOLD_NOT_PROMOTED", + "authorization": AUTHORIZATION.copy(), } owner = { - "record_id": "QF-0210-OWNER-PUBLICATION-REAUTHORIZATION-E535FF63464D", - "decision": "AUTHORIZE_V0_2_10_PUBLICATION", - "version": "0.2.10", + "record_id": record_id, + "decision": "AUTHORIZE_SELECTED_PACKAGE_RELEASE", + "status": "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "version": version, "authorized_source_sha": source, - "authorized_tag": "v0.2.10", + "authorized_tag": f"v{version}", "authorized_tag_target_sha": source, - "authorized_wheel_sha256": "61641c97860cb25fb4c7a01e5c7ab724dc58616b97802cea2b9926fd10912454", - "authorized_sdist_sha256": "82e54d0d5d05c1ff224e1af72f90d0bbd84e13e93f7cc5f881ccd16242e4f3f3", - "authorized_manifest_sha256": "a8d7e13cd3bf5292b9b941975d569512fed9c4985a9f9d7cd8a5c9efe9af6287", - "authorized_artifacts": { - "wheel": {"filename": "qf_solver-0.2.10-py3-none-any.whl", "bytes": 1509431, "sha256": "61641c97860cb25fb4c7a01e5c7ab724dc58616b97802cea2b9926fd10912454"}, - "sdist": {"filename": "qf_solver-0.2.10.tar.gz", "bytes": 1114201, "sha256": "82e54d0d5d05c1ff224e1af72f90d0bbd84e13e93f7cc5f881ccd16242e4f3f3"}, - "manifest": {"filename": "qf_solver-0.2.10-e535ff-SHA256SUMS.txt", "bytes": 190, "sha256": "a8d7e13cd3bf5292b9b941975d569512fed9c4985a9f9d7cd8a5c9efe9af6287"}, - }, - "authorization": { - "selected_package_publication_allowed": True, - "tag_creation_allowed": True, - "pypi_publication_allowed": True, - "github_release_allowed": True, - "zenodo_selected_artifact_publication_allowed": True, - "whole_repository_archive_cleared": False, - "whole_repository_archive_publication_allowed": False, - "ledger_update_allowed": False, - "wp14_promotion_allowed": False, - "numeric_maturity_promotion_allowed": False, - "historical_result_reclassification_allowed": False, - "certification_or_universal_validation_claims_allowed": False, - }, + "authorized_artifacts": {**artifacts, "manifest": manifest_binding}, + "authorized_wheel_sha256": artifacts["wheel"]["sha256"], + "authorized_sdist_sha256": artifacts["sdist"]["sha256"], + "authorized_manifest_sha256": manifest_binding["sha256"], + "authorization": AUTHORIZATION.copy(), "selected_package_publication_allowed": True, "tag_creation_allowed": True, "pypi_publication_allowed": True, "github_release_allowed": True, "zenodo_selected_artifact_publication_allowed": True, - "g03_status": "FAIL_PRESERVED", "whole_repository_archive_cleared": False, "whole_repository_archive_publication_allowed": False, - "wp14_status": "HOLD_NOT_PROMOTED", "wp14_promotion_allowed": False, "numeric_maturity_promotion_allowed": False, "historical_result_reclassification_allowed": False, "ledger_update_allowed": False, "certification_or_universal_validation_claims_allowed": False, + "g03_status": "FAIL_PRESERVED", + "wp14_status": "HOLD_NOT_PROMOTED", "preserved_gates": { "g03_status": "FAIL_PRESERVED", "whole_repository_archive_cleared": False, "wp14_status": "HOLD_NOT_PROMOTED", }, } + if version == "0.2.11": + maturities = { + "rotating_modal": "EXPERIMENTAL", + "campbell": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY", + } + contract["maturities"] = maturities + contract["release_gates"] = gates + contract["publication_phase"] = { + "current_phase": "PRE_PUBLICATION", + "version_doi": "NOT_ASSIGNED", + "release_date": "NOT_SET", + "concept_doi": "10.5281/zenodo.22697897", + } + owner["publication_scope"] = "SELECTED_DISTRIBUTION" + owner["publication_phase"] = contract["publication_phase"] + owner["maturities"] = maturities + owner["release_gates"] = gates + return dist, contract, owner + + +def test_exact_selected_assets_and_manifest_are_accepted(tmp_path: Path) -> None: + dist, contract, _ = _fixture(tmp_path) + result = verify_artifact_set(dist, contract) + assert result["status"] == "PASS_EXACT_RELEASE_ASSETS" + assert len(result["files"]) == 3 + + +def test_changed_bytes_missing_artifact_or_extra_archive_are_rejected(tmp_path: Path) -> None: + dist, contract, _ = _fixture(tmp_path) + (dist / "qf_solver-0.2.11.tar.gz").write_bytes(b"changed source bytes") + with pytest.raises(ValueError, match="differs from the authorized contract"): + verify_artifact_set(dist, contract) + + dist, contract, _ = _fixture(tmp_path / "missing") + (dist / "qf_solver-0.2.11.tar.gz").unlink() + with pytest.raises(ValueError, match="only the two selected packages"): + verify_artifact_set(dist, contract) + + dist, contract, _ = _fixture(tmp_path / "extra") + (dist / "repository-source.zip").write_bytes(b"uncleared repository archive") + with pytest.raises(ValueError, match="only the two selected packages"): + verify_artifact_set(dist, contract) + + +@pytest.mark.parametrize("version,schema_version", [("0.2.10", 1), ("0.2.11", 2)]) +def test_versioned_owner_contracts_authorize_exact_selected_bytes( + tmp_path: Path, version: str, schema_version: int, +) -> None: + _, contract, owner = _fixture(tmp_path, version, schema_version) + validate_owner_artifact_scope(contract, owner) + + +def test_historical_0210_contract_remains_readable_without_rewriting_it() -> None: + contract_path = ROOT / "qualification/0_2_10/authorized_selected_package_release_contract_e535ff.json" + owner_path = ROOT / "qualification/0_2_10/owner_publication_reauthorization_e535ff.json" + contract = json.loads(contract_path.read_text(encoding="utf-8")) + owner = json.loads(owner_path.read_text(encoding="utf-8")) + validate_owner_artifact_scope(contract, owner) + assert contract["source_sha"] == "e535ff63464ddd7d76c2898df25470350b5154f1" + assert owner["authorized_source_sha"] == contract["source_sha"] + + +def test_actual_0211_owner_authorization_matches_the_frozen_candidate() -> None: + owner_path = ROOT / "qualification/0_2_11/owner_publication_authorization_afd2efa4.json" + owner = json.loads(owner_path.read_text(encoding="utf-8")) + contract = { + "schema_version": 2, + "status": owner["status"], + "package_version": owner["version"], + "release_tag": owner["authorized_tag"], + "source_sha": owner["authorized_source_sha"], + "tag_target_sha": owner["authorized_tag_target_sha"], + "publication_scope": owner["publication_scope"], + "audited_artifacts": {kind: owner["authorized_artifacts"][kind] for kind in ("wheel", "sdist")}, + "sha256_manifest": owner["authorized_artifacts"]["manifest"], + "authorization": owner["authorization"], + "g03_status": owner["g03_status"], + "whole_repository_archive_cleared": owner["whole_repository_archive_cleared"], + "whole_repository_archive_publication_allowed": owner["whole_repository_archive_publication_allowed"], + "wp14_status": owner["wp14_status"], + "release_gates": owner["release_gates"], + "maturities": owner["maturities"], + "publication_phase": owner["publication_phase"], + } validate_owner_artifact_scope(contract, owner) + assert owner["authorized_source_sha"] == "afd2efa469a18e66581720ff4801f50de7d29eb4" + assert owner["g03_status"] == "FAIL_PRESERVED" + assert owner["whole_repository_archive_cleared"] is False + assert owner["wp14_status"] == "HOLD_NOT_PROMOTED" + +def test_changed_source_version_or_hash_fails_closed(tmp_path: Path) -> None: + _, contract, owner = _fixture(tmp_path) changed_source = {**contract, "source_sha": "c" * 40} with pytest.raises(ValueError, match="exact source accepted by the Owner"): validate_owner_artifact_scope(changed_source, owner) + wrong_version = {**contract, "package_version": "0.2.12"} + with pytest.raises(ValueError, match="exact source accepted by the Owner"): + validate_owner_artifact_scope(wrong_version, owner) + changed_artifacts = {**contract, "audited_artifacts": { **contract["audited_artifacts"], - "wheel": {"filename": "qf_solver-0.2.10-py3-none-any.whl", "bytes": 1509431, "sha256": "3" * 64}, + "wheel": {**contract["audited_artifacts"]["wheel"], "sha256": "3" * 64}, }} with pytest.raises(ValueError, match="exact artifact accepted by the Owner"): validate_owner_artifact_scope(changed_artifacts, owner) - changed_manifest = {**contract, "sha256_manifest": { - **contract["sha256_manifest"], "sha256": "4" * 64, - }} - with pytest.raises(ValueError, match="exact artifact accepted by the Owner"): - validate_owner_artifact_scope(changed_manifest, owner) - changed_flat_hash = {**owner, "authorized_wheel_sha256": "4" * 64} - with pytest.raises(ValueError, match="direct artifact hashes"): - validate_owner_artifact_scope(contract, changed_flat_hash) +def test_unknown_contract_schema_owner_or_archive_scope_is_rejected(tmp_path: Path) -> None: + _, contract, owner = _fixture(tmp_path) + with pytest.raises(ValueError, match="Unknown selected-release contract schema"): + validate_owner_artifact_scope({**contract, "schema_version": 99}, owner) - revoked_authorization = {**owner, "authorization": { - **owner["authorization"], "pypi_publication_allowed": False, - }} + with pytest.raises(ValueError, match="only an explicit selected-distribution scope"): + validate_owner_artifact_scope({**contract, "publication_scope": "WHOLE_REPOSITORY_ARCHIVE"}, owner) + + g03_cleared = {**contract, "g03_status": "PASS", "whole_repository_archive_cleared": True} + with pytest.raises(ValueError, match="release contract does not preserve G03"): + validate_owner_artifact_scope(g03_cleared, owner) + + +def test_owner_permissions_cannot_promote_wp14_or_maturity(tmp_path: Path) -> None: + _, contract, owner = _fixture(tmp_path) + denied = {**owner, "authorization": {**owner["authorization"], "whole_repository_archive_publication_allowed": True}} with pytest.raises(ValueError, match="permissions broaden or weaken"): - validate_owner_artifact_scope(contract, revoked_authorization) + validate_owner_artifact_scope(contract, denied) - cleared_g03 = {**owner, "g03_status": "PASS"} + promoted = {**owner, "wp14_status": "PASS"} with pytest.raises(ValueError, match="preserved release boundary"): - validate_owner_artifact_scope(contract, cleared_g03) + validate_owner_artifact_scope(contract, promoted) + + +def test_0211_owner_contract_requires_matching_scope_gates_and_prepublication_state(tmp_path: Path) -> None: + _, contract, owner = _fixture(tmp_path) + with pytest.raises(ValueError, match="gate decisions differ"): + validate_owner_artifact_scope({**contract, "release_gates": None}, owner) + + with pytest.raises(ValueError, match="same selected scope"): + validate_owner_artifact_scope(contract, {**owner, "publication_scope": "WHOLE_REPOSITORY_ARCHIVE"}) + + fake_doi = {**owner, "publication_phase": { + **owner["publication_phase"], "version_doi": "10.5281/zenodo.12345678" + }} + with pytest.raises(ValueError, match="no-DOI prepublication phase"): + validate_owner_artifact_scope(contract, fake_doi) diff --git a/tests/unit/test_publication_metadata_gate.py b/tests/unit/test_publication_metadata_gate.py index 36b6ba30..f92bec64 100644 --- a/tests/unit/test_publication_metadata_gate.py +++ b/tests/unit/test_publication_metadata_gate.py @@ -1,7 +1,8 @@ -"""Publication must reject a tagged source with stale public-facing metadata.""" +"""Publication metadata must match the current release phase and evidence.""" from __future__ import annotations +import json from pathlib import Path import yaml @@ -10,62 +11,303 @@ ROOT = Path(__file__).resolve().parents[2] +VERSION = "0.2.11" +TAG = "v0.2.11" +VERSION_DOI = "10.5281/zenodo.99999999" +RELEASE_DATE = "2026-10-07" -def test_unreleased_candidate_is_not_mistaken_for_a_published_tag() -> None: - failures = check_publication_metadata(ROOT, "v0.2.11") - assert "CITATION.cff does not identify this version" not in failures - assert "CITATION.cff needs a release date" in failures - assert "CITATION.cff needs an assigned version DOI" in failures - assert "CHANGELOG.md has no released entry for this version" in failures - assert "README.md release line does not identify this version" in failures - # Final-state documentation is not proof of publication. The missing - # release date, assigned DOI and released changelog keep this gate closed. - assert "README.md still denies publication of this version" not in failures - - -def test_metadata_gate_rejects_nonstable_or_mismatched_tag() -> None: - assert check_publication_metadata(ROOT, "../../v0.2.9") == [ - "release tag must be a stable vMAJOR.MINOR.PATCH tag" +def _write_release_copy(root: Path, phase: str, *, doi: str | None = None, released: bool = False) -> None: + root.mkdir(parents=True, exist_ok=True) + citation_fields = [ + "cff-version: 1.2.0", + f'message: "Use concept DOI 10.5281/zenodo.22697897. {"Published version DOI is available." if doi else "This metadata identifies version 0.2.11 without asserting a release date or version DOI."}"', + "type: software", + f'version: "{VERSION}"', ] - assert "pyproject.toml version differs from release tag" in check_publication_metadata(ROOT, "v0.2.9") - assert "pyproject.toml version differs from release tag" in check_publication_metadata(ROOT, "v0.2.10") - - -def test_metadata_gate_accepts_consistent_release_copy(tmp_path: Path) -> None: - version, tag = "0.2.10", "v0.2.10" + if released: + citation_fields.extend([f'date-released: "{RELEASE_DATE}"', f'doi: "{doi}"']) + citation_fields.extend([ + 'title: "QF Solver"', + 'license: "Apache-2.0"', + 'authors: [{family-names: "Farinazzo", given-names: "Quentin"}]', + ]) + changelog_heading = f"## {VERSION} - Released" if released else f"## {VERSION} - Release candidate; not published" + old_version = "0.2.10" files = { - "pyproject.toml": f'[project]\nversion = "{version}"\n', - "CITATION.cff": f'version: "{version}"\ndate-released: 2026-10-02\ndoi: "10.5281/zenodo.99999999"\n', - "README.md": f"| Release line | `{version}` |\n", - "CHANGELOG.md": f"## {version} - Released\n", - "docs/index.md": f"**Current release:** [`{version}`]\n", + "pyproject.toml": f'[project]\nname = "qf-solver"\nversion = "{VERSION}"\n', + "CITATION.cff": "\n".join(citation_fields) + "\n", + "README.md": ( + f"The current QF Solver release is `{VERSION}`. Rotating_modal and Campbell remain EXPERIMENTAL.\n" + "At 100 rad/s the high-frequency pair is ambiguous. GYRO-06 provides internal mesh-convergence " + "evidence, not independent physical validation.\n" + "Historical G03 remains FAIL_PRESERVED; the full-repository archive is not cleared and WP14 remains " + "HOLD_NOT_PROMOTED.\n" + + (f"Version DOI: https://doi.org/{doi}\n" if released else "") + ), + "CHANGELOG.md": ( + f"{changelog_heading}\n\n" + + ("QF Solver 0.2.11 release.\n" if released else "This release candidate is not published.\n") + + f"\n## {old_version} - Released\n" + ), + "docs/index.md": ( + f"**Current release documentation:** `{VERSION}`\n" + + (f"Version DOI: https://doi.org/{doi}\n" if released else "") + + ("This metadata identifies version 0.2.11 without asserting a publication date or version DOI.\n" if not released else "") + ), "docs/getting-started/installation.md": ( - f"The selected QF Solver `{version}` wheel and sdist are the audited PyPI release\n" - "artifacts.\n" + f"This guide applies to QF Solver `{VERSION}`.\n" + + (f"The selected QF Solver `{VERSION}` wheel and sdist are the audited PyPI release artifacts.\n" + if released else "Install after its release upload.\n") + ), + "docs/capabilities/index.md": ( + f"**Current release documentation:** `{VERSION}`\n" + "rotating_modal and Campbell remain EXPERIMENTAL.\n" + ), + "docs/reference/feuille_de_route.md": ( + f"Version {VERSION} is the current documentation baseline.\n" + + (f"QF Solver {VERSION} is the current published release.\n" if released else "") + ), + "CONTRIBUTING.md": ( + f"QF Solver `{VERSION}` (`{TAG}`) is the current published release.\n" + if released else f"QF Solver `{old_version}` (`v{old_version}`) is the current published release.\n" + ), + "SUPPORT.md": ( + f"latest published release, currently `{VERSION}`\n" + if released else f"latest published release, currently `{old_version}`\n" + ), + "SECURITY.md": ( + f"Current release: `{VERSION}` / `{TAG}`\n" + if released else f"Current release: `{old_version}` / `v{old_version}`\n" + ), + "docs/whats-new/0.2.11.md": ( + "At 100 rad/s, the high-frequency pair remains ambiguous. GYRO-06 is internal mesh-convergence evidence, " + "not independent physical validation. Both routes remain EXPERIMENTAL.\n" + ), + "docs/verification/0_2_11/README.md": ( + "The complete repository archive is not cleared. Historical G03 is FAIL_PRESERVED and WP14 is " + "HOLD_NOT_PROMOTED.\n" ), - "docs/capabilities/index.md": f"**Current release:** `{version}` / `{tag}`\n", - "docs/reference/feuille_de_route.md": f"QF Solver {version} is the current published release.\n", - "CONTRIBUTING.md": f"QF Solver `{version}` (`{tag}`)\n", - "SUPPORT.md": f"currently `{version}`\n", - "SECURITY.md": f"Current release: `{version}` / `{tag}`\n", } for relative, content in files.items(): - path = tmp_path / relative + path = root / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") - assert check_publication_metadata(tmp_path, tag) == [] - (tmp_path / "README.md").write_text( - f"| Release line | `{version}` |\nNo {version} package has been published.\n", encoding="utf-8" + + +def _write_postpublication_evidence(root: Path, *, doi: str = VERSION_DOI) -> tuple[Path, Path]: + wheel_name = f"qf_solver-{VERSION}-py3-none-any.whl" + sdist_name = f"qf_solver-{VERSION}.tar.gz" + manifest_name = f"qf_solver-{VERSION}-afd2efa4-SHA256SUMS.txt" + wheel_hash = "a" * 64 + sdist_hash = "b" * 64 + manifest_hash = "c" * 64 + source_sha = "d" * 40 + contract = { + "status": "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "package_version": VERSION, + "release_tag": TAG, + "source_sha": source_sha, + "tag_target_sha": source_sha, + "publication_scope": "SELECTED_DISTRIBUTION", + "audited_artifacts": { + "wheel": {"filename": wheel_name, "sha256": wheel_hash}, + "sdist": {"filename": sdist_name, "sha256": sdist_hash}, + }, + "sha256_manifest": {"filename": manifest_name, "sha256": manifest_hash}, + "g03_status": "FAIL_PRESERVED", + "whole_repository_archive_cleared": False, + "wp14_status": "HOLD_NOT_PROMOTED", + "authorization": { + "selected_package_publication_allowed": True, + "github_release_allowed": True, + "pypi_publication_allowed": True, + "zenodo_selected_artifact_publication_allowed": True, + "whole_repository_archive_cleared": False, + "whole_repository_archive_publication_allowed": False, + }, + } + contract_path = root / "release-contract.json" + contract_path.write_text(json.dumps(contract), encoding="utf-8") + all_files = [ + {"filename": wheel_name, "sha256": wheel_hash}, + {"filename": sdist_name, "sha256": sdist_hash}, + {"filename": manifest_name, "sha256": manifest_hash}, + ] + record = { + "schema_version": 1, + "status": "PUBLICATION_VERIFIED", + "publication_phase": "POST_PUBLICATION", + "version": VERSION, + "tag": TAG, + "source_sha": source_sha, + "version_doi": doi, + "release_date": RELEASE_DATE, + "concept_doi": "10.5281/zenodo.22697897", + "tag_exists": True, + "main_contains_tag_target": True, + "tag_verification": {"target_sha": source_sha, "annotated": True}, + "github_release": {"published": True, "tag": TAG, "selected_assets": all_files}, + "pypi": { + "published": True, + "version": VERSION, + "install_probe": "PASS", + "files": all_files[:2], + }, + "zenodo": { + "published": True, + "doi_resolves": True, + "version_doi": doi, + "concept_doi": "10.5281/zenodo.22697897", + "release_date": RELEASE_DATE, + "whole_repository_archive_present": False, + "files": all_files, + }, + } + record_path = root / "publication-record.json" + record_path.write_text(json.dumps(record), encoding="utf-8") + return contract_path, record_path + + +def test_candidate_prepublication_metadata_is_valid_without_a_version_doi() -> None: + failures = check_publication_metadata(ROOT, TAG, "PRE_PUBLICATION") + assert failures == [] + + +def test_prepublication_rejects_a_doi_or_date_claim(tmp_path: Path) -> None: + _write_release_copy(tmp_path, "PRE_PUBLICATION", doi=VERSION_DOI, released=True) + failures = check_publication_metadata(tmp_path, TAG, "PRE_PUBLICATION") + assert "CITATION.cff version differs from the release" not in failures + assert "PRE_PUBLICATION CITATION.cff must not assert a version DOI" in failures + assert "PRE_PUBLICATION CITATION.cff must not assert a release date" in failures + + assert "PRE_PUBLICATION must not claim a version DOI" in check_publication_metadata( + tmp_path, TAG, "PRE_PUBLICATION", version_doi=VERSION_DOI ) - assert "README.md still denies publication of this version" in check_publication_metadata(tmp_path, tag) - (tmp_path / "README.md").write_text( - f"| Release line | `{version}` |\nThe `{version}` candidate is not yet published.\n", encoding="utf-8" + + +def test_postpublication_requires_doi_and_date_and_accepts_captured_values(tmp_path: Path) -> None: + _write_release_copy(tmp_path, "POST_PUBLICATION", released=False) + missing = check_publication_metadata(tmp_path, TAG, "POST_PUBLICATION") + assert "POST_PUBLICATION needs a captured Zenodo version DOI" in missing + assert "POST_PUBLICATION needs the factual ISO release date" in missing + + _write_release_copy(tmp_path, "POST_PUBLICATION", doi=VERSION_DOI, released=True) + contract_path, record_path = _write_postpublication_evidence(tmp_path) + assert check_publication_metadata( + tmp_path, + TAG, + "POST_PUBLICATION", + version_doi=VERSION_DOI, + release_date=RELEASE_DATE, + publication_record=record_path, + release_contract=contract_path, + ) == [] + + +def test_postpublication_rejects_concept_doi_and_mismatched_citation(tmp_path: Path) -> None: + _write_release_copy(tmp_path, "POST_PUBLICATION", doi=VERSION_DOI, released=True) + contract_path, record_path = _write_postpublication_evidence(tmp_path) + citation = tmp_path / "CITATION.cff" + citation.write_text(citation.read_text(encoding="utf-8").replace(VERSION_DOI, "10.5281/zenodo.22697897"), encoding="utf-8") + failures = check_publication_metadata( + tmp_path, + TAG, + "POST_PUBLICATION", + version_doi=VERSION_DOI, + release_date=RELEASE_DATE, + publication_record=record_path, + release_contract=contract_path, + ) + assert "CITATION.cff DOI differs from the captured version DOI" in failures + + concept_doi = check_publication_metadata( + tmp_path, + TAG, + "POST_PUBLICATION", + version_doi="10.5281/zenodo.22697897", + release_date=RELEASE_DATE, + publication_record=record_path, + release_contract=contract_path, ) - assert "README.md still denies publication of this version" in check_publication_metadata(tmp_path, tag) + assert "the project concept DOI cannot be used as a version DOI" in concept_doi + + +def test_postpublication_requires_all_channels_and_exact_public_hashes(tmp_path: Path) -> None: + _write_release_copy(tmp_path, "POST_PUBLICATION", doi=VERSION_DOI, released=True) + contract_path, record_path = _write_postpublication_evidence(tmp_path) + record = json.loads(record_path.read_text(encoding="utf-8")) + record["zenodo"]["whole_repository_archive_present"] = True + record["github_release"]["selected_assets"].pop() + record_path.write_text(json.dumps(record), encoding="utf-8") + failures = check_publication_metadata( + tmp_path, + TAG, + "POST_PUBLICATION", + version_doi=VERSION_DOI, + release_date=RELEASE_DATE, + publication_record=record_path, + release_contract=contract_path, + ) + assert "GitHub Release or its selected asset hashes do not match the contract" in failures + assert "Zenodo record, DOI, selected files or archive boundary do not match the contract" in failures + + +def test_unstable_or_mismatched_tag_is_rejected() -> None: + assert check_publication_metadata(ROOT, "../../v0.2.9", "PRE_PUBLICATION") == [ + "release tag must be a stable vMAJOR.MINOR.PATCH tag" + ] + assert "pyproject.toml version differs from release tag" in check_publication_metadata( + ROOT, "v0.2.10", "PRE_PUBLICATION" + ) + + +def test_public_claim_gate_rejects_forbidden_rotordynamics_claims(tmp_path: Path) -> None: + claims = ( + "This is qualified rotordynamics.", + "This is a validated general critical-speed prediction.", + "This is an industrial rotor solver.", + "The whole-repository archive is cleared.", + ) + for index, claim in enumerate(claims): + root = tmp_path / str(index) + _write_release_copy(root, "PRE_PUBLICATION") + readme = root / "README.md" + readme.write_text(readme.read_text(encoding="utf-8") + claim + "\n", encoding="utf-8") + failures = check_publication_metadata(root, TAG, "PRE_PUBLICATION") + assert any("prohibited public claim detected" in failure for failure in failures), claim + + +def test_public_claim_gate_rejects_whole_archive_clearance_claim(tmp_path: Path) -> None: + _write_release_copy(tmp_path, "PRE_PUBLICATION") + readme = tmp_path / "README.md" + readme.write_text(readme.read_text(encoding="utf-8") + "The whole-repository archive is cleared.\n", encoding="utf-8") + failures = check_publication_metadata(tmp_path, TAG, "PRE_PUBLICATION") + assert any("whole|full" in failure and "cleared" in failure for failure in failures) + + +def test_selected_release_scope_is_distinct_from_repository_archive_policy() -> None: + from scripts.public_release_scope import publication_scope_decision + + assert publication_scope_decision("SELECTED_DISTRIBUTION", "FAIL_PRESERVED", False) == "ALLOWED" + assert publication_scope_decision("WHOLE_REPOSITORY_ARCHIVE", "FAIL_PRESERVED", False) == "DENIED" + assert publication_scope_decision("SELECTED_DISTRIBUTION", "FAIL_PRESERVED", True) == "DENIED" + assert publication_scope_decision("SELECTED_DISTRIBUTION", "PASS", True) == "ALLOWED" + assert publication_scope_decision("WHOLE_REPOSITORY_ARCHIVE", "PASS", True) == "ALLOWED" + assert publication_scope_decision("WHOLE_REPOSITORY_ARCHIVE", "PASS", False) == "DENIED" + assert publication_scope_decision("unknown", "FAIL_PRESERVED", False) == "DENIED" + + +def test_public_release_policy_keeps_channel_and_archive_clearance_separate() -> None: + policy = " ".join((ROOT / "PUBLIC_RELEASE_POLICY.md").read_text(encoding="utf-8").split()) + assert "SELECTED_DISTRIBUTION_CLEARANCE" in policy + assert "WHOLE_REPOSITORY_ARCHIVE_CLEARANCE" in policy + assert "automatically generated `Source code (zip)`" in policy + assert "version DOI absent" in policy + assert "fails the `POST_PUBLICATION` gate" in policy -def test_pypi_workflow_publishes_only_audited_selected_archives() -> None: +def test_pypi_workflow_uses_prepublication_phase_and_selected_artifact_gates() -> None: workflow = yaml.safe_load((ROOT / ".github/workflows/publish-pypi.yml").read_text(encoding="utf-8")) assert set(workflow["jobs"]) == {"preflight", "quality", "docs", "build", "publish"} assert workflow["jobs"]["publish"]["needs"] == ["preflight", "quality", "docs", "build"] @@ -74,13 +316,14 @@ def test_pypi_workflow_publishes_only_audited_selected_archives() -> None: preflight = str(workflow["jobs"]["preflight"]) build = str(workflow["jobs"]["build"]) assert "check_publication_metadata.py" in preflight + assert "PRE_PUBLICATION" in preflight assert "git merge-base --is-ancestor" in preflight assert "prepare_wp14_recovered_evidence.py" in str(workflow["jobs"]["quality"]) assert "mkdocs build --strict" in str(workflow["jobs"]["docs"]) events = workflow.get("on", workflow.get(True)) assert "default" not in events["workflow_dispatch"]["inputs"]["release_tag"] assert "default" not in events["workflow_dispatch"]["inputs"]["contract_path"] - assert "audited_artifacts" in str(workflow["jobs"]["preflight"]) + assert "publication_scope" in preflight assert "audit_wp14_public_scope.py" in build assert "verify_authorized_release_artifacts.py" in build assert "gh release download" in build From addb6607d2a7043f016ef79529e67805dcb7ceaf Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 12:21:16 +0200 Subject: [PATCH 14/15] release: bind selected release to candidate audit --- .../verify_authorized_release_artifacts.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/scripts/verify_authorized_release_artifacts.py b/scripts/verify_authorized_release_artifacts.py index eb6e799d..4d1705f0 100644 --- a/scripts/verify_authorized_release_artifacts.py +++ b/scripts/verify_authorized_release_artifacts.py @@ -80,6 +80,132 @@ def _check_tool_bindings(root: Path, commit: str, bindings: Any) -> None: raise ValueError(f"Release tool binding mismatch: {relative}") +def _validate_candidate_audit( + root: Path, head: str, source_sha: str, contract: dict[str, Any], +) -> None: + reference = contract.get("candidate_audit") + if not isinstance(reference, dict): + raise ValueError("The selected-release contract must bind the exact final candidate audit record.") + path = _relative_path(reference.get("path", "")) + commit = reference.get("commit_sha") + expected_hash = reference.get("sha256") + if ( + not isinstance(commit, str) + or not re.fullmatch(r"[0-9a-f]{40}", commit) + or not isinstance(expected_hash, str) + or not re.fullmatch(r"[0-9a-f]{64}", expected_hash) + or git_run(["merge-base", "--is-ancestor", source_sha, commit], cwd=root).returncode + or git_run(["merge-base", "--is-ancestor", commit, head], cwd=root).returncode + ): + raise ValueError("The final candidate audit is not an exact descendant audit of the authorized source.") + committed = _committed_bytes(root, head, path) + at_commit = git_run(["show", f"{commit}:{path}"], cwd=root, check=True).stdout + if hashlib.sha256(committed).hexdigest() != expected_hash or hashlib.sha256(at_commit).hexdigest() != expected_hash: + raise ValueError("The final candidate audit record bytes differ from their frozen hash.") + audit = json.loads(committed) + candidate = audit.get("candidate", {}) + if ( + audit.get("record_id") != reference.get("record_id") + or audit.get("status") != "READY_FOR_OWNER_RELEASE_AUTHORIZATION" + or not isinstance(candidate, dict) + or candidate.get("package_version") != contract.get("package_version") + or candidate.get("package_source_sha") != source_sha + or candidate.get("expected_tag") != contract.get("release_tag") + or candidate.get("expected_tag_target_sha") != source_sha + or candidate.get("tag_created") is not False + or not isinstance(audit.get("release_gates"), dict) + or audit.get("release_gates", {}).get("ready_for_owner_release_authorization") is not True + ): + raise ValueError("The candidate audit does not describe this exact frozen, untagged release candidate.") + + expected_gate_keys = {f"QF0211_G0{number}" for number in range(1, 9)} + audit_gates = audit.get("release_gates", {}) + contract_gates = contract.get("release_gates", {}) + if any(audit_gates.get(key) != contract_gates.get(key) for key in expected_gate_keys): + raise ValueError("The final candidate audit and selected-release contract gate results differ.") + + ci = audit.get("ci", {}) + if not isinstance(ci, dict): + raise ValueError("The candidate audit CI record is malformed.") + jobs = ci.get("quality_jobs", {}) + if not isinstance(jobs, dict): + raise ValueError("The candidate audit CI job matrix is malformed.") + if ( + ci.get("quality_head_sha") != source_sha + or ci.get("quality_conclusion") != "SUCCESS" + or ci.get("documentation_head_sha") != source_sha + or ci.get("documentation_conclusion") != "SUCCESS" + or ci.get("ci_sha_exact_match") is not True + or any(jobs.get(name) != "PASS" for name in ( + "ubuntu_python_3_10", + "ubuntu_python_3_13", + "windows_python_3_10", + "windows_python_3_13", + "wp14_recovered_evidence_targeted", + "documentation_evidence", + "full_engineering_campaign", + )) + ): + raise ValueError("The candidate audit does not contain successful exact-source Quality and Documentation CI.") + + built = audit.get("build_and_artifacts", {}) + if not isinstance(built, dict): + raise ValueError("The candidate audit build record is malformed.") + expected_artifacts = contract.get("audited_artifacts", {}) + expected_manifest = contract.get("sha256_manifest", {}) + if not isinstance(expected_artifacts, dict) or not isinstance(expected_manifest, dict): + raise ValueError("The selected-release artifact bindings are malformed.") + wheel = built.get("wheel", {}) + sdist = built.get("sdist", {}) + manifest = built.get("manifest", {}) + if ( + wheel.get("filename") != expected_artifacts.get("wheel", {}).get("filename") + or wheel.get("sha256") != expected_artifacts.get("wheel", {}).get("sha256") + or wheel.get("size_bytes") != expected_artifacts.get("wheel", {}).get("bytes") + or wheel.get("independent_builds_byte_identical") is not True + or sdist.get("filename") != expected_artifacts.get("sdist", {}).get("filename") + or sdist.get("sha256") != expected_artifacts.get("sdist", {}).get("sha256") + or sdist.get("size_bytes") != expected_artifacts.get("sdist", {}).get("bytes") + or sdist.get("normalized_builds_byte_identical") is not True + or manifest.get("filename") != expected_manifest.get("filename") + or manifest.get("sha256") != expected_manifest.get("sha256") + or manifest.get("size_bytes") != expected_manifest.get("bytes") + or audit.get("build_and_artifacts", {}).get("twine_check") != "PASS" + ): + raise ValueError("The candidate audit artifact bytes or reproducibility results differ from the Owner contract.") + probes = built.get("installed_package_probes", {}) + if not isinstance(probes, dict): + raise ValueError("The candidate audit installation-probe record is malformed.") + if ( + probes.get("isolated_no_index_no_deps_install") != "PASS" + or probes.get("imports_outside_checkout_and_safe_path") != "PASS" + or probes.get("installed_import_origins") != "PASS" + or probes.get("version_and_cli_probes") != "PASS" + or probes.get("static_smoke") != "PASS" + or probes.get("modal_smoke") != "PASS" + or probes.get("rotating_modal_smoke") != "PASS_EXPERIMENTAL" + or probes.get("campbell_smoke") != "PASS_EXPERIMENTAL" + ): + raise ValueError("The candidate audit installed-package probes are incomplete or failing.") + + scan = audit.get("selected_scope_audit", {}) + if not isinstance(scan, dict): + raise ValueError("The candidate audit selected-scope scan record is malformed.") + selected_scope = contract.get("selected_scope") + if not isinstance(selected_scope, dict): + raise ValueError("The selected-release contract has no bounded selected-scope counts.") + if ( + scan.get("status") != "PASS_BOUNDED_PUBLIC_SURFACES" + or scan.get("selected_package_files") != selected_scope.get("package_source_files") + or scan.get("selected_package_findings") != 0 + or scan.get("served_document_files") != selected_scope.get("served_document_files") + or scan.get("served_document_findings") != 0 + or scan.get("whole_repository_scanned") is not False + or scan.get("whole_repository_g03") != "FAIL_PRESERVED" + ): + raise ValueError("The candidate audit selected-scope scans do not match the frozen release contract.") + + def verify_artifact_set(dist: Path, contract: dict[str, Any]) -> dict[str, Any]: """Require exactly the two frozen packages and their exact SHA-256 manifest.""" artifacts = contract.get("audited_artifacts") @@ -453,6 +579,7 @@ def _validate_contract(root: Path, contract_path: Path, candidate_path: Path) -> raise ValueError("The contract does not identify the exact prospective Owner authorization.") _validate_superseded_owner_record(root, head, owner_record, release_contract) validate_owner_artifact_scope(release_contract, owner_record) + _validate_candidate_audit(root, head, source_sha, release_contract) resolution = release_contract.get("authorization_resolution", {}) resolution_hashes = { "owner_approved_wheel_sha256": release_contract["audited_artifacts"]["wheel"]["sha256"], From dced860aa7337e2d160180c032fa09f6c0ca2bfe Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 7 Oct 2026 12:24:36 +0200 Subject: [PATCH 15/15] release: bind authorized 0.2.11 selected artifacts --- ...cted_package_release_contract_afd2efa.json | 165 ++++++++++++++++++ .../unit/test_authorized_release_artifacts.py | 14 ++ 2 files changed, 179 insertions(+) create mode 100644 qualification/0_2_11/authorized_selected_package_release_contract_afd2efa.json diff --git a/qualification/0_2_11/authorized_selected_package_release_contract_afd2efa.json b/qualification/0_2_11/authorized_selected_package_release_contract_afd2efa.json new file mode 100644 index 00000000..be999f27 --- /dev/null +++ b/qualification/0_2_11/authorized_selected_package_release_contract_afd2efa.json @@ -0,0 +1,165 @@ +{ + "schema_version": 2, + "contract_id": "QF-0211-AUTHORIZED-SELECTED-DISTRIBUTION-AFD2EFA4-2026-10-07", + "record_type": "PROSPECTIVE_AUTHORIZED_SELECTED_DISTRIBUTION_RELEASE", + "status": "OWNER_AUTHORIZED_SELECTED_PACKAGE_RELEASE", + "publication_scope": "SELECTED_DISTRIBUTION", + "package_version": "0.2.11", + "release_tag": "v0.2.11", + "source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "tag_target_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "source_tree_sha": "8b135547a9dcde4420c18eea7a39565a1ff446e8", + "source_contract": { + "path": "qualification/0_2_11/wp07_candidate_build_contract_afd2efa4.json", + "commit_sha": "1176612212c970579cc43c6695aa4af238a905c4", + "sha256": "3aaea04c62f2c622a143fdb039c43fa2128ddadc4a11c32e9105e1c17570158e", + "status": "FROZEN_CANDIDATE_BUILD", + "publication_allowed": false + }, + "candidate_audit": { + "path": "qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json", + "commit_sha": "5c64c51a74375aef35db00c33c40861044d892cc", + "sha256": "c3c1fcda86a1b9e90fee6b1ea2829361d9471d993b6dcc08f1bc5d8d667b206e", + "record_id": "QF-0211-WP07-FINAL-CANDIDATE-AUDIT-AFD2EFA4-2026-10-07" + }, + "owner_authorization": { + "path": "qualification/0_2_11/owner_publication_authorization_afd2efa4.json", + "commit_sha": "f87e423765a67612cce8ebeb6e1c8932a21d0481", + "sha256": "da883ec9a890d25bdc5cc0cf9a71efa54d36c848fc43a427cf2487e79b37ff25", + "record_id": "QF-0211-OWNER-SELECTED-PUBLICATION-AUTHORIZATION-AFD2EFA4-2026-10-07" + }, + "authorization_resolution": { + "status": "RESOLVED_BY_EXACT_OWNER_AUTHORIZATION", + "owner_approved_source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "candidate_source_sha": "afd2efa469a18e66581720ff4801f50de7d29eb4", + "owner_approved_wheel_sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8", + "candidate_wheel_sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8", + "owner_approved_sdist_sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e", + "candidate_sdist_sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e", + "owner_approved_manifest_sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364", + "candidate_manifest_sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364" + }, + "audited_artifacts": { + "wheel": { + "filename": "qf_solver-0.2.11-py3-none-any.whl", + "bytes": 1559189, + "sha256": "bcf860f0a008f4a2c3c9efbe2301753a59c687ecc101d6f799dd52557d0fcba8" + }, + "sdist": { + "filename": "qf_solver-0.2.11.tar.gz", + "bytes": 1177522, + "sha256": "bdf61fb59e5617778a2c539ab9729ab9b0912bc841ac9207d4cce1fba175824e" + } + }, + "sha256_manifest": { + "filename": "qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt", + "bytes": 190, + "sha256": "b461928d9443f5164c555ba6d520efe00ddeaea23b7d4af6215442f54b942364" + }, + "manifest_repository_path": "qualification/0_2_11/qf_solver-0.2.11-afd2efa4-SHA256SUMS.txt", + "build_provenance": { + "source_materialization": "Selected Git blobs from exactly afd2efa469a18e66581720ff4801f50de7d29eb4; no working-tree bytes.", + "candidate_audit_record": "qualification/0_2_11/wp07_final_candidate_audit_afd2efa4.json", + "independent_builds": 2, + "wheel_byte_identical": true, + "sdist_raw_builds_byte_identical": false, + "sdist_difference": "Gzip metadata only, covered by the pre-bound normalizer.", + "sdist_normalized_byte_identical": true, + "normalization_tool": "scripts/normalize_public_sdist.py", + "source_date_epoch": 1791305555, + "build_environment": { + "python": "3.13.1", + "build": "1.5.0", + "setuptools": "82.0.1", + "wheel": "0.47.0", + "twine": "7.0.0" + }, + "twine_check": "PASS", + "installed_probes": "PASS", + "governance_remediation_rebuilt_candidate": false + }, + "selected_scope": { + "package_source_files": 562, + "package_findings": 0, + "served_document_files": 753, + "served_document_findings": 0, + "historical_document_files_excluded": 208, + "whole_repository_scanned": false, + "whole_repository_g03": "FAIL_PRESERVED" + }, + "release_gates": { + "QF0211_G01": "INHERITED_PASS", + "QF0211_G02": "INHERITED_PASS", + "QF0211_G03": "INHERITED_PASS", + "QF0211_G04": "INHERITED_PASS", + "QF0211_G05": "INHERITED_PASS", + "QF0211_G06": "INHERITED_PASS", + "QF0211_G07": "PASS", + "QF0211_G08": "PASS" + }, + "maturities": { + "rotating_modal": "EXPERIMENTAL", + "campbell": "EXPERIMENTAL", + "campbell_100_rad_s_ambiguity": "PRESERVED", + "gyro_06_evidence": "INTERNAL_MESH_CONVERGENCE_ONLY" + }, + "publication_phase": { + "current_phase": "PRE_PUBLICATION", + "version_doi": "NOT_ASSIGNED", + "release_date": "NOT_SET", + "concept_doi": "10.5281/zenodo.22697897" + }, + "authorization": { + "selected_package_publication_allowed": true, + "tag_creation_allowed": true, + "pypi_publication_allowed": true, + "github_release_allowed": true, + "zenodo_selected_artifact_publication_allowed": true, + "whole_repository_archive_cleared": false, + "whole_repository_archive_publication_allowed": false, + "ledger_update_allowed": false, + "wp14_promotion_allowed": false, + "numeric_maturity_promotion_allowed": false, + "historical_result_reclassification_allowed": false, + "certification_or_universal_validation_claims_allowed": false + }, + "g03_status": "FAIL_PRESERVED", + "whole_repository_archive_cleared": false, + "whole_repository_archive_publication_allowed": false, + "wp14_status": "HOLD_NOT_PROMOTED", + "remediation_root_causes": { + "B01": { + "classification": "RELEASE_PHASE_MODEL_MISSING", + "resolution": "Explicit PRE_PUBLICATION and POST_PUBLICATION metadata validation; no DOI/date is required before Zenodo publication, while postpublication requires a captured record and exact channel/hash evidence." + }, + "B02": { + "classification": "VERSION_HARDCODED_AUTHORIZATION", + "resolution": "Artifact verification is contract-driven, schema-versioned, exact-source and exact-bytes bound, with an explicit read adapter for unchanged historical schema-1 contracts." + }, + "B03": { + "classification": "SELECTED_RELEASE_AND_FULL_ARCHIVE_COUPLED", + "resolution": "Selected wheel/sdist/manifest clearance is an independent bounded decision; whole-repository clearance remains separately gated and denied while G03 is FAIL_PRESERVED." + } + }, + "tool_bindings": { + "scripts/verify_authorized_release_artifacts.py": "00b2778f2725c1e8675eccbe0587d4f7a3bb65ae5f21850ecb52a74ad7a378ef", + "scripts/public_release_scope.py": "c44299fa59d04b70c283d63e82f91f8dd5d9315a78f7ab7aedfa5ae7901d21a7", + "scripts/check_publication_metadata.py": "c0843deefdfa56b67460bef0d4e157aa71029f7060ba9391715c51c8cbf831bb", + "scripts/check_distribution.py": "456ba6fd025053073a8eaf46609a19f7b674edb2ca99cd024f963f8ef8848860", + "scripts/audit_wp14_public_scope.py": "fa2a39d03b2f14bfcbbaf725c7c9f25fe84b117a2824f3355765c0fa8c26d616", + "scripts/git_tools.py": "9192aef8e8610ee4f651cac11a18af189678f561d9c85c20c64c77be47a287fa", + "scripts/verify_public_package.py": "7e25532f5a8c2c80b0a8ab711c235fc45a34eb6219efdef0386543d00f1bc86c", + "scripts/probe_installed_package.py": "46576afa82a8285730c4afc67e41fd58f5a8deb753f054f5a35cc0442eb18675", + "scripts/normalize_public_sdist.py": "1c3594c61b46af5601fda7993644aa613fb1523edb4709526b0ee35a01213d2c", + ".github/workflows/publish-pypi.yml": "eb4db4df2cec3a7c1a4895235a418588f1c76da64c730d06485bc5d50dcf2cfe", + "PUBLIC_RELEASE_POLICY.md": "107187551267fcf6d6fe00594dbe89967f12acc22bff25a0ca053485ff1e37f5" + }, + "release_constraints": [ + "This contract authorizes only the exact selected distribution. It does not authorize or clear a whole-repository archive.", + "The current workflow instruction does not create a tag or publish GitHub, PyPI or Zenodo; a later explicit Owner instruction is required to execute those mutations.", + "GitHub-generated Source code ZIP/TAR files are not selected audited artifacts and are not cleared while G03 remains failed.", + "Zenodo may receive only the wheel, normalized sdist and this selected checksum manifest; no repository snapshot or qualification archive is permitted.", + "Historical G03 remains FAIL_PRESERVED, whole_repository_archive_cleared remains false, and historical WP14 remains HOLD_NOT_PROMOTED.", + "Rotating modal and Campbell remain EXPERIMENTAL. The 100 rad/s high-frequency ambiguity is preserved, and GYRO-06 is internal mesh-convergence evidence only." + ] +} diff --git a/tests/unit/test_authorized_release_artifacts.py b/tests/unit/test_authorized_release_artifacts.py index 14d017e2..d126bfef 100644 --- a/tests/unit/test_authorized_release_artifacts.py +++ b/tests/unit/test_authorized_release_artifacts.py @@ -197,6 +197,20 @@ def test_actual_0211_owner_authorization_matches_the_frozen_candidate() -> None: assert owner["wp14_status"] == "HOLD_NOT_PROMOTED" +def test_actual_0211_release_contract_binds_only_the_authorized_distribution() -> None: + contract_path = ROOT / "qualification/0_2_11/authorized_selected_package_release_contract_afd2efa.json" + owner_path = ROOT / "qualification/0_2_11/owner_publication_authorization_afd2efa4.json" + contract = json.loads(contract_path.read_text(encoding="utf-8")) + owner = json.loads(owner_path.read_text(encoding="utf-8")) + validate_owner_artifact_scope(contract, owner) + assert contract["source_sha"] == "afd2efa469a18e66581720ff4801f50de7d29eb4" + assert contract["publication_scope"] == "SELECTED_DISTRIBUTION" + assert contract["g03_status"] == "FAIL_PRESERVED" + assert contract["whole_repository_archive_cleared"] is False + assert contract["wp14_status"] == "HOLD_NOT_PROMOTED" + assert contract["authorization"]["whole_repository_archive_publication_allowed"] is False + + def test_changed_source_version_or_hash_fails_closed(tmp_path: Path) -> None: _, contract, owner = _fixture(tmp_path) changed_source = {**contract, "source_sha": "c" * 40}