open-standards/.github/workflows/standards-build.yml
David Charlot cf277b4828
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m49s
standards workspaces build + test / aac-rs (pull_request) Successful in 37s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 39s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 34s
standards workspaces build + test / map-rs (pull_request) Successful in 39s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m49s
standards workspaces build + test / openpay (pull_request) Successful in 4m12s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m17s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m7s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m33s
standards workspaces build + test / arl-rs (pull_request) Successful in 3m23s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m31s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m31s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 2m29s
standards workspaces build + test / sense-rs (pull_request) Successful in 4m6s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 1m59s
ci(standards-build): exercise the macOS energy backend, which nothing did
`jcp-energy`'s IOReport meter — the Apple Silicon rung of the energy ladder — is
behind `#[cfg(target_os = "macos")]`, and `jcp-rs` is pinned to the linux runner.
A platform-gated test plus a single-platform runner assignment is **zero**
coverage, and it is indistinguishable from passing: the code compiles out, the
suite reports green, and nothing has been measured. Same family as a
feature-gated test run with no `--features`, with `target_os` as the gate.

Not hypothetical. On 2026-09-19 the counter read 0 µJ across a full run on the
dev Mac — `IoReport::open()` succeeding while `read_uj()` returned nothing — and
by 2026-09-21 it had healed with **no code change in between**
(`git log --since=2026-09-17 -- crates/jcp-energy/` is empty; 8 of 8 runs now
pass). Both observations came from a human running the suite by hand. CI saw
neither, and could not have.

That transience is the argument for the job rather than against it. A
permanently dead counter gets noticed once and then known. One that reads zero
for a while and silently recovers means `Meter::detect()` can, during such a
window, select a backend measuring nothing in preference to the time×power rung
below it — and then it heals, so anyone investigating afterwards finds a working
counter and concludes there was never a problem. A human spot-check is worst
precisely at catching that; a job that runs on every change is the instrument
for it.

Deliberately narrow: `-p jcp-energy --lib` only. `jcp-rs` stays on linux, so this
**adds** the missing platform rather than trading one for the other, and costs
one small crate instead of a second full workspace run. Every
`cfg(target_os = "macos")` block in the crate lives in its lib tests, so `--lib`
reaches all of them: 8 tests, 3 of them the IOReport ones.

Verified to catch the real symptom, not merely to pass: forcing `read_uj()` to
return `Some(0)` makes `cumulative_advances_after_cpu_burn` fail with
`energy must advance: before=0 after=0` — byte-identical to the 2026-09-19
observation. Probe reverted and the revert checked with `git status`.

This does **not** change energy-metering semantics. `Meter::detect()` still has
no liveness probe, so it will still prefer a zero-reading IOReport backend over
the rung below it; deciding what detect() should do when a counter reads zero is
a semantic change to the ladder and is left alone deliberately. This commit only
makes the condition visible.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 13:47:40 -04:00

256 lines
14 KiB
YAML

