Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions .github/ISSUE_TEMPLATE/defect.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions .github/ISSUE_TEMPLATE/refused-translation.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -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
56 changes: 21 additions & 35 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
13 changes: 5 additions & 8 deletions .github/workflows/hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion docs/reading-the-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
18 changes: 16 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/recast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from __future__ import annotations

__version__ = "0.0.1.dev0"
__version__ = "0.0.1"

from recast.engines import (
ArtifactContract,
Expand Down
6 changes: 3 additions & 3 deletions tests/test_f2py_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading