Skip to content

feat(scaffold): v2 six-language templates and a single-pass scanner (#186, #197) - #228

Merged
hyperpolymath merged 12 commits into
mainfrom
deliver-186-197
Sep 24, 2026
Merged

hyperpolymath merged 12 commits into
mainfrom
deliver-186-197

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes #186 and #197.

#186 — the scaffold emitted the retired v1 shape

aletheia/scripts/create-template.sh now generates the v2 project shape (.adoc docs,
LICENSE + LICENSES/, 0-AI-MANIFEST.a2ml, .machine_readable/rsr-profile.a2ml,
.editorconfig, Dockerfile-free, no flake.nix/LICENSE.txt/v1 justfile), for all six
Tier-1 canon languages: Rust, Zig, Elixir, Haskell, Ada/SPARK, Agda.

Architecture is templates/common/ plus a per-language overlay, with an unresolved-@@
placeholder guard that exits non-zero, --list-languages, --verify, --force and --no-git.

A generated project reaches 26/26 (100%), Bronze + Silver ACHIEVED, with no hand edits, and
there is a regression test (test_scaffold_passes_bronze_and_silver) that fails if that stops
being true. Each language genuinely builds and tests with its own compiler — the evidence is in
DELIVERY-186-197.md, per language.

Two of the six also ship an opt-in proof gate as a template asset:

  • templates/rust/.github/workflows/proof.yml — Creusot, just proof
  • templates/ada/.github/workflows/proof.yml — SPARK/gnatprove, just proof

Both are real gates, not labels: a false obligation fails them non-zero.

#197 — the scanner walked the tree seven times

ScanSet consolidates Scanner::walk_files into a single traversal that feeds every check, with
per-check budget semantics unchanged.

  • 7 traversals → 1
  • 212 ms → 150 ms on a tree of 18,602 files across 673 directories
  • verdicts byte-identical before and after

Found and fixed along the way: the scanner ignored .gitignore

Building #186 surfaced a real bug: any build output in the tree flipped the compliance gate.
just check followed by aletheia . on a generated Rust project went from 26/26 to 18/26,
Bronze NOT MET
, purely because target/ and obj/ exist.

checks.rs now loads .gitignore per directory during the single walk, and config.rs implements
the matching rules (comments, ! negation with last-match-wins, trailing-/ directory-only rules,
anchoring, and nested-file scoping). Repository .gitignore semantics are kept separate from the
config file's [ignore] section. Three integration tests cover it, including a negative control.

Evidence

tests      : 135 passing (83 lib + 52 integration)
             cargo fmt --check clean; clippy --all-targets -D warnings clean
estate     : just check exit 0; just self-verify 26/26, Bronze + Silver ACHIEVED
templates  : all six at 26/26 against their real toolchains, zero unresolved placeholders
rust proof : "Proved (2 files)"                    exit 0   (Creusot 0.14)
             negative control: half-sum + 1        exit 1
ada proof  : 35 checks, 100% proved                exit 0   (gnatprove 13.2.0)
             negative control: Clamp'Result - 1    exit 1
air-gap    : unshare -rn build + test for Rust and Ada   ok

Both proofs were re-run on this exact branch's content after rebasing, through the real generator,
not in the template directory.

What this PR does not claim

  • The two proof.yml files have never run on a GitHub runner. They are template assets: they
    live under aletheia/templates/..., so they run in generated projects, never in this repo.
    Pushing this branch cannot exercise them. Every command in them was run by hand against a
    from-scratch toolchain — that is how three defects in the Rust job and one in the Ada job were
    found and fixed (creusot-rustc resolved from the wrong place, missing provers and why3/why3find,
    a stale hard-coded Why3 pin, and gnatprove installed without a compiler) — but "known-good
    commands in the right order" is a weaker claim than a green run, and this PR does not claim the
    stronger one.
  • templates/agda/.github/workflows/ci.yml installs the distro Agda, while .tool-versions
    pins 2.6.4.3. They coincide on Debian 13; that is not evidence about another base image.
  • aletheia/LICENSE still says PMPL-1.0-or-later while its sources say MPL-2.0. Pre-existing,
    not touched here.
  • The absolute-zero submodule shows as deleted in the local clone used to build this; it is not
    part of any commit here.

Full write-up, including the negative results and the false-gate investigation: DELIVERY-186-197.md.

Commits

12 commits, rebased onto current main — no merge commit, no conflicts. The two fix(ci) commits
and three docs commits in the middle exist because the proof workflows were wrong as first
written and reading them had not shown it.

Two changes to the same walk.

#197: the seven file-list checks each walked the tree separately. They now
share a single memoised traversal filling four buckets in one pass. Depth,
skip-dir, submodule and per-check budget semantics are unchanged, so each
bucket receives exactly the files its own walk saw.

Measured on 18,602 files / 673 dirs, best of 10: 212 ms -> 150 ms. Verdicts
are byte-identical to the previous binary across five repositories.

New: the walk reads `.gitignore` as it descends, so generated build output no
longer counts as source. Previously `aletheia .` on a tree that had just been
built reported Bronze NOT MET, flagging `obj/b__main.ads` and friends for
missing SPDX headers - the gate was only valid on a clean checkout, and any CI
job running it after a build would fail.

Rules are pushed as the walk enters a directory and popped on the way out, so
a nested `.gitignore` stays scoped to its own subtree. A directory-only rule
(`obj/`) is evaluated against the entry's real file type, so it skips the
directory without exempting a file of the same name.

Implemented: comments, blank lines, `!` negation with last-match-wins, trailing
slash, leading-slash anchoring, and `*` crossing directory boundaries (matching
this crate's existing glob_match). Not implemented, and not claimed: character
classes, backslash escapes, and re-inclusion inside an ignored directory.

.gitignore is kept separate from the existing [ignore] config: [ignore] is a
choice made in aletheia's config, whereas .gitignore is the repository's own
declaration of what is not part of the published artefact.
Closes #186. `create-template.sh` scaffolded the retired v1 shape
(LICENSE.txt / justfile / flake.nix) and no fresh project could pass Bronze.

The template tree is now shared files plus a per-language overlay, and the
generator renders six Tier-1 languages: rust, zig, elixir, haskell, ada, agda.
Languages banned or outside Tier-1 are rejected with the estate reasoning
rather than scaffolded into something that cannot pass.

Every language reaches Bronze + Silver (26/26) with no hand edits, builds and
tests with its real toolchain, and needs no network: `cargo build --offline`,
`zig build`, `mix test`, `cabal build --offline`, `gprbuild`, `agda` all
succeed inside `unshare -rn`.

Rust here means Rust/Creusot and Ada here means Ada/SPARK, and both are
actually verified rather than labelled:

  * Rust: Creusot 0.14 translates and Why3 discharges every obligation
    ("Proved (2 files)"), exit 0. `src/impl.rs` is a single source of truth
    `include!`d by both the zero-dependency main crate and the verification
    crate, with contracts gated behind `cfg(creusot)`, so the proof cannot
    drift from the shipped code. `clamp` and `midpoint` carry real
    preconditions and postconditions, including the exact half-sum identity.
  * Ada: the core package declares `pragma SPARK_Mode (On)` and gnatprove
    discharges 35 checks, 100% proved (Z3/Alt-Ergo/CVC5), exit 0. The GPR sets
    --level=2 and --checks-as-errors; without the latter gnatprove exits 0 even
    when a check is unproved, which would make the gate decorative.

Both `just proof` recipes fail with exit 1 on a deliberately false
postcondition, verified in both directions.

Also fixed here, each found while proving the templates rather than assumed:

  * haskell/Justfile used `@@Args@@`, an unknown placeholder, so the
    generator's own unresolved-placeholder guard aborted: `-l haskell` exited
    1. Now `{{ARGS}}`, matching the other five.
  * MOD_ADA was derived by inserting underscores at case boundaries in the
    camel form, which is wrong for single-letter segments: `g-ada` produced
    the unit `GAda` while the template ships `g_ada.ads`, and GNAT failed with
    `file "gada.ads" not found`. Now derived from the project name directly.
  * zig: the template's own test file failed `zig fmt --check`.
  * Unpinned setup actions are now pinned to full SHAs resolved from real
    tags: mlugg/setup-zig, erlef/setup-beam, haskell-actions/setup.

New opt-in proof workflows (`.github/workflows/proof.yml`) for rust and ada.
They are pinned and match the commands verified by hand, but they have not yet
run on a GitHub runner - their first green run is what makes them
load-bearing, and the templates do not claim CI-verified proofs until then.
Records what was delivered and, more usefully, what was not: the specification
Creusot could not verify and why, the fake gate gnatprove would have given
without --checks-as-errors, and the three template defects found only by
running the real toolchains.

Includes the reproduction and the negative control for the .gitignore fix, the
commit split, and the two things deliberately left uncommitted (the
uninitialised `absolute-zero` submodule, and the executable bits the sandbox
restore dropped).

No claims are made for the two proof workflows beyond what has been run: they
are pinned, their shell logic was exercised, and they have not yet executed on
a GitHub runner.
The divergence between `midpoint` and `meanFloor` follows the toolchains, not
taste: Creusot has no bitvector theory and SPARK's provers cannot discharge the
identity, so the proof-carrying templates use the formulation that can be
proved. Unifying would add an unnecessary precondition to three languages.

Kept, with the reasoning recorded, rather than changed for symmetry.
Found by rebuilding the Creusot toolchain from scratch, which meant running the
workflow's own commands by hand.

`cargo creusot` does not look for `creusot-rustc` on PATH - it wants it under
$XDG_DATA_HOME/creusot/toolchains/<channel>/bin/, alongside the Creusot prelude
installed as a why3find package and a generated why3.conf. Installing the two
binaries and running `cargo creusot config --update` leaves all three missing,
and the job dies with:

  creusot-rustc not found (expected at
    ".../toolchains/nightly-2026-08-03/bin/creusot-rustc")

The workflow now runs Creusot's own installer for those pieces
(prelude, why3-conf, creusot-rustc, cargo-creusot, cargo-creusot-config), and
checks the result with `cargo creusot version` before attempting a proof.

Every other command in the toolchain half was verified to run as written: the
apt list, the clone, the rustup toolchain install, opam init and switch, and
both opam pins for Creusot's why3/why3find forks. Reading the channel out of
Creusot's `rust-toolchain` is now tested too - it is TOML without the .toml
extension, so `head -1` silently yields "[toolchain]" and installs the wrong
toolchain.

Also records the Ada re-verification (35/35 checks, negative control exits 1)
and why the first negative-control attempt looked like a false gate when it was
a stale PIPESTATUS in my own shell.
Second pass on the Rust proof workflow, after actually building the toolchain
on a clean box and driving `just proof` to a green run.

Three more things it was missing:

1. The provers. Creusot's why3.conf names tools by bare name and why3 is run
   with the data dir's bin/ first on PATH, so alt-ergo/cvc4 being absent is a
   hard failure at the first prove. The `provers` component fetches the exact
   versions the drivers name (alt-ergo 2.6.2, z3 4.15.3, cvc4 1.8,
   cvc5 1.3.1); apt z3/cvc5 is a different build behind the same driver name.
   `cargo creusot version` now resolves all four, and the job asserts it does.

2. why3 and why3find in the data dir. cargo-creusot resolves them as
   $XDG_DATA_HOME/creusot/bin/why3[find], not from PATH. The installer has a
   `why3` component, but it builds a second opam switch from creusot-deps.opam
   inside the data dir, which pulls in the GTK why3 IDE; its last act is to
   symlink those two binaries, so do the same from the switch we already have.

3. Pins derived, not duplicated. The hard-coded why3 commit went stale - the
   checkout now declares a different one - and a workflow that silently
   changes toolchain under a passing build is not a gate. Creusot itself is
   pinned (CREUSOT_REV) and both forks are read out of that revision's
   creusot-deps.opam, failing loudly if the parse comes up empty.

Verified by running the whole thing here: `just proof` reports
"Proved (2 files)" with why3 pinned to the same commit the workflow will use,
and a planted false postcondition fails the gate with
"Goal Coma.vc_midpoint: x (4/5) / 1 unproved file" and exit 1.
The two proof workflows shipped with a caveat that they had never been
executed. Rebuilding the Creusot toolchain from nothing to exercise them turned
up three defects in the Rust one, all now fixed: creusot-rustc installed where
cargo-creusot looks, the provers and why3/why3find in the data dir, and the
Why3 pin derived from creusot-deps.opam instead of hard-coded (the hard-coded
one had already gone stale).

Also writes down two results that matter more than the happy path: the
negative control on a fresh tree (Goal Coma.vc_midpoint: x (4/5), exit 1), and
the one time it wrongly reported "Proved (2 files)" on a false obligation -
reproduced deliberately, traced to a tree copied along with its verif/ and
target/, and neutralised by the templates' already-ignoring those paths.

The caveat that neither workflow has run on a GitHub runner stays.
Same species of defect as the Rust one: the workflow assumed a tool the runner
does not have. gnatprove is a frontend, not a toolchain - the release archive
the job downloads contains a single binary, no compiler, no gprbuild - so the
job would have died at `gprbuild --version` in the step after the install.

ci.yml already installs `gnat gprbuild` and has done since the template was
written; proof.yml, which was added later, did not, and reading the two files
side by side is what shows it.
Four defects across the two workflows now: three in the Rust job (creusot-rustc
location, missing provers/why3, stale hard-coded pin) and one in the Ada job
(GNAT and gprbuild never installed). The Ada proof is re-run on a regenerated
template: 35 checks proved, 0 unproved, exit 0.
Auditing every workflow for the defect just found in the Ada and Rust proof
jobs (a step using a tool nothing installed) turned up one honest gap rather
than a bug: agda/ci.yml installs the distro's Agda while .tool-versions pins
2.6.4.3. They coincide on Debian 13, which is not evidence about any other base
image.
The section said installing to /usr/local meant the toolchain outlived the
session, unlike /home/user/build. The sandbox then reclaimed everything outside
the workspace - Rust, Cargo, just, Zig, opam, Creusot, gnatprove - which is a
clean refutation. No location here is durable.

The rebuild recipes are unchanged in substance and are the reason this is
survivable rather than fatal; what changes is that they must be treated as
re-runnable, not as a one-off. Also records the two settings that are easy to
lose and hard to diagnose: XDG_DATA_HOME decides where cargo-creusot looks for
its tools (why3, why3find, provers and creusot-rustc itself), and both
creusot-rustc and cargo creusot must run under Creusot's pinned nightly.
A rebase is a rewrite, so the rebased commits were not assumed to be equivalent
to the tested ones. The toolchain was rebuilt from nothing and both proofs were
re-run through the generator on the rebased tree:

  rust positive  Proved (2 files), exit 0
  rust negative  false postcondition, exit 1
  ada  positive  35/35 checks proved, exit 0
  ada  negative  false postcondition, exit 1

and the suite: 135 tests, fmt and clippy clean, `just check` exit 0,
`just self-verify` 26/26.

Two toolchain lessons recorded, both of which cost time here: cargo creusot
resolves creusot-rustc inside its data dir and refuses to run without it even
when the binary is on PATH, and `--component a b` makes rustup read `b` as a
toolchain name rather than a second component.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c16abd47-4f2a-42f8-a4b1-830bef752e5a

📥 Commits

Reviewing files that changed from the base of the PR and between e64b2c2 and e4c7a11.

⛔ Files ignored due to path filters (1)
  • aletheia/templates/common/.github/workflows/actions.lock is excluded by !**/*.lock
📒 Files selected for processing (98)
  • DELIVERY-186-197.md
  • aletheia/scripts/create-template.sh
  • aletheia/src/checks.rs
  • aletheia/src/config.rs
  • aletheia/templates/ada/.editorconfig
  • aletheia/templates/ada/.github/workflows/ci.yml
  • aletheia/templates/ada/.github/workflows/proof.yml
  • aletheia/templates/ada/.gitignore
  • aletheia/templates/ada/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/ada/.tool-versions
  • aletheia/templates/ada/@@MOD_NAME@@.gpr
  • aletheia/templates/ada/Justfile
  • aletheia/templates/ada/README.adoc
  • aletheia/templates/ada/src/@@MOD_NAME@@.adb
  • aletheia/templates/ada/src/@@MOD_NAME@@.ads
  • aletheia/templates/ada/src/main.adb
  • aletheia/templates/ada/tests/run_tests.adb
  • aletheia/templates/ada/tests/tests.gpr
  • aletheia/templates/agda/.editorconfig
  • aletheia/templates/agda/.github/workflows/ci.yml
  • aletheia/templates/agda/.gitignore
  • aletheia/templates/agda/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/agda/.tool-versions
  • aletheia/templates/agda/@@PROJECT_NAME@@.agda-lib
  • aletheia/templates/agda/Justfile
  • aletheia/templates/agda/README.adoc
  • aletheia/templates/agda/src/@@MOD_CAMEL@@.agda
  • aletheia/templates/agda/src/Properties.agda
  • aletheia/templates/bronze-rust/README-template.adoc
  • aletheia/templates/common/.gitattributes
  • aletheia/templates/common/.github/workflows/governance.yml
  • aletheia/templates/common/.github/workflows/hypatia-scan.yml
  • aletheia/templates/common/.well-known/ai.txt
  • aletheia/templates/common/.well-known/humans.txt
  • aletheia/templates/common/.well-known/security.txt
  • aletheia/templates/common/0-AI-MANIFEST.a2ml
  • aletheia/templates/common/CHANGELOG.adoc
  • aletheia/templates/common/CODE_OF_CONDUCT.adoc
  • aletheia/templates/common/CONTRIBUTING.adoc
  • aletheia/templates/common/LICENSE
  • aletheia/templates/common/LICENSES/CC-BY-SA-4.0.txt
  • aletheia/templates/common/LICENSES/MPL-2.0.txt
  • aletheia/templates/common/MAINTAINERS.adoc
  • aletheia/templates/common/SECURITY.adoc
  • aletheia/templates/elixir/.editorconfig
  • aletheia/templates/elixir/.formatter.exs
  • aletheia/templates/elixir/.github/workflows/ci.yml
  • aletheia/templates/elixir/.gitignore
  • aletheia/templates/elixir/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/elixir/.tool-versions
  • aletheia/templates/elixir/Justfile
  • aletheia/templates/elixir/README.adoc
  • aletheia/templates/elixir/lib/@@MOD_NAME@@.ex
  • aletheia/templates/elixir/lib/@@MOD_NAME@@/cli.ex
  • aletheia/templates/elixir/mix.exs
  • aletheia/templates/elixir/test/@@MOD_NAME@@_test.exs
  • aletheia/templates/elixir/test/test_helper.exs
  • aletheia/templates/haskell/.editorconfig
  • aletheia/templates/haskell/.github/workflows/ci.yml
  • aletheia/templates/haskell/.gitignore
  • aletheia/templates/haskell/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/haskell/.tool-versions
  • aletheia/templates/haskell/@@PROJECT_NAME@@.cabal
  • aletheia/templates/haskell/Justfile
  • aletheia/templates/haskell/README.adoc
  • aletheia/templates/haskell/app/Main.hs
  • aletheia/templates/haskell/cabal.project
  • aletheia/templates/haskell/src/Core.hs
  • aletheia/templates/haskell/test/Main.hs
  • aletheia/templates/rust/.editorconfig
  • aletheia/templates/rust/.github/workflows/ci.yml
  • aletheia/templates/rust/.github/workflows/proof.yml
  • aletheia/templates/rust/.gitignore
  • aletheia/templates/rust/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/rust/.tool-versions
  • aletheia/templates/rust/Cargo.toml
  • aletheia/templates/rust/Justfile
  • aletheia/templates/rust/README.adoc
  • aletheia/templates/rust/src/impl.rs
  • aletheia/templates/rust/src/lib.rs
  • aletheia/templates/rust/src/main.rs
  • aletheia/templates/rust/tests/integration_test.rs
  • aletheia/templates/rust/verification/Cargo.toml
  • aletheia/templates/rust/verification/README.adoc
  • aletheia/templates/rust/verification/src/lib.rs
  • aletheia/templates/rust/verification/why3find.json
  • aletheia/templates/zig/.editorconfig
  • aletheia/templates/zig/.github/workflows/ci.yml
  • aletheia/templates/zig/.gitignore
  • aletheia/templates/zig/.machine_readable/rsr-profile.a2ml
  • aletheia/templates/zig/.tool-versions
  • aletheia/templates/zig/Justfile
  • aletheia/templates/zig/README.adoc
  • aletheia/templates/zig/build.zig
  • aletheia/templates/zig/src/main.zig
  • aletheia/templates/zig/src/root.zig
  • aletheia/templates/zig/test/integration_test.zig
  • aletheia/tests/integration_tests.rs
 _______________________________________
< Your code's Big O notation: `O(WTF)`. >
 ---------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • 🔴 Error committing to branch - (🔄 Check to retry)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath merged commit e477b40 into main Sep 24, 2026
31 of 33 checks passed
@hyperpolymath
hyperpolymath deleted the deliver-186-197 branch September 24, 2026 01:56
@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

❌ Failed to create Coding Agent finishing-touch task. Please try again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create-template.sh still scaffolds the retired v1 shape (LICENSE.txt / justfile / flake.nix)

1 participant