name: standards workspaces build + test
# Build gate for every Rust workspace in the repo.
#
# It started as three. Then a toolchain bump compiled the rest for the first
# time and found FIVE more that did not build — smart-byte (47k LOC), openpay
# (six separate breakages across five crates), eoc, mesh and joule-ui — all
# broken by the same handful of upstream migrations (hmac -> KeyInit, reqwest
# feature-gating, cipher 0.4 -> 0.5, a struct gaining fields), and all invisible
# because nothing ever compiled them. Partial coverage is how that happens, so
# the matrix now covers everything.
#
# The original case, still the clearest: smart-byte-rs had never been compiled by
# anything, and it did not build. A single wrong `rand` pin — 0.10 against code
# written for 0.8, whose crypto stack requires rand_core 0.6 — silently disabled
# the entire identity pillar, BBS+ included. `cargo check` would have caught it
# on day one. An unbuilt standard is not a standard, so this workflow is the
# floor under every claim the specs make.
#
# One workflow with a matrix rather than sixteen files: the bootstrap step is
# identical everywhere, and a shared dependency change should rebuild all of
# them anyway. `fail-fast: false` so one red workspace does not mask the others.
on:
push:
branches: [main]
pull_request:
# Deliberately NO `paths:` filter.
#
# The first attempt used `**/*.rs` and `**/Cargo.toml`, and the workflow then
# silently did not fire — pushes touching exactly those files triggered other
# workflows but not this one, so `**`-prefixed globs evidently do not match here
# the way they do on GitHub. A build gate that quietly fails to run is worse than
# no gate at all: it reports nothing and everyone assumes green.
#
# This is the repo-wide floor, so it runs on every push and every PR. The cost is
# CI minutes on doc-only changes. The alternative cost is what this branch spent
# a day fixing — five workspaces silently broken because nothing compiled them.
jobs:
build:
name: ${{ matrix.name }}
# Spread across the fleet's runners rather than pinning every job to `linux`.
# For most of these the runner is a capacity choice, not a correctness one —
# and 17 jobs on a single runner queue behind whatever else is using it.
# `jouleclaw-rs` is the exception: see its matrix entry.
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { name: jcp-rs, dir: jcp/jcp-rs, runner: linux, conformance: jcp-conformance }
- { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance }
- { name: smart-byte-rs, dir: smart-byte/smart-byte-rs, runner: linux }
- { name: openpay, dir: openpay, runner: linux-arm64 }
- { name: joule-code-rs, dir: joule-code/joule-code-rs, runner: linux-arm64 }
- { name: map-rs, dir: map/map-rs, runner: linux-arm64, conformance: map-conformance }
- { name: aac-rs, dir: aac/aac-rs, runner: linux-arm64, conformance: aac-conformance }
- { name: joulecontract-rs, dir: joulecontract/joulecontract-rs, runner: linux-arm64, z3: true }
- { name: jouledesk-rs, dir: jouledesk/jouledesk-rs, runner: macos }
- { name: arl-rs, dir: arl/arl-rs, runner: macos }
- { name: sense-rs, dir: sense/sense-rs, runner: macos }
- { name: proof-rs, dir: proof/proof-rs, runner: linux, z3: true, conformance: proof-conformance }
- { name: joulehook-rs, dir: joulehook/joulehook-rs, runner: macos }
- { name: eoc-rs, dir: eoc/eoc-rs, runner: linux }
- { name: mesh-rs, dir: mesh/mesh-rs, runner: linux-arm64 }
- { name: joule-ui-rs, dir: joule-ui/joule-ui-rs, runner: macos }
# jouleclaw-rs is the one entry whose runner IS a correctness choice.
# It reaches candle → ug → gemm, which uses inline aarch64 assembly
# (`fmla {0:v}.8h, ...`) requiring the ARMv8.2 FP16 extension.
# `aarch64-apple-darwin` carries target_feature="fp16" in its baseline
# because Apple Silicon guarantees it; `aarch64-unknown-linux-gnu`
# baselines at ARMv8.0-A and does not, so the same asm fails to
# ASSEMBLE — a codegen error `cargo check` never reaches. Verified on a
# native arm64 Linux container (rustc 1.98.0): without the flag, 12
# errors; with it, 4457 tests pass.
#
# CAPACITY: this is by far the largest entry — its target tree is
# ~16 GB, against a 120 GB volume on tx-dev-linux-arm64 that already
# holds ~74 GB of other workspaces' caches. If linux-arm64 jobs start
# failing for no apparent reason, check `df -h /mnt/lima-colima-txarm`
# on the txarm VM before suspecting the code: a full cache volume
# fails whatever happens to be building, not whatever changed.
- { name: jouleclaw-rs, dir: jouleclaw/jouleclaw-rs, runner: linux-arm64, rustflags: "-C target-feature=+fp16", cache_suffix: "-nodebug" }
steps:
- uses: actions/checkout@v5
# Two workspaces shell out to `z3`, and the runner image has no solver.
#
# This step used to be load-bearing for correctness: without a solver the
# ladder escalated past its real rungs into a model STAND-IN that reported
# Proved, so every goal on a solverless machine came back proved. That is
# now fixed at the source — the real ladders hold decision procedures only
# and an absent rung yields Unproved (proof-solvers `ladder_is_sound` and
# `ladder_never_fabricates_a_proof_when_a_rung_is_absent`).
#
# So the suites pass with or without z3, and this step is kept purely for
# COVERAGE: with a solver present the real T1 rung actually runs, and the
# SMT paths are exercised rather than skipped. The absent-rung semantics
# stay covered either way — that regression test uses a goal z3 cannot
# parse, so it asserts the same thing on a machine that has one.
#
# Both z3 jobs are pinned to Linux runners deliberately: the macOS runner is
# a host executor with no apt. `sudo` may or may not exist depending on
# whether the container runs as root, so fall back to a bare invocation.
- name: Install z3
if: matrix.z3
shell: bash
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq z3
z3 --version
# The docker runner image ships no Rust at all, so rustup is bootstrapped
# here. dtolnay/rust-toolchain cannot be used: it shells out to `rustup`,
# which the image lacks. Harmless where cargo already exists.
- name: Install Rust (workspace pin)
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# jouleclaw-rs is the CAPACITY entry above. Linux embeds full DWARF in every
# test binary, so with default debuginfo its aarch64 test target outgrows
# the ~15 GB left on the shared txarm volume and the link fails with
# "No space left on device" (reproduced with the same toolchain, flags and
# image). Without debuginfo the same target is ~6 GB and the whole suite
# passes. Panic messages keep their file:line: that comes from the panic
# location, not from DWARF.
#
# Set through $GITHUB_ENV rather than a matrix field on purpose: a matrix
# field would hand every other job CARGO_PROFILE_DEV_DEBUG="", which cargo
# can reject. `cache_suffix` gives this job a fresh cache namespace so it
# cannot restore a full-debuginfo target and hold both builds at once;
# for every other job the suffix is empty and its key string is unchanged.
- name: Trim debuginfo (arm64 jouleclaw)
if: matrix.name == 'jouleclaw-rs'
shell: bash
run: echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV"
- name: Cache cargo registry + target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
${{ matrix.dir }}/target
key: standards-${{ matrix.name }}${{ matrix.cache_suffix }}-${{ hashFiles(format('{0}/Cargo.lock', matrix.dir), 'rust-toolchain.toml') }}
restore-keys: |
standards-${{ matrix.name }}${{ matrix.cache_suffix }}-
# --all-targets is load-bearing: a plain `cargo check` does not build test
# targets, and smart-byte's dev-dependencies drifted independently of its
# normal ones (rand_chacha 0.10 against rand 0.8). The library compiled
# while the tests did not.
- name: Check (all targets)
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo check --workspace --all-targets
- name: Test
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo test --workspace
# Run each standard's conformance suite.
#
# These suites live in a binary's `main()`, not in `#[test]`, so
# `cargo test` never executes them — verified, not assumed:
# `cargo test -p aac-conformance` runs **0 tests** while
# `cargo run -p aac-conformance` runs **40 checks**; map is 1 test vs 15
# checks. Same shape for jcp, proof and sandbox. The suites include
# negative vectors (`C-neg`, `Cneg`), which are the ones that catch a
# verifier ACCEPTING what it must reject.
#
# This is the `main()` instance of the same bug as a feature-gated test:
# the code compiles under `--all-targets`, reports nothing, and looks
# covered. Compiling a suite is not running it.
#
# `--bin` is explicit because these crates also ship a `gen-vectors`
# binary; `cargo run -p <crate>` is ambiguous with two binaries and fails
# rather than choosing — which is how this step would silently never run.
#
# No path argument: these binaries take none. They check the
# implementation against in-code expectations, which is a different claim
# from validating the published `conformance/v1` files on disk — nothing
# in CI does that yet, and this step does not pretend to.
- name: Conformance suite
if: matrix.conformance
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo run -q -p ${{ matrix.conformance }} --bin ${{ matrix.conformance }}
# ── The macOS energy backend is exercised NOWHERE otherwise ───────────────
#
# `jcp-energy`'s IOReport meter — the Apple Silicon rung of the energy
# ladder — is behind `#[cfg(target_os = "macos")]`, and `jcp-rs` is pinned to
# the linux runner above. A platform-gated test plus a single-platform runner
# assignment is zero coverage, and it is indistinguishable from passing: the
# code compiles out, the suite reports green, and nothing has been measured.
# Same family as a feature-gated test with no `--features`, with `target_os`
# as the gate.
#
# This is not hypothetical. On 2026-09-19 the counter read 0 µJ across a full
# run on the dev Mac — `IoReport::open()` succeeding while `read_uj()`
# returned nothing — and by 2026-09-21 it had healed with no code change in
# between. Both observations came from a human running the suite by hand;
# CI saw neither. A meter that silently reads zero and later recovers is
# exactly the failure a human spot-check is worst at catching.
#
# Deliberately narrow: `-p jcp-energy` only. `jcp-rs` stays on linux, so this
# ADDS the platform that was missing rather than trading one for the other,
# and it costs one small crate rather than a second full workspace run.
#
# Verified to catch the real symptom: forcing `read_uj()` to return `Some(0)`
# makes `cumulative_advances_after_cpu_burn` fail with
# `energy must advance: before=0 after=0` — byte-identical to what was
# observed on 2026-09-19.
macos-energy-backend:
name: macOS energy backend (jcp-energy)
runs-on: macos
steps:
- uses: actions/checkout@v4
- name: Install Rust (workspace pin)
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Cache cargo registry + target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
jcp/jcp-rs/target
key: standards-jcp-energy-macos-${{ hashFiles('jcp/jcp-rs/Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
standards-jcp-energy-macos-
# `--lib` keeps this to the crate's own tests, which is where every
# `cfg(target_os = "macos")` block in this crate lives.
- name: Energy backend tests (the cfg(macos) blocks)
working-directory: jcp/jcp-rs
run: cargo test --lib -p jcp-energy