1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
name: ci
on:
push:
branches:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# The cross-binding gates below compare SURFACES, so this job needs the toolchains those
# surfaces are written in -- otherwise they skip, exit 0, and report success having compared
# four of seven. That is the same shape as check-exports.sh sitting in a job with no Julia:
# a gate that runs and covers less than it appears to. Measured before fixing: with a
# CI-like PATH, check-answers.sh compared 5 and silently skipped julia and zig.
- uses: julia-actions/setup-julia@v2
with:
version: "1.12"
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Playwright, for the wasm surface
working-directory: web-tests
run: npm install && npx playwright install --with-deps chromium
- name: Install icarus-verilog (RTL bit-exactness gate)
run: sudo apt-get update && sudo apt-get install -y iverilog
# GH_TOKEN for the same reason the landscape step needs it, in a quieter and worse shape.
# This gate's JLL drift branch asks `gh release view v<x>` whether the release has tarballs
# the Artifacts manifest never picked up -- the v0.18.0 failure, where all three uploaded, the
# manifest commit lost a rebase, and the JLL served v0.17.0 under a 0.18.0 version number.
# Unauthenticated, that call fails and the branch reports "no release yet, so nothing to point
# at" AND PASSES. It does not go red; it goes quiet, which is the reading this whole file
# argues against.
- name: One version across every package
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: scripts/check-versions.sh --selftest && scripts/check-versions.sh
- name: Every C ABI symbol reaches every binding, or the gap is written down
# --selftest first. This gate discovered symbols by grepping for `pub extern "C" fn`, which
# cannot see the twelve certificate accessors a macro generates -- so twelve exports were
# never checked against any binding while it reported perfect parity.
run: scripts/check-parity.sh --selftest && scripts/check-parity.sh
# The node editor is a surface like any other, and check-parity.sh does not look at it. That
# gap was real: the model layer had nine constraints and the editor offered six, for as long
# as nobody compared the lists. --selftest first, because this gate is a pile of greps over a
# moving file and the first version of it passed an editor with a node type cut out.
- name: The visual editor can say everything the model layer can
run: scripts/check-editor-parity.sh --selftest && scripts/check-editor-parity.sh
# And MEANS it. Parity is a check on vocabulary and passes an editor whose fromModel drops a
# k, a soft price or an objective term -- which still draws every node type, still runs, and
# answers a different question. Same pair as check-parity vs check-semantics.
- name: One model through the API and through the picture, same compiled size
run: scripts/check-editor-model.sh
# Parity proves the names exist; this proves they compute the same thing. Bindings whose
# toolchain the runner lacks skip; a binding that is present and produces nothing FAILS.
- name: Every binding compiles one model to the same bytes
env:
FERROTHERM_REQUIRE_ALL: "1"
run: scripts/check-semantics.sh --selftest && scripts/check-semantics.sh
# And SOLVES it to the same answer, which is a different question. check-semantics compares
# the model BEFORE it is solved, and its harness calls compile() explicitly -- so it exercised
# the exact step Zig's `solve` was skipping, and stayed green while that binding could not
# solve anything at all.
- name: Every binding solves one model to the same answer
env:
FERROTHERM_REQUIRE_ALL: "1"
run: scripts/check-answers.sh --selftest && scripts/check-answers.sh
# And one HIGHER-ORDER model, which is a different claim: the model above is pairwise, so a
# surface that quietly routed a k-body term through the reduction would still answer it
# correctly. The three-body parity term cannot be expressed pairwise at all, and the ancilla
# count is in the comparison so a surface that reduced it anyway is caught reporting the
# wrong saving for the right answer.
- name: Every binding solves one higher-order model to the same answer
env:
FERROTHERM_REQUIRE_ALL: "1"
run: scripts/check-hubo-answers.sh --selftest && scripts/check-hubo-answers.sh
# Parity proves a symbol is DECLARED in each binding. For Julia that is an @cfn line, which
# says nothing about whether a function wraps it -- both OMMX functions were once declared and
# exported with no body in between, and the module loaded fine.
- name: Unit tests
# --workspace, not the root package alone. This was `cargo test --release`, which builds
# and tests only `ferrotherm` -- so ferrotherm-serve, ferrotherm-cloud and
# ferrotherm-silicon were never compiled by CI at all, and their tests never ran.
run: cargo test --release --workspace
# The tool that found the 0.12.0 headline defect ran in no job and no script. It aborted on
# ffi.rs's deny-by-default lint, so five of the six crates and all 21 examples had never been
# linted at all. The three lints this codebase deliberately does not follow are recorded in
# Cargo.toml's [workspace.lints.clippy] with the reason for each; everything else is denied.
- name: Clippy, across the whole workspace
run: cargo clippy --release --workspace --all-targets -- -D warnings
# THE GPU PATH IS EXECUTED HERE, not merely compiled.
#
# This step used to run `cargo test -p ferrotherm-gpu` on a runner with no adapter, where
# every hardware-gated test skips. That caught an API break and verified no physics at all:
# the fastest sampler in the stack had ZERO CI coverage, and its correctness rested on
# whichever machine somebody remembered to run by hand. A second vendor then found a SIGSEGV
# that had been latent for releases.
#
# lavapipe is a software Vulkan implementation, so a hosted runner can execute the real
# shader. It says nothing about speed and everything about correctness, which is exactly what
# a gate should assert -- the same split the README already makes for the DX12/WARP run.
- name: Install lavapipe, a software Vulkan device
run: sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers
- name: GPU backend, actually run against a software Vulkan adapter
run: |
icd=$(ls /usr/share/vulkan/icd.d/lvp_icd*.json 2>/dev/null | head -1)
test -n "$icd" || { echo "::error::mesa-vulkan-drivers installed but no lvp ICD found"; exit 1; }
echo "using $icd"
out=$(VK_ICD_FILENAMES="$icd" cargo test --release -p ferrotherm-gpu 2>&1)
echo "$out"
# A SKIP IS NOW A FAILURE. A driver was installed on purpose, so "no GPU adapter" means
# it did not load -- and the shader went unverified while the job stayed green, which is
# the exact shape of the hole this step was added to close.
if grep -q "no GPU adapter" <<<"$out"; then
echo "::error::lavapipe is installed but wgpu found no adapter; the GPU path did NOT run"
exit 1
fi
# Name the test rather than counting them: a count goes stale the moment one is added,
# and this is the one that puts the whole conformance suite through the GPU.
grep -q "conform_can_finally_score_the_gpu_path ... ok" <<<"$out" || {
echo "::error::the conformance case did not run on the GPU path"
exit 1
}
# Same shape: no macmon on a Linux runner, so the measuring tests skip and the parsing and
# refusal logic -- which is most of the crate -- still runs.
- name: Power meter builds, and its tests skip rather than fail without a backend
run: cargo test --release -p ferrotherm-meter
# The OMMX bridge hand-rolls protobuf, so the check that matters is the REFERENCE
# implementation reading what it writes. Installed here on purpose; the test skips without it.
- name: OMMX bridge, checked against the reference implementation
run: |
python3 -m pip install --quiet ommx
OMMX_PYTHON=python3 cargo test --release --test ommx_reference
- name: Examples build
run: cargo build --release --examples
# RUN them, not merely build them.
#
# This step was two examples while `cargo build --examples` compiled all twenty, so eighteen
# were checked for compiling and never for working. `dtm_scale` had been panicking on a
# missing argument the whole time and nothing could see it, because a panic at runtime is
# invisible to a build.
#
# Every example that runs unattended is run, and a non-zero exit fails the job -- which is
# what the seven gate examples use to report a physics check failing. 115 s for seventeen of
# them, measured, which is affordable on every push.
#
# Three are excluded by name, with the reason, rather than by a silent filter:
# depth_vs_dimension 199 s too slow for every push
# reach_on_z1 107 s the flagship; slow, and run before a release
# dtm_scale needs a Fashion-MNIST idx3-ubyte file this runner does not have
# exact_reach a measurement, not a gate: it runs the size up until the node budget
# runs out, which is the opposite of what a per-push check should do
# sdp_in_tree same: it sweeps an expensive bound past the point where it pays
# maxcut_shootout needs a G-set instance file this runner does not have
# planar_exact exact max-cut up to 10,000 spins; minutes, not seconds
# toroidal_bound needs a G-set instance file this runner does not have
# icm_scaling a trend measurement across five lattice sizes; minutes
# docs.rs is a published surface and nothing here gated it. Turned on with 22 broken
# intra-doc links already in the tree -- most of them prose like `[0,1]` or `reals[x]` that
# rustdoc read as a link and could not resolve, two of them real (`Verdict::Runnable` and
# `crate::graph::GRAPH_BUILDS` name items that do not exist). All fixed in the same commit
# that added this step, because a gate turned on over a failing tree is a gate that gets
# turned off again.
# The landscape claims are FALSIFIABLE statements the 2026-08 survey staked, and re-running
# that survey is a day's work while checking whether it still holds is five seconds. It was
# never in CI, so the one gate whose whole purpose is to notice the world moving was the one
# thing nobody ran on a schedule.
#
# GH_TOKEN IS NOT OPTIONAL HERE, and leaving it out is why this step was red for every run
# between landing and now. `check-landscape.sh` drives `gh api`, `gh` on a runner has no
# credentials of its own, and the script correctly refuses rather than reporting "unchanged"
# -- "I could not look" and "nothing moved" are different answers and only one of them is
# safe. So the gate was doing exactly the right thing and CI failed on every commit, which
# makes a red build mean nothing and is how a real failure gets waved through. The runner's
# automatic token turns 60 unauthenticated requests an hour, shared across every runner on
# that IP, into 5,000 for this repository.
- name: The competitive landscape has not moved out from under the survey
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: scripts/check-landscape.sh --selftest && scripts/check-landscape.sh
- name: The published documentation has no broken links
run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps
- name: Every unattended example runs, and a gate example failing fails the build
# FERROTHERM_ALLOW_BUSY, deliberately. `flips_bench` and `parity_bench` refuse to print a
# throughput rate above a load average of 2, because a rate taken under contention measures
# the run queue -- and a hosted runner that has just finished `cargo build --release
# --examples` is exactly that. What this step checks is that the examples RUN, not what they
# measure, so the guard is overridden here and the numbers from this job are not quotable.
# The override is not silence: both print the load average and a caveat above their tables.
env:
FERROTHERM_ALLOW_BUSY: "1"
run: |
set -euo pipefail
# mixing_expressivity is the only NEW skip: it draws 40,000 samples at each of four betas
# over five shapes and three seeds, which is four minutes. trained_tradeoff (16 s) and
# metric_calibration (3 s) RUN here -- both assert an ordering, so a run that exits 0 is
# worth having on every commit.
skip="depth_vs_dimension reach_on_z1 dtm_scale exact_reach sdp_in_tree maxcut_shootout planar_exact toroidal_bound icm_scaling hubo_vs_reduction hfs_reach mixing_expressivity"
ran=0
for f in examples/*.rs; do
n=$(basename "$f" .rs)
case " $skip " in *" $n "*) echo " skipped $n"; continue;; esac
echo " running $n"
cargo run --release --quiet --example "$n" >/dev/null
ran=$((ran + 1))
done
# A floor: if the glob ever stops matching, this passes over nothing.
if [ "$ran" -lt 15 ]; then
echo "only $ran examples ran; this checked almost nothing" >&2
exit 2
fi
echo " $ran examples ran, all exit 0"
# Do the tests have teeth? A green suite says the code runs, not that a wrong answer would be
# noticed -- and the 0.12.0 release exists because 320 green tests could not tell an invented LP
# bound from a correct one. This breaks the code on purpose and requires the named test to go red.
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Every recorded mutation is caught by the test named for it
run: scripts/mutation-suite.sh
c-abi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Build the library the header describes
run: cargo build --release
- name: The header compiles and links against it
run: |
cc -I include include/check.c -L target/release -lferrotherm -o /tmp/check
LD_LIBRARY_PATH=target/release /tmp/check
python:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
# The range we CLAIM, not one point in it. requires-python said >=3.9 while CI tested a
# single version, so the floor was a claim nobody had ever run.
python-version: "${{ matrix.python }}"
- name: Build the library the bindings load
run: cargo build --release
- name: Python tests
run: |
pip install pytest
python -m pytest python/test_model.py python/test_readme.py -q
# The stub is the surface an editor, a type checker and any model writing code against this
# package actually read. It is generated, so this regenerates and diffs -- renaming a
# parameter without regenerating fails here rather than autocompleting a lie.
- name: the type stub matches the library it describes
run: scripts/check-stubs.sh
wasm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: wasm cdylib builds clean
run: RUSTFLAGS='-C strip=symbols' cargo build --release --lib --target wasm32-unknown-unknown
# Not a byte comparison against a fresh build: a wasm binary is not reproducible across
# toolchain versions, so that fails on a runner whose rustc differs from the author's and
# says nothing about whether the artefact works. What matters is that every symbol the pages
# reach for is present -- a missing export makes the call `undefined`, and calling undefined
# in a click handler leaves the page looking fine and doing nothing.
- name: the committed wasm exports everything the pages call
run: scripts/check-wasm-exports.sh --selftest && scripts/check-wasm-exports.sh
# A page that loads is not a page that works. This instantiates the committed binary and
# fits a model through it, then requires the answer to match the native library EXACTLY --
# so a stale wasm, or a marshalling bug in the fitting path, fails here rather than in a
# click handler where nobody sees it.
- name: fitting agrees between the browser binary and the native library
run: |
cargo build --release --lib
scripts/check-fit.sh
zig:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
# `zig build test` runs cargo itself and links with an rpath, so there is one command here
# rather than a hand-written link line that can drift from what a consumer would use.
- name: Zig tests
working-directory: zig
run: zig build test --summary all
julia:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: julia-actions/setup-julia@v2
with:
version: "1.12"
- name: Build the library the binding loads
run: cargo build --release
# Moved here from the `test` job, which has no Julia -- so it printed "skipping", exited 0,
# and had never checked anything in CI while reading as a step that passed. REQUIRE_JULIA
# makes the skip a failure, so it cannot go quiet again.
- name: Every exported binding name resolves
env:
FERROTHERM_REQUIRE_JULIA: "1"
run: scripts/check-exports.sh --selftest && scripts/check-exports.sh
- name: Julia tests
working-directory: julia/Ferrotherm
env:
FERROTHERM_LIB: ${{ github.workspace }}/target/release/libferrotherm.so
run: julia --project=. -e 'using Pkg; Pkg.test()'
editor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install
working-directory: web-tests
run: npm install && npx playwright install --with-deps chromium
# Through the gate, not `npm test` directly, so the one command that covers BOTH pages is the
# same one a developer can run. `docs/ide.html` had no gate at all until this: the editor
# gates drive graph.html, and a hand-run of `node editor.test.mjs` looked like coverage while
# leaving the workbench untested through two releases.
- name: Browser pages run
env:
FERROTHERM_REQUIRE_ALL: "1"
run: scripts/check-pages.sh --selftest && scripts/check-pages.sh