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: 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..34b8861 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" @@ -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. 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, 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