physverify 0.7.0__tar.gz → 0.8.0__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 (91) hide show
  1. physverify-0.8.0/PKG-INFO +132 -0
  2. physverify-0.8.0/README.md +88 -0
  3. {physverify-0.7.0 → physverify-0.8.0}/pyproject.toml +23 -4
  4. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/__init__.py +2 -3
  5. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/agent.py +93 -9
  6. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/butcher.py +2 -2
  7. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/checks.py +1 -1
  8. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/cli.py +87 -5
  9. physverify-0.8.0/src/physverify/confirm.py +338 -0
  10. physverify-0.8.0/src/physverify/flow.py +272 -0
  11. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/fluxes.py +1 -1
  12. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/identities.py +1 -1
  13. physverify-0.8.0/src/physverify/isolate.py +109 -0
  14. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/lint.py +23 -1
  15. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/mms.py +10 -2
  16. physverify-0.8.0/src/physverify/operators.py +215 -0
  17. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/order.py +38 -11
  18. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/report.py +15 -0
  19. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/scan.py +10 -7
  20. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/smoke.py +3 -1
  21. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stencils.py +65 -0
  22. physverify-0.8.0/src/physverify/steps.py +97 -0
  23. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/calibrate.py +0 -2
  24. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/checks.py +1 -2
  25. physverify-0.8.0/src/physverify/stochastic/directions.py +84 -0
  26. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/exact.py +2 -2
  27. physverify-0.8.0/src/physverify/stochastic/ito.py +87 -0
  28. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/stats.py +46 -2
  29. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/studies.py +21 -5
  30. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/symbolic.py +1 -1
  31. physverify-0.8.0/src/physverify.egg-info/PKG-INFO +132 -0
  32. {physverify-0.7.0 → physverify-0.8.0}/src/physverify.egg-info/SOURCES.txt +16 -0
  33. {physverify-0.7.0 → physverify-0.8.0}/src/physverify.egg-info/requires.txt +2 -0
  34. {physverify-0.7.0 → physverify-0.8.0}/tests/test_agent.py +53 -2
  35. {physverify-0.7.0 → physverify-0.8.0}/tests/test_batch8_lessons.py +9 -2
  36. {physverify-0.7.0 → physverify-0.8.0}/tests/test_butcher_properties.py +10 -2
  37. {physverify-0.7.0 → physverify-0.8.0}/tests/test_cli.py +13 -0
  38. physverify-0.8.0/tests/test_confirm.py +319 -0
  39. physverify-0.8.0/tests/test_corpus_ledger.py +27 -0
  40. physverify-0.8.0/tests/test_correctness_fixes.py +142 -0
  41. physverify-0.8.0/tests/test_directions.py +94 -0
  42. {physverify-0.7.0 → physverify-0.8.0}/tests/test_docs_examples.py +16 -0
  43. {physverify-0.7.0 → physverify-0.8.0}/tests/test_figures.py +11 -2
  44. {physverify-0.7.0 → physverify-0.8.0}/tests/test_helpers_2026_10.py +0 -1
  45. {physverify-0.7.0 → physverify-0.8.0}/tests/test_ising.py +7 -1
  46. physverify-0.8.0/tests/test_new_checks_2026_11.py +142 -0
  47. {physverify-0.7.0 → physverify-0.8.0}/tests/test_new_problems.py +1 -2
  48. physverify-0.8.0/tests/test_operators.py +112 -0
  49. {physverify-0.7.0 → physverify-0.8.0}/tests/test_order.py +34 -1
  50. physverify-0.8.0/tests/test_quadrature.py +76 -0
  51. {physverify-0.7.0 → physverify-0.8.0}/tests/test_roadmap_0_7.py +7 -1
  52. physverify-0.8.0/tests/test_sandbox_installs.py +52 -0
  53. {physverify-0.7.0 → physverify-0.8.0}/tests/test_scan.py +29 -1
  54. physverify-0.8.0/tests/test_scan_flow.py +150 -0
  55. {physverify-0.7.0 → physverify-0.8.0}/tests/test_seeded_bugs.py +1 -1
  56. {physverify-0.7.0 → physverify-0.8.0}/tests/test_v040_helpers.py +12 -1
  57. physverify-0.7.0/PKG-INFO +0 -95
  58. physverify-0.7.0/README.md +0 -53
  59. physverify-0.7.0/src/physverify.egg-info/PKG-INFO +0 -95
  60. {physverify-0.7.0 → physverify-0.8.0}/LICENSE +0 -0
  61. {physverify-0.7.0 → physverify-0.8.0}/setup.cfg +0 -0
  62. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/__main__.py +0 -0
  63. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/constants.py +0 -0
  64. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/demo.py +1 -1
  65. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/loaders.py +0 -0
  66. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/probes.py +0 -0
  67. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/pytest_plugin.py +0 -0
  68. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/references.py +0 -0
  69. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/scaffold.py +0 -0
  70. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/specscan.py +0 -0
  71. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/steppers.py +0 -0
  72. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/__init__.py +0 -0
  73. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/ising.py +0 -0
  74. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/reference.py +0 -0
  75. {physverify-0.7.0 → physverify-0.8.0}/src/physverify/stochastic/sampler.py +0 -0
  76. {physverify-0.7.0 → physverify-0.8.0}/src/physverify.egg-info/dependency_links.txt +0 -0
  77. {physverify-0.7.0 → physverify-0.8.0}/src/physverify.egg-info/entry_points.txt +0 -0
  78. {physverify-0.7.0 → physverify-0.8.0}/src/physverify.egg-info/top_level.txt +0 -0
  79. {physverify-0.7.0 → physverify-0.8.0}/tests/test_awkward_input.py +0 -0
  80. {physverify-0.7.0 → physverify-0.8.0}/tests/test_batch7_lessons.py +0 -0
  81. {physverify-0.7.0 → physverify-0.8.0}/tests/test_corpus_lessons.py +1 -1
  82. {physverify-0.7.0 → physverify-0.8.0}/tests/test_method_bugs.py +0 -0
  83. {physverify-0.7.0 → physverify-0.8.0}/tests/test_mms.py +0 -0
  84. {physverify-0.7.0 → physverify-0.8.0}/tests/test_probes.py +0 -0
  85. {physverify-0.7.0 → physverify-0.8.0}/tests/test_properties.py +1 -1
  86. {physverify-0.7.0 → physverify-0.8.0}/tests/test_real_projects.py +1 -1
  87. {physverify-0.7.0 → physverify-0.8.0}/tests/test_reference_problems.py +1 -1
  88. {physverify-0.7.0 → physverify-0.8.0}/tests/test_references.py +0 -0
  89. {physverify-0.7.0 → physverify-0.8.0}/tests/test_scaffold.py +0 -0
  90. {physverify-0.7.0 → physverify-0.8.0}/tests/test_studies.py +1 -1
  91. {physverify-0.7.0 → physverify-0.8.0}/tests/test_versions.py +0 -0
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: physverify
3
+ Version: 0.8.0
4
+ Summary: Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies, conserved quantities, limiting cases and exact patch tests.
5
+ Author: Ram Chandra Giri
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rcgiri-physics/physverify
8
+ Project-URL: Documentation, https://rcgiri-physics.github.io/physverify/
9
+ Project-URL: Issues, https://github.com/rcgiri-physics/physverify/issues
10
+ Project-URL: Changelog, https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md
11
+ Keywords: verification,method of manufactured solutions,numerical methods,physics,simulation,testing
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Education
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Development Status :: 3 - Alpha
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.24
30
+ Requires-Dist: tomli>=1.1; python_version < "3.11"
31
+ Provides-Extra: symbolic
32
+ Requires-Dist: sympy>=1.12; extra == "symbolic"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8; extra == "dev"
35
+ Requires-Dist: pytest-cov>=6.1; extra == "dev"
36
+ Requires-Dist: hypothesis>=6; extra == "dev"
37
+ Requires-Dist: sympy>=1.12; extra == "dev"
38
+ Requires-Dist: ruff>=0.6; extra == "dev"
39
+ Provides-Extra: docs
40
+ Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
41
+ Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
42
+ Requires-Dist: mkdocstrings[python]<2,>=1.0.8; extra == "docs"
43
+ Dynamic: license-file
44
+
45
+ # physverify: confirm physics claims by running the code
46
+
47
+ [![tests](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml/badge.svg)](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/physverify)](https://pypi.org/project/physverify/) [![Python](https://img.shields.io/pypi/pyversions/physverify)](https://pypi.org/project/physverify/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23534512.svg)](https://doi.org/10.5281/zenodo.23534512) [![Docs](https://img.shields.io/badge/docs-site-blue)](https://rcgiri-physics.github.io/physverify/)
48
+
49
+ **A suspected bug in simulation code is a claim. physverify turns it into a run against a reference written from the physics, and gives a verdict with the numbers.**
50
+
51
+ The claim can come from an AI code review, a colleague, a static check or your own doubt. Reading the code again rarely settles it; a run against an exact solution, a conservation law or a convergence order does. physverify supplies those checks for numerical ODE and PDE code (order of convergence, manufactured solutions, conservation, Runge-Kutta tables, exact references, a few Monte Carlo statistics), a sandbox for running code you do not trust, and plain-language verdicts. It needs only NumPy. Current release: 0.8.0 (v0.8.0); actively developed, contributions welcome.
52
+
53
+ ![A claim from a review becomes a run against a reference from the physics, and the run gives a verdict: confirmed, minor, rejected or open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-to-verdict.svg)
54
+
55
+ ## What confirming claims found on 100 repositories
56
+
57
+ Five AI reviewers (three Claude models, Qwen 3.8 Flash and GPT 5.6 Terra) reviewed 100 public physics repositories with one fixed prompt. Fifty had a defect reported earlier; fifty had been tested and found clean. On the clean fifty, 119 review claims were drawn and each was given a check run in the sandbox:
58
+
59
+ ![Of 119 claims checked on repositories tested clean: 40 changed a result, 51 were real but minor, 10 were wrong, 18 stayed open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-outcomes.svg)
60
+
61
+ - **40 claims were real defects that change a result:** 33 distinct defects in 19 of the 50 "clean" repositories. 32 have been reported to their maintainers.
62
+ - **51 were real but minor** (invalid input, display, docstrings, code paths the repository never uses), **10 were wrong** and **18 could not be settled.**
63
+ - Nine of the ten wrong claims came from one model. Where a reading was recorded before the run (20 claims), the run changed 3 verdicts.
64
+ - Against the 67 defects reported earlier, the reviewers named 36 to 49; `physverify scan`, which reads code without running it, named 11.
65
+
66
+ So AI review finds many real defects, including ones earlier testing missed, but a claim is not a finding until something runs. The counts hold for this sample only and are not a bug rate for physics code. The labels come from two automated checkers following written rules, not from people. Tables: `scripts/study100.py` regenerates them from the per-claim file, released with the preprint ([doi:10.5281/zenodo.24224433](https://doi.org/10.5281/zenodo.24224433)).
67
+
68
+ ## Confirmed by maintainers
69
+
70
+ Some defects confirmed or fixed by the projects' own maintainers after a report:
71
+
72
+ | Project | What was found | Outcome |
73
+ |---|---|---|
74
+ | [SymPy](https://github.com/sympy/sympy/issues/30810) | three units differ from their exact definitions | confirmed against primary sources |
75
+ | [Astropy](https://github.com/astropy/astropy/issues/20681) | "surface tension" registered as J m² instead of J/m² | confirmed; fix in progress |
76
+ | [PyGMT](https://github.com/GenericMappingTools/pygmt/issues/4960) | strings with a space treated as datetimes | fixed |
77
+ | [EinsteinPy](https://github.com/einsteinpy/einsteinpy/issues/705) | de Sitter scale factor on a spatial coordinate | fixed |
78
+ | [TESPy](https://github.com/oemof/tespy/issues/1127) | entropy pressure term with the wrong units | fixed |
79
+ | [PyAstronomy](https://github.com/sczesla/PyAstronomy/issues/71) | two constants modules defaulted to different unit systems (CGS and SI), so `planck` and other functions were wrong | fixed |
80
+ | [qmat](https://github.com/Parallel-in-Time/qmat/issues/23) | Runge-Kutta schemes labelled one order too high | fixed; now uses `physverify.butcher` in CI |
81
+
82
+ [The full list](https://rcgiri-physics.github.io/physverify/confirmed/) has more projects and the details.
83
+
84
+ ## Try it
85
+
86
+ ```bash
87
+ pip install physverify
88
+ physverify demo
89
+ ```
90
+
91
+ The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
92
+
93
+ ![Error against step size: the correct solver follows the slope-2 line, the broken one levels off](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/convergence.svg)
94
+
95
+ New to this? [Start here](https://rcgiri-physics.github.io/physverify/start-here/) takes about ten minutes and includes a prompt to paste if you use an AI coding assistant.
96
+
97
+ ## Use it on your own code
98
+
99
+ - **Suspected bugs from a review (yours or an AI's):** write each claim down, pick a reference that does not come from the code, and run the smallest test that tells the two answers apart. [Checking an AI code review](https://rcgiri-physics.github.io/physverify/ai-review/) walks through it with examples.
100
+ - **An ODE solver:** `physverify init verify_solver.py --solver mysolver --order 4` writes a spec; fill in the equations (from the physics, not from your code) and run `physverify run verify_solver.py`.
101
+ - **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
102
+ - **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks order, stage order and stability from the coefficients alone.
103
+ - **Code you have not read yet:** `physverify scan PATH` reads the source, runs nothing, and flags a fixed set of patterns (Runge-Kutta tables below their stated order, grid spacings that do not match their grid, near-miss constants, removed NumPy/SciPy features, undefined names, unit tables with the wrong dimension). A clean scan means only that those patterns were not found. `scan` is kept working but no longer gets new rules; the work now goes into confirming claims.
104
+
105
+ Code from other people runs only in the [sandbox](https://github.com/rcgiri-physics/physverify/tree/main/sandbox) (no network, read-only repository).
106
+
107
+ ## Run it in CI
108
+
109
+ A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/)):
110
+
111
+ ```yaml
112
+ - uses: rcgiri-physics/physverify@v0.8.0
113
+ with:
114
+ specs: tests/verify_*.py
115
+ ```
116
+
117
+ [Getting started](https://rcgiri-physics.github.io/physverify/getting-started/) · [Recipes](https://rcgiri-physics.github.io/physverify/recipes/) · [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/) · [Troubleshooting](https://rcgiri-physics.github.io/physverify/troubleshooting/) · [API](https://rcgiri-physics.github.io/physverify/api/)
118
+
119
+ ## Other evidence
120
+
121
+ - **Seeded bugs:** of 37 bugs added one at a time to five small solvers, the checks flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The same person wrote the bugs and the checks, so this shows the tool works as designed, not how it does on other code. `python scripts/reproduce_all.py` regenerates the tables.
122
+ - **Earlier corpus testing (October 2026):** of the first 74 defects reported to authors, 14 were found by a physverify check as such, 11 by running the code at all, and the rest by the reference in each repository's test script or by reading; the [ledger](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/LEDGER.md) lists them by ID. Details: [results](https://rcgiri-physics.github.io/physverify/results/).
123
+
124
+ ## Limits
125
+
126
+ Works best for ODEs; PDEs only in 1D so far. Your code needs a way to add a source term, or a small wrapper that can itself be wrong. A PASS means these checks found nothing wrong; it does not prove the simulation correct, and it is only as independent as the reference you wrote: see [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/).
127
+
128
+ ## Contributing, citing, licence
129
+
130
+ Help is welcome: a trial report on your own solver, seeded bugs written by someone other than the author, adapters, documentation. See [CONTRIBUTING.md](https://github.com/rcgiri-physics/physverify/blob/main/CONTRIBUTING.md), the [code of conduct](https://github.com/rcgiri-physics/physverify/blob/main/CODE_OF_CONDUCT.md), [SUPPORT.md](https://github.com/rcgiri-physics/physverify/blob/main/SUPPORT.md), [SECURITY.md](https://github.com/rcgiri-physics/physverify/blob/main/SECURITY.md) on running untrusted code, and [AGENTS.md](https://github.com/rcgiri-physics/physverify/blob/main/AGENTS.md) for AI assistants. To cite, use [doi:10.5281/zenodo.23534512](https://doi.org/10.5281/zenodo.23534512) or [CITATION.cff](https://github.com/rcgiri-physics/physverify/blob/main/CITATION.cff). Changes: [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md); plans: [ROADMAP.md](https://github.com/rcgiri-physics/physverify/blob/main/ROADMAP.md). The methods are standard and other tools do parts of this, some better (NodePy for Runge-Kutta analysis); see [related work and credits](https://rcgiri-physics.github.io/physverify/related-work/). MIT licence, copyright (c) 2026 Ram Chandra Giri.
131
+
132
+ <!-- mcp-name: io.github.rcgiri-physics/physverify -->
@@ -0,0 +1,88 @@
1
+ # physverify: confirm physics claims by running the code
2
+
3
+ [![tests](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml/badge.svg)](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/physverify)](https://pypi.org/project/physverify/) [![Python](https://img.shields.io/pypi/pyversions/physverify)](https://pypi.org/project/physverify/) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23534512.svg)](https://doi.org/10.5281/zenodo.23534512) [![Docs](https://img.shields.io/badge/docs-site-blue)](https://rcgiri-physics.github.io/physverify/)
4
+
5
+ **A suspected bug in simulation code is a claim. physverify turns it into a run against a reference written from the physics, and gives a verdict with the numbers.**
6
+
7
+ The claim can come from an AI code review, a colleague, a static check or your own doubt. Reading the code again rarely settles it; a run against an exact solution, a conservation law or a convergence order does. physverify supplies those checks for numerical ODE and PDE code (order of convergence, manufactured solutions, conservation, Runge-Kutta tables, exact references, a few Monte Carlo statistics), a sandbox for running code you do not trust, and plain-language verdicts. It needs only NumPy. Current release: 0.8.0 (v0.8.0); actively developed, contributions welcome.
8
+
9
+ ![A claim from a review becomes a run against a reference from the physics, and the run gives a verdict: confirmed, minor, rejected or open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-to-verdict.svg)
10
+
11
+ ## What confirming claims found on 100 repositories
12
+
13
+ Five AI reviewers (three Claude models, Qwen 3.8 Flash and GPT 5.6 Terra) reviewed 100 public physics repositories with one fixed prompt. Fifty had a defect reported earlier; fifty had been tested and found clean. On the clean fifty, 119 review claims were drawn and each was given a check run in the sandbox:
14
+
15
+ ![Of 119 claims checked on repositories tested clean: 40 changed a result, 51 were real but minor, 10 were wrong, 18 stayed open](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/claim-outcomes.svg)
16
+
17
+ - **40 claims were real defects that change a result:** 33 distinct defects in 19 of the 50 "clean" repositories. 32 have been reported to their maintainers.
18
+ - **51 were real but minor** (invalid input, display, docstrings, code paths the repository never uses), **10 were wrong** and **18 could not be settled.**
19
+ - Nine of the ten wrong claims came from one model. Where a reading was recorded before the run (20 claims), the run changed 3 verdicts.
20
+ - Against the 67 defects reported earlier, the reviewers named 36 to 49; `physverify scan`, which reads code without running it, named 11.
21
+
22
+ So AI review finds many real defects, including ones earlier testing missed, but a claim is not a finding until something runs. The counts hold for this sample only and are not a bug rate for physics code. The labels come from two automated checkers following written rules, not from people. Tables: `scripts/study100.py` regenerates them from the per-claim file, released with the preprint ([doi:10.5281/zenodo.24224433](https://doi.org/10.5281/zenodo.24224433)).
23
+
24
+ ## Confirmed by maintainers
25
+
26
+ Some defects confirmed or fixed by the projects' own maintainers after a report:
27
+
28
+ | Project | What was found | Outcome |
29
+ |---|---|---|
30
+ | [SymPy](https://github.com/sympy/sympy/issues/30810) | three units differ from their exact definitions | confirmed against primary sources |
31
+ | [Astropy](https://github.com/astropy/astropy/issues/20681) | "surface tension" registered as J m² instead of J/m² | confirmed; fix in progress |
32
+ | [PyGMT](https://github.com/GenericMappingTools/pygmt/issues/4960) | strings with a space treated as datetimes | fixed |
33
+ | [EinsteinPy](https://github.com/einsteinpy/einsteinpy/issues/705) | de Sitter scale factor on a spatial coordinate | fixed |
34
+ | [TESPy](https://github.com/oemof/tespy/issues/1127) | entropy pressure term with the wrong units | fixed |
35
+ | [PyAstronomy](https://github.com/sczesla/PyAstronomy/issues/71) | two constants modules defaulted to different unit systems (CGS and SI), so `planck` and other functions were wrong | fixed |
36
+ | [qmat](https://github.com/Parallel-in-Time/qmat/issues/23) | Runge-Kutta schemes labelled one order too high | fixed; now uses `physverify.butcher` in CI |
37
+
38
+ [The full list](https://rcgiri-physics.github.io/physverify/confirmed/) has more projects and the details.
39
+
40
+ ## Try it
41
+
42
+ ```bash
43
+ pip install physverify
44
+ physverify demo
45
+ ```
46
+
47
+ The demo checks a correct solver (all checks pass), then the same solver with the damping 10% too large (all fail):
48
+
49
+ ![Error against step size: the correct solver follows the slope-2 line, the broken one levels off](https://raw.githubusercontent.com/rcgiri-physics/physverify/main/docs/images/convergence.svg)
50
+
51
+ New to this? [Start here](https://rcgiri-physics.github.io/physverify/start-here/) takes about ten minutes and includes a prompt to paste if you use an AI coding assistant.
52
+
53
+ ## Use it on your own code
54
+
55
+ - **Suspected bugs from a review (yours or an AI's):** write each claim down, pick a reference that does not come from the code, and run the smallest test that tells the two answers apart. [Checking an AI code review](https://rcgiri-physics.github.io/physverify/ai-review/) walks through it with examples.
56
+ - **An ODE solver:** `physverify init verify_solver.py --solver mysolver --order 4` writes a spec; fill in the equations (from the physics, not from your code) and run `physverify run verify_solver.py`.
57
+ - **Errors you already have:** `check_order(hs, errors, expected=2)` gives PASS, FAIL or INCONCLUSIVE with a reason.
58
+ - **A Runge-Kutta table:** `check_tableau(A, b, c, claimed=3)` from `physverify.butcher` checks order, stage order and stability from the coefficients alone.
59
+ - **Code you have not read yet:** `physverify scan PATH` reads the source, runs nothing, and flags a fixed set of patterns (Runge-Kutta tables below their stated order, grid spacings that do not match their grid, near-miss constants, removed NumPy/SciPy features, undefined names, unit tables with the wrong dimension). A clean scan means only that those patterns were not found. `scan` is kept working but no longer gets new rules; the work now goes into confirming claims.
60
+
61
+ Code from other people runs only in the [sandbox](https://github.com/rcgiri-physics/physverify/tree/main/sandbox) (no network, read-only repository).
62
+
63
+ ## Run it in CI
64
+
65
+ A failed check fails the job, like a broken unit test ([CI guide](https://rcgiri-physics.github.io/physverify/ci/)):
66
+
67
+ ```yaml
68
+ - uses: rcgiri-physics/physverify@v0.8.0
69
+ with:
70
+ specs: tests/verify_*.py
71
+ ```
72
+
73
+ [Getting started](https://rcgiri-physics.github.io/physverify/getting-started/) · [Recipes](https://rcgiri-physics.github.io/physverify/recipes/) · [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/) · [Troubleshooting](https://rcgiri-physics.github.io/physverify/troubleshooting/) · [API](https://rcgiri-physics.github.io/physverify/api/)
74
+
75
+ ## Other evidence
76
+
77
+ - **Seeded bugs:** of 37 bugs added one at a time to five small solvers, the checks flagged 35 (plain "compare with one answer" tests: 27), with no false alarms. The same person wrote the bugs and the checks, so this shows the tool works as designed, not how it does on other code. `python scripts/reproduce_all.py` regenerates the tables.
78
+ - **Earlier corpus testing (October 2026):** of the first 74 defects reported to authors, 14 were found by a physverify check as such, 11 by running the code at all, and the rest by the reference in each repository's test script or by reading; the [ledger](https://github.com/rcgiri-physics/physverify/blob/main/results/corpus/LEDGER.md) lists them by ID. Details: [results](https://rcgiri-physics.github.io/physverify/results/).
79
+
80
+ ## Limits
81
+
82
+ Works best for ODEs; PDEs only in 1D so far. Your code needs a way to add a source term, or a small wrapper that can itself be wrong. A PASS means these checks found nothing wrong; it does not prove the simulation correct, and it is only as independent as the reference you wrote: see [What a PASS means](https://rcgiri-physics.github.io/physverify/assumptions/).
83
+
84
+ ## Contributing, citing, licence
85
+
86
+ Help is welcome: a trial report on your own solver, seeded bugs written by someone other than the author, adapters, documentation. See [CONTRIBUTING.md](https://github.com/rcgiri-physics/physverify/blob/main/CONTRIBUTING.md), the [code of conduct](https://github.com/rcgiri-physics/physverify/blob/main/CODE_OF_CONDUCT.md), [SUPPORT.md](https://github.com/rcgiri-physics/physverify/blob/main/SUPPORT.md), [SECURITY.md](https://github.com/rcgiri-physics/physverify/blob/main/SECURITY.md) on running untrusted code, and [AGENTS.md](https://github.com/rcgiri-physics/physverify/blob/main/AGENTS.md) for AI assistants. To cite, use [doi:10.5281/zenodo.23534512](https://doi.org/10.5281/zenodo.23534512) or [CITATION.cff](https://github.com/rcgiri-physics/physverify/blob/main/CITATION.cff). Changes: [CHANGELOG.md](https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md); plans: [ROADMAP.md](https://github.com/rcgiri-physics/physverify/blob/main/ROADMAP.md). The methods are standard and other tools do parts of this, some better (NodePy for Runge-Kutta analysis); see [related work and credits](https://rcgiri-physics.github.io/physverify/related-work/). MIT licence, copyright (c) 2026 Ram Chandra Giri.
87
+
88
+ <!-- mcp-name: io.github.rcgiri-physics/physverify -->
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "physverify"
7
- version = "0.7.0"
8
- description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and a seeded-bug benchmark."
7
+ version = "0.8.0"
8
+ description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies, conserved quantities, limiting cases and exact patch tests."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
@@ -38,8 +38,8 @@ Changelog = "https://github.com/rcgiri-physics/physverify/blob/main/CHANGELOG.md
38
38
 
39
39
  [project.optional-dependencies]
40
40
  symbolic = ["sympy>=1.12"]
41
- dev = ["pytest>=8", "pytest-cov>=6.1", "hypothesis>=6", "sympy>=1.12"]
42
- docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10"]
41
+ dev = ["pytest>=8", "pytest-cov>=6.1", "hypothesis>=6", "sympy>=1.12", "ruff>=0.6"]
42
+ docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10", "mkdocstrings[python]>=1.0.8,<2"]
43
43
 
44
44
  [project.scripts]
45
45
  physverify = "physverify.cli:main"
@@ -53,3 +53,22 @@ where = ["src"]
53
53
  [tool.pytest.ini_options]
54
54
  testpaths = ["tests"]
55
55
  pythonpath = ["src", "benchmark"]
56
+
57
+ [tool.ruff]
58
+ extend-exclude = ["build"]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["F", "B", "UP", "I"]
62
+ # zip() truncating to the shorter input is relied on in places; strict= is a separate review
63
+ ignore = ["B905"]
64
+
65
+ [tool.ruff.lint.isort]
66
+ known-first-party = ["physverify", "pvbench"]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ # results/ cites line numbers in the seeded-bug problems, so their imports stay where they are
70
+ "benchmark/pvbench/problems/*" = ["F401", "I001"]
71
+ # the import itself is what this script checks
72
+ "sandbox/probe.py" = ["F401", "I001"]
73
+ # short teaching examples; the closure is called inside its loop
74
+ "docs/examples/*" = ["B023"]
@@ -2,11 +2,10 @@
2
2
 
3
3
  from .checks import Check, OrderResult, Result, SkipCheck, requires
4
4
  from .mms import ManufacturedODE, mms_order_study, zero_source
5
- from .order import check_order, fit_order, pairwise_orders
6
- from .order import check_exact
5
+ from .order import check_exact, check_order, fit_order, pairwise_orders
7
6
  from .studies import invariant_study, ode_checks, patch_study, reference_study
8
7
 
9
- __version__ = "0.7.0"
8
+ __version__ = "0.8.0"
10
9
 
11
10
  __all__ = [
12
11
  "Check",
@@ -10,8 +10,11 @@ Hook (`physverify hook`)
10
10
  assistant. Otherwise it exits 0 and prints nothing. See docs/agent.md.
11
11
 
12
12
  MCP server (`physverify mcp`)
13
- Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0), with one
14
- tool, `run_checks`, that takes spec paths and returns the JSON report. It is
13
+ Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0), with two
14
+ tools: `run_checks`, that takes spec paths and returns the JSON report, and
15
+ `confirm_claim`, that judges one claim file (docs/confirm.md). Both run the code
16
+ in a fresh Python process with a time limit (physverify.isolate), so an edit to
17
+ the solver between two calls is always seen. It is
15
18
  written with the standard library only, so it has no extra dependency; it
16
19
  implements the parts of the protocol a tool server needs, not the whole
17
20
  specification. It serves both protocol eras on the same process:
@@ -32,6 +35,7 @@ import json
32
35
  import sys
33
36
  from pathlib import Path
34
37
 
38
+ RUN_TIMEOUT = 600.0 # seconds for one run_checks call in its own process
35
39
  SKIP_DIRS = {"venv", ".venv", "env", "node_modules", "build", "dist", "__pycache__", "site-packages"}
36
40
  MAX_REPORTED = 10
37
41
 
@@ -155,7 +159,7 @@ UNSUPPORTED_PROTOCOL_VERSION = -32022
155
159
  CACHE_HINTS = {"ttlMs": 300_000, "cacheScope": "public"} # the tool list does not change while the server runs
156
160
  INSTRUCTIONS = (
157
161
  "Run physverify checks on physics simulation code after writing or editing it. "
158
- "The tool executes the project's verify_*.py spec files (Python), so use it only on projects you trust."
162
+ "The tools execute the project's verify_*.py spec files and claim files (Python), so use them only on projects you trust."
159
163
  )
160
164
 
161
165
  TOOLS = [
@@ -173,9 +177,31 @@ TOOLS = [
173
177
  "properties": {
174
178
  "specs": {"type": "array", "items": {"type": "string"}, "description": "spec file paths"},
175
179
  "project": {"type": "string", "description": "project folder to search for verify_*.py"},
180
+ "timeout": {"type": "number", "description": f"seconds for the whole run (default {RUN_TIMEOUT:g})"},
176
181
  },
177
182
  },
178
- }
183
+ },
184
+ {
185
+ "name": "confirm_claim",
186
+ "description": (
187
+ "Judge one suspected defect with physverify confirm. Takes a claim file that defines CLAIM, "
188
+ "control() and checks() with a reference written from the physics (docs/confirm.md), runs it "
189
+ "in a fresh process and returns the verdict: confirmed, rejected or not decidable, with the "
190
+ "reason and each check's status and message. Appends the result to a ledger file. Runs the "
191
+ "code, so it needs trusted=true or the sandbox (PHYSVERIFY_REPO set)."
192
+ ),
193
+ "inputSchema": {
194
+ "type": "object",
195
+ "properties": {
196
+ "claim": {"type": "string", "description": "path of the claim file"},
197
+ "trusted": {"type": "boolean", "description": "the code under test is trusted (required outside the sandbox)"},
198
+ "ledger": {"type": "string", "description": "ledger file (default confirm_ledger.jsonl)"},
199
+ "timeout": {"type": "number", "description": "seconds for the whole run (default 300)"},
200
+ "allow_dependent_reference": {"type": "string", "description": "reason, recorded, to run although the reference seems to use the code under test"},
201
+ },
202
+ "required": ["claim"],
203
+ },
204
+ },
179
205
  ]
180
206
 
181
207
 
@@ -193,12 +219,68 @@ def _run_checks_tool(arguments: dict) -> dict:
193
219
  specs = find_specs(Path(arguments.get("project") or "."))
194
220
  if not specs:
195
221
  return {"content": [{"type": "text", "text": "No spec files found (verify_*.py defining checks())."}], "isError": True}
196
- report = run_specs(specs)
222
+ from .confirm import parse_timeout
223
+
224
+ try:
225
+ timeout = parse_timeout(RUN_TIMEOUT if arguments.get("timeout") is None else arguments["timeout"])
226
+ except ValueError as exc:
227
+ return {"content": [{"type": "text", "text": f"run_checks: {exc}"}], "isError": True}
228
+ report = run_specs_isolated(specs, timeout)
197
229
  failed = bool(report["summary"]["not_ok"] or report["load_errors"])
198
230
  text = (failure_summary(report) + "\n\n" if failed else "") + json.dumps(report, indent=2)
199
231
  return {"content": [{"type": "text", "text": text}], "isError": False}
200
232
 
201
233
 
234
+ def run_specs_isolated(specs, timeout: float | None = RUN_TIMEOUT) -> dict:
235
+ """run_specs in a fresh Python process (physverify.isolate): the solver is imported as it is on disk now,
236
+ and a run longer than `timeout` seconds is stopped and reported as a load error of every spec."""
237
+ from .isolate import run_isolated
238
+ from .report import to_json
239
+
240
+ specs = [Path(s).resolve() for s in specs]
241
+ run = run_isolated("specs", specs, timeout)
242
+ if run["status"] == "done":
243
+ return run["result"]
244
+ why = (f"timed out after {timeout:g} s; the run was stopped" if run["status"] == "timeout" else run["error"])
245
+ if run["stderr_tail"]:
246
+ why += "; last output: " + run["stderr_tail"][-500:]
247
+ report = json.loads(to_json([], ", ".join(str(s) for s in specs)))
248
+ report["load_errors"] = [{"spec": str(s), "error": why} for s in specs]
249
+ return report
250
+
251
+
252
+ def _confirm_claim_tool(arguments: dict) -> dict:
253
+ """confirm_claim: the same runner and verdict rules as `physverify confirm`."""
254
+ from .confirm import (
255
+ DEFAULT_LEDGER,
256
+ DEFAULT_TIMEOUT,
257
+ Refused,
258
+ confirm_claim,
259
+ format_verdict,
260
+ parse_timeout,
261
+ )
262
+
263
+ claim = arguments.get("claim")
264
+ if not isinstance(claim, str) or not claim:
265
+ return {"content": [{"type": "text", "text": "confirm_claim needs `claim`, the path of a claim file."}], "isError": True}
266
+ try:
267
+ timeout = parse_timeout(DEFAULT_TIMEOUT if arguments.get("timeout") is None else arguments["timeout"])
268
+ except ValueError as exc:
269
+ return {"content": [{"type": "text", "text": f"confirm_claim: {exc}"}], "isError": True}
270
+ try:
271
+ record = confirm_claim(claim, trusted=arguments.get("trusted") is True,
272
+ ledger=arguments.get("ledger") or DEFAULT_LEDGER,
273
+ timeout=timeout,
274
+ allow_dependent_reference=arguments.get("allow_dependent_reference") or None)
275
+ except Refused as exc:
276
+ return {"content": [{"type": "text", "text": str(exc)}], "isError": True}
277
+ return {"content": [{"type": "text", "text": format_verdict(record) + "\n\n" + json.dumps(record, indent=2)}],
278
+ "isError": False}
279
+
280
+
281
+ TOOL_HANDLERS = {"run_checks": _run_checks_tool, "confirm_claim": _confirm_claim_tool}
282
+
283
+
202
284
  def handle(message: dict):
203
285
  """Answer one JSON-RPC message; returns the response dict, or None for a notification."""
204
286
  from . import __version__
@@ -242,9 +324,10 @@ def handle(message: dict):
242
324
  if method == "tools/list":
243
325
  return modern({"tools": TOOLS}, cacheable=True)
244
326
  if method == "tools/call":
245
- if params.get("name") != "run_checks":
327
+ tool = TOOL_HANDLERS.get(params.get("name"))
328
+ if tool is None:
246
329
  return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
247
- return modern(_run_checks_tool(params.get("arguments") or {}))
330
+ return modern(tool(params.get("arguments") or {}))
248
331
  return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
249
332
 
250
333
  # Legacy era, after the handshake: results carry no resultType or cache hints.
@@ -253,9 +336,10 @@ def handle(message: dict):
253
336
  if method == "tools/list":
254
337
  return ok({"tools": TOOLS})
255
338
  if method == "tools/call":
256
- if params.get("name") != "run_checks":
339
+ tool = TOOL_HANDLERS.get(params.get("name"))
340
+ if tool is None:
257
341
  return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
258
- return ok(_run_checks_tool(params.get("arguments") or {}))
342
+ return ok(tool(params.get("arguments") or {}))
259
343
  return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
260
344
 
261
345
 
@@ -13,14 +13,14 @@ were slower to adjudicate than this check.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from functools import lru_cache
16
+ from functools import cache
17
17
 
18
18
  import numpy as np
19
19
 
20
20
  from .checks import FAIL, PASS, Result
21
21
 
22
22
 
23
- @lru_cache(maxsize=None)
23
+ @cache
24
24
  def trees(order: int) -> tuple:
25
25
  """All rooted trees with `order` vertices, each a sorted tuple of child subtrees."""
26
26
  if order == 1:
@@ -6,8 +6,8 @@ import contextvars
6
6
  import importlib
7
7
  import threading
8
8
  import time
9
+ from collections.abc import Callable
9
10
  from dataclasses import dataclass
10
- from typing import Callable
11
11
 
12
12
  import numpy as np
13
13
 
@@ -5,6 +5,10 @@
5
5
  physverify init [SPEC.py] [--solver MODULE] [--function NAME] [--dim N] [--order P]
6
6
  physverify scan PATH (reads the source, runs nothing: constants, RK tables, grid spacing, inverse pairs,
7
7
  undefined names, removed APIs, typo-like literals, unit tables)
8
+ physverify confirm CLAIM.py [--ledger FILE] [--timeout S] [--json FILE|-] [--trusted]
9
+ (judge one suspected defect; runs the code; exit 0 rejected, 1 confirmed,
10
+ 2 not decidable; see docs/confirm.md)
11
+ physverify confirm init CLAIM.py --text "..." --file F --line N
8
12
  physverify hook (Claude Code PostToolUse hook; see docs/agent.md)
9
13
  physverify mcp (MCP server on stdio; see docs/agent.md)
10
14
 
@@ -43,7 +47,15 @@ def expand_specs(patterns) -> list[str]:
43
47
 
44
48
 
45
49
  def _cmd_run(args) -> int:
46
- from .report import NOTE, load_spec, run_checks, to_html, to_json, to_markdown, to_text
50
+ from .report import (
51
+ NOTE,
52
+ load_spec,
53
+ run_checks,
54
+ to_html,
55
+ to_json,
56
+ to_markdown,
57
+ to_text,
58
+ )
47
59
 
48
60
  try:
49
61
  args.specs = expand_specs(args.specs)
@@ -129,7 +141,7 @@ def _cmd_scan(args) -> int:
129
141
  import json
130
142
 
131
143
  from .probes import DEFAULT_PROBES, PROBES, probe
132
- from .scan import STAGES, WARN, scan, to_dict
144
+ from .scan import STAGES, WARN, python_files, scan, to_dict
133
145
 
134
146
  known = STAGES + PROBES if args.run else STAGES
135
147
  stages = args.only.split(",") if args.only else (STAGES + DEFAULT_PROBES if args.run else STAGES)
@@ -142,12 +154,13 @@ def _cmd_scan(args) -> int:
142
154
  print(f"physverify: {args.path} does not exist", file=sys.stderr)
143
155
  return 2
144
156
  findings, unparsed = scan(args.path, stages=[s for s in stages if s in STAGES])
157
+ count = len(python_files(args.path))
145
158
  probes = [s for s in stages if s in PROBES]
146
159
  if probes:
147
160
  print("physverify: --run calls the code; use it only on code you trust or inside the sandbox", file=sys.stderr)
148
161
  findings += probe(args.path, probes=probes, timeout=args.timeout)
149
162
  if args.json:
150
- text = json.dumps(to_dict(findings, unparsed), indent=1)
163
+ text = json.dumps({**to_dict(findings, unparsed), "files_scanned": count}, indent=1)
151
164
  if args.json == "-":
152
165
  print(text)
153
166
  else:
@@ -161,11 +174,65 @@ def _cmd_scan(args) -> int:
161
174
  skip = sum(f.level == "SKIP" for f in findings)
162
175
  ran = (f", {skip} skipped (not passed)" if probes else "")
163
176
  how = "the probes called the code with small inputs" if probes else "the scan reads the code and runs nothing"
164
- print(f"{warn} warnings, {len(findings) - warn - skip} notes{ran}, {len(unparsed)} files not parsed"
177
+ broken = len(unparsed) + sum(f.stage == "names" and "does not parse" in f.message for f in findings)
178
+ if not count:
179
+ print(f"physverify: no Python files found in {args.path}; nothing was scanned")
180
+ return 0
181
+ print(f"{count} files scanned: {warn} warnings, {len(findings) - warn - skip} notes{ran}, {broken} files not parsed"
165
182
  f" ({how}; a clean scan means only that these patterns were not found)")
166
183
  return 1 if any(f.level == WARN for f in findings) else 0
167
184
 
168
185
 
186
+ def _seconds(text) -> float:
187
+ """argparse type for a time limit: a finite number of seconds above 0."""
188
+ from .confirm import parse_timeout
189
+
190
+ try:
191
+ return parse_timeout(text)
192
+ except ValueError as exc:
193
+ raise argparse.ArgumentTypeError(str(exc)) from None
194
+
195
+
196
+ def _cmd_confirm(args) -> int:
197
+ import json
198
+
199
+ from .confirm import (
200
+ EXIT_CODES,
201
+ Refused,
202
+ confirm_claim,
203
+ format_verdict,
204
+ write_template,
205
+ )
206
+
207
+ if args.claim == "init":
208
+ if not args.init_path or args.text is None or args.file is None or args.line is None:
209
+ print("physverify: usage: physverify confirm init CLAIM.py --text \"...\" --file F --line N", file=sys.stderr)
210
+ return 2
211
+ try:
212
+ path = write_template(args.init_path, args.text, args.file, args.line)
213
+ except (ValueError, OSError) as exc:
214
+ print(f"physverify: {exc}", file=sys.stderr)
215
+ return 2
216
+ print(f"wrote {path}; fill in source, reference and impact, and write control() and checks() from the physics")
217
+ return 0
218
+ if args.init_path:
219
+ print(f"physverify: unexpected argument {args.init_path!r} (one claim file per run)", file=sys.stderr)
220
+ return 2
221
+ try:
222
+ record = confirm_claim(args.claim, trusted=args.trusted, ledger=args.ledger, timeout=args.timeout,
223
+ allow_dependent_reference=args.allow_dependent_reference)
224
+ except Refused as exc:
225
+ print(f"physverify: {exc}", file=sys.stderr)
226
+ return 2
227
+ if args.json == "-":
228
+ print(json.dumps(record, indent=2, ensure_ascii=False))
229
+ else:
230
+ if args.json:
231
+ Path(args.json).write_text(json.dumps(record, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
232
+ print(format_verdict(record))
233
+ return EXIT_CODES[record["verdict"]]
234
+
235
+
169
236
  def _cmd_references(args) -> int:
170
237
  from . import references
171
238
 
@@ -223,10 +290,25 @@ def build_parser() -> argparse.ArgumentParser:
223
290
  sc.add_argument("--timeout", type=float, default=60.0, help="with --run: seconds per script or file of functions (default 60)")
224
291
  sc.set_defaults(func=_cmd_scan)
225
292
 
293
+ cf = sub.add_parser("confirm", help="judge one suspected defect against a reference from the physics and record the verdict"
294
+ " (runs the code: --trusted or the sandbox only); see docs/confirm.md")
295
+ cf.add_argument("claim", help="the claim file (a .py file defining CLAIM, control() and checks()), or 'init'")
296
+ cf.add_argument("init_path", nargs="?", metavar="CLAIM.py", help="with init: the claim file to write")
297
+ cf.add_argument("--ledger", default="confirm_ledger.jsonl", help="JSON-lines file each run is appended to (default confirm_ledger.jsonl)")
298
+ cf.add_argument("--timeout", type=_seconds, default=300.0, help="seconds for the whole run before it is stopped (default 300)")
299
+ cf.add_argument("--json", metavar="FILE", help="write the ledger line as JSON to FILE, or to stdout with - (instead of the text verdict)")
300
+ cf.add_argument("--trusted", action="store_true", help="the code under test is trusted, so it may run outside the sandbox")
301
+ cf.add_argument("--allow-dependent-reference", metavar="REASON",
302
+ help="run even though the reference seems to use the code under test; REASON is recorded")
303
+ cf.add_argument("--text", help="with init: the claim as stated")
304
+ cf.add_argument("--file", help="with init: the file the claim is about, relative to the repository root")
305
+ cf.add_argument("--line", type=int, help="with init: the line in that file")
306
+ cf.set_defaults(func=_cmd_confirm)
307
+
226
308
  r = sub.add_parser("references", help="list the exact reference solutions in physverify.references")
227
309
  r.set_defaults(func=_cmd_references)
228
310
 
229
- s = sub.add_parser("mcp", help="run a minimal MCP server on stdio with a run_checks tool")
311
+ s = sub.add_parser("mcp", help="run a minimal MCP server on stdio with the tools run_checks and confirm_claim")
230
312
  s.set_defaults(func=_cmd_mcp)
231
313
  return parser
232
314