From 141ec37ee935fb394151644ebf46787fb8ee0e37 Mon Sep 17 00:00:00 2001 From: Buggy <107155783+Magic-Man-us@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:04:42 -0400 Subject: [PATCH 1/2] chore: pin the counts quoted in prose to the generated figures Cargo.toml claimed 6,365 public functions across 71 modules. 6,365 is the sum of the map's functions and methods, and 71 is the public top-level count, not the module count -- so both halves named the wrong thing. It is 4,124 functions across 295 modules. That string is what crates.io renders and cannot be edited once a version is published. check_counts.py reads the two files that already derive counts and are already kept current by CI -- docs/MODULE_MAP.md and the generator's COVERAGE.md -- and checks every hand-written figure against whichever of them owns it. The two disagree by design: the map counts a macro-generated item once, the generator counts what it emitted. Also corrects "296 modules" to 295 in the bindings README, and replaces "4,000+ validated functions" in pyproject.toml with the 4,086 the generator actually bound. --- .github/workflows/ci.yml | 8 ++ Cargo.toml | 2 +- bindings/python/README.md | 2 +- bindings/python/pyproject.toml | 2 +- tools/check_counts.py | 140 +++++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 3 deletions(-) create mode 100755 tools/check_counts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24c7ee6..4eae8a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,14 @@ jobs: - name: Every module is documented run: python3 tools/check_module_docs.py + # The counts quoted in Cargo.toml, pyproject.toml and the READMEs are + # written by hand from the two generated files above. This pins them + # to those files, so a library that grows fails the build instead of + # shipping a stale figure to a registry, where the description of a + # published version cannot be edited. + - name: Quoted counts match the generated figures + run: python3 tools/check_counts.py + coverage: name: Coverage runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index e14fa1e..4c2b068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "rust_physics_engine" version = "0.2.0" edition = "2021" -description = "A zero-dependency Rust library for physics, mathematics and engineering computation — 6,365 public functions across 71 modules" +description = "A zero-dependency Rust library for physics, mathematics and engineering computation — 4,124 public functions across 295 modules" license = "MIT" repository = "https://github.com/Magic-Man-us/RustPhysicsEngine" homepage = "https://github.com/Magic-Man-us/RustPhysicsEngine" diff --git a/bindings/python/README.md b/bindings/python/README.md index 318dd32..24ba724 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -148,7 +148,7 @@ extension. ## What is not bound 4,086 of the library's 4,149 free functions, 2,254 of its 2,277 methods, -416 of its 426 types and all 106 of its constants, across 296 modules. +416 of its 426 types and all 106 of its constants, across 295 modules. The rest is mostly three things: functions generic over a type parameter, which cannot be monomorphised without knowing what to monomorphise to; diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 5db9f9b..a65b82f 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "numeria" -description = "Physics, mathematics and engineering computation: 4,000+ validated functions across 71 domains, in Rust, callable from Python" +description = "Physics, mathematics and engineering computation: 4,086 functions across 71 domains, in Rust, callable from Python" readme = "README.md" license = { text = "MIT" } requires-python = ">=3.9" diff --git a/tools/check_counts.py b/tools/check_counts.py new file mode 100755 index 0000000..df10930 --- /dev/null +++ b/tools/check_counts.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Check the counts quoted in prose against the two generated files. + +Nothing here derives a count itself. Two files already do, and CI already +keeps both honest: + + docs/MODULE_MAP.md gen_module_map.py, a syntactic scan of src/ + bindings/python/COVERAGE.md generate.py, written beside the bindings + +They disagree, correctly. The module map counts a macro-generated item +once, because it appears once in the source; the binding generator counts +what it actually emitted, so `unit_ctor!` shows up as its thirty +constructors. A count is only meaningful against the file that owns it, +which is why each claim below names its source rather than sharing one +number. + +Prose is not rewritten, only checked -- the surrounding sentence usually +has to change with the figure, and a machine cannot write that sentence. +""" + +from __future__ import annotations + +import os +import re +import sys + +ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +MODULE_MAP = "docs/MODULE_MAP.md" +COVERAGE = "bindings/python/COVERAGE.md" + + +def read(rel: str) -> str: + with open(os.path.join(ROOT, rel), encoding="utf-8") as fh: + return fh.read() + + +def plain(n: str) -> int: + return int(n.replace(",", "")) + + +def module_map_facts() -> dict[str, int]: + text = read(MODULE_MAP) + pairs = { + "modules": r"\*\*([\d,]+) modules\*\*", + "top_level": r"\*\*([\d,]+) public top-level modules\*\*", + "files": r"\*\*([\d,]+) files\*\*", + "map_functions": r"\*\*([\d,]+) public functions\*\*", + "map_methods": r"\*\*([\d,]+) public methods\*\*", + "map_types": r"\*\*([\d,]+) public types\*\*", + } + out = {} + for key, pat in pairs.items(): + m = re.search(pat, text) + if not m: + sys.exit(f"{MODULE_MAP}: could not find the {key} figure -- regenerate it") + out[key] = plain(m.group(1)) + return out + + +def coverage_facts() -> dict[str, int]: + text = read(COVERAGE) + rows = { + "functions": "Free functions", + "methods": "Methods", + "classes": "Classes", + "constants": "Constants", + } + out = {} + for key, label in rows.items(): + m = re.search(rf"^\|\s*{label}\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|", text, re.M) + if not m: + sys.exit(f"{COVERAGE}: could not find the {label!r} row -- regenerate it") + out[f"rust_{key}"] = int(m.group(1)) + out[f"bound_{key}"] = int(m.group(2)) + return out + + +# (file, regex capturing one number, the fact it must equal). Anchored on +# surrounding words, because a bare number would match the wrong one the +# first time a sentence is reordered. A literal space in a pattern means +# "any whitespace" -- see `compile_claim` -- since these sentences are hard +# wrapped and a reflow must not read as a missing claim. +CLAIMS: list[tuple[str, str, str]] = [ + ("Cargo.toml", r"([\d,]+) public functions", "map_functions"), + ("Cargo.toml", r"public functions across ([\d,]+) modules", "modules"), + ("README.md", r"the ([\d,]+) modules", "modules"), + ("docs/GUIDE.md", r"([\d,]+) modules", "modules"), + ("bindings/python/pyproject.toml", r"([\d,]+) functions across", "bound_functions"), + ("bindings/python/pyproject.toml", r"functions across ([\d,]+) domains", "top_level"), + ("bindings/python/README.md", r"^([\d,]+) functions, ", "bound_functions"), + ("bindings/python/README.md", r"functions, ([\d,]+) methods", "bound_methods"), + ("bindings/python/README.md", r"methods, ([\d,]+) classes", "bound_classes"), + ("bindings/python/README.md", r"classes and ([\d,]+) constants", "bound_constants"), + ("bindings/python/README.md", r"constants across ([\d,]+) domains", "top_level"), + ("bindings/python/README.md", r"^([\d,]+) of the library's", "bound_functions"), + ("bindings/python/README.md", r"library's ([\d,]+) free functions", "rust_functions"), + ("bindings/python/README.md", r"free functions, ([\d,]+) of its", "bound_methods"), + ("bindings/python/README.md", r"of its ([\d,]+) methods", "rust_methods"), + ("bindings/python/README.md", r"methods, ([\d,]+) of its", "bound_classes"), + ("bindings/python/README.md", r"of its ([\d,]+) types", "rust_classes"), + ("bindings/python/README.md", r"all ([\d,]+) of its constants", "bound_constants"), + ("bindings/python/README.md", r"constants, across ([\d,]+) modules", "modules"), +] + + +def compile_claim(pattern: str) -> re.Pattern[str]: + return re.compile(pattern.replace(" ", r"\s+"), re.M) + + +def main() -> int: + facts = module_map_facts() | coverage_facts() + problems: list[str] = [] + for rel, pattern, key in CLAIMS: + text = read(rel) + m = compile_claim(pattern).search(text) + if not m: + problems.append(f"{rel}: no text matching {pattern!r} -- the sentence moved") + continue + found, want = plain(m.group(1)), facts[key] + if found != want: + line = text[: m.start(1)].count("\n") + 1 + problems.append( + f"{rel}:{line}: says {found:,} but {key} is {want:,}" + ) + if problems: + print("count check failed:", file=sys.stderr) + for p in problems: + print(f" {p}", file=sys.stderr) + print( + f"\nthe figures come from {MODULE_MAP} and {COVERAGE}, both generated;" + "\nfix the prose, not those files", + file=sys.stderr, + ) + return 1 + print(f"{len(CLAIMS)} quoted counts agree with {MODULE_MAP} and {COVERAGE}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a6cba7881daf74393db7d8a0a6f3a476cfacc5aa Mon Sep 17 00:00:00 2001 From: Buggy <107155783+Magic-Man-us@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:10:40 -0400 Subject: [PATCH 2/2] chore: cut the comments in the two files I added crates-release.yml was 46 comment lines against 48 of YAML. What is left is the two things reading the file cannot tell you: the trusted publisher is keyed to this filename, and the dispatch input exists for tags older than the file. --- .github/workflows/crates-release.yml | 49 ++-------------------------- tools/check_counts.py | 27 ++++----------- 2 files changed, 9 insertions(+), 67 deletions(-) diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml index d797989..32f7f03 100644 --- a/.github/workflows/crates-release.yml +++ b/.github/workflows/crates-release.yml @@ -1,37 +1,7 @@ name: Crates.io release -# Publishes the library crate to crates.io. Its sibling python-release.yml -# publishes the bindings to PyPI, and both answer to `push: tags: v*`, so one -# tag ships both registries. -# -# A file of its own rather than a job in python-release.yml, for the same -# reason that file gives for existing: a trusted publisher is registered -# against a workflow *filename*. One file per registry means either -# publisher can be reconfigured without disturbing the other, and a failure -# on one registry does not strand the other mid-run. -# -# ── How the upload is authorised ───────────────────────────────────── -# -# crates.io Trusted Publishing, the same OIDC exchange PyPI uses, so there -# is no registry token in this repository's secrets. The publisher -# registered on crates.io for `rust_physics_engine` names: -# -# repository Magic-Man-us/RustPhysicsEngine -# workflow crates-release.yml <- this file's name -# environment crates-io <- the job's environment, below -# -# crates-io-auth-action trades the workflow's OIDC token for a crates.io -# token scoped to this run, and revokes it in its post step. Renaming this -# file or changing that environment breaks the release until crates.io is -# told. -# -# ── Why workflow_dispatch ──────────────────────────────────────────── -# -# v0.2.0 was tagged before this file existed, so no tag push can ever fire -# it for that version. Dispatch runs from the default branch and checks out -# whichever existing tag it is given, which is the only way to publish a -# version whose tag predates this workflow. From v0.3.0 on the tag push is -# enough and this input should go unused. +# The crates.io trusted publisher is keyed to this file's name and the +# environment below. Renaming either silently breaks releases. on: push: @@ -39,6 +9,7 @@ on: workflow_dispatch: inputs: tag: + # For versions tagged before this file existed; no tag push can reach them. description: "An existing tag to publish, e.g. v0.2.0" required: true type: string @@ -52,20 +23,13 @@ jobs: publish: name: "Publish to crates.io" runs-on: ubuntu-latest - # Named so crates.io can be told to trust exactly this job, and so a - # required reviewer on the environment gates the one step here that - # cannot be undone: a published version can be yanked, never removed. environment: name: crates-io url: "https://crates.io/crates/rust_physics_engine" permissions: contents: read - # The OIDC token Trusted Publishing exchanges for an upload. The only - # elevated permission in the file, scoped to this job. id-token: write steps: - # A tag push carries the tag in ref_name; a dispatch carries it in the - # input. Both paths continue as one value from here. - name: The tag being published id: ref run: 'echo "tag=${{ inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"' @@ -75,19 +39,12 @@ jobs: ref: "${{ steps.ref.outputs.tag }}" persist-credentials: false - # The same gate python-release.yml runs, and the reason a malformed or - # mismatched tag stops here: check_version.py rejects anything that is - # not vX.Y.Z, and refuses a tag whose version disagrees with the - # manifests. crates.io will not re-use a version number either. - name: The tag, the crate and the bindings agree run: "python3 bindings/python/check_version.py '${{ steps.ref.outputs.tag }}'" - uses: rust-lang/crates-io-auth-action@v1 id: auth - # Verification is left on: the crate is dependency-free and builds in - # seconds, so the packaged artifact is compiled before upload rather - # than trusted. - name: Publish run: "cargo publish" env: diff --git a/tools/check_counts.py b/tools/check_counts.py index df10930..f686c81 100755 --- a/tools/check_counts.py +++ b/tools/check_counts.py @@ -1,21 +1,9 @@ #!/usr/bin/env python3 -"""Check the counts quoted in prose against the two generated files. +"""Check counts quoted in prose against the files that derive them. -Nothing here derives a count itself. Two files already do, and CI already -keeps both honest: - - docs/MODULE_MAP.md gen_module_map.py, a syntactic scan of src/ - bindings/python/COVERAGE.md generate.py, written beside the bindings - -They disagree, correctly. The module map counts a macro-generated item -once, because it appears once in the source; the binding generator counts -what it actually emitted, so `unit_ctor!` shows up as its thirty -constructors. A count is only meaningful against the file that owns it, -which is why each claim below names its source rather than sharing one -number. - -Prose is not rewritten, only checked -- the surrounding sentence usually -has to change with the figure, and a machine cannot write that sentence. +MODULE_MAP.md and COVERAGE.md disagree by design -- the map counts a +macro-generated item once, the generator counts what it emitted -- so each +claim names which of them owns it. """ from __future__ import annotations @@ -75,11 +63,8 @@ def coverage_facts() -> dict[str, int]: return out -# (file, regex capturing one number, the fact it must equal). Anchored on -# surrounding words, because a bare number would match the wrong one the -# first time a sentence is reordered. A literal space in a pattern means -# "any whitespace" -- see `compile_claim` -- since these sentences are hard -# wrapped and a reflow must not read as a missing claim. +# A literal space in a pattern means any whitespace: these sentences are +# hard wrapped and a reflow must not read as a missing claim. CLAIMS: list[tuple[str, str, str]] = [ ("Cargo.toml", r"([\d,]+) public functions", "map_functions"), ("Cargo.toml", r"public functions across ([\d,]+) modules", "modules"),