physverify 0.6.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.
- physverify-0.8.0/PKG-INFO +132 -0
- physverify-0.8.0/README.md +88 -0
- {physverify-0.6.0 → physverify-0.8.0}/pyproject.toml +23 -4
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/__init__.py +2 -3
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/agent.py +117 -19
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/butcher.py +113 -5
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/checks.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/cli.py +90 -5
- physverify-0.8.0/src/physverify/confirm.py +338 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/constants.py +4 -1
- physverify-0.8.0/src/physverify/flow.py +272 -0
- physverify-0.8.0/src/physverify/fluxes.py +58 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/identities.py +112 -10
- physverify-0.8.0/src/physverify/isolate.py +109 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/lint.py +87 -9
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/mms.py +10 -2
- physverify-0.8.0/src/physverify/operators.py +215 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/order.py +62 -7
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/probes.py +10 -4
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/references.py +20 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/report.py +16 -1
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/scan.py +97 -7
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/smoke.py +5 -2
- physverify-0.8.0/src/physverify/specscan.py +67 -0
- physverify-0.8.0/src/physverify/stencils.py +134 -0
- physverify-0.8.0/src/physverify/steps.py +97 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/calibrate.py +0 -2
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/checks.py +1 -2
- physverify-0.8.0/src/physverify/stochastic/directions.py +84 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/exact.py +2 -2
- physverify-0.8.0/src/physverify/stochastic/ito.py +87 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/stats.py +49 -3
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/studies.py +21 -5
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/symbolic.py +1 -1
- physverify-0.8.0/src/physverify.egg-info/PKG-INFO +132 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/SOURCES.txt +21 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/requires.txt +2 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_agent.py +53 -2
- physverify-0.8.0/tests/test_awkward_input.py +151 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_batch8_lessons.py +27 -2
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_butcher_properties.py +10 -2
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_cli.py +13 -0
- physverify-0.8.0/tests/test_confirm.py +319 -0
- physverify-0.8.0/tests/test_corpus_ledger.py +27 -0
- physverify-0.8.0/tests/test_correctness_fixes.py +142 -0
- physverify-0.8.0/tests/test_directions.py +94 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_docs_examples.py +16 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_figures.py +11 -2
- physverify-0.8.0/tests/test_helpers_2026_10.py +145 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_ising.py +7 -1
- physverify-0.8.0/tests/test_new_checks_2026_11.py +142 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_new_problems.py +1 -2
- physverify-0.8.0/tests/test_operators.py +112 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_order.py +34 -1
- physverify-0.8.0/tests/test_quadrature.py +76 -0
- physverify-0.8.0/tests/test_roadmap_0_7.py +89 -0
- physverify-0.8.0/tests/test_sandbox_installs.py +52 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_scan.py +151 -0
- physverify-0.8.0/tests/test_scan_flow.py +150 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_seeded_bugs.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_v040_helpers.py +12 -1
- physverify-0.6.0/PKG-INFO +0 -95
- physverify-0.6.0/README.md +0 -53
- physverify-0.6.0/src/physverify.egg-info/PKG-INFO +0 -95
- physverify-0.6.0/tests/test_awkward_input.py +0 -74
- {physverify-0.6.0 → physverify-0.8.0}/LICENSE +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/setup.cfg +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/__main__.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/demo.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/loaders.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/pytest_plugin.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/scaffold.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/steppers.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/__init__.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/ising.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/reference.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify/stochastic/sampler.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/dependency_links.txt +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/entry_points.txt +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/src/physverify.egg-info/top_level.txt +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_batch7_lessons.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_corpus_lessons.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_method_bugs.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_mms.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_probes.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_properties.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_real_projects.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_reference_problems.py +1 -1
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_references.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_scaffold.py +0 -0
- {physverify-0.6.0 → physverify-0.8.0}/tests/test_studies.py +1 -1
- {physverify-0.6.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
|
+
[](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [](https://pypi.org/project/physverify/) [](https://pypi.org/project/physverify/) [](https://doi.org/10.5281/zenodo.23534512) [](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
[](https://github.com/rcgiri-physics/physverify/actions/workflows/ci.yml) [](https://pypi.org/project/physverify/) [](https://pypi.org/project/physverify/) [](https://doi.org/10.5281/zenodo.23534512) [](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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.
|
|
8
|
-
description = "Verification checks for physics simulation code: manufactured solutions, order-of-accuracy studies and
|
|
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.
|
|
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
|
|
14
|
-
|
|
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
|
|
|
@@ -112,14 +116,18 @@ def hook_main(stdin=None, stderr=None) -> int:
|
|
|
112
116
|
event = json.load(stdin)
|
|
113
117
|
except (json.JSONDecodeError, ValueError):
|
|
114
118
|
return 0 # not a hook event; never block the assistant on our own input problem
|
|
115
|
-
|
|
119
|
+
if not isinstance(event, dict):
|
|
120
|
+
return 0
|
|
121
|
+
tool_input = event.get("tool_input")
|
|
122
|
+
tool_input = tool_input if isinstance(tool_input, dict) else {}
|
|
116
123
|
edited = tool_input.get("file_path") or tool_input.get("notebook_path") or ""
|
|
117
|
-
if edited and not edited.endswith(".py"):
|
|
124
|
+
if not isinstance(edited, str) or (edited and not edited.endswith(".py")):
|
|
118
125
|
return 0
|
|
119
126
|
cwd = Path(event.get("cwd") or ".").resolve()
|
|
120
|
-
|
|
127
|
+
edited_path = (cwd / edited).resolve() if edited else None # a relative path is relative to the session folder
|
|
128
|
+
if edited_path is not None and not edited_path.is_relative_to(cwd):
|
|
121
129
|
return 0 # an edit outside the session folder must not run that folder's code (GCVE-110-VULN-PYPI-2026-000391)
|
|
122
|
-
root = find_project_root(
|
|
130
|
+
root = find_project_root(edited_path.parent if edited_path is not None else cwd)
|
|
123
131
|
if not root.is_relative_to(cwd):
|
|
124
132
|
root = cwd # never climb above the session folder
|
|
125
133
|
specs = find_specs(root)
|
|
@@ -141,14 +149,17 @@ META_VERSION = "io.modelcontextprotocol/protocolVersion"
|
|
|
141
149
|
META_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities"
|
|
142
150
|
META_SERVER_INFO = "io.modelcontextprotocol/serverInfo"
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
PARSE_ERROR = -32700
|
|
153
|
+
INVALID_REQUEST = -32600
|
|
145
154
|
METHOD_NOT_FOUND = -32601
|
|
155
|
+
INVALID_PARAMS = -32602
|
|
156
|
+
INTERNAL_ERROR = -32603
|
|
146
157
|
UNSUPPORTED_PROTOCOL_VERSION = -32022
|
|
147
158
|
|
|
148
159
|
CACHE_HINTS = {"ttlMs": 300_000, "cacheScope": "public"} # the tool list does not change while the server runs
|
|
149
160
|
INSTRUCTIONS = (
|
|
150
161
|
"Run physverify checks on physics simulation code after writing or editing it. "
|
|
151
|
-
"The
|
|
162
|
+
"The tools execute the project's verify_*.py spec files and claim files (Python), so use them only on projects you trust."
|
|
152
163
|
)
|
|
153
164
|
|
|
154
165
|
TOOLS = [
|
|
@@ -166,9 +177,31 @@ TOOLS = [
|
|
|
166
177
|
"properties": {
|
|
167
178
|
"specs": {"type": "array", "items": {"type": "string"}, "description": "spec file paths"},
|
|
168
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})"},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
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"},
|
|
169
201
|
},
|
|
202
|
+
"required": ["claim"],
|
|
170
203
|
},
|
|
171
|
-
}
|
|
204
|
+
},
|
|
172
205
|
]
|
|
173
206
|
|
|
174
207
|
|
|
@@ -186,12 +219,68 @@ def _run_checks_tool(arguments: dict) -> dict:
|
|
|
186
219
|
specs = find_specs(Path(arguments.get("project") or "."))
|
|
187
220
|
if not specs:
|
|
188
221
|
return {"content": [{"type": "text", "text": "No spec files found (verify_*.py defining checks())."}], "isError": True}
|
|
189
|
-
|
|
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)
|
|
190
229
|
failed = bool(report["summary"]["not_ok"] or report["load_errors"])
|
|
191
230
|
text = (failure_summary(report) + "\n\n" if failed else "") + json.dumps(report, indent=2)
|
|
192
231
|
return {"content": [{"type": "text", "text": text}], "isError": False}
|
|
193
232
|
|
|
194
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
|
+
|
|
195
284
|
def handle(message: dict):
|
|
196
285
|
"""Answer one JSON-RPC message; returns the response dict, or None for a notification."""
|
|
197
286
|
from . import __version__
|
|
@@ -235,9 +324,10 @@ def handle(message: dict):
|
|
|
235
324
|
if method == "tools/list":
|
|
236
325
|
return modern({"tools": TOOLS}, cacheable=True)
|
|
237
326
|
if method == "tools/call":
|
|
238
|
-
|
|
327
|
+
tool = TOOL_HANDLERS.get(params.get("name"))
|
|
328
|
+
if tool is None:
|
|
239
329
|
return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
|
|
240
|
-
return modern(
|
|
330
|
+
return modern(tool(params.get("arguments") or {}))
|
|
241
331
|
return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
|
|
242
332
|
|
|
243
333
|
# Legacy era, after the handshake: results carry no resultType or cache hints.
|
|
@@ -246,12 +336,23 @@ def handle(message: dict):
|
|
|
246
336
|
if method == "tools/list":
|
|
247
337
|
return ok({"tools": TOOLS})
|
|
248
338
|
if method == "tools/call":
|
|
249
|
-
|
|
339
|
+
tool = TOOL_HANDLERS.get(params.get("name"))
|
|
340
|
+
if tool is None:
|
|
250
341
|
return _error(msg_id, INVALID_PARAMS, f"unknown tool {params.get('name')!r}")
|
|
251
|
-
return ok(
|
|
342
|
+
return ok(tool(params.get("arguments") or {}))
|
|
252
343
|
return _error(msg_id, METHOD_NOT_FOUND, f"method not found: {method}")
|
|
253
344
|
|
|
254
345
|
|
|
346
|
+
def _respond(message):
|
|
347
|
+
"""handle(), but a message that is not an object, or an exception, becomes an error response; the server keeps running."""
|
|
348
|
+
if not isinstance(message, dict): # a batch, a number, a string: not a request this server answers
|
|
349
|
+
return _error(None, INVALID_REQUEST, "invalid request: expected a JSON object")
|
|
350
|
+
try:
|
|
351
|
+
return handle(message)
|
|
352
|
+
except Exception as exc: # report the failure to the client
|
|
353
|
+
return _error(message.get("id"), INTERNAL_ERROR, f"{type(exc).__name__}: {exc}")
|
|
354
|
+
|
|
355
|
+
|
|
255
356
|
def mcp_main(stdin=None, stdout=None) -> int:
|
|
256
357
|
stdin = stdin or sys.stdin
|
|
257
358
|
stdout = stdout or sys.stdout
|
|
@@ -262,12 +363,9 @@ def mcp_main(stdin=None, stdout=None) -> int:
|
|
|
262
363
|
try:
|
|
263
364
|
message = json.loads(line)
|
|
264
365
|
except json.JSONDecodeError:
|
|
265
|
-
response =
|
|
366
|
+
response = _error(None, PARSE_ERROR, "parse error")
|
|
266
367
|
else:
|
|
267
|
-
|
|
268
|
-
response = handle(message)
|
|
269
|
-
except Exception as exc: # keep the server alive; report the failure to the client
|
|
270
|
-
response = {"jsonrpc": "2.0", "id": message.get("id"), "error": {"code": -32603, "message": f"{type(exc).__name__}: {exc}"}}
|
|
368
|
+
response = _respond(message)
|
|
271
369
|
if response is not None:
|
|
272
370
|
stdout.write(json.dumps(response) + "\n")
|
|
273
371
|
stdout.flush()
|