From aa94ff3d85b1dac8e50a339960d31652cb6beaa9 Mon Sep 17 00:00:00 2001 From: Yueqi Chen Date: Mon, 7 Sep 2026 19:21:04 -0600 Subject: [PATCH 1/5] CI runs on every push and pull request of the published edition; the corpus is a job; issue and PR templates The published edition's Actions are free, so its ci.yml and hygiene.yml get push, pull_request and (for hygiene) a weekly schedule, through swapped trigger blocks; this tree keeps manual dispatch and says why. A corpus job runs the twelve cases and fails on a diff of corpus/baseline.json, so the file cannot lag behind main again as it did today. Two issue templates -- a refused translation, where the argument is the contribution, and a defect -- a contact link to the private advisory form and to Discussions, and a pull request template with the three checks. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Yueqi Chen Carried-From: c7cf88c90aaa5304c6d4b3e4c5297e1709f4c44e --- .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/defect.md | 19 +++++++ .github/ISSUE_TEMPLATE/refused-translation.md | 22 ++++++++ .github/PULL_REQUEST_TEMPLATE.md | 10 ++++ .github/workflows/ci.yml | 56 +++++++------------ .github/workflows/hygiene.yml | 13 ++--- 6 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/defect.md create mode 100644 .github/ISSUE_TEMPLATE/refused-translation.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..dd40eef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/a85tract/RecastEngine/security/advisories/new + about: Report privately, never as a public issue; see SECURITY.md. + - name: Question or idea + url: https://github.com/a85tract/RecastEngine/discussions + about: Anything that is not a defect or a refused translation. diff --git a/.github/ISSUE_TEMPLATE/defect.md b/.github/ISSUE_TEMPLATE/defect.md new file mode 100644 index 0000000..063a11d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/defect.md @@ -0,0 +1,19 @@ +--- +name: A defect +about: A translation the gate passed that is wrong, a crash, or a plugin that misbehaves +labels: bug +--- + +**What happened.** The command, its output, and what should have happened +instead. If a gate passed something it should not have, this is the most +serious kind of report there is: say which gate and attach the evidence +manifest. + +``` +``` + +**Reproduction.** The smallest input that shows it. A corpus case can be +named instead of attached. + +**Version.** `recast version`, the Python version, and `gfortran --version` +if an oracle was involved. diff --git a/.github/ISSUE_TEMPLATE/refused-translation.md b/.github/ISSUE_TEMPLATE/refused-translation.md new file mode 100644 index 0000000..6103cd0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refused-translation.md @@ -0,0 +1,22 @@ +--- +name: A unit the engine refused or deferred +about: The engine said what it would not translate; you think it should have +labels: enhancement, area:frontend +--- + +**The Fortran.** The smallest module that reproduces it, inline or attached. +A unit the corpus already carries can be named instead. + +**What the engine said.** The refusal or the deferred block, copied from +`recast run` or from the evidence manifest -- the exact text, since a +refusal names its reason and the reason is what is being discussed. + +``` +``` + +**What you expected.** The NumPy the rule should have produced, if you have +an opinion, and why it is bit-exact with the Fortran. A rule that is added +here has to hold against f2py on the corpus, so the argument is the +contribution. + +**Version.** `recast version` and the Python version. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8164e34 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +**What this changes, and the evidence.** A new verifier shows the numbers +it produces; a new transform shows what lands in `deferred` as well as what +translates; a rule shows the corpus unit it holds on. See CONTRIBUTING.md, +"What a good PR looks like". + +**Checks.** + +- [ ] `tools/ci_local.sh` passes, or the CI on this pull request does +- [ ] every commit is signed off (`git commit -s`), and I am in `CLA-SIGNATORIES.md` +- [ ] no name or path from CONTRIBUTING.md's "already taken" list is added diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78b75a7..fa09029 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,9 @@ name: ci -# Automatic runs are off while the Actions allowance is exhausted, and as of -# 2026-08-21 the workflow itself is disabled on GitHub (`gh workflow disable`), -# so even a manual dispatch is refused. The allowance returns in September. -# -# To restore, both halves are needed and only one of them is visible here: -# gh workflow enable ci.yml -# ...then put back the push (main) and pull_request triggers below. -# Until then, tools/ci_local.sh runs these same jobs from the same file. on: + push: + branches: [main] + pull_request: workflow_dispatch: permissions: @@ -78,6 +73,23 @@ jobs: # rather than skip for want of fparser. - run: uv run --python ${{ matrix.python }} --extra dev --extra fortran --extra translate pytest --cov=recast + # The twelve corpus cases, run rather than trusted: corpus/baseline.json is + # what the engine says about them, and a change in what it says is a change + # in the engine, which this job makes visible as a diff rather than letting + # the file lag behind main. One minute; the cases are small on purpose. + corpus: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: true + - uses: astral-sh/setup-uv@v3 + - run: sudo apt-get update && sudo apt-get install -y gfortran + - run: uv python install 3.11 + - run: uv run --extra dev --extra fortran --extra translate --extra verify python tools/corpus.py run + - name: The baseline is what main says + run: git diff --exit-code -- corpus/baseline.json + # The contract, run rather than read. This is the suite an out-of-tree # extension has to pass to be an extension and not a fork, so the engine # running it against its own plugins is the minimum: a check nobody's plugins @@ -99,7 +111,7 @@ jobs: # transforms also have to be *constructible*: determinism is an attribute # of an instance, and a factory that cannot run leaves the agentic-gate # rule unable to judge anything. - - run: uv run --extra dev --extra fortran --extra translate --extra verify --extra numba --extra jax pytest conformance/ -rs + - run: uv run --extra dev --extra fortran --extra translate --extra verify pytest conformance/ -rs spine: runs-on: ubuntu-latest @@ -132,29 +144,3 @@ jobs: # the missing extra is reported when it is asked to parse, not before. - run: uv run --no-dev recast plugins | grep -qx " fortran" - # The port spine: frontend -> port.jax -> numpy-anchor -> differential.tolerance. - # This is the only job that installs jax, and until it existed - # tests/test_port_spine.py skipped in CI and had run on one laptop only -- - # the same gap the spine job above was created to close for the f2py chain. - # - # It needs no compiler, and that is a property of the oracle rather than a - # convenience: numpy-anchor re-derives the reference from the same Facts, so - # nothing here builds Fortran. The link this job cannot check is the one that - # does -- the NumPy translation being bit-exact against the Fortran, which is - # the spine job's claim above. - port-spine: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v3 - - run: uv python install 3.11 - - run: uv run --extra dev --extra fortran --extra translate --extra jax pytest tests/test_port_spine.py -v - # The public form of the same claim, on sources anyone can read. - - run: uv run --extra fortran --extra translate --extra jax recast run port corpus/toy_physics --config corpus/toy_physics/port.json --summary corpus/toy_physics/port-verification.json - # No `git diff --exit-code` here, deliberately, and the asymmetry with the - # spine job is the point. Bit-exactness is a device-independent claim, so - # its summary is committed *and* checked byte for byte. A ULP count is - # not: XLA's CPU backend does not promise the same last bit on x86 as on - # arm64, so the committed port-verification.json is a record for a reader - # without JAX, not a regression signal. What this job gates on is the - # verdict -- the run exits non-zero unless every unit passes its gate. diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml index a2b4c08..e337687 100644 --- a/.github/workflows/hygiene.yml +++ b/.github/workflows/hygiene.yml @@ -4,15 +4,12 @@ name: hygiene # after. Migrating 10k lines out of two NCAR-bound private repositories is # exactly the situation where a site path or an allocation account slips in. -# Automatic runs are off while the Actions allowance is exhausted, and as of -# 2026-08-21 the workflow itself is disabled on GitHub (`gh workflow disable`), -# so even a manual dispatch is refused. The allowance returns in September. -# -# To restore, both halves are needed and only one of them is visible here: -# gh workflow enable hygiene.yml -# ...then put back the push, pull_request, and Monday 06:00 UTC schedule triggers below. -# Until then, tools/ci_local.sh runs these same jobs from the same file. on: + push: + branches: [main] + pull_request: + schedule: + - cron: "0 6 * * 1" workflow_dispatch: permissions: From e938ca9a17bea8d7aef3a66e31cd822a89c8f292 Mon Sep 17 00:00:00 2001 From: Yueqi Chen Date: Mon, 7 Sep 2026 19:22:55 -0600 Subject: [PATCH 2/5] 0.0.1, and a release workflow that publishes a tagged release to PyPI The first version with a number: what the published edition is on the day it is published. release.yml builds the sdist and wheel from the tag, checks them, and uploads them to PyPI through trusted publishing, so no token is stored anywhere; the publish step needs PyPI to know this repository as recast-engine's publisher, and says so until it does. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Yueqi Chen Carried-From: 73789164c6e233a02aec03668fd09cd0769b40c1 --- .github/workflows/release.yml | 42 +++++++++++++++++++++++++++++++++++ README.md | 2 +- docs/cli.md | 2 +- docs/getting-started.md | 2 +- docs/reading-the-evidence.md | 2 +- pyproject.toml | 2 +- src/recast/__init__.py | 2 +- 7 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..889f034 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: release + +# A published GitHub release of a `v*` tag builds the sdist and wheel from +# that tag and uploads them to PyPI through trusted publishing (OIDC): no +# token lives anywhere. PyPI has to know this repository and this workflow +# as the publisher of `recast-engine` first; until it does, the publish step +# says so and the built files are still attached to the run. +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v3 + - run: uv python install 3.11 + - run: uv build + - run: uv run --with twine --no-project twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index e569419..8ab079e 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ the corpus of numerical libraries under `corpus/` is what it is proven on. ```console $ recast doctor -recast 0.0.1.dev0 python 3.11.16 +recast 0.0.1 python 3.11.16 ``` The remaining `doctor` output is a live plugin count and registry inventory; diff --git a/docs/cli.md b/docs/cli.md index 789bb50..49a38d2 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2,7 +2,7 @@ ```console $ recast doctor -recast 0.0.1.dev0 python 3.11.16 +recast 0.0.1 python 3.11.16 ``` `doctor` then prints the live plugin count and registry inventory. The count is diff --git a/docs/getting-started.md b/docs/getting-started.md index 974466e..7b014ef 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -81,7 +81,7 @@ Now check the installation: ```console $ recast doctor -recast 0.0.1.dev0 python 3.11.16 +recast 0.0.1 python 3.11.16 24 plugin(s) registered across 10 kinds ``` diff --git a/docs/reading-the-evidence.md b/docs/reading-the-evidence.md index 74528d4..1397bb6 100644 --- a/docs/reading-the-evidence.md +++ b/docs/reading-the-evidence.md @@ -223,7 +223,7 @@ deliberately leaves out. The differential one for `satvap`: "files": ["satvap_constants.py", "satvap_numpy.py"] }, "reference": {"oracle": "f2py-golden", "key": "f2py:satvap:8c6c49ccd27d2248"}, - "environment": {"engine": "recast 0.0.1.dev0", "platform": "macOS-26.5.2-arm64-arm-64bit", "python": "3.11.16"}, + "environment": {"engine": "recast 0.0.1", "platform": "macOS-26.5.2-arm64-arm-64bit", "python": "3.11.16"}, "cc_test": {"commit": "unknown", "version": "unknown"}, "cases": [], "result": { diff --git a/pyproject.toml b/pyproject.toml index 47de125..f1b0278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "recast-engine" -version = "0.0.1.dev0" +version = "0.0.1" description = "An agentic engine for modernizing legacy scientific software" readme = "README.md" requires-python = ">=3.11" diff --git a/src/recast/__init__.py b/src/recast/__init__.py index d580a2a..7bcfb40 100644 --- a/src/recast/__init__.py +++ b/src/recast/__init__.py @@ -11,7 +11,7 @@ from __future__ import annotations -__version__ = "0.0.1.dev0" +__version__ = "0.0.1" from recast.engines import ( ArtifactContract, From 3f90f27f3107e3bcb1b833ef46cc7ad5454933a1 Mon Sep 17 00:00:00 2001 From: Yueqi Chen Date: Mon, 7 Sep 2026 19:25:17 -0600 Subject: [PATCH 3/5] The published edition's CI passes: tier jobs and extras pruned, optional modules known to mypy, docs not formatted The first run of CI on the published edition found what a manual-only CI here had not. The port-spine job and the numba and jax extras that the conformance job asks for are the optional modules'; the export now drops that job and strips those extras from the jobs that stay. mypy is told that the three guarded imports may be absent. And the current ruff formats Python fences inside Markdown, which would rewrite the NumPy the docs quote from the emitter, parentheses and all, so the docs are excluded from the formatter. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Yueqi Chen Carried-From: d2c91fa1ebb9e02496e391162ee3261f54193b4c --- pyproject.toml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f1b0278..34b8861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,10 @@ line-length = 100 src = ["src", "tests"] # corpus/ is twelve upstream Fortran libraries vendored as submodules, to be # translated, not styled. A few carry Python of their own; it is theirs. -extend-exclude = ["corpus"] +# docs/ quotes code the engine emitted, parentheses and all; the formatter +# rewriting a fenced block there would misquote it. So the docs are not +# formatted, though their prose is linted like anything else. +extend-exclude = ["corpus", "docs/*.md", "*.md"] [tool.ruff.lint] select = ["E", "F", "I", "N", "UP", "B", "S", "RUF"] @@ -155,6 +158,17 @@ packages = ["recast"] strict = true warn_unreachable = true +# Modules an installation may lack -- the optional recipes, the optional +# conformance cases, the flat adapters -- are imported behind a guard; where +# they are absent the import is not an error. +[[tool.mypy.overrides]] +module = [ + "recast.recipes.optional", + "recast.conformance.builtin_optional", + "recast.fortran.flatten", +] +ignore_missing_imports = true + # fparser2 ships no py.typed marker. It is confined to recast.fortran, and the # node types it returns are untyped trees anyway -- the frontend's own boundary # (Unit, Facts) is where the types have to hold. From c75e4c3c8ae3c62b129ae2545247523d7c06a9d6 Mon Sep 17 00:00:00 2001 From: Yueqi Chen Date: Mon, 7 Sep 2026 19:29:06 -0600 Subject: [PATCH 4/5] The complex-valued oracle test skips without meson, as the other builds do ubuntu-latest ships gfortran but the test job installs no verify extra, so the build reached f2py and failed on a missing meson instead of skipping. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Yueqi Chen Carried-From: abed250a2e7f827e0d2c7b8b1ece2a113a8c25d7 --- tests/test_f2py_oracle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_f2py_oracle.py b/tests/test_f2py_oracle.py index 371ac11..039de7b 100644 --- a/tests/test_f2py_oracle.py +++ b/tests/test_f2py_oracle.py @@ -1982,7 +1982,7 @@ def test_a_scalar_logical_inout_goes_through_the_wrapper_as_an_integer() -> None """ -@pytest.mark.skipif(GFORTRAN is None, reason="needs gfortran") +@pytest.mark.skipif(GFORTRAN is None or not MESON, reason="needs gfortran and meson") def test_a_complex_valued_subprogram_is_compared_on_both_parts(tmp_path: Path) -> None: """A complex result or argument was ``unsupported declared dtype(s)`` and left uncompared, while the emitter built it as float64 (#20). Complex From e33749dd5990d10d92e8fca5453005e1887c6b64 Mon Sep 17 00:00:00 2001 From: Yueqi Chen Date: Mon, 7 Sep 2026 19:32:10 -0600 Subject: [PATCH 5/5] The two other builds that skipped on gfortran alone skip on meson too Co-Authored-By: Claude Fable 5.1 Signed-off-by: Yueqi Chen Carried-From: c23236216ceb602a7d12de3b4fdc0c5e4d05ea1d --- tests/test_f2py_oracle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_f2py_oracle.py b/tests/test_f2py_oracle.py index 039de7b..1c27fcd 100644 --- a/tests/test_f2py_oracle.py +++ b/tests/test_f2py_oracle.py @@ -2031,7 +2031,7 @@ def test_a_complex_valued_subprogram_is_compared_on_both_parts(tmp_path: Path) - """ -@pytest.mark.skipif(GFORTRAN is None, reason="needs gfortran") +@pytest.mark.skipif(GFORTRAN is None or not MESON, reason="needs gfortran and meson") def test_a_reference_reading_outside_its_array_declines_the_draw_by_name(tmp_path: Path) -> None: """A subscript outside the array is not a value the source computes: the reference reads whatever memory sits beside the buffer in its process, @@ -2076,7 +2076,7 @@ def test_a_reference_reading_outside_its_array_declines_the_draw_by_name(tmp_pat """ -@pytest.mark.skipif(GFORTRAN is None, reason="needs gfortran") +@pytest.mark.skipif(GFORTRAN is None or not MESON, reason="needs gfortran and meson") def test_an_error_stop_in_the_reference_is_a_report_not_a_dead_run(tmp_path: Path) -> None: """``error stop`` in the compiled reference is ``exit()`` in whatever process imported it: no report, no summary, every other unit's verdict