phaseprobe 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/workflows/ci.yml +24 -10
  2. phaseprobe-0.2.1/.github/workflows/publish-pypi.yml +166 -0
  3. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/workflows/release.yml +4 -7
  4. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/CHANGELOG.md +21 -0
  5. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/CITATION.cff +2 -2
  6. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/PKG-INFO +43 -13
  7. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/README.md +41 -11
  8. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/SCIENTIFIC_METHODS.md +3 -0
  9. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/assets/scipy-demo-session.txt +10 -10
  10. phaseprobe-0.2.1/assets/scipy-demo-static.png +0 -0
  11. phaseprobe-0.2.1/assets/scipy-demo.gif +0 -0
  12. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/docs/upstream/SCIPY_INTEGRATION_PROPOSAL.md +47 -11
  13. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/README.md +10 -5
  14. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/pyproject.toml +1 -1
  15. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/scripts/audit_package.py +36 -4
  16. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/scripts/generate_scipy_demo.py +8 -8
  17. phaseprobe-0.2.1/scripts/installed_smoke.py +199 -0
  18. phaseprobe-0.2.1/scripts/verify_artifacts.py +221 -0
  19. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/__init__.py +1 -1
  20. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/cli.py +2 -2
  21. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/config.py +69 -18
  22. phaseprobe-0.2.1/src/phaseprobe/data/examples/scipy-lorenz-negative.json +54 -0
  23. phaseprobe-0.2.1/src/phaseprobe/data/examples/scipy-lorenz.json +54 -0
  24. phaseprobe-0.2.1/src/phaseprobe/data/examples/scipy-predator-prey-coarse.json +45 -0
  25. phaseprobe-0.2.1/src/phaseprobe/data/examples/scipy-predator-prey.json +45 -0
  26. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/generate.py +21 -5
  27. phaseprobe-0.2.1/src/phaseprobe/py.typed +1 -0
  28. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/test_artifacts_replay.py +13 -0
  29. phaseprobe-0.2.1/tests/test_config.py +148 -0
  30. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/test_optional_dependency.py +7 -1
  31. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/test_scipy_adapter.py +52 -6
  32. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/uv.lock +1 -1
  33. phaseprobe-0.2.0/assets/scipy-demo-static.png +0 -0
  34. phaseprobe-0.2.0/assets/scipy-demo.gif +0 -0
  35. phaseprobe-0.2.0/tests/test_config.py +0 -69
  36. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  37. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  38. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  39. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/good_first_accessibility.yml +0 -0
  40. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/good_first_pendulum.yml +0 -0
  41. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/ISSUE_TEMPLATE/good_first_schema.yml +0 -0
  42. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/.gitignore +0 -0
  44. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/AGENTS.md +0 -0
  45. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/ARCHITECTURE.md +0 -0
  46. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/CODE_OF_CONDUCT.md +0 -0
  47. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/CONTRIBUTING.md +0 -0
  48. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/LICENSE +0 -0
  49. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/LIMITATIONS.md +0 -0
  50. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/PRIOR_ART.md +0 -0
  51. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/ROADMAP.md +0 -0
  52. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/SECURITY.md +0 -0
  53. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/assets/demo-session.txt +0 -0
  54. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/assets/demo-static.png +0 -0
  55. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/assets/demo.gif +0 -0
  56. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/assets/social-preview.jpg +0 -0
  57. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/docs/SCIPY_SOLVE_IVP_AUDIT.md +0 -0
  58. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/logistic-negative.json +0 -0
  59. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/logistic-scan.json +0 -0
  60. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/lorenz-negative.json +0 -0
  61. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/lorenz-perturb.json +0 -0
  62. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/predator-prey-check.json +0 -0
  63. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/predator-prey-negative.json +0 -0
  64. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/toggle-negative.json +0 -0
  65. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/configs/toggle-perturb.json +0 -0
  66. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/scipy/lorenz-negative.json +0 -0
  67. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/scipy/lorenz.json +0 -0
  68. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/scipy/predator-prey-coarse.json +0 -0
  69. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/scipy/predator-prey.json +0 -0
  70. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/examples/scipy/report.html +0 -0
  71. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/scripts/check_links.py +0 -0
  72. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/scripts/generate_demo.py +0 -0
  73. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/scripts/hygiene.py +0 -0
  74. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/__main__.py +0 -0
  75. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/adapters/__init__.py +0 -0
  76. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/adapters/loader.py +0 -0
  77. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/adapters/scipy.py +0 -0
  78. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/api.py +0 -0
  79. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/artifacts.py +0 -0
  80. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/__init__.py +0 -0
  81. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/__init__.py +0 -0
  82. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/logistic-negative.json +0 -0
  83. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/logistic-scan.json +0 -0
  84. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/lorenz-negative.json +0 -0
  85. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/lorenz-perturb.json +0 -0
  86. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/predator-prey-check.json +0 -0
  87. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/predator-prey-negative.json +0 -0
  88. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/toggle-negative.json +0 -0
  89. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/data/examples/toggle-perturb.json +0 -0
  90. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/engine.py +0 -0
  91. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/errors.py +0 -0
  92. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/examples/__init__.py +0 -0
  93. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/examples/scipy_models.py +0 -0
  94. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/__init__.py +0 -0
  95. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/_common.py +0 -0
  96. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/logistic.py +0 -0
  97. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/lorenz.py +0 -0
  98. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/predator_prey.py +0 -0
  99. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/models/toggle.py +0 -0
  100. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/replay.py +0 -0
  101. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/reporting.py +0 -0
  102. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/src/phaseprobe/types.py +0 -0
  103. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/conftest.py +0 -0
  104. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/fixtures/logistic_map-replay.json +0 -0
  105. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/fixtures/lorenz_scipy-replay.json +0 -0
  106. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/fixtures/predator_prey_scipy-replay.json +0 -0
  107. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/test_logistic_map_transition.py +0 -0
  108. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/test_lorenz_scipy_transition.py +0 -0
  109. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/generated/test_predator_prey_scipy_transition.py +0 -0
  110. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/test_cli.py +0 -0
  111. {phaseprobe-0.2.0 → phaseprobe-0.2.1}/tests/test_engine.py +0 -0
