Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

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

## Unreleased

- Added bounded offline `verify-evidence` validation for generated pytest evidence, replay
decisions, artifact hashes, safe relative paths, and the fixed regression template.
- The unreleased package identity is now `0.3.0` for the new generated-evidence contract.
- `generate-test` now writes a path-portable claim/decision evidence record that binds the
validated replay verdict to SHA-256 hashes of the copied fixture and generated pytest, the
exact reproduction command, PhaseProbe attribution, and explicit scientific limitations.
- Identical generated evidence is idempotent; conflicting evidence is rejected without overwrite.
- Invariant-only fixtures now use the narrower `simulation-replay-regression` claim kind, and the
unsigned evidence-integrity boundary is explicit.

## 0.2.1 — 2026-08-31

- Fixed Issue #4: all four SciPy quick-start configurations now ship inside the importable
Expand Down
3 changes: 1 addition & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ title: "PhaseProbe"
type: software
authors:
- name: "Ali"
version: 0.2.1
date-released: 2026-08-31
version: 0.3.0
url: "https://github.com/aliengineering-byte/phaseprobe"
repository-code: "https://github.com/aliengineering-byte/phaseprobe"
license: Apache-2.0
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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
phaseprobe verify-evidence tests/generated/logistic_map-pytest-evidence.json
python -m pytest -q tests/generated
phaseprobe report .phaseprobe/runs/<run-id>
```
Expand Down Expand Up @@ -134,6 +135,7 @@ does not import it. See [the audited contract](docs/SCIPY_SOLVE_IVP_AUDIT.md),
| `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 without conflicting overwrites | Executable test under `tests/generated/` |
| `verify-evidence` | Re-derive claims/replay decisions and validate bounded strict JSON, artifact hashes, paths, and the fixed template offline | Stable verified or invalid-input exit |
| `report` | Regenerate terminal, versioned JSON, and self-contained offline HTML evidence | Local report files |

Common options:
Expand Down Expand Up @@ -180,6 +182,22 @@ manifest.json sizes and SHA-256 hashes

Model names used for generated test paths are sanitized. The generated source comes from a fixed template; configuration strings never become executable Python.

`generate-test` validates the replay before writing anything. Alongside the fixed pytest template
and copied fixture, it writes `<model>-pytest-evidence.json`: a path-portable claim/decision record
containing repository/version attribution, the declared replay comparisons, SHA-256 hashes for
both executable artifacts, the exact pytest command, and explicit scientific limitations.
Run `phaseprobe verify-evidence <evidence.json>` before pytest to validate the strict schema,
recompute the hashes, re-execute the replay policy, derive the claim/decision fields, and confirm
that the regression still matches PhaseProbe's fixed template. Validation is offline, bounded to a
1 MiB evidence document and 32 MiB per generated artifact, rejects duplicate keys and unsafe paths,
and exits `2` for invalid input.
The record is unsigned: consumers must recompute the two artifact hashes to detect a recomputed or
substituted record. It is portable integrity metadata, not an authentication signature.

If `generate-test` reports a conflict, it has left the existing output untouched. An identical
artifact can be generated again safely; for different content, inspect the existing files and
choose a fresh output directory (or remove them only after deciding they are no longer needed).

## What PhaseProbe adds—and what it does not

| existing category | established strength | PhaseProbe’s narrower job |
Expand Down Expand Up @@ -261,6 +279,7 @@ python scripts/hygiene.py
```

See [CONTRIBUTING.md](CONTRIBUTING.md) and the concrete good-first-issue templates in `.github/ISSUE_TEMPLATE/`.
Report a reproducible defect with the [bug form](https://github.com/aliengineering-byte/phaseprobe/issues/new?template=bug_report.yml), or propose a bounded adapter or workflow with the [feature form](https://github.com/aliengineering-byte/phaseprobe/issues/new?template=feature_request.yml). Please do not include secrets or private model data.

## License and citation

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "phaseprobe"
version = "0.2.1"
version = "0.3.0"
description = "Find reproducible qualitative simulation transitions and turn them into regression tests."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/phaseprobe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
"run_simulation",
]

__version__ = "0.2.1"
__version__ = "0.3.0"
29 changes: 28 additions & 1 deletion src/phaseprobe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
NumericalFailure,
PhaseProbeError,
)
from phaseprobe.generate import generate_regression_test
from phaseprobe.generate import generate_regression_test, verify_generated_evidence
from phaseprobe.replay import verify_replay
from phaseprobe.reporting import json_report, regenerate_reports, terminal_report

Expand Down Expand Up @@ -81,6 +81,12 @@ def build_parser() -> argparse.ArgumentParser:
generate.add_argument("--output-directory", type=Path, default=Path("tests") / "generated")
generate.add_argument("--json", action="store_true")

verify_evidence = commands.add_parser(
"verify-evidence", help="verify generated pytest evidence and artifacts offline"
)
verify_evidence.add_argument("evidence", type=Path)
verify_evidence.add_argument("--json", action="store_true")

report = commands.add_parser(
"report", help="regenerate terminal, JSON, and offline HTML evidence"
)
Expand Down Expand Up @@ -172,6 +178,7 @@ def _generate_command(args: argparse.Namespace) -> int:
"status": "PYTEST REGRESSION GENERATED",
"test": str(generated.test_path),
"fixture": str(generated.fixture_path),
"evidence": str(generated.evidence_path),
}
if bool(getattr(args, "json", False)):
sys.stdout.write(json_report(data))
Expand All @@ -180,6 +187,24 @@ def _generate_command(args: argparse.Namespace) -> int:
print()
print(f"Test: {generated.test_path}")
print(f"Replay fixture: {generated.fixture_path}")
print(f"Execution evidence: {generated.evidence_path}")
return int(ExitCode.OK)


def _verify_evidence_command(args: argparse.Namespace) -> int:
evidence = args.evidence
if not isinstance(evidence, Path):
raise ConfigurationError("evidence must be a path")
result = verify_generated_evidence(evidence)
data = result.as_dict()
if bool(getattr(args, "json", False)):
sys.stdout.write(json_report(data))
else:
print(data["status"])
print()
print(f"Evidence SHA-256: {result.evidence_sha256}")
print(f"Replay fixture: {result.fixture_path}")
print(f"Pytest regression: {result.test_path}")
return int(ExitCode.OK)


Expand Down Expand Up @@ -221,6 +246,8 @@ def main(argv: Sequence[str] | None = None) -> int:
return _replay_command(args)
if args.command == "generate-test":
return _generate_command(args)
if args.command == "verify-evidence":
return _verify_evidence_command(args)
if args.command == "report":
return _report_command(args)
raise ConfigurationError(f"unknown command {args.command!r}")
Expand Down
Loading
Loading