Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ jobs:
- run: python -m ruff check .
- run: python -m mypy src tests
- run: python -m pytest --cov=phaseprobe --cov-report=term
- run: python -m phaseprobe perturb --config examples/scipy/lorenz-negative.json
- run: python -m phaseprobe check --config examples/scipy/predator-prey.json
- run: python -m phaseprobe perturb --example scipy-lorenz-negative
- run: python -m phaseprobe check --example scipy-predator-prey

package-and-hygiene:
runs-on: ubuntu-latest
Expand All @@ -65,18 +65,32 @@ jobs:
- run: python -m build
- run: python -m twine check dist/*
- run: python scripts/audit_package.py
- run: python -m venv .cache/packed-smoke
- run: .cache/packed-smoke/bin/python -m pip install dist/*.whl
- run: .cache/packed-smoke/bin/python -m phaseprobe --version
- run: .cache/packed-smoke/bin/python -m phaseprobe scan --example logistic-negative
- run: .cache/packed-smoke/bin/python -c "import importlib.util; assert importlib.util.find_spec('numpy') is None; assert importlib.util.find_spec('scipy') is None"
- run: python -m venv .cache/scipy-smoke
- run: .cache/scipy-smoke/bin/python -m pip install "$(find dist -name '*.whl')[scipy]"
- run: .cache/scipy-smoke/bin/python -m phaseprobe check --config examples/scipy/predator-prey.json
- run: >-
python scripts/verify_artifacts.py
--dist-dir dist
--work-root "${RUNNER_TEMP}/phaseprobe artifact gate"
- run: python scripts/check_links.py
- run: python scripts/hygiene.py
- uses: actions/upload-artifact@v4
with:
name: phaseprobe-packages
path: dist/*
if-no-files-found: error

windows-artifact-install:
name: artifact install / windows / Python 3.12
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: python -m pip install --upgrade pip build twine
- run: python -m build
- run: python -m twine check dist/*
- run: python scripts/audit_package.py
- run: >-
python scripts/verify_artifacts.py
--dist-dir dist
--work-root "$env:RUNNER_TEMP\phaseprobe artifact gate"
22 changes: 19 additions & 3 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ on:
workflow_dispatch:
inputs:
tag:
description: Immutable release tag (for example, v0.2.0)
description: Immutable release tag (for example, v0.2.1)
required: true
type: string
version:
description: Expected package version (for example, 0.2.0)
description: Expected package version (for example, 0.2.1)
required: true
type: string
commit:
Expand Down Expand Up @@ -90,13 +90,15 @@ jobs:
expected_version, wheel_path, sdist_path = sys.argv[1:]

with zipfile.ZipFile(wheel_path) as archive:
wheel_names = set(archive.namelist())
metadata_names = [
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
name for name in wheel_names if name.endswith(".dist-info/METADATA")
]
assert len(metadata_names) == 1, metadata_names
wheel_metadata = email.message_from_bytes(archive.read(metadata_names[0]))

with tarfile.open(sdist_path, "r:gz") as archive:
sdist_names = {member.name for member in archive.getmembers()}
metadata_members = [
member
for member in archive.getmembers()
Expand All @@ -107,6 +109,20 @@ jobs:
assert extracted is not None
sdist_metadata = email.message_from_bytes(extracted.read())

sdist_roots = {name.split("/", 1)[0] for name in sdist_names if "/" in name}
assert len(sdist_roots) == 1, sorted(sdist_roots)
sdist_root = next(iter(sdist_roots))
source_prefix = f"{sdist_root}/src/phaseprobe/"
runtime_files = {
name.removeprefix(f"{sdist_root}/src/")
for name in sdist_names
if name.startswith(source_prefix)
and not name.endswith(".py")
and "__pycache__" not in name.split("/")
}
assert "phaseprobe/py.typed" in runtime_files
assert not runtime_files - wheel_names, sorted(runtime_files - wheel_names)

for metadata in (wheel_metadata, sdist_metadata):
assert metadata["Name"] == "phaseprobe"
assert metadata["Version"] == expected_version
Expand Down
11 changes: 4 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,10 @@ jobs:
- run: python -m build
- run: python -m twine check dist/*
- run: python scripts/audit_package.py
- run: python -m venv .cache/tag-core-smoke
- run: .cache/tag-core-smoke/bin/python -m pip install dist/*.whl
- run: .cache/tag-core-smoke/bin/python -m phaseprobe --version
- run: .cache/tag-core-smoke/bin/python -m phaseprobe scan --example logistic
- run: python -m venv .cache/tag-scipy-smoke
- run: .cache/tag-scipy-smoke/bin/python -m pip install "$(find dist -name '*.whl')[scipy]"
- run: .cache/tag-scipy-smoke/bin/python -m phaseprobe check --config examples/scipy/predator-prey.json
- run: >-
python scripts/verify_artifacts.py
--dist-dir dist
--work-root "${RUNNER_TEMP}/phaseprobe tagged artifact gate"
- run: python scripts/hygiene.py
- uses: actions/upload-artifact@v4
with:
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

All notable changes are documented here. PhaseProbe follows semantic versioning.

## 0.2.1 — 2026-08-31

- Fixed Issue #4: all four SciPy quick-start configurations now ship inside the importable
package in both wheel and source distribution and load through `importlib.resources` as the
`scipy-lorenz`, `scipy-lorenz-negative`, `scipy-predator-prey`, and
`scipy-predator-prey-coarse` built-in examples.
- Preserved the four exact former `examples/scipy/` config paths as narrow compatibility aliases
when no file exists at the requested path. Existing user files take precedence; matching is
case-sensitive and separator-portable, with no fuzzy or basename fallback.
- Added actionable, versioned diagnostics for missing or malformed built-in resources without
changing arbitrary config-path or built-in-example behavior.
- Added Linux and Windows Python 3.12 release gates that build, inspect, install, and exercise the
wheel and sdist outside the checkout with `PYTHONPATH` removed. The gate runs scan, replay,
generated pytest, SciPy Lorenz, SciPy predator–prey, `pip check`, and a dependency-free base
wheel check.
- Added a source-derived runtime-resource audit and the PEP 561 `py.typed` marker.
- Generated regression creation is now idempotent for identical evidence and rejects conflicting
files instead of silently overwriting them.
- Added an analytic backward-time directional-event check while preserving all solver defaults
and the existing tolerance-based adaptive replay contract.

## 0.2.0 — 2026-08-02

- Added the backward-compatible `TrajectoryAdapter` protocol and shared engine dispatch; all v0.1
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ title: "PhaseProbe"
type: software
authors:
- name: "Ali"
version: 0.2.0
date-released: 2026-08-02
version: 0.2.1
date-released: 2026-08-31
url: "https://github.com/aliengineering-byte/phaseprobe"
repository-code: "https://github.com/aliengineering-byte/phaseprobe"
license: Apache-2.0
Expand Down
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Maintaining a simulation is risky when a tiny parameter or initial-condition change can cross a qualitative boundary while ordinary numeric assertions still look plausible. PhaseProbe runs a bounded, deterministic search, records exactly what it tested, and emits an offline report plus an executable pytest regression.

```console
$ pip install phaseprobe
$ pip install phaseprobe pytest
$ phaseprobe scan --example logistic
QUALITATIVE TRANSITION FOUND

Expand All @@ -29,14 +29,15 @@ No API key, LLM, GPU, Docker, account, telemetry, network connection, or hosted
```bash
pip install phaseprobe
pip install "phaseprobe[scipy]"
pip install pytest # needed only to execute a generated regression test
```

## Five-minute quick start

Requires Python 3.10 or newer on Windows or Linux.

```bash
pip install phaseprobe
pip install phaseprobe pytest
phaseprobe scan --example logistic
phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
Expand All @@ -59,22 +60,41 @@ Five-minute path:
[![SciPy demo: solve_ivp evidence, tolerance replay, and generated pytest](assets/scipy-demo-static.png)](assets/scipy-demo.gif)

The verified command transcript is [assets/scipy-demo-session.txt](assets/scipy-demo-session.txt),
with a self-contained [HTML example report](examples/scipy/report.html).
with a self-contained [HTML example report](examples/scipy/report.html). The following path starts
from a clean environment and does not require a source checkout:

```bash
python -m pip install "phaseprobe[scipy]"
phaseprobe perturb --config examples/scipy/lorenz.json
phaseprobe check --config examples/scipy/predator-prey.json
python -m venv .venv
# Linux: source .venv/bin/activate
# Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install "phaseprobe[scipy]" "pytest==8.4.1"
phaseprobe perturb --example scipy-lorenz
phaseprobe check --example scipy-predator-prey
phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
python -m pytest -q tests/generated
```

PhaseProbe 0.2.1 also recognizes the former Issue #4 command
`phaseprobe perturb --config examples/scipy/lorenz.json` when that relative file is absent and
loads the corresponding packaged example. This compatibility is limited to the four former
`examples/scipy/` paths: an existing file always takes precedence, matching is case-sensitive,
both slash styles are accepted, and unrelated missing paths remain errors. New documentation and
automation should use the installed-safe `--example scipy-*` names.

The Lorenz command searches a declared initial-`x` perturbation and reports only finite-time
divergence evidence. `examples/scipy/lorenz-negative.json` is its short-window negative control.
The predator–prey command checks the declared first integral with tightly resolved DOP853 settings;
`examples/scipy/predator-prey-coarse.json` deliberately fails the same policy with loose RK23
settings.
divergence evidence and prints `FINITE-TIME TRAJECTORY DIVERGENCE FOUND`. Its installed
short-window control is `--example scipy-lorenz-negative`. The predator–prey command checks the
declared first integral with tightly resolved DOP853 settings and prints `CHECK POLICY PASSED`;
`--example scipy-predator-prey-coarse` deliberately fails the same policy with loose RK23 settings.
Each successful command writes a replay fixture and offline report below
`.phaseprobe/runs/<run-id>/`.

Run `phaseprobe perturb --help` or `phaseprobe check --help` to list installed example names. A
missing packaged resource reports the PhaseProbe version and resource name. Adaptive SciPy replay
compares the declared state, observable, invariant, endpoint, event, and retained-grid
tolerances—it does not promise bitwise trajectory equality across platforms or dependency
versions.

For a direct Python API:

Expand Down Expand Up @@ -113,14 +133,15 @@ does not import it. See [the audited contract](docs/SCIPY_SOLVE_IVP_AUDIT.md),
| `perturb` | Baseline/perturbed twin runs over bounded initial-state changes | Finding or no finding, exit `0` |
| `check` | Execute a declared configuration policy for CI | Exit `1` only when policy fails |
| `replay` | Validate fixture integrity and re-execute model, parameters, seed, initial state, tolerances, and retention | Declared `exact` or `tolerance` comparison passes |
| `generate-test` | Validate and copy a fixture into a non-extensible pytest template | Executable test under `tests/generated/` |
| `generate-test` | Validate and copy a fixture into a non-extensible pytest template without conflicting overwrites | Executable test under `tests/generated/` |
| `report` | Regenerate terminal, versioned JSON, and self-contained offline HTML evidence | Local report files |

Common options:

```console
phaseprobe scan --config examples/configs/logistic-scan.json
phaseprobe perturb --example lorenz --json
phaseprobe perturb --example scipy-lorenz --json
phaseprobe check --example predator-prey
phaseprobe scan --example logistic --fail-on-finding
```
Expand All @@ -135,6 +156,8 @@ Exit codes are stable: `0` completed, `1` declared policy or explicit `--fail-on
| Lorenz system | `phaseprobe perturb --example lorenz` | Small initial separation exceeds the declared finite-time trajectory-distance threshold | Short window plus unreachable threshold reports no finding | [Lorenz, 1963](https://journals.ametsoc.org/view/journals/atsc/20/2/1520-0469_1963_020_0130_dnf_2_0_co_2.xml) |
| Predator–prey | `phaseprobe check --example predator-prey` | Refined RK4 step preserves the analytic first integral within tolerance | Coarse step fails the invariant-drift policy | [Lotka, 1920](https://doi.org/10.1073/pnas.6.7.410) |
| Genetic toggle | `phaseprobe perturb --example toggle` | Bounded initial-state perturbation reaches the opposite dominant state | Smaller declared range stays in the baseline basin | [Gardner, Cantor & Collins, 2000](https://www.nature.com/articles/35002131) |
| SciPy Lorenz | `phaseprobe perturb --example scipy-lorenz` | DOP853 twin trajectories cross the declared finite-time distance threshold | `--example scipy-lorenz-negative` shortens the window | [Lorenz, 1963](https://journals.ametsoc.org/view/journals/atsc/20/2/1520-0469_1963_020_0130_dnf_2_0_co_2.xml) |
| SciPy predator–prey | `phaseprobe check --example scipy-predator-prey` | Tight DOP853 settings preserve the declared first integral tolerance | `--example scipy-predator-prey-coarse` deliberately fails | [Lotka, 1920](https://doi.org/10.1073/pnas.6.7.410) |

Each configuration records the seed, fixed integration/iteration settings, tolerances, burn-in, observation window, classification rule, refinement rule, invalid-state policy, and trace cap. See [examples/README.md](examples/README.md) for equations and interpretation.

Expand Down
69 changes: 69 additions & 0 deletions RELEASE_NOTES_0.2.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# PhaseProbe 0.2.1 release notes

Status: prepared locally on 2026-08-31; not published.

## Result

PhaseProbe 0.2.1 fixes GitHub Issue #4. The four SciPy quick-start configurations are runtime
resources inside the `phaseprobe` package, are present in both wheel and sdist, and load through
`importlib.resources` with installed-safe `--example scipy-*` names.

The exact former relative config paths also remain compatible. PhaseProbe first reads an existing
file; only when it is absent does one of the four case-sensitive `examples/scipy/` paths resolve to
its matching packaged built-in. Both slash styles are accepted. Arbitrary missing paths, basename
matches, and case variants remain errors. The reporter's original command therefore succeeds from
an unrelated working directory after installation:

```text
phaseprobe perturb --config examples/scipy/lorenz.json
```

The reporter-confirmed scan, replay, generated pytest, Lorenz, and predator–prey behavior remains
intact. This patch does not change solver methods, tolerances, thresholds, search bounds, replay
semantics, or scientific claims.

## Root cause

Version 0.2.0 documented `examples/scipy/*.json` paths that existed only in the source checkout.
Hatchling included the top-level `examples/` tree in the sdist as source material but built wheels
only from `src/phaseprobe`; consequently neither the published wheel nor a wheel built while
installing the sdist contained those configurations at runtime. Editable/source tests and CI ran
from the repository root, where the relative paths existed, masking the release defect.

## Changes

- Added narrow compatibility for the four former checkout-relative SciPy paths while preserving
existing-file precedence and rejecting fuzzy matches.
- Added packaged SciPy Lorenz and predator–prey configurations plus positive/negative controls.
- Added missing/malformed built-in-resource diagnostics with package version and valid choices.
- Added source-derived wheel/sdist resource inventory checks and `py.typed`.
- Added clean wheel, clean sdist, and dependency-free base-wheel smoke verification outside the
checkout with `PYTHONPATH` removed on Linux and Windows Python 3.12.
- The installed smoke runs scan, replay, generated pytest, Lorenz, predator–prey, and `pip check`.
- Generated regressions are idempotent for identical evidence and reject conflicting overwrites.
- Added an analytic backward-time directional event check for the SciPy adapter.

## Verification commands

```text
python -m ruff format --check .
python -m ruff check .
python -m mypy src tests
python -m pytest --cov=phaseprobe --cov-report=term-missing
python -m build
python -m twine check dist/*
python scripts/audit_package.py
python scripts/verify_artifacts.py --dist-dir dist --work-root <outside-checkout>
python scripts/check_links.py
python scripts/hygiene.py
```

## Artifact provenance

Local review builds produce `dist/phaseprobe-0.2.1-py3-none-any.whl` and
`dist/phaseprobe-0.2.1.tar.gz`. Their SHA-256 values identify only those exact local files; they are
not permanent release hashes. The trusted publishing workflow rebuilds artifacts from the final
tag, so published hashes must be recorded from that separately authorized run.

PhaseProbe 0.2.1 is not yet published. These notes do not assert that remote CI has passed. No
merge, PyPI upload, GitHub release, tag, or Issue #4 closure is part of this pull request.
3 changes: 3 additions & 0 deletions SCIENTIFIC_METHODS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Tolerance replay compares retained times/states, named numeric observables, clas
invariant results and stored thresholds, endpoint/event evidence, and expected solver success.
Environment and solver evidence are retained for interpretation. Artifact hashes still detect
fixture tampering, but an adaptive trace hash is not required to match across supported platforms.
The adapter validation suite checks an exponential system with an analytic terminal crossing in
both forward and backward integration directions; this validates event plumbing, not exhaustive
root detection between adaptive internal steps.
See [the complete SciPy audit](docs/SCIPY_SOLVE_IVP_AUDIT.md).

PhaseProbe 0.1.0 produces bounded computational evidence. It does not perform formal verification, model validation against observations, or computer-assisted proof.
Expand Down
Loading
Loading