@@ -50,8 +50,8 @@ jobs:
50
50
  - run: python -m ruff check .
51
51
  - run: python -m mypy src tests
52
52
  - run: python -m pytest --cov=phaseprobe --cov-report=term
53
- - run: python -m phaseprobe perturb --config examples/scipy/lorenz-negative.json
54
- - run: python -m phaseprobe check --config examples/scipy/predator-prey.json
53
+ - run: python -m phaseprobe perturb --example scipy-lorenz-negative
54
+ - run: python -m phaseprobe check --example scipy-predator-prey
55
55
 
56
56
  package-and-hygiene:
57
57
  runs-on: ubuntu-latest
@@ -65,14 +65,10 @@ jobs:
65
65
  - run: python -m build
66
66
  - run: python -m twine check dist/*
67
67
  - run: python scripts/audit_package.py
68
- - run: python -m venv .cache/packed-smoke
69
- - run: .cache/packed-smoke/bin/python -m pip install dist/*.whl
70
- - run: .cache/packed-smoke/bin/python -m phaseprobe --version
71
- - run: .cache/packed-smoke/bin/python -m phaseprobe scan --example logistic-negative
72
- - 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"
73
- - run: python -m venv .cache/scipy-smoke
74
- - run: .cache/scipy-smoke/bin/python -m pip install "$(find dist -name '*.whl')[scipy]"
75
- - run: .cache/scipy-smoke/bin/python -m phaseprobe check --config examples/scipy/predator-prey.json
68
+ - run: >-
69
+ python scripts/verify_artifacts.py
70
+ --dist-dir dist
71
+ --work-root "${RUNNER_TEMP}/phaseprobe artifact gate"
76
72
  - run: python scripts/check_links.py
77
73
  - run: python scripts/hygiene.py
78
74
  - uses: actions/upload-artifact@v4
@@ -80,3 +76,21 @@ jobs:
80
76
  name: phaseprobe-packages
81
77
  path: dist/*
82
78
  if-no-files-found: error
79
+
80
+ windows-artifact-install:
81
+ name: artifact install / windows / Python 3.12
82
+ runs-on: windows-latest
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+ - uses: actions/setup-python@v5
86
+ with:
87
+ python-version: "3.12"
88
+ cache: pip
89
+ - run: python -m pip install --upgrade pip build twine
90
+ - run: python -m build
91
+ - run: python -m twine check dist/*
92
+ - run: python scripts/audit_package.py
93
+ - run: >-
94
+ python scripts/verify_artifacts.py
95
+ --dist-dir dist
96
+ --work-root "$env:RUNNER_TEMP\phaseprobe artifact gate"
@@ -0,0 +1,166 @@
1
+ name: Publish an existing GitHub release to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: Immutable release tag (for example, v0.2.1)
8
+ required: true
9
+ type: string
10
+ version:
11
+ description: Expected package version (for example, 0.2.1)
12
+ required: true
13
+ type: string
14
+ commit:
15
+ description: Expected 40-character commit for the tag
16
+ required: true
17
+ type: string
18
+ wheel_sha256:
19
+ description: Expected SHA-256 for the wheel release asset
20
+ required: true
21
+ type: string
22
+ sdist_sha256:
23
+ description: Expected SHA-256 for the source release asset
24
+ required: true
25
+ type: string
26
+
27
+ permissions: {}
28
+
29
+ concurrency:
30
+ group: pypi-${{ inputs.tag }}
31
+ cancel-in-progress: false
32
+
33
+ jobs:
34
+ verify:
35
+ name: Verify immutable release assets
36
+ if: >-
37
+ github.repository == 'aliengineering-byte/phaseprobe' &&
38
+ github.ref == 'refs/heads/main'
39
+ runs-on: ubuntu-latest
40
+ permissions: {}
41
+ steps:
42
+ - name: Download and verify the selected release
43
+ env:
44
+ TAG: ${{ inputs.tag }}
45
+ VERSION: ${{ inputs.version }}
46
+ EXPECTED_COMMIT: ${{ inputs.commit }}
47
+ WHEEL_SHA256: ${{ inputs.wheel_sha256 }}
48
+ SDIST_SHA256: ${{ inputs.sdist_sha256 }}
49
+ shell: bash
50
+ run: |
51
+ set -euo pipefail
52
+
53
+ [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
54
+ [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
55
+ [[ "$TAG" == "v$VERSION" ]]
56
+ [[ "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ ]]
57
+ [[ "$WHEEL_SHA256" =~ ^[0-9a-f]{64}$ ]]
58
+ [[ "$SDIST_SHA256" =~ ^[0-9a-f]{64}$ ]]
59
+
60
+ repository_url="https://github.com/${GITHUB_REPOSITORY}.git"
61
+ tag_refs="$(git ls-remote "$repository_url" "refs/tags/$TAG" "refs/tags/$TAG^{}")"
62
+ direct_commit="$(awk -v ref="refs/tags/$TAG" '$2 == ref { print $1 }' <<<"$tag_refs")"
63
+ peeled_commit="$(awk -v ref="refs/tags/$TAG^{}" '$2 == ref { print $1 }' <<<"$tag_refs")"
64
+ resolved_commit="${peeled_commit:-$direct_commit}"
65
+ [[ -n "$resolved_commit" ]]
66
+ [[ "$resolved_commit" == "$EXPECTED_COMMIT" ]]
67
+
68
+ wheel="phaseprobe-$VERSION-py3-none-any.whl"
69
+ sdist="phaseprobe-$VERSION.tar.gz"
70
+ release_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/$TAG"
71
+ mkdir dist
72
+ curl --fail --location --proto '=https' --tlsv1.2 \
73
+ --output "dist/$wheel" "$release_url/$wheel"
74
+ curl --fail --location --proto '=https' --tlsv1.2 \
75
+ --output "dist/$sdist" "$release_url/$sdist"
76
+ printf '%s %s\n%s %s\n' \
77
+ "$WHEEL_SHA256" "dist/$wheel" \
78
+ "$SDIST_SHA256" "dist/$sdist" | sha256sum --check --strict
79
+
80
+ python3 -m venv .twine-check
81
+ .twine-check/bin/python -m pip install --disable-pip-version-check 'twine==7.0.0'
82
+ .twine-check/bin/python -m twine check "dist/$wheel" "dist/$sdist"
83
+
84
+ python3 - "$VERSION" "dist/$wheel" "dist/$sdist" <<'PY'
85
+ import email
86
+ import sys
87
+ import tarfile
88
+ import zipfile
89
+
90
+ expected_version, wheel_path, sdist_path = sys.argv[1:]
91
+
92
+ with zipfile.ZipFile(wheel_path) as archive:
93
+ wheel_names = set(archive.namelist())
94
+ metadata_names = [
95
+ name for name in wheel_names if name.endswith(".dist-info/METADATA")
96
+ ]
97
+ assert len(metadata_names) == 1, metadata_names
98
+ wheel_metadata = email.message_from_bytes(archive.read(metadata_names[0]))
99
+
100
+ with tarfile.open(sdist_path, "r:gz") as archive:
101
+ sdist_names = {member.name for member in archive.getmembers()}
102
+ metadata_members = [
103
+ member
104
+ for member in archive.getmembers()
105
+ if member.isfile() and member.name.count("/") == 1 and member.name.endswith("/PKG-INFO")
106
+ ]
107
+ assert len(metadata_members) == 1, [member.name for member in metadata_members]
108
+ extracted = archive.extractfile(metadata_members[0])
109
+ assert extracted is not None
110
+ sdist_metadata = email.message_from_bytes(extracted.read())
111
+
112
+ sdist_roots = {name.split("/", 1)[0] for name in sdist_names if "/" in name}
113
+ assert len(sdist_roots) == 1, sorted(sdist_roots)
114
+ sdist_root = next(iter(sdist_roots))
115
+ source_prefix = f"{sdist_root}/src/phaseprobe/"
116
+ runtime_files = {
117
+ name.removeprefix(f"{sdist_root}/src/")
118
+ for name in sdist_names
119
+ if name.startswith(source_prefix)
120
+ and not name.endswith(".py")
121
+ and "__pycache__" not in name.split("/")
122
+ }
123
+ assert "phaseprobe/py.typed" in runtime_files
124
+ assert not runtime_files - wheel_names, sorted(runtime_files - wheel_names)
125
+
126
+ for metadata in (wheel_metadata, sdist_metadata):
127
+ assert metadata["Name"] == "phaseprobe"
128
+ assert metadata["Version"] == expected_version
129
+ assert metadata["Requires-Python"] == ">=3.10"
130
+ assert metadata["License-Expression"] == "Apache-2.0"
131
+ assert "scipy" in metadata.get_all("Provides-Extra", [])
132
+ requirements = metadata.get_all("Requires-Dist", [])
133
+ assert any(req.startswith("numpy") and "extra == 'scipy'" in req for req in requirements)
134
+ assert any(req.startswith("scipy") and "extra == 'scipy'" in req for req in requirements)
135
+ project_urls = metadata.get_all("Project-URL", [])
136
+ assert any(url.startswith("Source, https://github.com/aliengineering-byte/phaseprobe") for url in project_urls)
137
+ PY
138
+
139
+ - name: Store the verified distributions
140
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
141
+ with:
142
+ name: verified-distributions-${{ inputs.tag }}
143
+ path: dist/*
144
+ if-no-files-found: error
145
+ retention-days: 1
146
+
147
+ publish:
148
+ name: Publish verified distributions
149
+ needs: verify
150
+ runs-on: ubuntu-latest
151
+ environment:
152
+ name: pypi
153
+ url: https://pypi.org/project/phaseprobe/${{ inputs.version }}/
154
+ permissions:
155
+ id-token: write
156
+ steps:
157
+ - name: Retrieve the verified distributions
158
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
159
+ with:
160
+ name: verified-distributions-${{ inputs.tag }}
161
+ path: dist
162
+
163
+ - name: Publish distributions to PyPI
164
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
165
+ with:
166
+ packages-dir: dist/
@@ -20,13 +20,10 @@ jobs:
20
20
  - run: python -m build
21
21
  - run: python -m twine check dist/*
22
22
  - run: python scripts/audit_package.py
23
- - run: python -m venv .cache/tag-core-smoke
24
- - run: .cache/tag-core-smoke/bin/python -m pip install dist/*.whl
25
- - run: .cache/tag-core-smoke/bin/python -m phaseprobe --version
26
- - run: .cache/tag-core-smoke/bin/python -m phaseprobe scan --example logistic
27
- - run: python -m venv .cache/tag-scipy-smoke
28
- - run: .cache/tag-scipy-smoke/bin/python -m pip install "$(find dist -name '*.whl')[scipy]"
29
- - run: .cache/tag-scipy-smoke/bin/python -m phaseprobe check --config examples/scipy/predator-prey.json
23
+ - run: >-
24
+ python scripts/verify_artifacts.py
25
+ --dist-dir dist
26
+ --work-root "${RUNNER_TEMP}/phaseprobe tagged artifact gate"
30
27
  - run: python scripts/hygiene.py
31
28
  - uses: actions/upload-artifact@v4
32
29
  with:
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes are documented here. PhaseProbe follows semantic versioning.
4
4
 
5
+ ## 0.2.1 — 2026-08-31
6
+
7
+ - Fixed Issue #4: all four SciPy quick-start configurations now ship inside the importable
8
+ package in both wheel and source distribution and load through `importlib.resources` as the
9
+ `scipy-lorenz`, `scipy-lorenz-negative`, `scipy-predator-prey`, and
10
+ `scipy-predator-prey-coarse` built-in examples.
11
+ - Preserved the four exact former `examples/scipy/` config paths as narrow compatibility aliases
12
+ when no file exists at the requested path. Existing user files take precedence; matching is
13
+ case-sensitive and separator-portable, with no fuzzy or basename fallback.
14
+ - Added actionable, versioned diagnostics for missing or malformed built-in resources without
15
+ changing arbitrary config-path or built-in-example behavior.
16
+ - Added Linux and Windows Python 3.12 release gates that build, inspect, install, and exercise the
17
+ wheel and sdist outside the checkout with `PYTHONPATH` removed. The gate runs scan, replay,
18
+ generated pytest, SciPy Lorenz, SciPy predator–prey, `pip check`, and a dependency-free base
19
+ wheel check.
20
+ - Added a source-derived runtime-resource audit and the PEP 561 `py.typed` marker.
21
+ - Generated regression creation is now idempotent for identical evidence and rejects conflicting
22
+ files instead of silently overwriting them.
23
+ - Added an analytic backward-time directional-event check while preserving all solver defaults
24
+ and the existing tolerance-based adaptive replay contract.
25
+
5
26
  ## 0.2.0 — 2026-08-02
6
27
 
7
28
  - Added the backward-compatible `TrajectoryAdapter` protocol and shared engine dispatch; all v0.1
@@ -4,8 +4,8 @@ title: "PhaseProbe"
4
4
  type: software
5
5
  authors:
6
6
  - name: "Ali"
7
- version: 0.2.0
8
- date-released: 2026-08-02
7
+ version: 0.2.1
8
+ date-released: 2026-08-31
9
9
  url: "https://github.com/aliengineering-byte/phaseprobe"
10
10
  repository-code: "https://github.com/aliengineering-byte/phaseprobe"
11
11
  license: Apache-2.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: phaseprobe
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Find reproducible qualitative simulation transitions and turn them into regression tests.
5
5
  Project-URL: Documentation, https://github.com/aliengineering-byte/phaseprobe#readme
6
6
  Project-URL: Issues, https://github.com/aliengineering-byte/phaseprobe/issues
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  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.
44
44
 
45
45
  ```console
46
- $ python -m pip install .
46
+ $ pip install phaseprobe pytest
47
47
  $ phaseprobe scan --example logistic
48
48
  QUALITATIVE TRANSITION FOUND
49
49
 
@@ -62,12 +62,20 @@ $ python -m pytest -q tests/generated
62
62
 
63
63
  No API key, LLM, GPU, Docker, account, telemetry, network connection, or hosted service is required at runtime. PhaseProbe's base installation has no third-party runtime dependencies; NumPy and SciPy are isolated in the optional `scipy` extra.
64
64
 
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install phaseprobe
69
+ pip install "phaseprobe[scipy]"
70
+ pip install pytest # needed only to execute a generated regression test
71
+ ```
72
+
65
73
  ## Five-minute quick start
66
74
 
67
75
  Requires Python 3.10 or newer on Windows or Linux.
68
76
 
69
77
  ```bash
70
- python -m pip install .
78
+ pip install phaseprobe pytest
71
79
  phaseprobe scan --example logistic
72
80
  phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
73
81
  phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
@@ -90,22 +98,41 @@ Five-minute path:
90
98
  [![SciPy demo: solve_ivp evidence, tolerance replay, and generated pytest](assets/scipy-demo-static.png)](assets/scipy-demo.gif)
91
99
 
92
100
  The verified command transcript is [assets/scipy-demo-session.txt](assets/scipy-demo-session.txt),
93
- with a self-contained [HTML example report](examples/scipy/report.html).
101
+ with a self-contained [HTML example report](examples/scipy/report.html). The following path starts
102
+ from a clean environment and does not require a source checkout:
94
103
 
95
104
  ```bash
96
- python -m pip install "phaseprobe[scipy]"
97
- phaseprobe perturb --config examples/scipy/lorenz.json
98
- phaseprobe check --config examples/scipy/predator-prey.json
105
+ python -m venv .venv
106
+ # Linux: source .venv/bin/activate
107
+ # Windows PowerShell: .venv\Scripts\Activate.ps1
108
+ python -m pip install "phaseprobe[scipy]" "pytest==8.4.1"
109
+ phaseprobe perturb --example scipy-lorenz
110
+ phaseprobe check --example scipy-predator-prey
99
111
  phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
100
112
  phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
101
113
  python -m pytest -q tests/generated
102
114
  ```
103
115
 
116
+ PhaseProbe 0.2.1 also recognizes the former Issue #4 command
117
+ `phaseprobe perturb --config examples/scipy/lorenz.json` when that relative file is absent and
118
+ loads the corresponding packaged example. This compatibility is limited to the four former
119
+ `examples/scipy/` paths: an existing file always takes precedence, matching is case-sensitive,
120
+ both slash styles are accepted, and unrelated missing paths remain errors. New documentation and
121
+ automation should use the installed-safe `--example scipy-*` names.
122
+
104
123
  The Lorenz command searches a declared initial-`x` perturbation and reports only finite-time
105
- divergence evidence. `examples/scipy/lorenz-negative.json` is its short-window negative control.
106
- The predator–prey command checks the declared first integral with tightly resolved DOP853 settings;
107
- `examples/scipy/predator-prey-coarse.json` deliberately fails the same policy with loose RK23
108
- settings.
124
+ divergence evidence and prints `FINITE-TIME TRAJECTORY DIVERGENCE FOUND`. Its installed
125
+ short-window control is `--example scipy-lorenz-negative`. The predator–prey command checks the
126
+ declared first integral with tightly resolved DOP853 settings and prints `CHECK POLICY PASSED`;
127
+ `--example scipy-predator-prey-coarse` deliberately fails the same policy with loose RK23 settings.
128
+ Each successful command writes a replay fixture and offline report below
129
+ `.phaseprobe/runs/<run-id>/`.
130
+
131
+ Run `phaseprobe perturb --help` or `phaseprobe check --help` to list installed example names. A
132
+ missing packaged resource reports the PhaseProbe version and resource name. Adaptive SciPy replay
133
+ compares the declared state, observable, invariant, endpoint, event, and retained-grid
134
+ tolerances—it does not promise bitwise trajectory equality across platforms or dependency
135
+ versions.
109
136
 
110
137
  For a direct Python API:
111
138
 
@@ -144,7 +171,7 @@ does not import it. See [the audited contract](docs/SCIPY_SOLVE_IVP_AUDIT.md),
144
171
  | `perturb` | Baseline/perturbed twin runs over bounded initial-state changes | Finding or no finding, exit `0` |
145
172
  | `check` | Execute a declared configuration policy for CI | Exit `1` only when policy fails |
146
173
  | `replay` | Validate fixture integrity and re-execute model, parameters, seed, initial state, tolerances, and retention | Declared `exact` or `tolerance` comparison passes |
147
- | `generate-test` | Validate and copy a fixture into a non-extensible pytest template | Executable test under `tests/generated/` |
174
+ | `generate-test` | Validate and copy a fixture into a non-extensible pytest template without conflicting overwrites | Executable test under `tests/generated/` |
148
175
  | `report` | Regenerate terminal, versioned JSON, and self-contained offline HTML evidence | Local report files |
149
176
 
150
177
  Common options:
@@ -152,6 +179,7 @@ Common options:
152
179
  ```console
153
180
  phaseprobe scan --config examples/configs/logistic-scan.json
154
181
  phaseprobe perturb --example lorenz --json
182
+ phaseprobe perturb --example scipy-lorenz --json
155
183
  phaseprobe check --example predator-prey
156
184
  phaseprobe scan --example logistic --fail-on-finding
157
185
  ```
@@ -166,6 +194,8 @@ Exit codes are stable: `0` completed, `1` declared policy or explicit `--fail-on
166
194
  | 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) |
167
195
  | 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) |
168
196
  | 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) |
197
+ | 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) |
198
+ | 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) |
169
199
 
170
200
  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.
171
201
 
@@ -5,7 +5,7 @@
5
5
  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.
6
6
 
7
7
  ```console
8
- $ python -m pip install .
8
+ $ pip install phaseprobe pytest
9
9
  $ phaseprobe scan --example logistic
10
10
  QUALITATIVE TRANSITION FOUND
11
11
 
@@ -24,12 +24,20 @@ $ python -m pytest -q tests/generated
24
24
 
25
25
  No API key, LLM, GPU, Docker, account, telemetry, network connection, or hosted service is required at runtime. PhaseProbe's base installation has no third-party runtime dependencies; NumPy and SciPy are isolated in the optional `scipy` extra.
26
26
 
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install phaseprobe
31
+ pip install "phaseprobe[scipy]"
32
+ pip install pytest # needed only to execute a generated regression test
33
+ ```
34
+
27
35
  ## Five-minute quick start
28
36
 
29
37
  Requires Python 3.10 or newer on Windows or Linux.
30
38
 
31
39
  ```bash
32
- python -m pip install .
40
+ pip install phaseprobe pytest
33
41
  phaseprobe scan --example logistic
34
42
  phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
35
43
  phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
@@ -52,22 +60,41 @@ Five-minute path:
52
60
  [![SciPy demo: solve_ivp evidence, tolerance replay, and generated pytest](assets/scipy-demo-static.png)](assets/scipy-demo.gif)
53
61
 
54
62
  The verified command transcript is [assets/scipy-demo-session.txt](assets/scipy-demo-session.txt),
55
- with a self-contained [HTML example report](examples/scipy/report.html).
63
+ with a self-contained [HTML example report](examples/scipy/report.html). The following path starts
64
+ from a clean environment and does not require a source checkout:
56
65
 
57
66
  ```bash
58
- python -m pip install "phaseprobe[scipy]"
59
- phaseprobe perturb --config examples/scipy/lorenz.json
60
- phaseprobe check --config examples/scipy/predator-prey.json
67
+ python -m venv .venv
68
+ # Linux: source .venv/bin/activate
69
+ # Windows PowerShell: .venv\Scripts\Activate.ps1
70
+ python -m pip install "phaseprobe[scipy]" "pytest==8.4.1"
71
+ phaseprobe perturb --example scipy-lorenz
72
+ phaseprobe check --example scipy-predator-prey
61
73
  phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
62
74
  phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
63
75
  python -m pytest -q tests/generated
64
76
  ```
65
77
 
78
+ PhaseProbe 0.2.1 also recognizes the former Issue #4 command
79
+ `phaseprobe perturb --config examples/scipy/lorenz.json` when that relative file is absent and
80
+ loads the corresponding packaged example. This compatibility is limited to the four former
81
+ `examples/scipy/` paths: an existing file always takes precedence, matching is case-sensitive,
82
+ both slash styles are accepted, and unrelated missing paths remain errors. New documentation and
83
+ automation should use the installed-safe `--example scipy-*` names.
84
+
66
85
  The Lorenz command searches a declared initial-`x` perturbation and reports only finite-time
67
- divergence evidence. `examples/scipy/lorenz-negative.json` is its short-window negative control.
68
- The predator–prey command checks the declared first integral with tightly resolved DOP853 settings;
69
- `examples/scipy/predator-prey-coarse.json` deliberately fails the same policy with loose RK23
70
- settings.
86
+ divergence evidence and prints `FINITE-TIME TRAJECTORY DIVERGENCE FOUND`. Its installed
87
+ short-window control is `--example scipy-lorenz-negative`. The predator–prey command checks the
88
+ declared first integral with tightly resolved DOP853 settings and prints `CHECK POLICY PASSED`;
89
+ `--example scipy-predator-prey-coarse` deliberately fails the same policy with loose RK23 settings.
90
+ Each successful command writes a replay fixture and offline report below
91
+ `.phaseprobe/runs/<run-id>/`.
92
+
93
+ Run `phaseprobe perturb --help` or `phaseprobe check --help` to list installed example names. A
94
+ missing packaged resource reports the PhaseProbe version and resource name. Adaptive SciPy replay
95
+ compares the declared state, observable, invariant, endpoint, event, and retained-grid
96
+ tolerances—it does not promise bitwise trajectory equality across platforms or dependency
97
+ versions.
71
98
 
72
99
  For a direct Python API:
73
100
 
@@ -106,7 +133,7 @@ does not import it. See [the audited contract](docs/SCIPY_SOLVE_IVP_AUDIT.md),
106
133
  | `perturb` | Baseline/perturbed twin runs over bounded initial-state changes | Finding or no finding, exit `0` |
107
134
  | `check` | Execute a declared configuration policy for CI | Exit `1` only when policy fails |
108
135
  | `replay` | Validate fixture integrity and re-execute model, parameters, seed, initial state, tolerances, and retention | Declared `exact` or `tolerance` comparison passes |
109
- | `generate-test` | Validate and copy a fixture into a non-extensible pytest template | Executable test under `tests/generated/` |
136
+ | `generate-test` | Validate and copy a fixture into a non-extensible pytest template without conflicting overwrites | Executable test under `tests/generated/` |
110
137
  | `report` | Regenerate terminal, versioned JSON, and self-contained offline HTML evidence | Local report files |
111
138
 
112
139
  Common options:
@@ -114,6 +141,7 @@ Common options:
114
141
  ```console
115
142
  phaseprobe scan --config examples/configs/logistic-scan.json
116
143
  phaseprobe perturb --example lorenz --json
144
+ phaseprobe perturb --example scipy-lorenz --json
117
145
  phaseprobe check --example predator-prey
118
146
  phaseprobe scan --example logistic --fail-on-finding
119
147
  ```
@@ -128,6 +156,8 @@ Exit codes are stable: `0` completed, `1` declared policy or explicit `--fail-on
128
156
  | 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) |
129
157
  | 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) |
130
158
  | 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) |
159
+ | 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) |
160
+ | 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) |
131
161
 
132
162
  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.
133
163
 
@@ -19,6 +19,9 @@ Tolerance replay compares retained times/states, named numeric observables, clas
19
19
  invariant results and stored thresholds, endpoint/event evidence, and expected solver success.
20
20
  Environment and solver evidence are retained for interpretation. Artifact hashes still detect
21
21
  fixture tampering, but an adaptive trace hash is not required to match across supported platforms.
22
+ The adapter validation suite checks an exponential system with an analytic terminal crossing in
23
+ both forward and backward integration directions; this validates event plumbing, not exhaustive
24
+ root detection between adaptive internal steps.
22
25
  See [the complete SciPy audit](docs/SCIPY_SOLVE_IVP_AUDIT.md).
23
26
 
24
27
  PhaseProbe 0.1.0 produces bounded computational evidence. It does not perform formal verification, model validation against observations, or computer-assisted proof.
@@ -1,4 +1,4 @@
1
- $ phaseprobe perturb --config examples/scipy/lorenz.json
1
+ $ phaseprobe perturb --example scipy-lorenz
2
2
  FINITE-TIME TRAJECTORY DIVERGENCE FOUND
3
3
 
4
4
  Model: lorenz-scipy
@@ -10,12 +10,12 @@ Baseline regime: bounded-finite-window
10
10
  Changed regime: bounded-finite-window
11
11
  Invariant violations: 0
12
12
  Repeatable: true
13
- Replay: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/runs/lorenz/20260802T175927Z-786e1809/replay.json
13
+ Replay: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/runs/lorenz/20260831T174342Z-7fcc5f8d/replay.json
14
14
 
15
15
  Scope: Smallest reproducible perturbation found within the declared finite search; not a proof of global minimality.
16
- HTML report: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/runs/lorenz/20260802T175927Z-786e1809/report.html
16
+ HTML report: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/runs/lorenz/20260831T174342Z-7fcc5f8d/report.html
17
17
 
18
- $ phaseprobe check --config examples/scipy/predator-prey.json
18
+ $ phaseprobe check --example scipy-predator-prey
19
19
  CHECK POLICY PASSED
20
20
 
21
21
  Model: predator-prey-scipy
@@ -23,8 +23,8 @@ Baseline regime: positive-oscillation
23
23
  Changed regime: n/a
24
24
  Invariant violations: 0
25
25
  Repeatable: true
26
- Replay: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/runs/predator-prey/20260802T175928Z-073e812d/replay.json
27
- HTML report: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/runs/predator-prey/20260802T175928Z-073e812d/report.html
26
+ Replay: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/runs/predator-prey/20260831T174343Z-38b5e8de/replay.json
27
+ HTML report: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/runs/predator-prey/20260831T174343Z-38b5e8de/report.html
28
28
 
29
29
  $ phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
30
30
  REPLAY VERIFIED
@@ -36,11 +36,11 @@ baseline: declared tolerances=True
36
36
  $ phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
37
37
  PYTEST REGRESSION GENERATED
38
38
 
39
- Test: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/generated/test_predator_prey_scipy_transition.py
40
- Replay fixture: <repository>/.cache/phaseprobe-scipy-demo-uqyijm6y/generated/fixtures/predator_prey_scipy-replay.json
39
+ Test: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/generated/test_predator_prey_scipy_transition.py
40
+ Replay fixture: <repository>/.cache/phaseprobe-scipy-demo-4sai_vv1/generated/fixtures/predator_prey_scipy-replay.json
41
41
 
42
42
  $ python -m pytest -q tests/generated
43
43
  . [100%]
44
- 1 passed in 1.04s
44
+ 1 passed in 1.08s
45
45
 
46
- Measured demo run: lorenz=5.916s, predator-prey=1.397s, replay=1.343s, generate=1.300s, pytest=1.759s.
46
+ Measured demo run: lorenz=6.285s, predator-prey=1.409s, replay=1.336s, generate=1.340s, pytest=1.814s.
Binary file
@@ -44,8 +44,8 @@ plus solver configuration metadata; it does not hash or serialize callable sourc
44
44
 
45
45
  ```bash
46
46
  python -m pip install "phaseprobe[scipy]"
47
- phaseprobe perturb --config examples/scipy/lorenz.json
48
- phaseprobe check --config examples/scipy/predator-prey.json
47
+ phaseprobe perturb --example scipy-lorenz
48
+ phaseprobe check --example scipy-predator-prey
49
49
  phaseprobe replay .phaseprobe/runs/<run-id>/replay.json
50
50
  phaseprobe generate-test .phaseprobe/runs/<run-id>/replay.json
51
51
  ```
@@ -95,15 +95,51 @@ propose this external qualitative regression/replay layer.
95
95
  Project quick start and terminal assets:
96
96
  https://github.com/aliengineering-byte/phaseprobe#use-with-scipy
97
97
 
98
- ## Suggested discussion title
98
+ ## Discussion title and body
99
99
 
100
- `PhaseProbe: tolerance-aware qualitative regression fixtures for SciPy solve_ivp models`
100
+ `PhaseProbe: reproducible transition detection and regression generation for solve_ivp models`
101
101
 
102
- ## Human-editable outreach message
102
+ Hi SciPy community,
103
103
 
104
- > I built an optional, dependency-isolated PhaseProbe adapter for public `solve_ivp` models. It
105
- > keeps SciPy responsible for integration and adds bounded qualitative searches, explicit
106
- > tolerance replay, and generated pytest regressions. The examples include finite-time Lorenz
107
- > divergence with a negative control and a Lotka–Volterra invariant refinement check. I reviewed
108
- > related tolerance, event, work-bound, and callback discussions and do not propose a SciPy API
109
- > change. Would this workflow be useful to users maintaining scientific simulation regressions?
104
+ I built PhaseProbe, an open-source downstream testing tool for dynamical-system simulations. It
105
+ uses `scipy.integrate.solve_ivp` through a trajectory-level adapter to search bounded parameter or
106
+ initial-condition regions for reproducible qualitative transitions, preserve replay evidence, and
107
+ generate pytest regression tests from the findings.
108
+
109
+ The motivation is a practical testing problem: a numerical model can continue integrating
110
+ successfully while a small parameter or initial-condition change moves it into a different
111
+ qualitative regime. PhaseProbe records the search configuration, solver method and tolerances,
112
+ environment evidence, trajectory hashes, observables, classifications, and transition bracket so
113
+ that the result can be independently replayed.
114
+
115
+ The current release includes examples for Lorenz finite-time divergence and predator–prey
116
+ invariant drift. For adaptive SciPy integrations, replay is tolerance-based rather than claimed to
117
+ be bit-exact. The results are explicitly bounded, finite-time numerical evidence—not proofs of
118
+ chaos, exact bifurcation locations, Lyapunov estimates, or globally minimal perturbations.
119
+
120
+ Repository:
121
+ https://github.com/aliengineering-byte/phaseprobe
122
+
123
+ Release:
124
+ https://github.com/aliengineering-byte/phaseprobe/releases/tag/v0.2.0
125
+
126
+ PyPI:
127
+ https://pypi.org/project/phaseprobe/
128
+
129
+ Technical proposal and limitations:
130
+ https://github.com/aliengineering-byte/phaseprobe/blob/v0.2.0/docs/upstream/SCIPY_INTEGRATION_PROPOSAL.md
131
+
132
+ The release links above preserve the original 0.2.0 integration record. The installed-safe
133
+ `--example scipy-*` commands shown here are included in the planned 0.2.1 packaging fix.
134
+
135
+ I am not proposing to add PhaseProbe to SciPy core. I would value feedback on three points:
136
+
137
+ 1. Whether the tolerance-based replay evidence for adaptive `solve_ivp` trajectories is
138
+ scientifically and practically appropriate.
139
+ 2. Whether the trajectory-level adapter and recorded solver/environment metadata omit evidence
140
+ that SciPy users would expect.
141
+ 3. Whether a downstream example or ecosystem/documentation reference could be appropriate if the
142
+ tool proves useful to users.
143
+
144
+ Feedback on the design, terminology, numerical claims, and useful real-world test cases would be
145
+ very welcome.