Merge master 67d594c02 into cae8635124 under the master-landing lock

This commit is contained in:
igneum-labs 2026-10-08 21:10:37 +00:00
commit dedbc740cb
51 changed files with 2924 additions and 229 deletions

View file

@ -104,7 +104,7 @@ stock and at the 1,300 MHz lock, and 6.9 pJ per counted op on the Apple M5 Max (
| lop3 (8-bit truth table) | 7,274 | 1.42 | 0.99 | 0.72 | 0.52 | 24.1 / 13.0 | 0.030 / 0.055 | | 0.11 | |
| 32-lane xor-mask shuffle (butterfly over the 1 KB window), per lane-op | 181,580 | 1.24 | 0.87 | 0.63 | 0.45 | 55.8 / 29.4 | 0.011 / 0.021 | | 0.042 | routed with SPEF; 15:1x UK |
| 32-lane general crossbar, per lane-op | ROW_XBAR |
| 8 KB scratch, one random read (flop array: the pessimistic form) | ROW_SCRATCH |
| 8 KB scratch, one random 32-bit read of a 2,048 x 32 flop array (the pessimistic form of a chip's L1; an SRAM macro reads lower) | 868,159 | 207 | 145 | 104 | 75 | 2,400 / 1,400 per L2 hit | 0.043 / 0.074 | | 0.15 | routed with SPEF, 19:4x UK; the card's shared-memory read is unmeasured (owed) |
| int8 8x8x8 tile, per MAC | ROW_TILE |
Reading the floors: a lane's add costs the chip about 2.2 pJ at ASAP7 and 1.1 at N3, against the 5090's 6.2
@ -138,7 +138,7 @@ draws 13.9 mW, the run phase 36.9 mW for 8 lanes at 1.5 ns).
| core, 8 lanes, 32 registers | synthesis only (no wires, no clock tree) | 186,443 | 6.9 | 4.8 | 3.5 | 2.5 | 11.3 / 6.2 / 6.9 | 0.31 / 0.56 / 0.50 | 0.22 / 0.40 / 0.36 | 1.1 | synthesised; 14:0x UK |
| of which the sequential term (register file, imem and IR clock pins, no clock gating) | | | 2.4 | 1.7 | 1.2 | 0.9 | | | | | |
| of which the units, the read muxes and the butterfly | | | 4.5 | 3.1 | 2.3 | 1.6 | | | | | |
| core, 8 lanes, 32 registers | placed and routed, SPEF | ROW_CORE8_PLACED |
| core, 8 lanes, 32 registers, ungated | placed and routed, SPEF, clock tree (one run length of 300 cycles, the load phase subtracted, about plus or minus 10 percent) | 444,478 | 11.3 | 7.9 | 5.6 | 4.1 | 11.3 / 6.2 / 6.9 | 0.50 / 0.91 / 0.82 | 0.36 / 0.66 / 0.59 | 1.8 | placed 16:0x UK on a rented pod; +64 percent over synthesis (wires, and a clock tree of 2.5 pJ per lane-op that gating removes) |
| core, 32 lanes, 32 registers | synthesis only (steady state from 150 and 400 run cycles) | 600,381 | 5.55 | 3.9 | 2.8 | 2.0 | 11.3 / 6.2 / 6.9 | 0.25 / 0.45 / 0.41 | 0.18 / 0.32 / 0.29 | 0.90 | synthesised; 15:2x UK |
| core, 32 lanes, 16 registers | synthesis only | 443,258 | 4.2 | 2.9 | 2.1 | 1.5 | 11.3 / 6.2 / 6.9 | 0.18 / 0.34 / 0.30 | 0.13 / 0.24 / 0.22 | 0.68 | synthesised; one run length, about plus or minus 10 percent; 15:0x UK |
| the bare ARX lane (section 3, the floor) | routed | 11,631 | 2.2 | 1.5 | 1.1 | 0.8 | 11.3 / 6.2 / 6.9 | 0.10 / 0.18 / 0.16 | 0.07 / 0.13 / 0.11 | 0.35 | the lower bound |
@ -203,6 +203,128 @@ takes back on the card's own node (3.6x to 2.4x), which is the design. Node-for-
k 0.78 and the 64-register core at 1.09, so "near 0.9" is reached node-for-node by the window alone; what it does
not survive is the node step a chip project would buy (an N3 core gives back 0.4x, an N2 core 0.8x).
### 4c. The adversary's 64-register core: is the window a defence? (the coordinator's order, 15:3x UK)
Every row in this section is a MODEL of a chip core, never a lower bound on what a chip maker can build; the
synthesis gives the cost of the circuit as drawn, and a better circuit is always possible.
**The live-state analysis** (`tools/chip-model/rtl/flow/livestate.py`, run on build-3: programs drawn as the
core testbench draws them, the class v4 op weights, a load on one instruction in 16 as the dependent memory wait,
dst and src uniform over the window, the result fold reading every register at the end of the block; 64 drawn
programs, 1,024 waits per row):
| Window R | Live values at a wait (mean, min to max) | Of which necessary (reach a later address or the result, transitively) | Dead writes per block |
|---|---|---|---|
| 8 (the class ISA) | 7.0 of 8 (6 to 7) | 6.9 | 2.9 percent |
| 32 | 30.5 of 32 (29 to 31) | 30.0 | 2.9 percent |
| 64 | 61.7 of 64 (59 to 63) | 61.0 | 2.8 percent |
| 64 at a 1,024-instruction block | 61.5 of 64 (58 to 63) | 60.6 | 3.1 percent |
So under a fold that reads every register, 95 percent of the window is live AND necessary across every memory
wait: the adversary cannot shrink the state it keeps by liveness, and recomputing a value instead of keeping it
costs the dependent chain that produced it (every value feeds the result transitively). The window is a
defence ONLY because of the fold rule; a fold that read 8 of the 64 registers would let the chip drop the rest
(the dead fraction would rise toward the fraction never read before the fold), so the fold-reads-all rule is the
design rule that goes with the window.
**What the adversary can do with the state it must keep** is make it cheaper per access, not smaller. The
GPU-shaped row (4a, 64 registers in flops, every flop clocked every cycle, three 64:1 read muxes) is 9.7 pJ per
lane-op at ASAP7. The forms a chip maker would use:
| Form of the 64-register state (per lane, 256 bytes) | pJ per lane-op ASAP7 | N3 | k at the lock (N3) | Label |
|---|---|---|---|---|
| flops, no clock gating, 64:1 read muxes (the 4a row) | 9.7 | 4.9 | 0.78 | synthesised; a model |
| flops with the register-file clock gated (one of 64 registers written per cycle; the ICG cells allowed back in and inferred by Yosys, 320 gates) | 6.2 (225,441 cells; sequential 0.2) | 3.1 | 0.50 (0.70 node-for-node) | synthesised 16:0x UK; a model |
| the same gating on the 32-register base, for the penalty | 4.5 (156,833 cells; sequential 0.15) | 2.3 | 0.37 (0.51 node-for-node) | synthesised 16:0x UK; a model |
| the gated 32-register base PLACED AND ROUTED (SPEF, clock tree, 379,633 cells; steady state solved from 150 and 600 run cycles) | 6.7 (+49 percent over synthesis) | 3.4 | 0.54 (0.76 node-for-node) | placed 19:5x UK; the GDDR7 board at the lock 2.4x node-for-node, 2.8x a node ahead: the morning's headline figures to the digit |
| the gated 64-register window core PLACED AND ROUTED (563,339 cells; parasitics estimated from global routing, the SPEF lost to a full disk; steady state solved from 150 and 600 run cycles; plus or minus 15 percent) | 9.45 (+52 percent over synthesis; N5 6.6, N2 3.4) | 4.8 | 0.77 (1.07 node-for-node, 0.55 at N2) | placed 21:3x UK; the GDDR7 board at the lock 2.0x node-for-node, 2.45x a node ahead, 2.9x two ahead; the window's residual against the placed base +2.75 pJ per lane-op, +0.23 of k at the lock |
| latch-based register file (the clocked element halved; about 30 percent under the gated flop file, approximate) | about 0.7 x the gated row | | | modelled |
| SRAM-banked state shared across time-multiplexed lanes (one execution port serving many lanes' streams in turn, each lane's 256 bytes in a bank): modelled 8.5 to 10.5 from the access energies; BUILT as `core_tm` (8 lanes x 64 registers in banks, one port, round-robin, ungated): 8.8 pJ per lane-op synthesised (173,426 cells), against the SIMD ungated 9.7 | 8.8 (built) | 4.4 | 0.71 | synthesised 16:2x UK: port sharing saves 0.9 pJ of units and the bank-select muxes take most of it back; NOT the lever; the multi-family adversary lane's macro-window core reads 5.9 (its FakeRAM term modelled 2.0 to 7.0 pJ per access), within 5 percent of the gated flop row, so the file's form is not the lever either |
| values recomputed instead of kept | not available: 95 percent of the window is necessary (above) | | | measured on drawn programs |
The defence, then, is the penalty that remains after the adversary's best form: the gated 64-register file
against the gated 32-register file (the two synthesised rows above when they land; measured: 6.2 against 4.5 pJ per lane-op at ASAP7 synthesised, a penalty of 1.7 pJ; PLACED 9.45 against 6.7, a
penalty of 2.75 pJ, 1.4 at N3, +0.23 of k at the lock, 0.54 to 0.77 at N3 and 0.76 to 1.07 node-for-node; the
analytic estimate had been 1.5 pJ). So on placed rows the window takes the GDDR7 board at the lock from 2.4x to
2.0x node-for-node and from 2.8x to 2.45x a node ahead, for at most 5 percent per load on the card. Two corrections this
forces: the honest adversary's BASE core is the gated one (k 0.37 at N3, 0.51 node-for-node), under the ungated
0.56 and 0.78 of section 4, which are the GPU-shaped core a maker would not build; and placement costs more than
the +20 to +40 percent estimated (the ungated placed base reads 11.3 against 6.9 pJ: wires plus a 2.5 pJ clock tree
that gating removes), so the placed gated rows (on a rented pod, 17:30 UK) are the figures to serve. On the 32-lane core the same
penalty applies per lane (the register file does not amortise), so the window moves the 32-lane core from k 0.45
to about 0.57 at the lock at N3 (0.63 to about 0.80 node-for-node).
**The GPU side** (the hash lane's hand): the compiled allocation of the 64-register measurement pack (ptxas
registers per thread, local-memory spill bytes, occupancy) and the rate beside the 8-register base, clock
18:00 UK; until then the modelled reading stands: about 110 of 255 registers per thread, occupancy about half,
the rate expected to hold under the latency-bound chain (the 5090 hides about 330,000 ops per hash before compute
binds) and the energy to move little, the per-lane register traffic the unmeasured term.
The measured GPU side (the hash lane, 16:1x to 16:4x UK, RunPod secure pods, driver 580, the kit worker, 250 x
2^24, nvidia-smi 1 Hz; ptxas from nvcc 12.8 -Xptxas -v on the pack's kernel; pods destroyed, USD 1.22):
| Card, pack | MH/s | W | microjoules per hash | registers per thread (ptxas) | spill | blocks per SM (occupancy) | per load | Label |
|---|---|---|---|---|---|---|---|---|
| 5090, the base (mx8-devnet-epoch0) | 141.74 | 303.1 | 2.139 | 30 | 0 B | 24 (4,080 warps) | 16.7 nJ | measured |
| 5090, the window, arithmetic-only (hl-reg64, twice the base's work by construction) | 80.38 | 308.6 | 3.839 | 96 | 0 B | 20 (83 percent) | 15.0 nJ | measured: level per unit of work |
| 5090, the window, full chain (hl-reg64c: every load's address mixes all 64 registers) | 70.96 | 320.3 | 4.513 | 88 | 0 B | 20 (83 percent) | 17.6 nJ (+5 percent) | measured |
| 4090, the base | 62.67 | 208.9 | 3.333 | 29 | 0 B | 24 | 26.0 nJ | measured |
| 4090, the window, arithmetic-only | 31.57 | 210.3 | 6.663 | 104 | 0 B | 16 (67 percent) | 26.0 nJ | measured: level |
| 4090, the window, full chain | 31.38 | 216.5 | 6.898 | 87 | 0 B | 20 (83 percent) | 27.0 nJ (+4 percent) | measured |
So the card's side of the window defence is at most 5 percent per load: no spill on either card in either form, 88
to 104 registers per thread, occupancy 67 to 83 percent, and the rate per unit of work held within 5 percent under
the latency-bound chain. The sound class form is the full chain (the arithmetic-only fold fails the liveness rule;
class string `+reg64c`, pack hl-v6-win with `check_window_liveness` in its suite). The chip's side (this section's
gated rows) therefore carries the whole defence.
### 4d. The connected-state variant (cs64s27x16, the connected-state lane's structure) on the adversary's core
The connected-state lane's program (a 64-register window; per step a load whose address register is the previous
block's last dst, the word landing in m_j, then a 27-instruction block whose first instruction reads m_j and every
later one draws its src from the block's last four dsts, the last instruction injecting; 16 steps of text, 448
instructions, 16 passes per block; its liveness tool: 63 of 64 live at every address, about 11 registers in the
per-step dependent chain, about 20 touched per block) priced on the gated 64-register core with a 512-entry imem,
the program drawn by those rules in the testbench (`CS` mode of `tb_core_common.vh`), synthesis-only:
| Row | Cells | pJ per lane-op ASAP7 | N5 | N3 | N2 | k at the lock N5 / N3 / N2 | k at stock N5 / N3 |
|---|---|---|---|---|---|---|---|
| cs64s27x16 on the gated 64-register core, 512 imem | 253,059 | 6.3 | 4.4 | 3.2 | 2.3 | 0.71 / 0.51 / 0.37 | 0.39 / 0.28 |
| the class v4 draw on the gated 64-register core, 256 imem (4c) | 225,441 | 6.2 | 4.3 | 3.1 | 2.2 | 0.70 / 0.50 / 0.36 | 0.38 / 0.27 |
The chip's shadow for the program is 55,296 x 3.2 pJ = 0.18 microjoules per hash at N3 (0.24 node-for-node)
against 0.13 for the genesis window on the same core; the card pays +0.6 percent for the window on the 5090 (the
connected-state lane's measurement). The structure's other knobs do not reach the chip: the 16-pass loop and the
448 text cost the shared imem about 0.1 pJ per lane-op, hot-set banking is not needed (the gated file charges only
the written register), and the chain's width sets lane count, which is free. On the GDDR7 board at the lock the
window moves the chip's edge by about 1.1x (3.6x to 3.3x node-for-node), under the 1.25x gate that lane set.
### 4e. The mixed-resource lane's FP32 units (class-v6-mixedfp) on the adversary's lane
The mixed-resource lane's candidate adds four FP32 families (fadd, fmul, ffma, fcvt) to the shadow's draw, every
result injected by xor, with inputs masked to a 7-bit exponent range (never zero, denormal, NaN or Inf). The
adversary's simplified units (`rtl/fp32_units.v`): an FMA with the 24 x 24 mantissa multiplier, a 100-bit
alignment window, a full normaliser and RNE; a separate adder and multiplier; the int32 to float converter; the
exponent path narrowed to the range; no NaN, Inf, denormal or flag logic. The lane: an 8 x 32-bit window, the
four units, `d ^= bits(result)`. Routed with SPEF, random-input VCD, 42,936 cells, a 2 ns clock.
| Op (every unit evaluating each cycle: an UPPER bound per op, no operand isolation) | pJ per op ASAP7 | N5 | N3 | N2 | 5090 fp32_fma stock / lock | k at N3 vs stock / lock |
|---|---|---|---|---|---|---|
| fadd | 6.6 | 4.6 | 3.3 | 2.4 | 9.2 / 5.2 | 0.36 / 0.64 |
| fmul | 7.0 | 4.9 | 3.5 | 2.5 | 9.2 / 5.2 | 0.38 / 0.68 |
| ffma | 6.7 | 4.7 | 3.4 | 2.4 | 9.2 / 5.2 | 0.37 / 0.65 |
| fcvt | 6.9 | 4.8 | 3.5 | 2.5 | 9.2 / 5.2 (cvt unmeasured on the card) | 0.38 / 0.67 |
| random mix | 7.1 | 4.9 | 3.6 | 2.6 | | 0.39 / 0.68 |
Reading: the four read alike because all four units switch every cycle on the same operands, so each row is the
upper bound for its op (a chip isolates the idle units; by cell share about ffma 3.5 to 4, fmul 2.5, fadd 2, fcvt
1 pJ at ASAP7, approximate). Even on the upper bound the FP family is the chip's dearest per op relative to the
card: k 0.65 at N3 at the lock against 0.18 for the integer ARX lane floor, because the card does an FMA for 5.2
pJ (under its own int add at 6.2) while the chip's multiply, alignment and normaliser cost about three int ops.
On the units' floors the shadow's k_eff rises from 0.097 (class v4) to about 0.14 at the fp12 mix and 0.17 at
fp24 (0.20 and 0.24 with isolation taken as half), the core's per-op overhead on top. The GPU-cost budget (10
percent of energy per hash) is the binding side, and the vendor-rounding question is the class's, not the chip's.
## 5. The chip edge at the measured k
`E_chip = E_mem + N_ops x e_chip` (absolute: the chip's shadow cost is 102,100 x 3.5 pJ = 0.36 microjoules per hash
@ -270,7 +392,7 @@ to 2.3x and the strongest chips at 2.6x to 4.2x.
| 6 | prmt, lop3 | 0.64, 0.72 | 11.5, 13.0 | 0.056, 0.055 | not drawn (RTL rows only) |
| 7 | mulhi | 0.68 | 21.0 | 0.032 | yes (6) |
| 8 | 32-lane shuffle (butterfly) | 0.63 | 29.4 | 0.021 | yes (8) |
| 9 | L1 scratch read (8 KB flop array) against the card's L2 hit | ROW_SCRATCH_K | 1,400 | pending | not drawn |
| 9 | L1 scratch read (8 KB flop array) against the card's L2 hit | 104 (pJ per read) | 1,400 | 0.074 | not drawn |
| 10 | int8 8x8x8 tile, per MAC | ROW_TILE_K | 2.2 | pending | not drawn (the tensor lever is dead on other grounds) |
The order is set by the card's price, not the chip's: the chip pays 0.6 to 1.7 pJ for everything, and the card

View file

@ -6,7 +6,7 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
| Field | Value | Read from | Owner |
|---|---|---|---|
| Miner cut tip (release-2.0.1) | aa0e0f45 (9c844503 + the pin file; 9c844503 = 2826f37e + the six-target packaged peer list; the app crate byte-identical to 2ea7b43f's throughout, so the pow and app cells read on 9c844503 cover it by content) | the shipper's line 19:5x | shipper (ae892a8b0f78fe31c) |
| Miner cut tip (release-2.0.1) | aa0e0f45 is the shipped tip (the entries stand on its binaries); release-2.0.1's final tip is c30ab32c (aa0e0f45 + the pool lane's pool/ and docs a54dffa3 + the release manifest f03-manifest-201 7437a31a merged at 800faaf7 + the hand-merged spec 09; no app, node pin or packaging change). The clean build from the manifest (R2-F03-R01) read green on c30ab32c at 21:14 UK on build-4: kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host, and every component's own pin equals packaging/release-manifest.json. (9c844503 + the pin file; 9c844503 = 2826f37e + the six-target packaged peer list; the app crate byte-identical to 2ea7b43f's throughout, so the pow and app cells read on 9c844503 cover it by content) | the shipper's lines 19:5x and 21:0x; the steward's build 21:14 | shipper (ae892a8b0f78fe31c) |
| Node sha for the roll | 7cfa422a = ef0f2ed8 (291ee6ae + the 2.0.1 version) + the miner base-unit fix, amended (777214af did not compile: a self-recursive connect, caught by the steward's read at 19:50) | the node lane's and shipper's lines 19:5x | node lane (a283f5f0d364ceef0) |
| Node line read green tonight | 4cdcc488, d5981514, bee41b5e, 9fc9f42a, 5713d547, 417c4a57, 291ee6ae (ef0f2ed8 = 291ee6ae + the version bump, its own read on build-4 recorded as the literal) | the steward's matrices on build-2 and build-4 | CI steward |
| Network | igneum-devnet-4, a fresh genesis; EVM chain id 4465 (0x1171), set in devnet4_params, read by the canaries as eth_chainId on every candidate tonight, pinned by the digest be5f4068; every 2.0 devnet node, pool and reference app signs with it; mainnet's and the testnet's ids unchanged from the 0.3 line | the node lane's line 19:3x | node lane |

View file

@ -0,0 +1,48 @@
# The cross-backend same-work test (F03, Review B; specified by the CI steward, 8 October 2026, 20:1x UK)
One job context, six readers, three phases around a transition, bit-for-bit agreement. Run by the fleet lane with the freeze object; the evidence recorded against POW-01 through the batch tools.
## The job context (one file, `job-context.json`, written once by the steward or the hash lane and copied to every reader)
| Field | Value tonight | Source |
|---|---|---|
| object | the class v5 freeze: igneum-pow 1c420786, fingerprint cbc5bd0aa10585c8576e71e37a8ee47a045ae51754e9ddf749d0c21e6a535f88 | packaging/release-manifest.json (generator) |
| epoch seed bytes | edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 (the genesis seeds) | the hash lane's P01 line |
| day bytes | 69676e65756d2d6461792ffa50000000000000 | the hash lane's P01 line |
| class | 5; era 0:<the epoch hex> | the pack's program.json |
| prehash | 0000000000000000000000000000000000000000000000000000000000000001 | the P01 convention |
| nonce range | 0 .. 2^20 per phase (three phases, three ranges: [0, 2^20), [2^20, 2^21), [2^21, 2^22)) | this page |
| transition | the day boundary: day D for phase 1, the boundary block for phase 2 (the last 2^12 nonces of day D and the first 2^12 of day D+1 as the engine sees them under fast-time 60x), day D+1 for phase 3 | infra/fast-time/override-60x.json |
The transition is a dataset-day rotation (the class and era unchanged, the day bytes change), the one transition every live network crosses hourly at 60x; a class rotation (v5 to v6) is the same test with `class` and the second `day bytes` changed and runs only on a research object until a v6 floor is set.
## The six readers (every one writes `<reader>-<phase>.json` of the P01 driver's evidence shape: nonce, hash per line in the raw file; agree, disagree, missing, the first ten disagreements, the device line, the pack and program ids, the manifest sha in the JSON)
1. **node**: `igneumd` at the manifest's node sha, the engine's own re-check (`IgneumEngine::epoch_for` then `hash_bound` per nonce) through `igneum-miner --recheck-vectors` on the node binary's CPU path (the pool.rs seam), on build-2.
2. **CPU reference**: `igneum-pow hash-bound --count 2^20 --nonce <start>` from the manifest's generator commit, on build-2 (the hash lane's reference files are this reader).
3. **CUDA**: the kit's `igneum-worker-cuda --serve` driven by `tools/ci/p01-vectors.py` with the job context, on a 5090, 4090 and 3090 pod.
4. **OpenCL**: `igneum-worker-opencl --serve` the same way, on the AMD pod when one exists, else PC 1's RX 7600 (the only OpenCL retail cell tonight).
5. **Metal**: the Mac's own worker, the same driver, on the mini (the morning's run; never on this Mac).
6. **pool**: `igneum-pool` at the manifest's sha with its vendored node at the manifest's node sha, the member-side re-check (`pool/src/node.rs` → `kaspa_pow::igneum::IgneumEngine`) on the same job lines, on build-2.
## The three phases
Phase 1 (before): every reader on range 1 under day D. Phase 2 (during): every reader on range 2 where the engine's day moves from D to D+1 inside the range at the boundary nonce the fast-time clock sets; every reader must switch program and dataset at the same nonce. Phase 3 (after): every reader on range 3 under day D+1.
## Acceptance (the registry row POW-01, cell `harness:same-work`, PASS only when all hold)
- every reader's hash equals the CPU reference's for every nonce of every phase (zero disagreements, zero missing);
- the program id and the day each reader reports at phase 2's boundary are the same across readers (the transition is seen at one nonce);
- the pool's accepted-share verdict for a sample of 64 nonces per phase equals the node's (the seam closes by a build: `release-manifest-check.sh` refuses a redefined EpochSeeds and an unpinned vendored node);
- the run names the manifest sha (packaging/release-manifest.json), and the evidence sits at build-1:/srv/artefacts/tas/same-work-<run id>/.
A disagreement on any reader is a red to main within fifteen minutes (the coordinator's rule for the cross-checks).
## What is still to build (owners, defaults)
| Piece | Owner | Clock | Default if silent |
|---|---|---|---|
| `p01-vectors.py --job-context <file> --phase N` (the three ranges and the boundary assertion; the driver already drives the workers) | CI steward | 21:30 | built as specified |
| the node reader (`igneum-miner --recheck-vectors`, the seam with a count) | node lane | 22:00 | the steward builds it on a branch of release-2.0.0-node under rule 24 |
| the pool reader (the member-side re-check over a job-lines file) | pool lane | 22:30 | the pool's existing recheck_pack path over the context, driven by the steward |
| the pods and the mini | fleet lane | on the artefact line | as P01 |

View file

@ -133,8 +133,8 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
### bench:pc1-packs
- Command: `tools/ca3-v4-amend/pc1-ca4-packs.ps1 on PC 1 (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)`
- Box class: PC 1 bench
- Command: `tools/ca3-v4-amend/pc1-ca4-packs.ps1 on the project's own rig (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)`
- Box class: the project's own rig bench
- Fixtures: F0, F1
- Cases:
- GPU-02 Reproduce Ember clock-lock savings: partial: the paired stock and locked rows on the same board, host and workload (the rate held, 2.37 against 3.26 microjoules per hash on the class v5 pack); the historical 34 to 41 percent claim's full configuration set is owed
@ -144,8 +144,8 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
### bench:pc1-amd
- Command: `tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on PC 1 (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)`
- Box class: PC 1 bench
- Command: `tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on the project's own rig (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)`
- Box class: the project's own rig bench
- Fixtures: F0, F1
- Cases:
- GPU-01 Cover the declared commodity population: partial: the 8 GB AMD cell, fingerprints and rates at 1, 2, 4 and 5.5 GiB; one cell of P02's twelve
@ -243,7 +243,7 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- Box class: harness (network run on the 2.0 devnet plus Sepolia reads)
- Fixtures: none
- Cases:
- VER-03 Prove successful payment rather than inclusion: partial: a payment receipt is labelled payment only when the receipt is proven against the segment statement's receipts commitment under the finality certificate, inclusion-only receipts read inclusion, a tampered receipt and a flipped status fail; run on the Devnet 3 fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction
- VER-03 Prove successful payment rather than inclusion: partial: a payment receipt is labelled payment only when the receipt is proven against the segment statement's receipts commitment under the finality certificate, inclusion-only receipts read inclusion, a tampered receipt and a flipped status fail; run on the the earlier devnet fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction
- VER-04 Bound cross-chain oracle trust and replay: partial: the oracle's trust() names the deployer-installed table and the unchecked aggregator signature, a second hash at a stored certificate index is refused, a root claim at an unknown index is refused, another chain id is refused (tools/reference-apps/oracle/test.mjs on Sepolia); Review B F04: the Sepolia verifiers apply the two-thirds rule only, carry no lock-kind field, and an under-threshold (recovery-rule) certificate fails closed in submitCertificate, so no stored root is a recovery lock (the DEX lane's docs/bridge/light-client-bridge.md paragraph); the claimed-option review of the trust model is the reviewer's
- VER-05 Reconstruct required state without founder storage: partial: the public node and the reference reader on build-1 executed the 2.0 devnet from genesis with no snapshot (unit ExecStart without --igneum-exec-snapshot), and the pages fetch headers, certificates, coinbase bodies and account proofs from the public read RPC only; the archive and availability model beyond the 2,048-block state ring is the OPS no-founder exercise
- VER-06 Detect withholding, corruption and stale data: partial: the read service never serves a certificate from another network, a withheld header in the path and a corrupted proof node are detected, stale data prints its lock age beside every balance, and a client told no certificate never falls back to a trusted RPC balance; detection on the fault network F4 is not run
@ -336,8 +336,8 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
### bench:amd-intel-energy
- Command: `the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (PC 1, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/`
- Box class: PC 1 and PC 2 bench (OpenCL)
- Command: `the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (the project's own rig, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/`
- Box class: the project's own rig and PC 2 bench (OpenCL)
- Fixtures: F0, F1
- Cases:
- GPU-03 Measure the real 64-register GPU cost: partial: the 64-register window on AMD and Intel, rate per unit of work (RX 7600 0 percent, Arc B580 -0.3 percent, kernel throughput, quiet) with the B580 fingerprints equal on both packs and the offline RDNA allocation (160 VGPRs, no spill); energy owed (the 7600 job queued, the B580 counter unsupported unelevated); team-run, not under the standard's paired protocol or a wall meter
@ -367,7 +367,7 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
### pc:install-update
- Command: `signed jobs and relay runs on PC 1 (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)`
- Command: `signed jobs and relay runs on the project's own rig (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)`
- Box class: PC (the two Windows PCs; nothing on the Mac)
- Fixtures: F2
- Cases:
@ -375,16 +375,48 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- UX-07 Expose actionable failures and safe updates: partial: the install classes (the payload's stop step, the detached installer under a job, the installer refusing a wrong-version name, a stale install flag) and the no-click resume; the recovery-instruction and canary rows are the shipper's
- OPS-03 Separate update distribution from consensus authority: partial: the update path's read-back and the signed manifest's verify on the PC (a tampered or rolled-back payload is refused by the engine's manifest check); authority separation is the shipper's review
### harness:release-manifest
- Command: `bash tools/ci/release-manifest-check.sh (self-test, then the tree; the pins, the proof guests' hashes on disk, the fork freeze, the pool's vendored node, the proving manifest's source_commit provenance) and bash tools/ci/build-from-manifest.sh --box N on the release tree (kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host from packaging/release-manifest.json with the node vendored at the manifest's sha)`
- Box class: gate
- Fixtures: F0
- Cases:
- R2-F03-R01 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: full for the components the tree builds (node, miner, pool, app, prove-host from one manifest, no unpublished vendor tree); the GPU workers are the kit's cross-build and read under the shipper's kit-isa line, not this cell
### harness:same-work
- Command: `tools/ci/p01-vectors.py --job-context <job-context.json> --phase 1|2|3 per reader (CUDA, OpenCL, Metal workers), igneum-miner --recheck-vectors (node), igneum-pow hash-bound (CPU reference), the pool's member-side re-check; docs/plans/igneum-2.0-same-work-test.md`
- Box class: release (the readers on their pods and boxes)
- Fixtures: F0
- Cases:
- R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: partial until every reader has run: bit-for-bit agreement per nonce across node, CPU reference, CUDA, OpenCL, Metal and pool on one job context
- R2-F03-R03 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: partial: the day-boundary transition in phase 2 (the one transition every live network crosses hourly at 60x); a class rotation is the same test on a research object until a v6 floor is set
### canary:fresh-install
- Command: `the rule 33 fresh-install canary on a release tip (the founder, 8 October 2026): install from the published artefact on a non-AVX-512 box with an empty datadir, sync genesis to tip, five minutes mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read back; the record tools/ci/canary/<sha>.json (one file per sha, an artefacts list: fleet, windows, mac, hive) read by tools/ci/canary-check.sh <sha> [--artefact kind], which publish-manifest.sh and publish-public.sh refuse without`
- Box class: release (a non-AVX-512 box with an empty datadir)
- Fixtures: F0
- Cases:
- INT-07 One v6 object agrees in node, pool, CPU verifier and each supported GPU host across activation.: partial: V6-12's clean-install half (one published object installed fresh, synced, mined, proved and paid on one host per artefact); the cross-host agreement half (node, pool, CPU verifier, each GPU host across activation) is harness:same-work's
### review:k-lane-shadow-k
- Command: `floor lane 2's placed and routed cores on ASAP7 in tools/chip-model/rtl (the rows in docs/analysis/class-v6/floor/shadow-k.md); the register landing 50ff1611f recorded ADV-06 against it before the recorder existed`
- Box class: none
- Fixtures: none
- Cases:
- ADV-06 Separate process advantage from specialisation: partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains
## Automated cases with no harness in the matrix (NOT RUN, the reason)
- GOV-02 Approve thresholds before results: the approval is recorded in the registry's approval field; the automated half (thresholds frozen before any run_status) is the gate rule landing by 21:00
- GOV-04 Preserve raw and negative evidence: the evidence vault F9 (raw and negative evidence preserved) is the gate rule landing by 21:00: a PASS must carry its evidence file
- GOV-08 Invalidate stale evidence and control public status: the stale-evidence rule (evidence older than the manifest sha reads NOT RUN) is the gate rule landing by 21:00
- GPU-04 Find the memory-clock operating ladder: the memory-clock ladder has no harness tonight: PC 1 mines nothing under the Devnet 3 off order
- GPU-04 Find the memory-clock operating ladder: the memory-clock ladder has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order
- GPU-06 Measure accepted work under ordinary connectivity: accepted work under ordinary connectivity needs the fault network F4
- GPU-07 Survive sustained thermal and power operation: the sustained thermal and power soak has no harness tonight: PC 1 mines nothing under the Devnet 3 off order
- GPU-07 Survive sustained thermal and power operation: the sustained thermal and power soak has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order
- POW-05 Prevent amortised cheap winning attempts: amortised cheap winning attempts are the attack lanes' grind and era harnesses (tools/attack/f7-era, f9-grind), not in the release matrix; their rows come from those lanes
- ADV-06 Separate process advantage from specialisation: process-advantage separation is the adversary lanes' chip study
- ROT-03 Test miner-voted bring-forward governance: miner-voted bring-forward needs a vote harness on the fault network F4
- ROT-04 Resist seed selection and faster evaluators: seed-selection resistance is the census harness (the class v6 invention lane), not yet in the matrix
- EVM-01 Match the selected EVM semantics: no EVM conformance-vector harness is mapped tonight; the exec suite does not run the reference test vectors
@ -471,7 +503,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- INT-04 Corrupt existing pool state, disk-full and reorg cause safe recovery, not silent empty balances.: INT-04 (R1 integration gate): the gate's harness is the owner lane's (pool lane (a1c484c48a62948c2)); not yet named in the map
- INT-05 The supplied finality implementation matches the approved anchor rule after >window healing.: INT-05 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map
- INT-06 Recovery tests state and preserve their weaker fault bound; interfaces never label it as a stronger guarantee.: INT-06 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map
- INT-07 One v6 object agrees in node, pool, CPU verifier and each supported GPU host across activation.: INT-07 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
- INT-08 Census, production acceptance, schedule counters and live-dataset tests use the identical frozen contract.: INT-08 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
- INT-09 Eight- and twelve-GiB epoch transitions recover deliberately without relying on repeated OOM/watchdog cycles.: INT-09 (R1 integration gate): the gate's harness is the owner lane's (app lane); not yet named in the map
- INT-10 Metal/CUDA/OpenCL exact dataset geometry agrees; unsupported packs reject before launch.: INT-10 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
@ -490,9 +521,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- R2-F02-R01 Release build cannot activate test bypass.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- R2-F02-R02 Missing oracle or pinned keys prevents service readiness after enforcement activation.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- R2-F02-R03 Missing proof bytes retry without incorrectly marking a valid block permanently invalid.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- R2-F03-R01 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- R2-F03-R03 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- R2-F04-R01 Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window.: R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- R2-F04-R02 Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality.: R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- R2-F04-R03 Pause-only resume with historical backfill, missing historical data and all old keys returning.: R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
@ -530,4 +558,4 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
## Count
171 automated cases: 78 mapped to a cell, 150 NOT RUN with a reason.
171 automated cases: 83 mapped to a cell, 145 NOT RUN with a reason.

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
PRODUCT="app"
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
RELEASE_SHA="" CANARY_GUARD_SELF_TEST=0 VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
NODE_BIN="" NET_DIGEST="" MOVE_CLOCK="" DIGEST_GUARD_SELF_TEST=0
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0
while [ $# -gt 0 ]; do
@ -83,6 +83,8 @@ while [ $# -gt 0 ]; do
--network-digest) NET_DIGEST="$2"; shift 2 ;; # the network's CURRENT digest (a 64-hex, or "log:<file or journal:unit>[@user@host]" read by tools/digest-read.sh on the hub)
--move-clock) MOVE_CLOCK="$2"; shift 2 ;; # "HH:MM UTC, <reason>": the move's clock, when the entry's digest differs by design; logged in the notes
--self-test-digest-guard) DIGEST_GUARD_SELF_TEST=1; shift ;;
--release-sha) RELEASE_SHA="$2"; shift 2 ;; # rule 33 (8 Oct 2026): the release tip's commit; its fresh-install canary record must read PASS (tools/ci/canary-check.sh)
--self-test-canary-guard) CANARY_GUARD_SELF_TEST=1; shift ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
@ -109,6 +111,47 @@ if [ "$DIGEST_GUARD_SELF_TEST" = 1 ]; then
echo "digest guard self-test: a differing digest is refused without a move clock and accepted with one; an equal digest passes; the digest reader parses"
exit 0
fi
# Rule 33 (the founder, 8 October 2026, 21:3x UK, "no more lost time"): a release entry may not publish without a fresh-install
# canary record for its sha (install from the artefact on a non-AVX-512 box with an empty datadir, genesis to tip, five minutes
# mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read
# back): tools/ci/canary/<sha>.json read by tools/ci/canary-check.sh. A write to a real downloads folder needs --release-sha and a
# PASS record; a loopback --base-url test write and --verify-only do not publish and are not gated.
canary_guard() { # <release sha> [kind ...] -> 0 pass (the lines printed), 1 refused; a kind (mac, windows) asks for that entry's own artefact block
local sha="$1"; shift; local k
[ -n "$sha" ] || { echo "canary guard: REFUSED: a release entry needs --release-sha <release tip commit> with a fresh-install canary record (rule 33; tools/ci/canary-check.sh --form)"; return 1; }
if [ $# -eq 0 ]; then bash "$TOOLS/ci/canary-check.sh" "$sha" || { echo "canary guard: REFUSED: the entry's sha ${sha:0:12} has no PASS fresh-install canary record (rule 33)"; return 1; }; return 0; fi
for k in "$@"; do bash "$TOOLS/ci/canary-check.sh" "$sha" --artefact "$k" || { echo "canary guard: REFUSED: the $k entry's sha ${sha:0:12} has no PASS fresh-install canary record for its own artefact (rule 33)"; return 1; }; done
}
if [ "$CANARY_GUARD_SELF_TEST" = 1 ]; then
bash "$TOOLS/ci/canary-check.sh" --self-test >/dev/null || exit 1
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; S=0123456789abcdef0123456789abcdef01234567
canary_guard "" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: an entry with no --release-sha was accepted"; exit 1; }
CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a sha with no record was accepted"; exit 1; }
bash "$TOOLS/ci/canary-check.sh" --form | python3 -c "
import json,sys; r=json.load(sys.stdin); r['sha']='$S'; r['artefact']['sha256']='ab'*32; r['box']={'host':'build-4','isa_line':'kit-isa: clean'}
r['sync'].update(tip_height=10, seconds=1); r['quit']['seconds']=1; r['recorded_at']='t'; r['recorded_by']='self-test'; json.dump(r, open('$d/$S.json','w'))"
CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 || { echo "canary guard self-test: FAIL: a sha with a PASS record was refused"; exit 1; }
python3 -c "import json; p='$d/$S.json'; r=json.load(open(p)); r['mining']['refusals']=1; json.dump(r, open(p,'w'))"
CANARY_GUARD_FAILED=0; CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a record with a refusal was accepted"; exit 1; }
python3 -c "
import json,copy; p='$d/$S.json'; r=json.load(open(p)); blk={k:r[k] for k in ('artefact','box','datadir','sync','mining','shard','quit','lines_read_back')}; blk['mining']['refusals']=0; arts=[]
for kind in ('fleet','windows','mac'):
b=copy.deepcopy(blk); b['kind']=kind; arts.append(b)
arts[1]['mining']['refusals']=3
json.dump({'sha':r['sha'],'artefacts':arts,'verdict':'PASS','recorded_at':'t','recorded_by':'self-test'}, open(p,'w'))"
CANARY_DIR="$d" canary_guard "$S" mac >/dev/null 2>&1 || { echo "canary guard self-test: FAIL: the mac entry was refused though its own block passes"; exit 1; }
CANARY_DIR="$d" canary_guard "$S" windows >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: the windows entry passed on a failing windows block"; exit 1; }
CANARY_DIR="$d" canary_guard "$S" mac windows >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a mac plus windows publish passed with the windows block failing"; exit 1; }
echo "canary guard self-test: no sha, no record and a failing record are refused; a PASS record passes; an entry publishes on its own artefact's block (mac passes, windows refused on its own failing block); the record check's own self-test passes"
exit 0
fi
case "${BASE:-}" in http://127.0.0.1*|http://localhost*|http://\[::1\]*) CANARY_GATED=0 ;; *) CANARY_GATED=1 ;; esac
if [ "$VERIFY_ONLY" != 1 ] && [ "$CANARY_GATED" = 1 ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then
KINDS=""; [ -n "$MAC" ] && KINDS="$KINDS mac"; [ -n "$WIN" ] && KINDS="$KINDS windows"
# shellcheck disable=SC2086
canary_guard "$RELEASE_SHA" $KINDS || exit 1
NOTES="${NOTES:+$NOTES; }release: $RELEASE_SHA (fresh-install canary PASS)"
fi
if [ -n "$NODE_BIN" ] || [ -n "$NET_DIGEST" ]; then
[ -n "$NODE_BIN" ] && [ -n "$NET_DIGEST" ] || { echo "the digest guard needs both --node-bin and --network-digest" >&2; exit 2; }
ENTRY_DIGEST=$(bash "$TOOLS/digest-read.sh" binary "$NODE_BIN") || exit 1

View file

@ -32,10 +32,11 @@ KEY="$CFG/ota-signing-key"; PUB_KEY="$CFG/ota-signing-key.pub"
SIGNER="${IGNEUM_OTA_SIGN:-$ROOT/app/igneum-app/target/release/igneum-ota-sign}" # a built signer elsewhere (another worktree)
HOST="https://dl.igneum.network"
DO_APP=0 DO_WALLET=0 HIVE="" DO_ALIASES=0 DRY=0 DEPLOY=0 VERIFY=0 PRUNE=1 DEST="" BASE="" TRIES=12
RELEASE_SHA="" DO_APP=0 DO_WALLET=0 HIVE="" DO_ALIASES=0 DRY=0 DEPLOY=0 VERIFY=0 PRUNE=1 DEST="" BASE="" TRIES=12
while [ $# -gt 0 ]; do
case "$1" in
--app) DO_APP=1; shift ;;
--release-sha) RELEASE_SHA="$2"; shift 2 ;; # rule 33: the release tip's commit (read from the app manifest's notes when not given); its canary record must read PASS
--wallet) DO_WALLET=1; shift ;;
--hive) HIVE="$2"; shift 2 ;;
--aliases) DO_ALIASES=1; shift ;;
@ -107,6 +108,26 @@ PY
log " $name: $(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print(m.get("version"), "+".join(m.get("platforms",{})))' "$out") written, signed, verified; URLs under $BASE_PUB"
}
# Rule 33 (8 October 2026): nothing goes to dl/public/ for a release whose sha has no PASS fresh-install canary record. The app
# manifest carries "release: <sha>" in its notes (publish-manifest.sh --release-sha writes it); --release-sha overrides or supplies it
# for the HiveOS package. tools/ci/canary-check.sh reads tools/ci/canary/<sha>.json.
canary_gate() { # <sha> <what> [kind ...]: each kind (mac, windows, hive) must have its own PASS artefact block; no kind = the whole record
local sha="$1" what="$2"; shift 2; local k; local chk; chk="$(cd "$(dirname "$0")/../.." && pwd)/tools/ci/canary-check.sh"
[ -n "$sha" ] || { echo "canary guard: REFUSED: $what names no release sha (publish the token entry with publish-manifest.sh --release-sha, or pass --release-sha here); rule 33" >&2; return 1; }
if [ $# -eq 0 ]; then bash "$chk" "$sha" | scrub || { echo "canary guard: REFUSED: ${sha:0:12} has no PASS fresh-install canary record; rule 33" >&2; return 1; }; return 0; fi
for k in "$@"; do bash "$chk" "$sha" --artefact "$k" | scrub || { echo "canary guard: REFUSED: $what: the $k entry's sha ${sha:0:12} has no PASS fresh-install canary record for its own artefact; rule 33" >&2; return 1; }; done
}
if [ "$DO_APP" = 1 ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then
sha="$RELEASE_SHA"
[ -n "$sha" ] || sha=$(python3 -c 'import json,re,sys; m=json.load(open(sys.argv[1])); x=re.search(r"release: ([0-9a-f]{8,40})", str(m.get("notes",""))); print(x.group(1) if x else "")' "$SRC/igneum-app-latest.json" 2>/dev/null || true)
kinds=$(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); ks=[]
for p in m.get("platforms",{}):
ks.append("mac" if "mac" in p.lower() or "darwin" in p.lower() else "windows" if "win" in p.lower() else p)
print(" ".join(ks))' "$SRC/igneum-app-latest.json" 2>/dev/null || true)
# shellcheck disable=SC2086
canary_gate "$sha" "the app manifest" $kinds || exit 1
fi
if [ -n "$HIVE" ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then canary_gate "$RELEASE_SHA" "the HiveOS package" hive || exit 1; fi
if [ "$DO_APP" = 1 ]; then log "app manifest -> dl/public/"; publish_manifest igneum-app-latest.json; fi
if [ "$DO_WALLET" = 1 ]; then log "wallet manifest -> dl/public/"; publish_manifest igneum-wallet-latest.json; fi
if [ -n "$HIVE" ]; then

View file

@ -23,8 +23,14 @@ CPUSET = $(if $(LEASE_ON),--cpuset-cpus {cpuset},)
DOCKER := $(LEASEPFX) docker run --rm -u $(UID_GID) -e HOME=/tmp -e NUM_CORES=$(THREADS) $(CPUSET) -v $(WORK):/work
ORFS := $(DOCKER) -w /OpenROAD-flow-scripts/flow $(ORFS_IMG)
SIM := $(DOCKER) -w /work $(SIM_IMG)
# NO_DOCKER=1: the box IS the ORFS image (a rented pod started from openroad/orfs:latest with iverilog installed
# and /work a symlink to this directory); the same targets run natively.
ifeq ($(NO_DOCKER),1)
ORFS := env NUM_CORES=$(THREADS) bash -c 'cd /OpenROAD-flow-scripts/flow && exec "$$@"' --
SIM := env
endif
DESIGNS := arx mul prmt lop3 fold shfl xbar scratch tile core8 core32 core32r16 core8r64 core8i1k core8sel core32all
DESIGNS := arx mul prmt lop3 fold shfl xbar scratch tile core8 core32 core32r16 core8r64 core8i1k core8sel core32all core8g core8r64g coretm cs64 fp32
top = $(shell sed -n 's/^$(1) \([^ ]*\) .*/\1/p' flow/designs.txt)
# per-family simulation tags (the op field fixed per row where the family has several ops)
@ -37,13 +43,18 @@ SIMS_shfl := mix
SIMS_xbar := mix
SIMS_scratch := mix
SIMS_tile := mix
SIMS_core8 := mix mixld:+loads=1
SIMS_core32 := mix mixld:+loads=1
SIMS_core32r16 := mix mixld:+loads=1
SIMS_core8r64 := mix
SIMS_core8 := s150:+cycles=150 s600:+cycles=600
SIMS_core32 := s150:+cycles=150 s600:+cycles=600
SIMS_core32r16 := s150:+cycles=150 s600:+cycles=600
SIMS_core8r64 := s150:+cycles=150 s600:+cycles=600
SIMS_core8i1k := mix
SIMS_core8sel := mix
SIMS_core32all := mix
SIMS_core8g := s150:+cycles=150 s600:+cycles=600
SIMS_core8r64g := s150:+cycles=150 s600:+cycles=600
SIMS_coretm := mix
SIMS_cs64 := s150:+cycles=150 s600:+cycles=600
SIMS_fp32 := mix fadd:+op=0 fmul:+op=1 ffma:+op=2 fcvt:+op=3
.PHONY: rows table clean
@ -65,6 +76,7 @@ sim-%: flow-%
power-%: sim-%
$(ORFS) make DESIGN_CONFIG=/work/flow/$*.mk RUN_SCRIPT=/work/flow/power.tcl RUN_LOG_NAME_STEM=power run 2>&1 | tee logs/power-$*.log
rm -f sim/$*/*.vcd # the traces run to gigabytes each; the power log keeps every number (a full disk killed four runs on 8 October 2026)
# synthesis-only row (no placement, no parasitics): the 14:45 fallback
synth-%:

View file

@ -0,0 +1,12 @@
// Behavioural model of the ASAP7 integrated clock gate (latch_posedge_precontrol: the enable is latched while the
// clock is low, the gated clock is CLK AND the latched enable OR test). The liberty carries no function for it.
module ICGx1_ASAP7_75t_R(input CLK, input ENA, input SE, output GCLK);
reg en = 0;
always @(CLK or ENA or SE) if (!CLK) en = ENA | SE;
assign GCLK = CLK & en;
endmodule
module ICGx2_ASAP7_75t_R(input CLK, input ENA, input SE, output GCLK);
reg en = 0;
always @(CLK or ENA or SE) if (!CLK) en = ENA | SE;
assign GCLK = CLK & en;
endmodule

View file

@ -6,7 +6,7 @@ import re, sys, os, csv
work = sys.argv[1] if len(sys.argv) > 1 else '.'
# ops per cycle per design (the per-op divisor) and the GPU row each family is read against
OPS = {'arx': 1, 'mul': 1, 'prmt': 1, 'lop3': 1, 'fold': 1, 'shfl': 32, 'xbar': 32, 'scratch': 1, 'tile': 512, 'core8': 8, 'core32': 32, 'core32r16': 32, 'core8r64': 8, 'core8i1k': 8, 'core8sel': 8, 'core32all': 32}
OPS = {'arx': 1, 'mul': 1, 'prmt': 1, 'lop3': 1, 'fold': 1, 'shfl': 32, 'xbar': 32, 'scratch': 1, 'tile': 512, 'core8': 8, 'core32': 32, 'core32r16': 32, 'core8r64': 8, 'core8i1k': 8, 'core8sel': 8, 'core32all': 32, 'core8g': 8, 'core8r64g': 8, 'coretm': 1, 'cs64': 8, 'fp32': 1}
# 5090 measured pJ per counted op: (unlocked, at the 1,300 MHz lock); 15.1a
GPU = {
'arx:mix': (11.3, 6.2), 'arx:add': (11.3, 6.2), 'arx:sub': (11.3, 6.2), 'arx:xor': (11.3, 6.2), 'arx:or': (11.3, 6.2),
@ -17,7 +17,7 @@ GPU = {
'shfl:mix': (55.8, 29.4), 'xbar:mix': (55.8, 29.4),
'scratch:mix': (2400.0, 1400.0), # the card's L2 hit (no shared-memory probe measured: owed)
'tile:mix': (4.1, 2.2),
'core8:mix': (11.3, 6.2), 'core8:mixld': (11.3, 6.2), 'core32:mix': (11.3, 6.2), 'core32:mixld': (11.3, 6.2), 'core32r16:mix': (11.3, 6.2), 'core8r64:mix': (11.3, 6.2), 'core8i1k:mix': (11.3, 6.2), 'core8sel:mix': (11.3, 6.2), 'core32all:mix': (11.3, 6.2), # the class v4 draw: read against int_arx (the packs job read the whole mix at 10.8 / 6.4) # dependent u8 m8n8k16 per MAC; the wide s8 tile reads 1.36 / 0.83
'core8:mix': (11.3, 6.2), 'core8:mixld': (11.3, 6.2), 'core32:mix': (11.3, 6.2), 'core32:mixld': (11.3, 6.2), 'core32r16:mix': (11.3, 6.2), 'core8r64:mix': (11.3, 6.2), 'core8i1k:mix': (11.3, 6.2), 'core8sel:mix': (11.3, 6.2), 'core32all:mix': (11.3, 6.2), 'core8g:mix': (11.3, 6.2), 'core8r64g:mix': (11.3, 6.2), 'coretm:mix': (11.3, 6.2), 'cs64:mix': (11.3, 6.2), 'fp32:mix': (9.2, 5.2), 'fp32:fadd': (9.2, 5.2), 'fp32:fmul': (9.2, 5.2), 'fp32:ffma': (9.2, 5.2), 'fp32:fcvt': (9.2, 5.2), # the class v4 draw: read against int_arx (the packs job read the whole mix at 10.8 / 6.4) # dependent u8 m8n8k16 per MAC; the wide s8 tile reads 1.36 / 0.83
}
# per-node energy scaling from ASAP7 (a 7 nm-class predictive PDK at 0.70 V), approximate and claimed:
# N7 -> N5 x0.70 (TSMC: "30 percent lower power at the same speed"), N5 -> N3E x0.72 (TSMC: 25 to 30 percent),
@ -67,10 +67,10 @@ for d in OPS:
pairs = [('vcd:s150', 'vcd:s600', 150, 600), ('vcd:short', 'vcd:synth', 150, 800), ('vcd:s150', 'vcd:s400', 150, 400)]
for sh, lg, cs, cl in pairs:
if sh in rows and lg in rows:
rows['vcd:steady'] = steady(rows, period, sh, lg, cs, cl, 1028 if d in ('core8i1k', 'core32all') else LOAD_CYCLES)
rows['vcd:steady'] = steady(rows, period, sh, lg, cs, cl, 1028 if d in ('core8i1k', 'core32all') else (452 if d == 'cs64' else LOAD_CYCLES))
for tag, r in rows.items():
sub = tag.split(':')[1] if ':' in tag else 'prop'
key = f'{d}:{sub}' if sub in ('add','sub','xor','or','rotl','rotr','mul','mulhi','mad','mixld') else f'{d}:mix'
key = f'{d}:{sub}' if sub in ('add','sub','xor','or','rotl','rotr','mul','mulhi','mad','mixld','fadd','fmul','ffma','fcvt') else f'{d}:mix'
gpu = GPU.get(key, (None, None))
pj = r['total'] * period * 1e-12 / OPS[d] * 1e12 # W * s / ops -> pJ
pj_dyn = (r['internal'] + r['switching']) * period / OPS[d]

View file

@ -0,0 +1,18 @@
# ORFS design config for the programmable shadow core (core8: core_v6_8), ASAP7.
export PLATFORM = asap7
export DESIGN_NAME = core_v6_8
export DESIGN_NICKNAME = core8g
export VERILOG_FILES = /work/rtl/core_v6_8.v
export VERILOG_INCLUDE_DIRS = /work/rtl
export SDC_FILE = /work/flow/core8g.sdc
export CORE_UTILIZATION = 40
export CORE_ASPECT_RATIO = 1
export CORE_MARGIN = 0.5
export PLACE_DENSITY = 0.55
export CORNER = TC
export SKIP_LAST_GASP = 1
export WORK_HOME = /work/out/core8g
export SYNTH_MEMORY_MAX_BITS = 2000000
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
export INFER_CLKGATES = 1
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*

View file

@ -0,0 +1,10 @@
current_design core_v6_8
set clk_name core_clock
set clk_port_name clk
set clk_period 1500
set clk_io_pct 0.2
set clk_port [get_ports $clk_port_name]
create_clock -name $clk_name -period $clk_period $clk_port
set non_clock_inputs [all_inputs -no_clocks]
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]

View file

@ -0,0 +1,18 @@
# ORFS design config for the programmable shadow core (core8: core_v6_8r64), ASAP7.
export PLATFORM = asap7
export DESIGN_NAME = core_v6_8r64
export DESIGN_NICKNAME = core8r64g
export VERILOG_FILES = /work/rtl/core_v6_8r64.v
export VERILOG_INCLUDE_DIRS = /work/rtl
export SDC_FILE = /work/flow/core8r64g.sdc
export CORE_UTILIZATION = 40
export CORE_ASPECT_RATIO = 1
export CORE_MARGIN = 0.5
export PLACE_DENSITY = 0.55
export CORNER = TC
export SKIP_LAST_GASP = 1
export WORK_HOME = /work/out/core8r64g
export SYNTH_MEMORY_MAX_BITS = 2000000
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
export INFER_CLKGATES = 1
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*

View file

@ -0,0 +1,10 @@
current_design core_v6_8r64
set clk_name core_clock
set clk_port_name clk
set clk_period 1500
set clk_io_pct 0.2
set clk_port [get_ports $clk_port_name]
create_clock -name $clk_name -period $clk_period $clk_port
set non_clock_inputs [all_inputs -no_clocks]
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]

View file

@ -0,0 +1,18 @@
# ORFS design config for the programmable shadow core (core8: core_tm_8r64), ASAP7.
export PLATFORM = asap7
export DESIGN_NAME = core_tm_8r64
export DESIGN_NICKNAME = coretm
export VERILOG_FILES = /work/rtl/core_tm_8r64.v
export VERILOG_INCLUDE_DIRS = /work/rtl
export SDC_FILE = /work/flow/coretm.sdc
export CORE_UTILIZATION = 40
export CORE_ASPECT_RATIO = 1
export CORE_MARGIN = 0.5
export PLACE_DENSITY = 0.55
export CORNER = TC
export SKIP_LAST_GASP = 1
export WORK_HOME = /work/out/coretm
export SYNTH_MEMORY_MAX_BITS = 2000000
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
export INFER_CLKGATES = 1
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*

View file

@ -0,0 +1,10 @@
current_design core_tm_8r64
set clk_name core_clock
set clk_port_name clk
set clk_period 1500
set clk_io_pct 0.2
set clk_port [get_ports $clk_port_name]
create_clock -name $clk_name -period $clk_period $clk_port
set non_clock_inputs [all_inputs -no_clocks]
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]

View file

@ -0,0 +1,18 @@
# ORFS design config for the programmable shadow core (core8: core_v6_8cs64), ASAP7.
export PLATFORM = asap7
export DESIGN_NAME = core_v6_8cs64
export DESIGN_NICKNAME = cs64
export VERILOG_FILES = /work/rtl/core_v6_8cs64.v
export VERILOG_INCLUDE_DIRS = /work/rtl
export SDC_FILE = /work/flow/cs64.sdc
export CORE_UTILIZATION = 40
export CORE_ASPECT_RATIO = 1
export CORE_MARGIN = 0.5
export PLACE_DENSITY = 0.55
export CORNER = TC
export SKIP_LAST_GASP = 1
export WORK_HOME = /work/out/cs64
export SYNTH_MEMORY_MAX_BITS = 2000000
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
export INFER_CLKGATES = 1
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*

View file

@ -0,0 +1,10 @@
current_design core_v6_8cs64
set clk_name core_clock
set clk_port_name clk
set clk_period 1500
set clk_io_pct 0.2
set clk_port [get_ports $clk_port_name]
create_clock -name $clk_name -period $clk_period $clk_port
set non_clock_inputs [all_inputs -no_clocks]
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]

View file

@ -14,3 +14,8 @@ core8r64 core_v6_8r64 1500
core8i1k core_v6_8i1k 1500
core8sel core_v6_8sel 1500
core32all core_v6_32all 1500
core8g core_v6_8 1500
core8r64g core_v6_8r64 1500
coretm core_tm_8r64 1500
cs64 core_v6_8cs64 1500
fp32 lane_fp32 2000

View file

@ -0,0 +1,15 @@
# ORFS design config for the mul shadow-core family (top lane_fp32), ASAP7.
export PLATFORM = asap7
export DESIGN_NAME = lane_fp32
export DESIGN_NICKNAME = fp32
export VERILOG_FILES = /work/rtl/fp32_units.v
export VERILOG_INCLUDE_DIRS = /work/rtl
export SDC_FILE = /work/flow/fp32.sdc
export CORE_UTILIZATION = 40
export CORE_ASPECT_RATIO = 1
export CORE_MARGIN = 0.5
export PLACE_DENSITY = 0.55
export CORNER = TC
export SKIP_LAST_GASP = 1
export WORK_HOME = /work/out/fp32

View file

@ -0,0 +1,10 @@
current_design lane_fp32
set clk_name core_clock
set clk_port_name clk
set clk_period 2000
set clk_io_pct 0.2
set clk_port [get_ports $clk_port_name]
create_clock -name $clk_name -period $clk_period $clk_port
set non_clock_inputs [all_inputs -no_clocks]
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]

View file

@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Live-state analysis of a drawn shadow program on an R-register window (the coordinator's order, 15:3x UK).
The program is drawn as the core testbench draws it: NPROG instructions with the class v4 op weights, a load on
one instruction in 16 (the dependent memory wait), dst/src/src2 uniform over the R registers, and the result
fold reading every register at the end of the block. For every load (wait) the script reports the live set:
registers whose current value is read later (by an instruction, a later address, or the fold) before being
overwritten, split into those that feed a later ADDRESS or the RESULT and those that die inside an arithmetic
block. Dead writes (overwritten before any read) are counted too. Usage: livestate.py R [NPROG] [seeds]"""
import random, sys
R = int(sys.argv[1]) if len(sys.argv) > 1 else 64
N = int(sys.argv[2]) if len(sys.argv) > 2 else 256
SEEDS = int(sys.argv[3]) if len(sys.argv) > 3 else 64
W = [('add',12),('xor',10),('mul',8),('mad',8),('shfl',8),('rotl',7),('sub',6),('mulhi',6),('rotr',6),('or',4)]
ops = [o for o,w in W for _ in range(w)]
def draw(rng):
prog = []
for k in range(N):
op = 'load' if k % 16 == 15 else rng.choice(ops)
d, s, s2 = rng.randrange(R), rng.randrange(R), rng.randrange(R)
reads = [s] if op not in ('load',) else [s] # the load's address comes from src
if op in ('add','xor','mul','mad','sub','or','rotr','shfl','mulhi','rotl'): reads.append(d) # dst is read too (r[d] op= ...)
if op == 'mad': reads.append(s2)
if op == 'rotl': reads = [d]
prog.append((op, d, reads))
return prog
tot_live = tot_addr = tot_dead = tot_waits = 0
live_min, live_max = R, 0
for seed in range(SEEDS):
rng = random.Random(seed)
prog = draw(rng)
# a value's "version" = (reg, write index); the fold at the end reads every register
# forward pass: for each instruction i and register r, next read of r's current value before its next write
n = len(prog)
# necessity: a version is NECESSARY if it reaches an address (a load's src) or the fold, transitively
# compute transitively by backward dataflow over versions
writes_at = {} # (i) -> reg written
# build version ids: version of reg r valid after instruction i
cur = {r: ('init', r) for r in range(R)}
uses = {} # version -> list of (consumer index, consumer version or 'addr'/'fold')
versions = set(cur.values())
deps = {} # version -> set of versions it reads
for i, (op, d, reads) in enumerate(prog):
srcs = [cur[r] for r in reads]
if op == 'load':
v = ('load', i); deps[v] = set() # the returned word: its ADDRESS depends on srcs
for s in srcs: uses.setdefault(s, []).append(('addr', i))
else:
v = (op, i); deps[v] = set(srcs)
for s in srcs: uses.setdefault(s, []).append(('op', i))
cur[d] = v; versions.add(v)
fold = set(cur.values())
# necessary = reaches an address or the fold
necessary = set(fold)
for v, us in uses.items():
if any(k == 'addr' for k, _ in us): necessary.add(v)
changed = True
while changed:
changed = False
for v in list(versions):
if v in necessary:
for s in deps.get(v, ()):
if s not in necessary: necessary.add(s); changed = True
# per wait: the versions live at the load (written before it, read after it)
last_read = {}
for v, us in uses.items():
last_read[v] = max(i for _, i in us)
for v in fold: last_read[v] = n
written_at = {v: (v[1] if v[0] != 'init' else -1) for v in versions}
for i, (op, d, reads) in enumerate(prog):
if op != 'load': continue
live = [v for v in versions if written_at[v] < i and last_read.get(v, -1) > i]
nec = [v for v in live if v in necessary]
tot_live += len(live); tot_addr += len(nec); tot_waits += 1
live_min = min(live_min, len(live)); live_max = max(live_max, len(live))
dead = sum(1 for v in versions if v[0] not in ('init',) and v not in uses and v not in fold)
tot_dead += dead
print(f'R = {R}, NPROG = {N}, {SEEDS} drawn programs, {tot_waits} waits')
print(f'live values at a wait: mean {tot_live/tot_waits:.1f} of {R} (min {live_min}, max {live_max}); of which necessary (reach a later address or the result): {tot_addr/tot_waits:.1f}')
print(f'dead writes (overwritten before any read): {tot_dead/SEEDS:.1f} per {N}-instruction block ({100*tot_dead/SEEDS/N:.1f} percent)')

View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
# pod.sh: bootstrap a rented pod started from openroad/orfs:latest (RunPod, root): iverilog, /work -> this dir.
set -euo pipefail
cd "$(dirname "$0")/.."
export DEBIAN_FRONTEND=noninteractive
command -v iverilog >/dev/null || { apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq iverilog rsync python3 >/dev/null 2>&1; }
[ -e /work ] || ln -s "$(pwd)" /work
export PATH=/OpenROAD-flow-scripts/tools/install/OpenROAD/bin:/OpenROAD-flow-scripts/tools/install/yosys/bin:$PATH
echo "pod ready: $(nproc) cores, $(free -g | awk '/Mem/{print $2}') GB, yosys $(yosys -V | cut -d' ' -f2), $(which iverilog)"

View file

@ -4,6 +4,7 @@ set -euo pipefail
name=$1; tag=$2; shift 2
out=/work/sim/$name
simcells=$(yosys-config --datdir)/simcells.v
iverilog -g2005 -I /work/tb -o $out/sim_$tag $out/sim_net.v /work/tb/tb_$(sed -n "s/^$name \([^ ]*\) .*/\1/p" /work/flow/designs.txt).v $simcells
tbfile=${TB:-/work/tb/tb_$(sed -n "s/^$name \([^ ]*\) .*/\1/p" /work/flow/designs.txt).v}
iverilog -g2005 -I /work/tb -o $out/sim_$tag $out/sim_net.v /work/flow/asap7_icg_model.v $tbfile $simcells
( cd $out && vvp -n sim_$tag "$@" | tee sim_$tag.log && mv dump.vcd $tag.vcd )
ls -la $out/$tag.vcd

View file

@ -0,0 +1,82 @@
// The adversary's time-multiplexed core: ONE execution port (every class unit, once) serving LANES lanes' instruction
// streams round-robin, each lane's state (REGS x 32-bit) kept in its own bank; the imem and sequencer shared.
// One lane-op per cycle. Compared with core_v6 at the same LANES x REGS this removes LANES-1 copies of the units and
// keeps the register state and the imem: the energy per lane-op is the state's cost plus one unit set's.
// The butterfly shuffle across lanes needs every lane's source register in the same cycle, so the shuffle reads the
// bank-wide source column (as the SIMD core does) and the lane in turn takes its word. Loads return on ld_val.
`include "lane_common.vh"
module core_tm #(parameter LANES = 8, parameter LOG_LANES = 3, parameter REGS = 64, parameter LOG_REGS = 6,
parameter IW = 40, parameter IMEM_LOG = 8) (
input clk, input rst, input run,
input prog_we, input [9:0] prog_addr, input [IW-1:0] prog_data,
input cfg_en, input [9:0] cfg_n, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask, input [63:0] cfg_sel,
input [31:0] ld_val,
output [31:0] addr, output [31:0] out);
localparam IMEM = 1 << IMEM_LOG;
reg [IW-1:0] imem [0:IMEM-1];
reg [IMEM_LOG-1:0] pc; reg [IMEM_LOG-1:0] n_q; reg [IW-1:0] ir; reg [LOG_LANES-1:0] lane;
reg [31:0] m_q, wm_q, off_q, mask_q; reg [4:0] r_q;
integer i;
// the sequencer: the same instruction is issued to each lane in turn (LANES cycles per instruction)
always @(posedge clk) begin
if (prog_we) imem[prog_addr[IMEM_LOG-1:0]] <= prog_data;
if (rst) begin pc <= 0; ir <= 0; lane <= 0; n_q <= {IMEM_LOG{1'b1}}; m_q <= 32'h9e3779b1; r_q <= 5'd13; wm_q <= 32'h0fffffc0; off_q <= 3; mask_q <= 32'h0fffffff; end
else begin
if (cfg_en) begin n_q <= cfg_n[IMEM_LOG-1:0]; m_q <= cfg_m | 1; r_q <= cfg_r; wm_q <= cfg_wm; off_q <= cfg_off; mask_q <= cfg_mask; end
if (run) begin
if (lane == {LOG_LANES{1'b1}}) begin ir <= imem[pc]; pc <= (pc == n_q) ? {IMEM_LOG{1'b0}} : pc + 1'b1; end
lane <= lane + 1'b1;
end
end
end
wire [3:0] op = ir[3:0];
wire [LOG_REGS-1:0] dst = ir[4 +: LOG_REGS]; wire [LOG_REGS-1:0] src = ir[4+LOG_REGS +: LOG_REGS]; wire [LOG_REGS-1:0] src2 = ir[4+2*LOG_REGS +: LOG_REGS];
wire [4:0] imm = ir[4+3*LOG_REGS +: 5]; wire [7:0] aux = ir[9+3*LOG_REGS +: 8];
wire is_load = (op == 4'd12);
wire [4:0] rn = (imm == 0) ? 5'd1 : imm;
wire [LOG_LANES-1:0] smask = imm[LOG_LANES-1:0];
// the banked state: one bank per lane, read through the lane select (a chip's SRAM bank select)
reg [31:0] rf [0:LANES*REGS-1];
wire [31:0] d = rf[lane*REGS + dst];
wire [31:0] s = rf[lane*REGS + src];
wire [31:0] s2 = rf[lane*REGS + src2];
wire [31:0] sx = rf[(lane ^ smask)*REGS + src]; // the shuffle partner's source word
// the one execution port
function [7:0] pick; input [63:0] b; input [3:0] k; reg [7:0] v;
begin v = b[8*k[2:0] +: 8]; pick = k[3] ? {8{v[7]}} : v; end
endfunction
wire [4:0] sn = (s[4:0] == 0) ? 5'd1 : s[4:0];
wire mad = (op == 4'd8);
wire [63:0] p = (mad ? s : d) * (mad ? s2 : s);
wire [63:0] bytes = {s, d}; wire [15:0] sel = {aux, aux};
wire [31:0] prm = {pick(bytes, sel[15:12]), pick(bytes, sel[11:8]), pick(bytes, sel[7:4]), pick(bytes, sel[3:0])};
reg [31:0] lp; integer b;
always @* for (b = 0; b < 32; b = b + 1) lp[b] = aux[{d[b], s[b], s2[b]}];
reg [31:0] r;
always @* begin
case (op)
4'd0, 4'd13: r = d + s;
4'd1, 4'd15: r = d - s;
4'd2, 4'd14: r = d ^ s;
4'd3: r = d | s;
4'd4: r = `ROTL32(d, rn);
4'd5: r = `ROTR32(d, sn);
4'd6: r = p[31:0];
4'd7: r = p[63:32];
4'd8: r = p[31:0] + d;
4'd9: r = d ^ sx;
4'd10: r = prm;
4'd11: r = lp;
default: r = ld_val ^ (32'h9e3779b9 * (lane + 1));
endcase
end
wire [31:0] fx = s * m_q;
wire [4:0] frn = (r_q == 0) ? 5'd1 : r_q;
wire [31:0] fy = `ROTL32(fx, frn);
assign addr = is_load ? (((fy & wm_q) | off_q) & mask_q) : 32'd0;
always @(posedge clk) begin
if (rst) begin for (i = 0; i < LANES*REGS; i = i + 1) rf[i] <= 32'h9e3779b9 * (i + 1); end
else if (run) rf[lane*REGS + dst] <= r;
end
assign out = r;
endmodule

View file

@ -0,0 +1,7 @@
`include "core_tm.v"
module core_tm_8r64(input clk, input rst, input run, input prog_we, input [9:0] prog_addr, input [39:0] prog_data,
input cfg_en, input [9:0] cfg_n, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask, input [63:0] cfg_sel,
input [31:0] ld_val, output [31:0] addr, output [31:0] out);
core_tm #(.LANES(8), .LOG_LANES(3), .REGS(64), .LOG_REGS(6), .IW(40)) c(.clk(clk), .rst(rst), .run(run), .prog_we(prog_we), .prog_addr(prog_addr), .prog_data(prog_data),
.cfg_en(cfg_en), .cfg_n(cfg_n), .cfg_m(cfg_m), .cfg_r(cfg_r), .cfg_wm(cfg_wm), .cfg_off(cfg_off), .cfg_mask(cfg_mask), .cfg_sel(cfg_sel), .ld_val(ld_val), .addr(addr), .out(out));
endmodule

View file

@ -0,0 +1,7 @@
`include "core_v6.v"
module core_v6_8cs64(input clk, input rst, input run, input prog_we, input [9:0] prog_addr, input [40-1:0] prog_data,
input cfg_en, input [9:0] cfg_n, input [63:0] cfg_sel, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask,
input [31:0] ld_val, output [31:0] addr, output [31:0] out);
core_v6 #(.LANES(8), .LOG_LANES(3), .REGS(64), .LOG_REGS(6), .IW(40), .IMEM_LOG(9)) c(.clk(clk), .rst(rst), .run(run), .prog_we(prog_we), .prog_addr(prog_addr), .prog_data(prog_data),
.cfg_en(cfg_en), .cfg_n(cfg_n), .cfg_sel(cfg_sel), .cfg_m(cfg_m), .cfg_r(cfg_r), .cfg_wm(cfg_wm), .cfg_off(cfg_off), .cfg_mask(cfg_mask), .ld_val(ld_val), .addr(addr), .out(out));
endmodule

View file

@ -0,0 +1,123 @@
// The adversary's simplified FP32 units for the mixed-resource lane's candidate (class-v6-mixedfp): inputs are
// f(x) = as_float((x & 0x807FFFFF) | ((96 + ((x >> 23) & 63)) << 23)): never zero, denormal, NaN or Inf; exponents
// in [96, 159]; results normal or +0 (exact cancellation). The units drop NaN/Inf/denormal handling and the flags,
// keep the full 24-bit mantissa path, a full alignment and a full normaliser (the mantissas are uniform), RNE.
// Each lane reads two or three registers of an 8 x 32-bit window, applies f(), computes, xors the bits into dst.
`include "lane_common.vh"
// ---- the shared pieces ----
module fp_unpack(input [31:0] x, output s, output [8:0] e, output [23:0] m);
assign s = x[31];
assign e = 9'd96 + {3'b0, x[28:23]}; // the masked exponent, 96..159
assign m = {1'b1, x[22:0]};
endmodule
module lzc48(input [47:0] v, output reg [5:0] n); // leading-zero count (v != 0)
integer i; always @* begin n = 6'd47; for (i = 47; i >= 0; i = i - 1) if (v[i]) begin n = 6'd47 - i; i = -1; end end
endmodule
module lzc32(input [31:0] v, output reg [5:0] n);
integer i; always @* begin n = 6'd31; for (i = 31; i >= 0; i = i - 1) if (v[i]) begin n = 6'd31 - i; i = -1; end end
endmodule
// ---- the FMA: fma(a, b, c) = a*b + c, one rounding (RNE), exponents in the lane's ranges ----
module fp_fma(input [31:0] a, input [31:0] b, input [31:0] c, output [31:0] y);
wire sa, sb, sc; wire [8:0] ea, eb, ec; wire [23:0] ma, mb, mc;
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb); fp_unpack uc(c, sc, ec, mc);
wire [47:0] prod = ma * mb; // 48-bit product, binary point after bit 46
wire sp = sa ^ sb;
wire [9:0] ep = {1'b0, ea} + {1'b0, eb} - 10'd127; // product exponent (bias kept), 65..192
// align the addend to the product: a 100-bit window keeps full precision for exponent gaps up to about 96 (the lane's bound)
wire [9:0] diff = (ep >= {1'b0, ec}) ? ep - {1'b0, ec} : {1'b0, ec} - ep;
wire prod_big = (ep >= {1'b0, ec});
wire [99:0] pw = {2'b0, prod, 50'b0};
wire [99:0] cw = {2'b0, mc, 24'b0, 50'b0}; // the addend at the product's scale when exponents equal
wire [6:0] sh = (diff > 10'd99) ? 7'd99 : diff[6:0];
wire [99:0] smw = prod_big ? (cw >> sh) : (pw >> sh);
wire [99:0] bgw = prod_big ? pw : cw;
wire sbig = prod_big ? sp : sc; wire ssmall = prod_big ? sc : sp;
wire [9:0] ebig = prod_big ? ep : {1'b0, ec};
wire [100:0] sum = (sbig == ssmall) ? ({1'b0, bgw} + {1'b0, smw}) : ({1'b0, bgw} - {1'b0, smw});
wire [100:0] mag = sum[100] ? (~sum + 1'b1) : sum; // two's complement when the subtraction went negative
wire ssum = sum[100] ? ssmall : sbig;
// normalise: find the leading one in the 101-bit magnitude
reg [6:0] lz; integer i;
always @* begin lz = 7'd100; for (i = 100; i >= 0; i = i - 1) if (mag[i]) begin lz = 7'd100 - i; i = -1; end end
wire [100:0] norm = mag << lz; // leading one at bit 100
wire [23:0] mant = norm[100:77];
wire guard = norm[76]; wire sticky = |norm[75:0];
wire round_up = guard & (sticky | mant[0]);
wire [24:0] mr = {1'b0, mant} + round_up;
wire carry = mr[24];
wire [9:0] eres = ebig + 10'd2 - lz + carry; // the leading one of bgw sat at bit 98 (two headroom bits)
wire zero = (mag == 0);
wire [7:0] eout = eres[7:0];
assign y = zero ? 32'h0 : {ssum, eout, carry ? mr[23:1] : mr[22:0]};
endmodule
// ---- the adder and the multiplier as their own units ----
module fp_add(input [31:0] a, input [31:0] b, output [31:0] y);
wire sa, sb; wire [8:0] ea, eb; wire [23:0] ma, mb;
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb);
wire abig = (ea > eb) || (ea == eb && ma >= mb);
wire [8:0] ebig = abig ? ea : eb; wire [8:0] esm = abig ? eb : ea;
wire [23:0] mbig = abig ? ma : mb; wire [23:0] msm = abig ? mb : ma;
wire sbig = abig ? sa : sb; wire ssm = abig ? sb : sa;
wire [8:0] diff = ebig - esm; wire [6:0] sh = (diff > 9'd70) ? 7'd70 : diff[6:0];
wire [73:0] bw = {1'b0, mbig, 49'b0}; wire [73:0] sw = {1'b0, msm, 49'b0} >> sh;
wire [74:0] sum = (sbig == ssm) ? ({1'b0, bw} + {1'b0, sw}) : ({1'b0, bw} - {1'b0, sw});
reg [6:0] lz; integer i;
always @* begin lz = 7'd74; for (i = 74; i >= 0; i = i - 1) if (sum[i]) begin lz = 7'd74 - i; i = -1; end end
wire [74:0] norm = sum << lz;
wire [23:0] mant = norm[74:51]; wire guard = norm[50]; wire sticky = |norm[49:0];
wire round_up = guard & (sticky | mant[0]);
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
wire [9:0] eres = {1'b0, ebig} + 10'd1 - lz + carry;
wire zero = (sum == 0);
assign y = zero ? 32'h0 : {sbig, eres[7:0], carry ? mr[23:1] : mr[22:0]};
endmodule
module fp_mul(input [31:0] a, input [31:0] b, output [31:0] y);
wire sa, sb; wire [8:0] ea, eb; wire [23:0] ma, mb;
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb);
wire [47:0] prod = ma * mb;
wire top = prod[47];
wire [23:0] mant = top ? prod[47:24] : prod[46:23];
wire guard = top ? prod[23] : prod[22]; wire sticky = top ? |prod[22:0] : |prod[21:0];
wire round_up = guard & (sticky | mant[0]);
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
wire [9:0] eres = {1'b0, ea} + {1'b0, eb} - 10'd127 + top + carry;
assign y = {sa ^ sb, eres[7:0], carry ? mr[23:1] : mr[22:0]};
endmodule
// ---- int32 to float, RNE ----
module fp_cvt(input [31:0] a, output [31:0] y);
wire s = a[31]; wire [31:0] mag = s ? (~a + 1'b1) : a;
wire [5:0] lz; lzc32 l(mag, lz);
wire [31:0] norm = mag << lz; // leading one at bit 31
wire [23:0] mant = norm[31:8]; wire guard = norm[7]; wire sticky = |norm[6:0];
wire round_up = guard & (sticky | mant[0]);
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
wire [7:0] e = 8'd127 + 8'd31 - lz + carry;
assign y = (mag == 0) ? 32'h0 : {s, e, carry ? mr[23:1] : mr[22:0]};
endmodule
// ---- the lane: op 0 fadd, 1 fmul, 2 ffma, 3 fcvt; d ^= bits(result) ----
module lane_fp32(
input clk, input rst,
input [1:0] op, input [2:0] dst, input [2:0] src, input [2:0] src2,
input ld_en, input [31:0] ld_val,
output [31:0] out);
reg [31:0] rf [0:7];
reg [1:0] op_q; reg [2:0] dst_q, src_q, src2_q; reg ld_q; reg [31:0] ld_val_q;
integer i;
always @(posedge clk) begin
if (rst) begin op_q <= 0; dst_q <= 0; src_q <= 0; src2_q <= 0; ld_q <= 0; ld_val_q <= 0; end
else begin op_q <= op; dst_q <= dst; src_q <= src; src2_q <= src2; ld_q <= ld_en; ld_val_q <= ld_val; end
end
wire [31:0] d = rf[dst_q]; wire [31:0] s = rf[src_q]; wire [31:0] s2 = rf[src2_q];
wire [31:0] ya, ym, yf, yc;
fp_add A(d, s, ya);
fp_mul M(d, s, ym);
fp_fma F(s, s2, d, yf);
fp_cvt C(s, yc);
reg [31:0] res;
always @* case (op_q) 2'd0: res = d ^ ya; 2'd1: res = d ^ ym; 2'd2: res = d ^ yf; default: res = d ^ yc; endcase
always @(posedge clk) begin
if (rst) begin for (i = 0; i < 8; i = i + 1) rf[i] <= 32'h9e3779b9 * (i + 1) + 9; end
else rf[dst_q] <= ld_q ? ld_val_q : res;
end
assign out = res;
endmodule

View file

@ -21,10 +21,32 @@ module tb;
@(negedge clk); cfg_en = 1; cfg_n = `NPROG - 1; cfg_sel = `SEL; cfg_m = 32'h9e3779b1; cfg_r = 5'd13; cfg_wm = 32'h0fffffc0; cfg_off = 3; cfg_mask = 32'h0fffffff;
@(negedge clk); cfg_en = 0;
// the program: `NPROG instructions drawn with the class v4 weights
`ifdef CS
// the connected-state draw: step = load + 27-instruction spine block; `NPROG = 16 x 28 = 448
begin : cs
integer st, q, last0, last1, last2, last3, mreg, areg, dreg, sreg, s2reg;
areg = 0; mreg = 1;
for (st = 0; st < `NPROG / 28; st = st + 1) begin
@(negedge clk); w = {$random, $random}; mreg = $random & 63;
prog_we = 1; prog_addr = st*28; prog_data = {w[`IW-1:4], 4'd12}; prog_data[4 +: 6] = mreg; prog_data[10 +: 6] = areg; // load: dst m_j, src a_j
last0 = mreg; last1 = mreg; last2 = mreg; last3 = mreg;
for (q = 0; q < 27; q = q + 1) begin
@(negedge clk); w = {$random, $random}; opc = draw_op($random); dreg = $random & 63;
case ($random & 3) 0: sreg = last0; 1: sreg = last1; 2: sreg = last2; default: sreg = last3; endcase
s2reg = last0;
if (q == 26 && !(opc == 4'd0 || opc == 4'd1 || opc == 4'd2 || opc == 4'd8 || opc == 4'd9)) opc = 4'd0; // the last instruction injects
prog_we = 1; prog_addr = st*28 + 1 + q; prog_data = {w[`IW-1:4], opc}; prog_data[4 +: 6] = dreg; prog_data[10 +: 6] = sreg; prog_data[16 +: 6] = s2reg;
last3 = last2; last2 = last1; last1 = last0; last0 = dreg;
end
areg = last0;
end
end
`else
for (k = 0; k < `NPROG; k = k + 1) begin
@(negedge clk); w = {$random, $random}; opc = (loads && (k % 16 == 15)) ? 4'd12 : draw_op($random);
prog_we = 1; prog_addr = k; prog_data = {w[`IW-1:4], opc};
end
`endif
@(negedge clk); prog_we = 0; run = 1;
for (n = 0; n < cycles; n = n + 1) begin
@(negedge clk); ld_val = $random; acc = acc ^ out ^ addr;

View file

@ -0,0 +1,6 @@
`define TOP core_tm_8r64
`define HALF 750
`define IW 40
`define NPROG 256
`define SEL 64'hfedcba9876543210
`include "tb_core_common.vh"

View file

@ -0,0 +1,3 @@
`define TOP core_v6_8
`define HALF 750
`include "tb_core_legacy.vh"

View file

@ -0,0 +1,7 @@
`define TOP core_v6_8cs64
`define HALF 750
`define IW 40
`define NPROG 448
`define CS 1
`define SEL 64'hfedcba9876543210
`include "tb_core_common.vh"

View file

@ -0,0 +1,22 @@
`timescale 1ps/1ps
module tb;
reg clk = 0, rst = 1; reg [1:0] op = 0; reg [2:0] dst = 0, src = 0, src2 = 0; reg ld_en = 0; reg [31:0] ld_val = 0;
wire [31:0] out;
lane_fp32 dut(.clk(clk), .rst(rst), .op(op), .dst(dst), .src(src), .src2(src2), .ld_en(ld_en), .ld_val(ld_val), .out(out));
integer n, fixed_op, cycles; reg [31:0] acc = 0;
always #1000 clk = ~clk;
// a reference check of the units against the host's float arithmetic is the mixed lane's own (the ranges are its);
// this bench drives random registers and reports the checksum
initial begin
if (!$value$plusargs("op=%d", fixed_op)) fixed_op = -1;
if (!$value$plusargs("cycles=%d", cycles)) cycles = 3000;
$dumpfile("dump.vcd"); $dumpvars(0, tb.dut);
repeat (4) @(negedge clk); rst = 0;
for (n = 0; n < cycles; n = n + 1) begin
@(negedge clk);
op = (fixed_op < 0) ? $random : fixed_op; dst = $random; src = $random; src2 = $random;
ld_en = (($random & 7) == 0); ld_val = $random; acc = acc ^ out;
end
$display("CHECKSUM %08x", acc); $finish;
end
endmodule

View file

@ -16,8 +16,10 @@
| the kit ISA check (`kit-isa-check.sh`; in the gate as a self-test, in `merge-to-master.sh` over any executable a landing adds under packaging/kits or bin, and in the shipper's cut gate over the kit) | Any binary whose disassembly carries a zmm register, an EVEX opmask or an EVEX-only mnemonic (AVX-512): a fleet binary comes only from the cross-build kit at the x86-64-v3 baseline, never from a box's native gate build (79 fleet hosts died on one, 8 Oct 2026) | 8 Oct 2026 |
| kill-by-name rule 5 and the no-kill shim (`kill-by-name-check.sh`, `no-kill-shim/{pkill,killall}` exit 97 when first in PATH) | A `pgrep -f`/`pkill -f` pattern that is a bare path, a log name or an unanchored word; only `^`-anchored command patterns, the bracket form, a variable, -x or -F pass (fifteen Mac processes died to a grep, 8 Oct 2026) | 8 Oct 2026 |
| a "cut" batch needs its read-back (`test-record.mjs`) | A batch declaring `cut` without the binary's build-N:/srv path, its commit string read back equal to the manifest sha, and the kit ISA check's clean line; a sha is cut only when its binary exists on build-1 with its commit string read back | 8 Oct 2026 |
| rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (`canary-check.sh`; `tools/ci/canary/<sha>.json`, the form from `--form`; refused without by `packaging/ota/publish-manifest.sh --release-sha`, `publish-public.sh` and deploy-win.sh; the canary cell maps INT-07) | A release entry published before its sha had run a fresh install on a non-AVX-512 box with an empty datadir, synced, mined five minutes with zero refusals, claimed, proved and paid or queued one shard and quit inside a bound, every line read back (the founder's "no more lost time", 8 October 2026) |
| the INT suite is generated from the master edition's integration gates (`int-suite.mjs`; the owner per the coordinator's crosswalk) and INT-17 is a rule of the writer: a cell declaring a missing oracle, pinned keys or mandatory real-proof fixture writes BLOCKED, never PASS | A registry whose INT suite drifts from traceability.json; a batch cell with `prereqs` where any is not "present" written as anything but BLOCKED | 8 Oct 2026 |
| the REV suite is generated from an external review's findings.json and dispatch.md (`review-suite.mjs`; one case per required regression, NOT RUN, the owner from the dispatch table) | A registry whose REV suite differs from the generator's output (--check) | 8 Oct 2026 |
| F03 (Review B): one release manifest (`packaging/release-manifest.json` on a release branch) and every component built from it (`release-manifest-check.sh`, `build-from-manifest.sh`) | A tree whose own pins disagree with the manifest: the Windows node-source pin, the proving manifest's elf and vk sha256s and the files' hashes, the node fork's freeze fingerprint, the pool's vendored node checkout, a redefined EpochSeeds in the pool (the shadow_reps seam closes by a build against the pinned node); the build script puts the fork at the manifest's node sha and checks kaspad with igneum-pow (rule 19), the miner, the pool, the app and the prove host on a box | 8 Oct 2026 |
| a registry landing carries its batches (`tools/ci/batches/<run id>.json`; `merge-to-master.sh` replays them onto master's copy at the merge) | Nothing by itself: the registry is a hot file, and a branch whose own copy of it was recorded during a seven-minute gate lost the race to another lane's rows three times in a row (8 Oct 2026, 19:1x UK). A branch that adds batch files is merged with master's registry, every added batch replayed through `test-record.mjs --record` (idempotent), and the evidence rules run on the merged result; rule 26 does not bind the registry path for such a branch | 8 Oct 2026 |
| the registry's evidence rules (`registry-evidence-check.sh`, called by `merge-to-master.sh` after rule 26) | A landing that sets a case's run_status to PASS without an evidence_path that exists (in the tree at the landing, or on a build box over ssh; a box that does not answer is a line, not a refusal); a landing that changes a file under docs/analysis/ or a path a registry row names without moving that row's `updated` (the row and its evidence move together, GOV-04); a PASS whose evidence record pins another manifest than the registry's pinned_manifest_sha (stale evidence reads NOT RUN, GOV-08); a run_status written while the registry carries no approval block (thresholds before results, GOV-02) | 8 Oct 2026 |
| the acceptance layer (`test-record.mjs`, `test-map.json`, `test-map-doc.mjs`; the founder's Test and Acceptance Standard, docs/plans/igneum-2.0-test-registry.json) | An automated case of the registry with no cell in the map and no NOT RUN reason; a map naming an unknown case; a stale harness-map page (generated from the JSON); the recorder's self-test: a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, never an accept text, and a case with no harness reads NOT RUN with its reason, never PASS by inference | 8 Oct 2026 |

View file

@ -0,0 +1,28 @@
{
"run_id": "canary-20261008-01",
"manifest_sha": "c30ab32c",
"method": "team-reported",
"evidence_dir": "tools/ci/canary (no record yet)",
"boxes": [],
"release_identity": {
"commit": "c30ab32c (release-2.0.1 final tip; 2.0.2 open at c608b341)",
"lockfile": "",
"binary": "the shipped 2.0.1 entries on aa0e0f45's binaries",
"network_object": "igneum-devnet-4, chain id 4465",
"activation": "",
"profile_hashes": ""
},
"claim_impact": "INT-07's clean-install half reads BLOCKED until a release tip carries a PASS fresh-install canary record; no published entry may move before one does (rule 33)",
"note": "Rule 33 recorded at 21:4x UK: the canary cell exists, its check and the publish-path guards are in the tree; no 2.0.x sha has a record yet, so the case is BLOCKED (prerequisite: the shipper's 2.0.2 canary tonight writes tools/ci/canary/<sha>.json).",
"cells": [
{
"cell": "canary:fresh-install",
"cases": [
"INT-07"
],
"status": "BLOCKED",
"evidence": "tools/ci/canary-check.sh; packaging/ota/publish-manifest.sh; packaging/ota/publish-public.sh",
"note": "no fresh-install canary record exists for any 2.0.x release tip; the 2.0.2 canary is the shipper's tonight"
}
]
}

View file

@ -0,0 +1,39 @@
{
"run_id": "f03-manifest-20261008-01",
"manifest_sha": "c30ab32c",
"method": "static",
"evidence_dir": "build-1:/srv/artefacts/tas/f03-manifest-20261008-01 (build-from-manifest-c30ab32c-build4.log, sha256 6040ded8e99f0871\u2026); the build tree build-4:/srv/builds/igneum-wt-f03-201 at c30ab32c",
"boxes": [
"build-4"
],
"release_identity": {
"commit": "c30ab32c",
"lockfile": "the release tree's Cargo.lock files at c30ab32c",
"binary": "cargo check only: no binary is claimed by this cell; the fleet binaries are the shipper's kit on aa0e0f45",
"network_object": "igneum-devnet-4, chain id 4465",
"activation": "none (a build fact)",
"profile_hashes": "release-manifest-check: every component's own pin equals packaging/release-manifest.json"
},
"claim_impact": "F03's first rung: one manifest, one build, no unpublished vendor tree; the same-work rung (R02) and the transition rung (R03) stay NOT RUN until the readers run on the job context",
"note": "R2-F03-R01, the clean build from one manifest: every component builds from packaging/release-manifest.json on release-2.0.1's final tip c30ab32c with the node vendored at the manifest's sha 7cfa422a as a real checkout (vendor/igneum-node and vendor/igneum-node-exec; a link ships as nothing); the pool builds only from the release tree (its igneum-pow is the class v5 freeze cbc5bd0a, rule 19; master's a65e4c5a refuses it). Earlier runs on aa0e0f45 were red on the pool (KeyReveal.sig_scheme, the pool-review-b shape not yet in the tree) and on prove-host (the exec vendor missing); both closed by the tree, not by the script. The workers are the kit's cross-build (the shipper's kit-isa line), not this build. The build: tools/ci/build-from-manifest.sh --box 4 on release-2.0.1 c30ab32c, 21:11 to 21:14 UK, kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app and igneum-prove-host all check green from packaging/release-manifest.json, and release-manifest-check reads every component's own pin equal to the manifest; the log on build-1 (sha256 6040ded8e99f0871...).",
"cells": [
{
"cell": "harness:release-manifest",
"cases": [
"R2-F03-R01"
],
"status": "PASS",
"evidence": "build-1:/srv/artefacts/tas/f03-manifest-20261008-01/build-from-manifest-c30ab32c-build4.log; build-4:/srv/builds/igneum-wt-f03-201; tools/ci/build-from-manifest.sh; tools/ci/release-manifest-check.sh"
},
{
"cell": "check:freeze",
"cases": [
"GOV-01"
],
"status": "NOT RUN",
"in_progress": true,
"evidence": "docs/plans/igneum-2.0-f0-manifest.md; build-4:/srv/builds/igneum-wt-f03-201/vendor/igneum-node/packaging/pow-freeze.txt; build-4:/srv/builds/igneum-wt-f03-201",
"note": "GOV-01 moves with its evidence page: the F0 manifest's cut-tip row gained the final tip c30ab32c and the 21:14 UK build-from-manifest fact; the freeze itself is unchanged (class v5 1c420786, fingerprint cbc5bd0a) and GOV-01 stays NOT RUN in progress until the signing block at 23:30"
}
]
}

View file

@ -0,0 +1,26 @@
{
"run_id": "floor-k-20261008-rows-repeat",
"manifest_sha": "86e5b0fb",
"cut_tip": "class-v6-floor-k 86e5b0fb (the amendment carries shadow-k.md; floor lane 2's rows ADV-05 and ADV-06 move with it on its word, method model, no decision changed; the steward's rows no longer cite the directory since d2e1c192)",
"evidence_dir": "docs/analysis/class-v6/floor/shadow-k.md",
"boxes": [],
"cells": [
{
"cell": "adversary:mf-placed",
"cases": [
"ADV-05"
],
"status": "RUNNING",
"method": "model",
"evidence": "docs/analysis/class-v6/floor/shadow-k.md",
"note": "repeat for ADV-05 at this manifest on floor lane 2's word (placed and routed RTL on ASAP7, a model); the rtl directory tools/chip-model/rtl/ is the flow, cited by its document since the recorder takes files only"
},
{
"cell": "review:k-lane-shadow-k",
"status": "NOT RUN",
"method": "model",
"evidence": "docs/analysis/class-v6/floor/shadow-k.md",
"note": "repeat of team-2026-10-08 for ADV-06 at this manifest on floor lane 2's word; no decision changed"
}
]
}

View file

@ -15,5 +15,5 @@
"evidence": "docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md"
}
],
"note": " Evidence narrowed at 21:3x UK from the class-v6 directory to the two experiments' own files (a directory citation bound every file beneath it to these rows and refused every other lane's class-v6 landing); the verdicts are unchanged."
"note": " Evidence narrowed at 21:3x UK from the class-v6 directory to the two experiments' own files (a directory citation bound every file beneath it to these rows and refused every other lane's class-v6 landing); the verdicts are unchanged. Replayed at the rule 33 landing (21:4x UK): the narrowed evidence reaches master's rows only through a replay, and the merge replayed added batches alone until this landing."
}

33
tools/ci/build-from-manifest.sh Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# F03 (Review B): every component is built from the one release manifest. Reads packaging/release-manifest.json of this tree,
# puts the node fork at the manifest's node sha under vendor/igneum-node (the pool's path dependency, so the EpochSeeds seam closes
# by a build against the pinned node), then on a box at gate priority (tools/build-remote.sh): cargo check of kaspad with the
# igneum-pow feature (rule 19 proves the generator's fingerprint at build time), igneum-miner, the pool crate (igneum-pool), the app
# crate (igneum-app) and the prove host; then tools/ci/release-manifest-check.sh over the tree and the fork. One red = exit 1.
# tools/ci/build-from-manifest.sh [--box N] [--dry] from the release branch's worktree; --dry prints the plan
set -euo pipefail
ROOT=$(git rev-parse --show-toplevel); cd "$ROOT"; BOX=""; DRY=0
while [ $# -gt 0 ]; do case "$1" in --box) BOX="$2"; shift 2 ;; --dry) DRY=1; shift ;; *) echo "unknown $1" >&2; exit 2 ;; esac; done
M=packaging/release-manifest.json; [ -f "$M" ] || { echo "build-from-manifest: no $M on this tree" >&2; exit 2; }
NODE=$(python3 -c "import json;print(json.load(open('$M'))['node']['sha'])"); FP=$(python3 -c "import json;print(json.load(open('$M'))['generator']['fingerprint'])")
echo "build-from-manifest: node $NODE, generator fingerprint ${FP:0:16}, miner app $(git rev-parse --short HEAD)"
FORK=vendor/igneum-node; MIRROR="${IGNEUM_NODE_MIRROR:-build@188.40.146.49:/srv/igneum-node.git}"
if [ "$DRY" = 1 ]; then echo "plan: $FORK at $NODE from $MIRROR; cargo check kaspad(+igneum-pow), igneum-miner, igneum-pool, igneum-app, igneum-prove-host on box ${BOX:-auto} at gate priority; then release-manifest-check.sh $FORK"; exit 0; fi
if [ -d "$FORK/.git" ] || [ -f "$FORK/.git" ]; then git -C "$FORK" fetch -q "$MIRROR" "$NODE" 2>/dev/null || git -C "$FORK" fetch -q "$MIRROR" release-2.0.0-node; git -C "$FORK" checkout -q --detach "$NODE"
else mkdir -p vendor && git clone -q "$MIRROR" "$FORK" && git -C "$FORK" checkout -q --detach "$NODE"; fi
# the proving workspace names the same fork vendor/igneum-node-exec (proving/igneum-prove/Cargo.toml): a second checkout at the same
# sha, never a link (build-remote ships directories as overlays to the box; a link ships as nothing)
if [ -L vendor/igneum-node-exec ]; then rm -f vendor/igneum-node-exec; fi
if [ -d vendor/igneum-node-exec/.git ] || [ -f vendor/igneum-node-exec/.git ]; then git -C vendor/igneum-node-exec fetch -q "$MIRROR" "$NODE" 2>/dev/null || true; git -C vendor/igneum-node-exec checkout -q --detach "$NODE"
else git -C "$FORK" worktree add -q --detach "$ROOT/vendor/igneum-node-exec" "$NODE" 2>/dev/null || git clone -q "$MIRROR" vendor/igneum-node-exec && git -C vendor/igneum-node-exec checkout -q --detach "$NODE"; fi
echo "build-from-manifest: $FORK at $(git -C "$FORK" rev-parse --short HEAD); vendor/igneum-node-exec at $(git -C vendor/igneum-node-exec rev-parse --short HEAD)"
run() { local name="$1" dir="$2"; shift 2; echo "build-from-manifest: $name"; ( cd "$dir" && IGNEUM_AGENT=f03 bash "$ROOT/tools/build-remote.sh" ${BOX:+--box "$BOX"} --no-fetch --priority gate -- "$@" ) > "/tmp/f03-$name.log" 2>&1 || { echo "build-from-manifest: RED: $name (see /tmp/f03-$name.log)" >&2; grep -m3 -E '^error|RED|panicked' "/tmp/f03-$name.log" | cut -c1-160 >&2; return 1; }; echo "build-from-manifest: $name ok"; }
rc=0
run kaspad "$FORK" check --release -p kaspad --features kaspad/igneum-pow || rc=1
run igneum-miner "$FORK" check --release -p igneum-miner || rc=1
run igneum-pool pool check --release || rc=1
run igneum-app app/igneum-app check --release || rc=1
run prove-host proving/igneum-prove check --release -p igneum-prove-host || rc=1
bash tools/ci/release-manifest-check.sh "$FORK" || rc=1
[ "$rc" = 0 ] && echo "build-from-manifest: every component builds from the manifest and every pin agrees"
exit $rc

174
tools/ci/canary-check.sh Executable file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env bash
# Rule 33 (the founder's "no more lost time", 8 October 2026, 21:3x UK): a release entry may not publish without a
# fresh-install canary record for its sha. The record is one JSON file in the tree, tools/ci/canary/<sha>.json (the
# release tip's commit, 8 to 40 hex), written by the lane that ran the canary, with the eight lines the founder named,
# each read back with its evidence path on a box (build-N:/srv/... or /srv/...). This check reads the record and says
# PASS or names the first line that is missing or failing; the publish path (packaging/ota/publish-manifest.sh,
# packaging/ota/publish-public.sh, deploy-win.sh) refuses an entry whose sha has no PASS record.
#
# tools/ci/canary-check.sh <sha> [--artefact <kind>] exit 0: PASS (the line printed); 1: no record or a line fails (named); 2: bad args
# a record holds one artefact block at the top level, or an "artefacts" list (one block per entry's artefact:
# kind fleet | windows | mac | hive, each from its own non-AVX-512 box with its own eight lines); --artefact asks
# for that kind's block, so a Mac entry publishes on the Mac canary and the Windows entry waits for its own
# tools/ci/canary-check.sh --form prints the record form (every field, with what it must hold)
# tools/ci/canary-check.sh --self-test
#
# The record (tools/ci/canary/<sha>.json):
# sha the release tip's commit (the file name's sha, full or 8+)
# artefact {url or path, sha256}: what was installed, the published artefact itself, not a box's native build
# box {host, isa_line}: a non-AVX-512 box; isa_line is the kit-isa clean line read on it (tools/ci/kit-isa-check.sh)
# or the host's cpu flags line showing no avx512
# datadir {path, empty_at_start: true, read_back}: the empty datadir before the install
# sync {genesis_height: 0, tip_height, seconds, read_back}: genesis to the network's tip
# mining {minutes >= 5, refusals: 0, accepted_blocks >= 1, read_back}
# shard {claimed: true, proved: true, paid_or_queued: "paid" | "queued", read_back}
# quit {bounded: true, seconds, read_back}: the process ends on its own quit inside the bound
# lines_read_back a list of the eight line names, each with an evidence path on a box
# verdict "PASS" (anything else is not a canary record for publishing)
# recorded_at, recorded_by UTC stamp, the lane
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"; ROOT="${CANARY_ROOT:-$(cd "$HERE/../.." && pwd -P)}"
DIR="${CANARY_DIR:-$ROOT/tools/ci/canary}"
form() {
cat <<'EOF'
{
"sha": "<release tip commit, full>",
"artefact": {"url": "https://dl.igneum.network/public/<file>", "sha256": "<64 hex>"},
"box": {"host": "build-N or <name>", "isa_line": "kit-isa: clean ... | cpu flags: no avx512"},
"datadir": {"path": "/srv/canary/<sha>/data", "empty_at_start": true, "read_back": "build-N:/srv/canary/<sha>/01-datadir.txt"},
"sync": {"genesis_height": 0, "tip_height": 0, "seconds": 0, "read_back": "build-N:/srv/canary/<sha>/02-sync.txt"},
"mining": {"minutes": 5, "refusals": 0, "accepted_blocks": 1, "read_back": "build-N:/srv/canary/<sha>/03-mining.txt"},
"shard": {"claimed": true, "proved": true, "paid_or_queued": "paid", "read_back": "build-N:/srv/canary/<sha>/04-shard.txt"},
"quit": {"bounded": true, "seconds": 0, "read_back": "build-N:/srv/canary/<sha>/05-quit.txt"},
"lines_read_back": [
{"line": "install", "evidence": "build-N:/srv/canary/<sha>/00-install.txt"},
{"line": "box", "evidence": "build-N:/srv/canary/<sha>/00-box.txt"},
{"line": "datadir", "evidence": "build-N:/srv/canary/<sha>/01-datadir.txt"},
{"line": "sync", "evidence": "build-N:/srv/canary/<sha>/02-sync.txt"},
{"line": "mining", "evidence": "build-N:/srv/canary/<sha>/03-mining.txt"},
{"line": "shard", "evidence": "build-N:/srv/canary/<sha>/04-shard.txt"},
{"line": "quit", "evidence": "build-N:/srv/canary/<sha>/05-quit.txt"},
{"line": "version", "evidence": "build-N:/srv/canary/<sha>/00-version.txt"}
],
"verdict": "PASS",
"recorded_at": "<UTC>",
"recorded_by": "<lane>"
}
EOF
}
check() { # <sha> [kind] -> prints the verdict line; 0 pass, 1 fail
local sha="$1" kind="${2:-}" f
case "$sha" in *[!0-9a-fA-F]*|"") echo "canary: REFUSED: '$sha' is not a commit sha"; return 1 ;; esac
[ "${#sha}" -ge 8 ] || { echo "canary: REFUSED: the sha must be 8 hex or more"; return 1; }
f=""; for c in "$DIR/$sha.json" "$DIR"/"${sha:0:8}"*.json; do [ -f "$c" ] && { f="$c"; break; }; done
[ -n "$f" ] || { echo "canary: REFUSED: no fresh-install canary record for ${sha:0:12} (rule 33: tools/ci/canary/<sha>.json, the eight lines read back; tools/ci/canary-check.sh --form)"; return 1; }
python3 - "$f" "$sha" "$kind" <<'PY'
import json, sys
f, sha = sys.argv[1], sys.argv[2].lower()
try: r = json.load(open(f))
except Exception as e: print(f"canary: REFUSED: {f} is not JSON: {e}"); sys.exit(1)
def red(m): print(f"canary: REFUSED: {sha[:12]}: {m} ({f})"); sys.exit(1)
def need(obj, key, typ=None):
if key not in obj: red(f"the record has no '{key}'")
v = obj[key]
if typ and not isinstance(v, typ): red(f"'{key}' is not {typ.__name__ if not isinstance(typ, tuple) else '/'.join(t.__name__ for t in typ)}")
return v
rs = str(need(r, 'sha')).lower()
if not (rs.startswith(sha) or sha.startswith(rs)) or len(rs) < 8: red(f"the record's sha {rs[:12]} is not {sha[:12]}")
def box_path(p): return isinstance(p, str) and (':/' in p or p.startswith('/srv/'))
want_kind = sys.argv[3] if len(sys.argv) > 3 else ''
blocks = r['artefacts'] if isinstance(r.get('artefacts'), list) else [r]
if not blocks: red('the artefacts list is empty')
if want_kind:
blocks = [b for b in blocks if str(b.get('kind', b.get('artefact', {}).get('kind', ''))).lower() == want_kind.lower()]
if not blocks: red(f"no artefact block of kind '{want_kind}' (the entry's own canary: fleet, windows, mac or hive, each from its own box)")
lines_out = []
for r_ in blocks:
r = r_
a = need(r, 'artefact', dict)
if not (a.get('url') or a.get('path')): red("artefact names no url or path (the published artefact, not a native build)")
if not (isinstance(a.get('sha256'), str) and len(a['sha256']) == 64): red("artefact.sha256 is not 64 hex")
b = need(r, 'box', dict)
if not b.get('host'): red("box.host is empty")
isa = str(b.get('isa_line', '')).lower()
if not isa or ('clean' not in isa and 'no avx512' not in isa and 'no avx-512' not in isa): red("box.isa_line does not read a non-AVX-512 box (the kit-isa clean line or a cpu flags line with no avx512)")
d = need(r, 'datadir', dict)
if d.get('empty_at_start') is not True: red("datadir.empty_at_start is not true")
if not box_path(d.get('read_back')): red("datadir.read_back is not a box path")
s = need(r, 'sync', dict)
if s.get('genesis_height') != 0: red("sync.genesis_height is not 0 (the sync starts at genesis)")
if not (isinstance(s.get('tip_height'), int) and s['tip_height'] > 0): red("sync.tip_height is not a positive height")
if not box_path(s.get('read_back')): red("sync.read_back is not a box path")
m = need(r, 'mining', dict)
if not (isinstance(m.get('minutes'), (int, float)) and m['minutes'] >= 5): red("mining.minutes is under 5")
if m.get('refusals') != 0: red(f"mining.refusals is {m.get('refusals')!r}, not 0")
if not (isinstance(m.get('accepted_blocks'), int) and m['accepted_blocks'] >= 1): red("mining.accepted_blocks is under 1")
if not box_path(m.get('read_back')): red("mining.read_back is not a box path")
h = need(r, 'shard', dict)
if h.get('claimed') is not True or h.get('proved') is not True: red("shard.claimed and shard.proved must both be true")
if h.get('paid_or_queued') not in ('paid', 'queued'): red("shard.paid_or_queued must be 'paid' or 'queued'")
if not box_path(h.get('read_back')): red("shard.read_back is not a box path")
q = need(r, 'quit', dict)
if q.get('bounded') is not True: red("quit.bounded is not true")
if not (isinstance(q.get('seconds'), (int, float)) and q['seconds'] >= 0): red("quit.seconds is not a number")
if not box_path(q.get('read_back')): red("quit.read_back is not a box path")
lines = need(r, 'lines_read_back', list)
names = {str(x.get('line')) for x in lines if isinstance(x, dict)}
want = {'install', 'box', 'datadir', 'sync', 'mining', 'shard', 'quit', 'version'}
missing = sorted(want - names)
if missing: red(f"lines_read_back lacks {', '.join(missing)}")
for x in lines:
if isinstance(x, dict) and x.get('line') in want and not box_path(x.get('evidence')): red(f"line {x.get('line')} has no box evidence path")
top = json.load(open(f))
v = r.get('verdict', top.get('verdict'))
if v != 'PASS': red(f"verdict is {v!r}, not PASS")
if not (r.get('recorded_at') or top.get('recorded_at')) or not (r.get('recorded_by') or top.get('recorded_by')): red("recorded_at or recorded_by is empty")
lines_out.append(f"{r.get('kind', 'artefact')} on {b['host']} ({str(a.get('sha256'))[:12]}…): genesis to {s['tip_height']} in {s.get('seconds')} s, {m['minutes']} min mining, 0 refusals, {m['accepted_blocks']} accepted, one shard {h['paid_or_queued']}, quit in {q['seconds']} s; eight lines read back")
print(f"canary: PASS: {sha[:12]} fresh-install canary: " + '; '.join(lines_out) + f"; recorded {blocks[0].get('recorded_at') or json.load(open(f)).get('recorded_at')} by {blocks[0].get('recorded_by') or json.load(open(f)).get('recorded_by')}")
PY
}
if [ "${1:-}" = --form ]; then form; exit 0; fi
if [ "${1:-}" = --self-test ]; then
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0; export CANARY_DIR="$d"
SHA=0123456789abcdef0123456789abcdef01234567
form | python3 -c "
import json,sys; r=json.load(sys.stdin); r['sha']='$SHA'; r['artefact']['sha256']='ab'*32; r['box']={'host':'build-4','isa_line':'kit-isa: clean: no AVX-512 encoding in 3 binaries'}
r['sync'].update(tip_height=3847, seconds=412); r['mining'].update(minutes=5, refusals=0, accepted_blocks=2); r['quit']['seconds']=3; r['recorded_at']='2026-10-08T21:30:00Z'; r['recorded_by']='shipper'
json.dump(r, open('$d/$SHA.json','w'))"
bash "$ME" "$SHA" >/dev/null 2>&1 || { echo "self-test failed: a complete PASS record was refused: $(bash "$ME" "$SHA" 2>&1)"; fails=1; }
bash "$ME" "${SHA:0:12}" >/dev/null 2>&1 || { echo "self-test failed: the record was not found by its short sha"; fails=1; }
out=$(bash "$ME" ffffffffffff 2>&1) && { echo "self-test failed: a sha with no record passed"; fails=1; }; case "$out" in *"no fresh-install canary record"*) ;; *) echo "self-test failed: the missing record was not named: $out"; fails=1 ;; esac
mut() { python3 -c "
import json,sys; p='$d/$SHA.json'; r=json.load(open(p)); exec(sys.argv[1]); json.dump(r, open(p,'w'))" "$1"; }
for case in "r['mining']['refusals']=1|refusals" "r['mining']['minutes']=4|under 5" "r['mining']['accepted_blocks']=0|accepted_blocks" "r['shard']['proved']=False|shard.claimed and shard.proved" "r['shard']['paid_or_queued']='lost'|paid_or_queued" "r['quit']['bounded']=False|quit.bounded" "r['datadir']['empty_at_start']=False|empty_at_start" "r['sync']['genesis_height']=100|genesis" "r['box']['isa_line']='avx512f present'|non-AVX-512" "r['lines_read_back']=r['lines_read_back'][:7]|lacks version" "r['lines_read_back'][2]['evidence']='notes.txt'|no box evidence" "r['verdict']='FAIL'|not PASS" "r['artefact']={'url':'x','sha256':'short'}|sha256"; do
cp "$d/$SHA.json" "$d/keep.json"; mut "${case%%|*}"
out=$(bash "$ME" "$SHA" 2>&1) && { echo "self-test failed: a record with ${case%%|*} passed"; fails=1; }
case "$out" in *"${case##*|}"*) ;; *) echo "self-test failed: the failing line was not named for ${case%%|*}: $out"; fails=1 ;; esac
cp "$d/keep.json" "$d/$SHA.json"
done
# the multi-artefact form: one file per sha, an artefacts list (fleet, windows, mac), each block its own box and eight lines; --artefact picks one
python3 -c "
import json; p='$d/$SHA.json'; r=json.load(open(p)); blk={k:r[k] for k in ('artefact','box','datadir','sync','mining','shard','quit','lines_read_back')}
import copy; arts=[]
for kind,host in (('fleet','lp-4090-11'),('windows','pc-2'),('mac','mini')):
b=copy.deepcopy(blk); b['kind']=kind; b['box']['host']=host; b['artefact']['url']='https://dl.igneum.network/public/'+kind; arts.append(b)
m={'sha':r['sha'],'artefacts':arts,'verdict':'PASS','recorded_at':r['recorded_at'],'recorded_by':'shipper'}; json.dump(m, open(p,'w'))"
bash "$ME" "$SHA" >/dev/null 2>&1 || { echo "self-test failed: a three-artefact record was refused: $(bash "$ME" "$SHA" 2>&1)"; fails=1; }
bash "$ME" "$SHA" --artefact mac >/dev/null 2>&1 || { echo "self-test failed: the mac block of a three-artefact record was refused"; fails=1; }
out=$(bash "$ME" "$SHA" --artefact hive 2>&1) && { echo "self-test failed: a kind with no block passed"; fails=1; }; case "$out" in *"no artefact block of kind 'hive'"*) ;; *) echo "self-test failed: the missing kind was not named: $out"; fails=1 ;; esac
python3 -c "import json; p='$d/$SHA.json'; r=json.load(open(p)); r['artefacts'][1]['mining']['refusals']=2; json.dump(r, open(p,'w'))"
out=$(bash "$ME" "$SHA" --artefact windows 2>&1) && { echo "self-test failed: a failing windows block passed under --artefact windows"; fails=1; }
bash "$ME" "$SHA" --artefact mac >/dev/null 2>&1 || { echo "self-test failed: the mac block was refused because the windows block fails (each entry publishes on its own canary)"; fails=1; }
out=$(bash "$ME" "$SHA" 2>&1) && { echo "self-test failed: the whole record passed with one failing block"; fails=1; }
echo "not json" > "$d/$SHA.json"; bash "$ME" "$SHA" >/dev/null 2>&1 && { echo "self-test failed: a non-JSON record passed"; fails=1; }
out=$(bash "$ME" "not-a-sha" 2>&1) && { echo "self-test failed: a non-sha argument passed"; fails=1; }
[ "$fails" = 0 ] && echo "self-test passed: a complete fresh-install canary record is PASS and found by its short sha; no record, a refusal, under five minutes, no accepted block, an unproved or lost shard, an unbounded quit, a non-empty datadir, a sync not from genesis, an AVX-512 box, a missing read-back line, a non-box evidence path, a non-PASS verdict or a bad artefact hash is refused and named; a one-file-per-sha record with an artefacts list (fleet, windows, mac, hive) passes whole or per --artefact kind, and a failing block fails its own kind and the whole, never another kind"
exit $fails
fi
KIND=""; SHA_ARG=""
while [ $# -gt 0 ]; do case "$1" in --artefact) KIND="$2"; shift 2 ;; *) SHA_ARG="$1"; shift ;; esac; done
[ -n "$SHA_ARG" ] || { echo "usage: $0 <sha> [--artefact <kind>] | --form | --self-test" >&2; exit 2; }
check "$SHA_ARG" "$KIND"

View file

@ -0,0 +1 @@
# Rule 33 fresh-install canary records, one per release tip sha (tools/ci/canary-check.sh --form; the publish path refuses an entry without a PASS record here)

View file

@ -80,7 +80,9 @@ the registry's evidence rules: a PASS names evidence that exists, a touched evid
the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)
F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)
the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)
rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (the record check and the publish guard, self-tests)
the REV suite is generated from Review B's findings and dispatch and matches them (self-test, then the tree)
F03 (Review B): every component's own pin equals packaging/release-manifest.json where a release branch carries one (self-test, then the tree)
the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)
the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)
every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)

View file

@ -24,7 +24,7 @@ export function suite(tr) {
return { code: 'INT', title: 'INT: the master edition\'s integration gates (R1, the full-system review)', source: 'docs/plans/igneum-2.0-master/traceability.json integration_gates', gate: 'Integration gates closed', owner: 'the owner lanes per the coordinator\'s crosswalk', fixtures: ['F0', 'F5'], summary: `${tests.length} integration gates; each reads NOT RUN until its owner lane records a run`, tests };
}
export function merge(reg, s) { const old = (reg.suites || []).find((x) => x.code === s.code); const live = new Map((old?.tests || []).map((t) => [t.id, t]));
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record', 'evidence_records', 'blocked_on', 'method_recorded', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
if (old?.notes) s.notes = old.notes; reg.suites = [...(reg.suites || []).filter((x) => x.code !== s.code), s]; return reg; }
const canon = (o) => JSON.stringify(o, (k, v) => (v && typeof v === 'object' && !Array.isArray(v)) ? Object.fromEntries(Object.keys(v).sort().map((x) => [x, v[x]])) : v);
function mapReasons(map, s) { const mapped = new Set(Object.values(map.cells || {}).flatMap((c) => c.cases || [])); map.not_run = map.not_run || {}; let n = 0;
@ -33,8 +33,8 @@ if (args.includes('--self-test')) {
let fails = 0; const tr = { integration_gates: [{ id: 'INT-01', requirement: 'r one', status: 'PROPOSED / NOT RUN', source: 'R1' }, { id: 'INT-05', requirement: 'r five', status: 'x', source: 'R1' }, { id: 'INT-07', requirement: 'r seven', status: 'x', source: 'R1' }] };
const s = suite(tr); if (!(s.tests[2].definition && /V6-12/.test(s.tests[2].definition) && s.tests[2].accept === 'r seven')) { console.log('self-test failed: INT-07 does not carry V6-12 as a definition beside its verbatim requirement'); fails = 1; }
if (!(s.tests.length === 3 && s.tests[0].id === 'INT-01' && s.tests[0].accept === 'r one' && /proving/.test(s.tests[0].owner_lane) && /node/.test(s.tests[1].owner_lane) && s.tests[0].run_status === 'NOT RUN' && s.tests[0].source[0] === 'R1')) { console.log('self-test failed: the INT cases are not shaped from the gates with the crosswalk owners'); fails = 1; }
const reg = { suites: [{ code: 'INT', notes: [{ text: 'n' }], tests: [{ id: 'INT-01', run_status: 'NOT RUN', in_progress_since: 't', run_id: 'r' }] }] }; const m = merge(reg, s); const i = m.suites.find((x) => x.code === 'INT');
if (!(i.tests[0].in_progress_since === 't' && i.tests[1].run_status === 'NOT RUN' && i.notes?.length === 1)) { console.log('self-test failed: regenerating lost live fields or notes'); fails = 1; }
const reg = { suites: [{ code: 'INT', notes: [{ text: 'n' }], tests: [{ id: 'INT-01', run_status: 'NOT RUN', in_progress_since: 't', evidence_records: { 'c:x': { decision: 'PASS' } }, run_id: 'r' }] }] }; const m = merge(reg, s); const i = m.suites.find((x) => x.code === 'INT');
if (!(i.tests[0].in_progress_since === 't' && i.tests[0].evidence_records?.['c:x']?.decision === 'PASS' && i.tests[1].run_status === 'NOT RUN' && i.notes?.length === 1)) { console.log('self-test failed: regenerating lost live fields or notes'); fails = 1; }
const map = { cells: { c: { cases: ['INT-01'] } }, not_run: {} }; if (!(mapReasons(map, s) === 2 && !map.not_run['INT-01'] && /node lane/.test(map.not_run['INT-05']) && /CI steward/.test(map.not_run['INT-07']))) { console.log('self-test failed: the map reasons'); fails = 1; }
if (!fails) console.log('self-test passed: one INT case per integration gate, the requirement verbatim, source R1, NOT RUN, the owner from the crosswalk; regenerating keeps live fields and notes; unmapped gates get a NOT RUN reason naming the owner'); process.exit(fails);
}

View file

@ -283,7 +283,7 @@ success 4 u push run
node tools/ci/test-record.mjs --record tools/ci/batches/r-branch.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "batch r-branch"
git checkout -q master; printf '{"run_id":"r-master","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > /tmp/r-master.json
node tools/ci/test-record.mjs --record /tmp/r-master.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "master row" ) >/dev/null 2>&1
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse branch) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with replay" 2>&1 && python3 -c "
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse branch) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with replay" 2>&1 && python3 -c "
import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c['id']:c.get('run_id') for s in d['suites'] for c in s['tests']}; print('rows', t)" )
case "$out" in *"'X-1': 'r-branch'"*"'X-2': 'r-master'"*|*"'X-2': 'r-master'"*"'X-1': 'r-branch'"*) ;; *) echo "self-test failed: the batch replay did not land both the branch's row and master's row: $out"; fails=1 ;; esac
# the page race: the branch adds cell c3 to the map (page regenerated), master adds c4 (page regenerated); the merge regenerates the page with both
@ -295,8 +295,17 @@ import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c
# both at once: the branch records a batch (its registry copy conflicts with master's) and adds a map cell; the page must regenerate
# after the registry is rebuilt, never from a copy with conflict markers (8 October 2026, 20:24 UK: the REV landing lost to this)
( cd "$rb" && git checkout -q -b both pbase && printf '{"run_id":"r-both","manifest_sha":"m","method":"native","cells":[{"cell":"c1","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-both.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-both.json >/dev/null && python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c5']={'command':'v','box_class':'b','fixtures':[],'cases':['X-1']}; json.dump(m,open('tools/ci/test-map.json','w'))" && node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m both ) >/dev/null 2>&1
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge both" 2>&1 && grep -c -E '^### c[45]$' docs/plans/igneum-2.0-test-harness-map.md && python3 -c "import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); print('ok' if any(c.get('run_id')=='r-both' for s in d['suites'] for c in s['tests']) else 'no-batch')" )
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge both" 2>&1 && grep -c -E '^### c[45]$' docs/plans/igneum-2.0-test-harness-map.md && python3 -c "import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); print('ok' if any(c.get('run_id')=='r-both' for s in d['suites'] for c in s['tests']) else 'no-batch')" )
case "$out" in *replayed*2*ok*) ;; *) echo "self-test failed: a landing with both a batch and a map change did not land both (the page before the registry rebuild?): $out"; fails=1 ;; esac
# a MODIFIED batch replays too (8 October 2026, 21:4x UK: a re-record of the kills batch on a branch never reached master's rows because
# the replay read added batches only): master carries r-mod.json, the branch re-records it as FAIL, the merge must move X-2 to FAIL
( cd "$rb" && git checkout -q master && git tag premod && printf '{"run_id":"r-mod","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-mod.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-mod.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m r-mod && git tag mbase
git checkout -q -b mod mbase && printf '{"run_id":"r-mod","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"FAIL","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-mod.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-mod.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m mod
git checkout -q master && printf '{"run_id":"r-m2","manifest_sha":"m","method":"native","cells":[{"cell":"c1","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-m2.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-m2.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m m2 ) >/dev/null 2>&1 || { echo "self-test failed: the modified-batch fixture did not build"; fails=1; }
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse mod) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=0 && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge mod" 2>&1 && python3 -c "
import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c['id']:c.get('run_status') for s in d['suites'] for c in s['tests']}; print('status', t)" )
case "$out" in *"'X-2': 'FAIL'"*) ;; *) echo "self-test failed: a batch modified on the branch did not replay onto master's registry: $out"; fails=1 ;; esac
( cd "$rb" && git checkout -q master && git reset -q --hard premod && git checkout -q both ) >/dev/null 2>&1 # the fixture back to where the later cases expect it
push_race "To x
! [remote rejected] HEAD -> master (failed to update ref)
remote: error: cannot lock ref 'refs/heads/master': is at a but expected b" || { echo "self-test failed: a lost compare-and-swap was not read as a race"; fails=1; }
@ -313,7 +322,7 @@ error: failed to push some refs" && { echo "self-test failed: a red check was re
rm -rf "$ld/master-landing"; unset IGNEUM_LOCK_SSH IGNEUM_LOCK_DIR IGNEUM_LOCK_CAP IGNEUM_LOCK_STALE IGNEUM_LOCK_POLL
# the branch-side merge of master under the lock: master moved (c4 and a row), the branch (c3 and a batch) takes it with the transforms
( cd "$rb" && git checkout -q both ) >/dev/null 2>&1
out=$( cd "$rb" && git checkout -q both && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE0=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge master into both" branch 2>&1 && git merge-base --is-ancestor master HEAD && echo ancestor && grep -c -E '^### c[45]$' docs/plans/igneum-2.0-test-harness-map.md && python3 -c "import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); print('ok' if any(c.get('run_id')=='r-both' for s in d['suites'] for c in s['tests']) and any(c.get('run_id')=='r-master' for s in d['suites'] for c in s['tests']) else 'rows-lost')" )
out=$( cd "$rb" && git checkout -q both && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE0=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge master into both" branch 2>&1 && git merge-base --is-ancestor master HEAD && echo ancestor && grep -c -E '^### c[45]$' docs/plans/igneum-2.0-test-harness-map.md && python3 -c "import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); print('ok' if any(c.get('run_id')=='r-both' for s in d['suites'] for c in s['tests']) and any(c.get('run_id')=='r-master' for s in d['suites'] for c in s['tests']) else 'rows-lost')" )
case "$out" in *ancestor*2*ok*) ;; *) echo "self-test failed: the branch-side merge of master under the lock did not carry master's rows and cells plus the branch's: $out"; fails=1 ;; esac
[ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix; a registry landing's batches replay onto master's copy at the merge; a map change regenerates the harness page at the merge; a push that lost the ref race retries without re-running the hook; the master-landing lock holds for the whole landing and master merges into the branch under it"
exit $fails
@ -349,7 +358,7 @@ git fetch -q "$REMOTE" master
if ! git merge-base --is-ancestor "$REMOTE/master" "$SHA"; then
echo "merge-to-master: master moved since the branch point ($(git rev-parse --short "$REMOTE/master")); merging it into $BRANCH under the lock"
PRE_SHA="$SHA"; BASE0=$(git merge-base "$SHA" "$REMOTE/master")
BATCHES=$(git diff --name-only --diff-filter=A "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true); NOTES=$(git diff --name-only --diff-filter=A "$BASE0" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true)
BATCHES=$(git diff --name-only --diff-filter=AM "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true); NOTES=$(git diff --name-only --diff-filter=AM "$BASE0" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true)
MAP_CHANGED=0; git diff --quiet "$BASE0" "$SHA" -- "${MAP_PATH:-tools/ci/test-map.json}" 2>/dev/null || MAP_CHANGED=1
REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"; MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}"; PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}"
[ -n "$NOTES" ] || [ "$MAP_CHANGED" = 1 ] || git diff --quiet "$BASE0" "$SHA" -- "$REGISTRY_PATH" 2>/dev/null || BATCHES="${BATCHES:-.}"
@ -366,8 +375,8 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
# every batch the branch added onto master's copy of the registry (tools/ci/test-record.mjs --record, idempotent), so the branch's
# copy is never what lands and rule 26 does not bind the registry path for such a branch (the evidence rules run on the merged result)
REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true)
NOTES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true) # {suite, text}, replayed through test-record.mjs --note-file
BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true)
NOTES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true) # {suite, text}, replayed through test-record.mjs --note-file
REVGEN=0; git diff --quiet "$BASE" "$SHA" -- tools/ci/review-suite.mjs tools/ci/int-suite.mjs docs/analysis/review-2026-10-08-b/findings.json docs/analysis/review-2026-10-08-b/dispatch.md docs/plans/igneum-2.0-master/traceability.json 2>/dev/null || REVGEN=1 # the REV and INT suites regenerate on the merged tree
[ -n "$NOTES" ] || [ "$REVGEN" = 1 ] && BATCHES="${BATCHES:-.}" # the registry is rebuilt from master's copy whenever any transform rides
RULE26_SKIP_PATHS=""; [ -n "$BATCHES" ] && RULE26_SKIP_PATHS="$REGISTRY_PATH"

View file

@ -11,7 +11,13 @@ the worker never answered.
tools/ci/p01-vectors.py --worker <bin> [--worker-arg=X ...] --pack <dir> --reference <file> --count 1000000
(a worker argument that itself starts with "--" must be given as --worker-arg=--serve; argparse reads "--worker-arg --serve" as two options)
[--start 0] [--job-nonces 1048576] [--prehash <64 hex>] [--manifest <sha>] --out <evidence.json>
tools/ci/p01-vectors.py --worker <bin> [--worker-arg X ...] --job-context <job-context.json> --phase 1|2|3 --out <evidence.json>
tools/ci/p01-vectors.py --self-test
The job context (the same-work test, docs/plans/igneum-2.0-same-work-test.md) names two packs and two references (day D and
day D+1), the prehash, the range width (range_log2, 20) and the boundary nonce; phase N runs nonces [(N-1)*2^w, N*2^w): phase 1
under day D, phase 3 under day D+1, phase 2 under day D up to the boundary nonce and day D+1 from it, no job line crossing the
boundary, and the evidence carries the boundary block (the nonce, the program id and day bytes on each side, the two hashes
either side) that the readers are compared on.
The job line is pool.rs's: `job <seq> <prehash hex> <share_target64 hex16> <start nonce> <nonces> <epoch seed bytes hex> <day bytes hex> class=<c> era=<era hex>`.
"""
import argparse, json, os, subprocess, sys, tempfile, time
@ -31,9 +37,36 @@ def pack_fields(pack):
cls = j.get('program_class', '?'); era = j.get('era_seed_bytes', '')
return j.get('seed_bytes', ''), j['dataset']['day_bytes'], cls, era, j.get('program_id', '?'), j.get('generator', '?')
def segments_for(a):
"""[(start, end, pack dir, reference path)] with no job crossing a segment edge; one segment for the plain form."""
if not a.job_context: return [(a.start, a.start + a.count, a.pack, a.reference)], None
jc = json.load(open(a.job_context)); w = int(jc.get('range_log2', 20)); n = int(a.phase)
if n not in (1, 2, 3): raise SystemExit('p01-vectors: --phase must be 1, 2 or 3')
base = os.path.dirname(os.path.abspath(a.job_context))
pth = lambda x: x if os.path.isabs(x) else os.path.join(base, x)
packs, refs = jc['packs'], jc['references']
s0, e0 = (n - 1) << w, n << w
if n == 1: segs = [(s0, e0, pth(packs['D']), pth(refs['D']))]
elif n == 3: segs = [(s0, e0, pth(packs['D1']), pth(refs['D1']))]
else:
b = int(jc['boundary_nonce'])
if not (s0 < b < e0): raise SystemExit(f'p01-vectors: the boundary nonce {b} is not inside phase 2 [{s0}, {e0})')
segs = [(s0, b, pth(packs['D']), pth(refs['D'])), (b, e0, pth(packs['D1']), pth(refs['D1']))]
a.start, a.count = s0, e0 - s0
if jc.get('prehash'): a.prehash = jc['prehash']
if jc.get('manifest') and not a.manifest: a.manifest = jc['manifest']
return segs, jc
def run(a):
ref = read_reference(a.reference)
seed_bytes, day_bytes, cls, era, program_id, generator = pack_fields(a.pack)
segs, jc = segments_for(a)
ref = {}; seg_fields = []
for (ss, se, pack, rpath) in segs:
r = read_reference(rpath); ref.update({k: v for k, v in r.items() if ss <= k < se}); seg_fields.append((ss, se, pack_fields(pack)))
seed_bytes, day_bytes, cls, era, program_id, generator = seg_fields[0][2]
def fields_at(n):
for (ss, se, f) in seg_fields:
if ss <= n < se: return ss, se, f
return None
p = subprocess.Popen([a.worker] + a.worker_arg, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
ready = None; t0 = time.time()
for line in p.stdout:
@ -44,8 +77,9 @@ def run(a):
def feed():
nonlocal seq, start
while start < end and len(pending) < 4:
n = min(a.job_nonces, end - start); seq += 1
p.stdin.write(f"job {seq} {a.prehash} {'f'*16} {start} {n} {seed_bytes} {day_bytes} class={cls} era={era}\n"); p.stdin.flush()
ss, se, (sb, db, c, er, _pid, _gen) = fields_at(start)
n = min(a.job_nonces, end - start, se - start); seq += 1 # a job never crosses a segment edge (the day boundary)
p.stdin.write(f"job {seq} {a.prehash} {'f'*16} {start} {n} {sb} {db} class={c} era={er}\n"); p.stdin.flush()
pending.add(seq); start += n
feed()
for line in p.stdout:
@ -72,7 +106,15 @@ def run(a):
'worker': a.worker, 'worker_args': a.worker_arg, 'device_line': ready, 'manifest_sha': a.manifest, 'prehash': a.prehash,
'nonces': {'start': a.start, 'count': a.count}, 'answered': len(got), 'agree': agree, 'disagree': disagree_count[0], 'missing': len(missing),
'first_disagreements': disagree, 'first_missing': missing[:10], 'worker_errors': errors[:10], 'seconds': round(time.time() - t0, 1), 'at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
ev['verdict'] = 'PASS' if (agree == a.count and not disagree and not missing) else 'FAIL'
if jc is not None:
ev['job_context'] = os.path.abspath(a.job_context); ev['phase'] = int(a.phase); ev['object'] = jc.get('object')
ev['segments'] = [{'start': ss, 'end': se, 'program_id': f[4], 'day_bytes': f[1], 'class': f[2]} for (ss, se, f) in seg_fields]
if len(seg_fields) == 2:
b = seg_fields[1][0]; fb, fa = seg_fields[0][2], seg_fields[1][2]
ev['boundary'] = {'nonce': b, 'program_id_before': fb[4], 'program_id_after': fa[4], 'day_bytes_before': fb[1], 'day_bytes_after': fa[1],
'hash_before': got.get(b - 1), 'hash_after': got.get(b), 'reference_before': ref.get(b - 1), 'reference_after': ref.get(b),
'switched': bool(fb[1] != fa[1] and got.get(b - 1) == ref.get(b - 1) and got.get(b) == ref.get(b))}
ev['verdict'] = 'PASS' if (agree == a.count and not disagree and not missing and (jc is None or len(seg_fields) < 2 or ev['boundary']['switched'])) else 'FAIL'
return ev, (0 if ev['verdict'] == 'PASS' else 1)
disagree_count = [0]; errors = []
@ -92,6 +134,7 @@ for line in sys.stdin:
for k in range(start, start + n):
if os.environ.get("DROP") == "1" and k == 9: continue
h = (k * 0x9e3779b97f4a7c15) % (1 << 64)
if p[7] == "dd" * 19: h ^= 0xdd00dd00dd00dd00 # day D+1's bytes hash differently (a reader on the wrong day disagrees)
if os.environ.get("WRONG") == "1" and k == 7: h ^= 1
print(f"found {seq} {k} {h:016x}", flush=True)
print(f"done {seq} {n} 1.0", flush=True)
@ -107,15 +150,40 @@ for line in sys.stdin:
if not (rc == 1 and ev.get('verdict') == 'FAIL' and ev['disagree'] == 1 and ev['first_disagreements'][0]['nonce'] == 7): print(f"self-test failed: one wrong hash was not a FAIL naming nonce 7: rc={rc} {ev.get('first_disagreements')}"); fails = 1
rc, ev = go({'DROP': '1'}, 'drop')
if not (rc == 1 and ev['missing'] == 1 and ev['first_missing'] == [9]): print(f"self-test failed: an unanswered nonce was not a FAIL naming nonce 9: rc={rc} {ev.get('first_missing')}"); fails = 1
if not fails: print('self-test passed: a clean million-shape run is PASS with the counts; one wrong hash is FAIL naming the nonce, the gpu and cpu hashes; an unanswered nonce is FAIL naming it; the job lines carry the pack fields and the all-pass target')
# the job-context form: two packs (day D cc.., day D+1 dd..), two references, range_log2 4 (16 nonces a phase), boundary 24 inside phase 2
pack2 = os.path.join(d, 'pack2'); os.makedirs(pack2)
json.dump({'seed_bytes': 'aa' * 32, 'program_class': 'v5', 'era_seed_bytes': 'bb' * 32, 'program_id': '0x2', 'generator': 5, 'dataset': {'day_bytes': 'dd' * 19, 'log2_words': 20}}, open(os.path.join(pack2, 'program.json'), 'w'))
refD = os.path.join(d, 'refD.txt'); open(refD, 'w').write(''.join(f"{k} {(k * 0x9e3779b97f4a7c15) % (1 << 64):016x}\n" for k in range(0, 48)))
refD1 = os.path.join(d, 'refD1.txt'); open(refD1, 'w').write(''.join(f"{k} {((k * 0x9e3779b97f4a7c15) % (1 << 64)) ^ 0xdd00dd00dd00dd00:016x}\n" for k in range(0, 48)))
jc = os.path.join(d, 'job-context.json')
json.dump({'object': 'fake', 'prehash': '00' * 31 + '01', 'range_log2': 4, 'boundary_nonce': 24, 'manifest': 'deadbeef', 'packs': {'D': pack, 'D1': pack2}, 'references': {'D': refD, 'D1': refD1}}, open(jc, 'w'))
def gojc(phase, tag, boundary=None):
if boundary is not None:
j = json.load(open(jc)); j['boundary_nonce'] = boundary; json.dump(j, open(jc, 'w'))
out = os.path.join(d, f'{tag}.json')
r = subprocess.run([sys.executable, __file__, '--worker', sys.executable, '--worker-arg', w, '--job-context', jc, '--phase', str(phase), '--job-nonces', '8', '--out', out], capture_output=True, text=True)
return r.returncode, (json.load(open(out)) if os.path.exists(out) else {}), r.stdout + r.stderr
for ph, s0, pid in ((1, 0, '0x1'), (3, 32, '0x2')):
rc, ev, o = gojc(ph, f'jc{ph}')
if not (rc == 0 and ev.get('verdict') == 'PASS' and ev['nonces'] == {'start': s0, 'count': 16} and ev['segments'][0]['program_id'] == pid and 'boundary' not in ev): print(f"self-test failed: phase {ph} of the job context was not a PASS on its range and pack: rc={rc} {ev.get('nonces')} {ev.get('segments')} {o[-200:]}"); fails = 1
rc, ev, o = gojc(2, 'jc2')
b = ev.get('boundary', {})
if not (rc == 0 and ev.get('verdict') == 'PASS' and ev['nonces'] == {'start': 16, 'count': 16} and b.get('nonce') == 24 and b.get('program_id_before') == '0x1' and b.get('program_id_after') == '0x2' and b.get('switched') is True and len(ev['segments']) == 2): print(f"self-test failed: phase 2 did not switch pack at the boundary nonce 24 with the boundary block: rc={rc} {b} {o[-200:]}"); fails = 1
rc, ev, o = gojc(2, 'jc2bad', boundary=40)
if rc == 0 or 'not inside phase 2' not in o: print(f"self-test failed: a boundary outside phase 2 was not refused: rc={rc} {o[-200:]}"); fails = 1
if not fails: print('self-test passed: a clean million-shape run is PASS with the counts; one wrong hash is FAIL naming the nonce, the gpu and cpu hashes; an unanswered nonce is FAIL naming it; the job-context form runs each phase on its range and pack, splits phase 2 at the boundary nonce with the boundary block, and refuses a boundary outside phase 2; the job lines carry the pack fields and the all-pass target')
return fails
if __name__ == '__main__':
if '--self-test' in sys.argv: sys.exit(self_test())
ap = argparse.ArgumentParser(); ap.add_argument('--worker', required=True); ap.add_argument('--worker-arg', action='append', default=[]); ap.add_argument('--pack', required=True)
ap.add_argument('--reference', required=True); ap.add_argument('--count', type=int, default=1_000_000); ap.add_argument('--start', type=int, default=0); ap.add_argument('--job-nonces', type=int, default=1 << 20)
ap = argparse.ArgumentParser(); ap.add_argument('--worker', required=True); ap.add_argument('--worker-arg', action='append', default=[]); ap.add_argument('--pack', default='')
ap.add_argument('--reference', default=''); ap.add_argument('--count', type=int, default=1_000_000); ap.add_argument('--start', type=int, default=0); ap.add_argument('--job-nonces', type=int, default=1 << 20)
ap.add_argument('--prehash', default='00' * 31 + '01'); ap.add_argument('--manifest', default=''); ap.add_argument('--out', required=True)
a = ap.parse_args(); ev, rc = run(a)
ap.add_argument('--job-context', default=''); ap.add_argument('--phase', type=int, default=0)
a = ap.parse_args()
if a.job_context and not a.phase: ap.error('--job-context needs --phase 1|2|3')
if not a.job_context and not (a.pack and a.reference): ap.error('--pack and --reference, or --job-context with --phase')
ev, rc = run(a)
os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True); json.dump(ev, open(a.out, 'w'), indent=2)
print(f"p01-vectors: {ev.get('verdict', 'ERROR')}: answered {ev.get('answered')} agree {ev.get('agree')} disagree {ev.get('disagree')} missing {ev.get('missing')} in {ev.get('seconds')} s -> {a.out}")
sys.exit(rc)

View file

@ -179,7 +179,9 @@ tree_checks() {
run "the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)" bash tools/ci/kit-isa-check.sh --self-test
run "F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)" bash tools/ci/proof-rule-bypass-check.sh --self-test
run "the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)" python3 tools/ci/test-map-merge.py --self-test
run "rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (the record check and the publish guard, self-tests)" bash -c 'bash tools/ci/canary-check.sh --self-test >/dev/null && bash packaging/ota/publish-manifest.sh --self-test-canary-guard >/dev/null'
run "the REV suite is generated from Review B's findings and dispatch and matches them (self-test, then the tree)" bash tools/ci/review-suite-check.sh
run "F03 (Review B): every component's own pin equals packaging/release-manifest.json where a release branch carries one (self-test, then the tree)" bash -c 'bash tools/ci/release-manifest-check.sh --self-test >/dev/null && bash tools/ci/release-manifest-check.sh'
run "the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)" bash -c 'node tools/ledger/export-public.mjs --self-test && node tools/ledger/export-public.mjs --check'
run "the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)" node tools/ledger-page.mjs --self-test
run "every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)" bash tools/ci/workflow-timeouts-check.sh --self-test

View file

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# F03 (Review B, 8 October 2026): one release manifest (packaging/release-manifest.json) pins node, generator, dataset policy,
# acceptance rule, host ABI, miner app, pool, proof guests, verifying keys and activation; every component's own pin must equal it:
# 1. packaging/windows/node-source.pin == manifest.node.sha
# 2. the node fork's packaging/pow-freeze.txt carries manifest.generator.fingerprint (the fork at <fork dir>, when given)
# 3. proving/igneum-prove/elf/manifest.json's elf and vk sha256s == manifest.proof_guests, and the files on disk hash to them
# 4. the pool's vendored node checkout (pool/../vendor/igneum-node, when present) is at manifest.node.sha
# tools/ci/release-manifest-check.sh [--tree <dir>] [<fork dir>] exit 0 when every pin agrees, 1 with every disagreement named
# tools/ci/release-manifest-check.sh --self-test
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
check() { # <tree> [<fork dir>] ; prints "red ..." lines
local tree="$1" fork="${2:-}" rc=0
python3 - "$tree" "$fork" <<'PY' || rc=1
import json, sys, os, hashlib, subprocess
tree, fork = sys.argv[1], sys.argv[2]; red = []
m = json.load(open(os.path.join(tree, 'packaging/release-manifest.json')))
pin = os.path.join(tree, 'packaging/windows/node-source.pin')
if os.path.exists(pin):
p = open(pin).read().split()[0] if open(pin).read().split() else ''
if not (p.startswith(m['node']['sha']) or m['node']['sha'].startswith(p)): red.append(f"red node-source.pin reads {p}, the manifest pins node {m['node']['sha']}")
else: red.append('red packaging/windows/node-source.pin is missing')
pm_path = os.path.join(tree, 'proving/igneum-prove/elf/manifest.json')
if os.path.exists(pm_path):
pm = json.load(open(pm_path))
for g in ('shard', 'aggregator'):
for k in ('elf_sha256', 'vk_sha256'):
if pm[g][k] != m['proof_guests'][g][k]: red.append(f"red proof guest {g} {k}: the proving manifest reads {pm[g][k][:18]}, the release manifest {m['proof_guests'][g][k][:18]}")
for fk, sk in (('elf', 'elf_sha256'), ('vk', 'vk_sha256')):
fp = os.path.join(tree, 'proving/igneum-prove/elf', pm[g][fk])
if os.path.exists(fp):
h = '0x' + hashlib.sha256(open(fp, 'rb').read()).hexdigest()
if h != m['proof_guests'][g][sk]: red.append(f"red proof guest {g} {fk} on disk hashes to {h[:18]}, the release manifest pins {m['proof_guests'][g][sk][:18]}")
else: red.append('red proving/igneum-prove/elf/manifest.json is missing')
# provenance (V6-10, the proving lane's class, 8 October 2026): a proving manifest that names its source_commit must name a commit
# the tree's HEAD contains; a manifest pinned from a commit this tree never carried (the 5 October manifest had no provenance at all)
# is red. A manifest without source_commit is a note, not a red, until igneum-prove-pin writes one everywhere.
if os.path.exists(pm_path) and os.path.isdir(os.path.join(tree, '.git')) or os.path.exists(pm_path) and os.path.isfile(os.path.join(tree, '.git')):
sc = pm.get('source_commit')
if sc:
r = subprocess.run(['git', '-C', tree, 'merge-base', '--is-ancestor', sc, 'HEAD'], capture_output=True, text=True)
if r.returncode != 0: red.append(f"red proving manifest source_commit {sc[:12]} is not an ancestor of this tree's HEAD (pinned from a commit this branch never carried)")
else: print('release-manifest: note: the proving manifest names no source_commit (pre-provenance pin)')
if fork:
fz = os.path.join(fork, 'packaging/pow-freeze.txt')
if os.path.exists(fz):
if m['generator']['fingerprint'] not in open(fz).read(): red.append(f"red the fork's packaging/pow-freeze.txt does not carry the manifest's generator fingerprint {m['generator']['fingerprint'][:16]}")
else: red.append(f'red {fz} is missing')
try:
head = subprocess.run(['git', '-C', fork, 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
if head and not head.startswith(m['node']['sha']): red.append(f"red the fork checkout is at {head[:8]}, the manifest pins node {m['node']['sha']}")
except Exception: pass
vend = os.path.join(tree, 'vendor/igneum-node')
if os.path.isdir(vend):
head = subprocess.run(['git', '-C', vend, 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
if head and not head.startswith(m['node']['sha']): red.append(f"red the pool's vendored node checkout is at {head[:8]}, the manifest pins node {m['node']['sha']} (the shadow_reps seam closes by a build against the pinned node)")
nodeas = os.path.join(tree, 'pool/src/node.rs')
if os.path.exists(nodeas) and 'struct EpochSeeds' in open(nodeas).read(): red.append('red pool/src/node.rs redefines EpochSeeds; it must import kaspa_pow::igneum::EpochSeeds')
# V6-12 (R1 p. 213, the master): the signed manifest binds lockfile hashes, kernel and host binaries, supported devices and drivers, the prover
# server patch, memory thresholds and tuner identity; a manifest without the block reads BLOCKED on INT-07, never a pass of it
v = m.get('v6_12') or {}
for k in ('lockfile_sha256', 'kernel_binaries', 'host_binaries', 'supported_devices', 'supported_drivers', 'prover_server_patch', 'memory_thresholds', 'tuner_identity', 'signature'):
if k not in v: print(f'note V6-12 field {k} is not in the manifest (INT-07 reads BLOCKED until it is)')
for r in red: print(r)
sys.exit(1 if red else 0)
PY
return $rc
}
if [ "${1:-}" = --self-test ]; then
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0
mk() { local t="$d/$1"; mkdir -p "$t/packaging/windows" "$t/proving/igneum-prove/elf" "$t/pool/src"; printf 'elf' > "$t/proving/igneum-prove/elf/a.elf"; printf 'vk' > "$t/proving/igneum-prove/elf/a.vk"
local es="0x$(printf 'elf' | shasum -a 256 | cut -d' ' -f1)" vs="0x$(printf 'vk' | shasum -a 256 | cut -d' ' -f1)"
printf '{"shard":{"elf":"a.elf","elf_sha256":"%s","vk":"a.vk","vk_sha256":"%s"},"aggregator":{"elf":"a.elf","elf_sha256":"%s","vk":"a.vk","vk_sha256":"%s"}}\n' "$es" "$vs" "$es" "$vs" > "$t/proving/igneum-prove/elf/manifest.json"
printf '{"node":{"sha":"%s"},"generator":{"fingerprint":"ffff0000"},"proof_guests":{"shard":{"elf_sha256":"%s","vk_sha256":"%s"},"aggregator":{"elf_sha256":"%s","vk_sha256":"%s"}}}\n' "$2" "$es" "$vs" "$es" "$vs" > "$t/packaging/release-manifest.json"
printf '%s\n' "$3" > "$t/packaging/windows/node-source.pin"; printf 'use kaspa_pow::igneum::EpochSeeds;\n' > "$t/pool/src/node.rs"; }
mk agree abcd1234 abcd1234; mk pinoff abcd1234 ffff1234
bash "$ME" --tree "$d/agree" >/dev/null 2>&1 || { echo "self-test failed: agreeing pins were refused: $(bash "$ME" --tree "$d/agree" 2>&1)"; fails=1; }
out=$(bash "$ME" --tree "$d/pinoff" 2>&1) && { echo "self-test failed: a node-source pin off the manifest passed"; fails=1; }; case "$out" in *"node-source.pin reads ffff1234"*) ;; *) echo "self-test failed: the pin was not named: $out"; fails=1 ;; esac
printf 'elf2' > "$d/agree/proving/igneum-prove/elf/a.elf"; out=$(bash "$ME" --tree "$d/agree" 2>&1) && { echo "self-test failed: a guest file that hashes off the manifest passed"; fails=1; }; case "$out" in *"on disk hashes to"*) ;; *) echo "self-test failed: the hash drift was not named: $out"; fails=1 ;; esac
printf 'elf' > "$d/agree/proving/igneum-prove/elf/a.elf"; mkdir -p "$d/fork/packaging"; printf '# f\nffff0000 c1 v5 2026\n' > "$d/fork/packaging/pow-freeze.txt"
bash "$ME" --tree "$d/agree" "$d/fork" >/dev/null 2>&1 || { echo "self-test failed: a fork carrying the fingerprint was refused: $(bash "$ME" --tree "$d/agree" "$d/fork" 2>&1)"; fails=1; }
printf '# f\n00000000 c1 v5 2026\n' > "$d/fork/packaging/pow-freeze.txt"; out=$(bash "$ME" --tree "$d/agree" "$d/fork" 2>&1) && { echo "self-test failed: a fork without the fingerprint passed"; fails=1; }
printf 'pub struct EpochSeeds {}\n' > "$d/agree/pool/src/node.rs"; mkdir -p "$d/agree/vendor/igneum-node" && ( cd "$d/agree/vendor/igneum-node" && git init -q && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m x ) >/dev/null 2>&1
out=$(bash "$ME" --tree "$d/agree" 2>&1) && { echo "self-test failed: a redefined EpochSeeds and an unpinned vendored node passed"; fails=1; }; case "$out" in *"redefines EpochSeeds"*) ;; *) echo "self-test failed: the seam was not named: $out"; fails=1 ;; esac
# provenance: a git tree whose proving manifest names a source_commit HEAD contains passes; one naming a commit HEAD never carried is red
mk prov abcd1234 abcd1234; ( cd "$d/prov" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -q -m x ) >/dev/null 2>&1; sc=$(git -C "$d/prov" rev-parse HEAD)
python3 - "$d/prov/proving/igneum-prove/elf/manifest.json" "$sc" <<'PY2'
import json,sys; p=sys.argv[1]; d=json.load(open(p)); d['source_commit']=sys.argv[2]; json.dump(d,open(p,'w'))
PY2
bash "$ME" --tree "$d/prov" >/dev/null 2>&1 || { echo "self-test failed: a manifest whose source_commit HEAD contains was refused: $(bash "$ME" --tree "$d/prov" 2>&1)"; fails=1; }
python3 - "$d/prov/proving/igneum-prove/elf/manifest.json" <<'PY2'
import json,sys; p=sys.argv[1]; d=json.load(open(p)); d['source_commit']='0'*40; json.dump(d,open(p,'w'))
PY2
out=$(bash "$ME" --tree "$d/prov" 2>&1) && { echo "self-test failed: a manifest pinned from a commit the tree never carried passed"; fails=1; }; case "$out" in *"not an ancestor"*) ;; *) echo "self-test failed: the provenance red was not named: $out"; fails=1 ;; esac
[ "$fails" = 0 ] && echo "self-test passed: agreeing pins pass; a node-source pin, a guest file's hash, a fork freeze file, the pool's vendored node checkout, a redefined EpochSeeds or a proving manifest pinned from a commit the tree never carried is red and named"
exit $fails
fi
TREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; FORK=""
while [ $# -gt 0 ]; do case "$1" in --tree) TREE="$2"; shift 2 ;; *) FORK="$1"; shift ;; esac; done
[ -f "$TREE/packaging/release-manifest.json" ] || { echo "release-manifest: no packaging/release-manifest.json on this tree: not a release branch, nothing to pin"; exit 0; }
rc=0; out=$(check "$TREE" "$FORK") || rc=1
printf '%s\n' "$out" | sed -n 's/^red /release-manifest: RED: /p; s/^note /release-manifest: /p' | grep . || true
[ "$rc" = 0 ] && echo "release-manifest: every component's own pin equals packaging/release-manifest.json"
exit $rc

View file

@ -36,7 +36,7 @@ function suite(findings, dispatchMd, prefix, sourceNote, master) {
}
function merge(reg, s) { // replace the suite of the same code, keeping live fields of cases that already exist
const old = (reg.suites || []).find((x) => x.code === s.code); const live = new Map((old?.tests || []).map((t) => [t.id, t]));
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record', 'evidence_records', 'blocked_on', 'method_recorded', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
reg.suites = [...(reg.suites || []).filter((x) => x.code !== s.code), s]; return reg;
}
if (args.includes('--self-test')) {
@ -49,9 +49,9 @@ if (args.includes('--self-test')) {
if (!(s.tests[0].title === 'r one' && s.tests[0].accept === 'r one')) { console.log('self-test failed: the title and accept are not the regression line verbatim'); fails = 1; }
if (!(s.tests[0].owner_lane === 'lane x, lane y' && s.tests[2].owner_lane === 'lane z')) { console.log(`self-test failed: owners not read from the dispatch table: ${s.tests.map((t) => t.owner_lane)}`); fails = 1; }
if (!(s.tests[0].finding === 'R2-F01' && s.tests[0].priority === 'P0' && s.tests[0].run_status === 'NOT RUN' && s.tests[0].method.includes('Automated'))) { console.log('self-test failed: finding, priority, NOT RUN or method missing'); fails = 1; }
const reg = { suites: [{ code: 'GOV', tests: [] }, { code: 'REV', tests: [{ id: 'R2-F01-R01', run_status: 'NOT RUN', in_progress_since: 't', run_id: 'r9' }] }] };
const reg = { suites: [{ code: 'GOV', tests: [] }, { code: 'REV', tests: [{ id: 'R2-F01-R01', run_status: 'NOT RUN', in_progress_since: 't', evidence_records: { 'c:x': { decision: 'PASS' } }, run_id: 'r9' }] }] };
const m = merge(JSON.parse(JSON.stringify(reg)), s); const rev = m.suites.find((x) => x.code === 'REV');
if (!(m.suites.length === 2 && rev.tests.length === 3 && rev.tests[0].in_progress_since === 't' && rev.tests[0].run_id === 'r9' && rev.tests[1].run_status === 'NOT RUN')) { console.log('self-test failed: a regenerated suite did not keep the existing case\'s live fields or dropped another suite'); fails = 1; }
if (!(m.suites.length === 2 && rev.tests.length === 3 && rev.tests[0].in_progress_since === 't' && rev.tests[0].evidence_records?.['c:x']?.decision === 'PASS' && rev.tests[0].run_id === 'r9' && rev.tests[1].run_status === 'NOT RUN')) { console.log('self-test failed: a regenerated suite did not keep the existing case\'s live fields or dropped another suite'); fails = 1; }
const map = { cells: { c1: { cases: ['R2-F01-R01'] } }, not_run: { 'REV-F02-1': 'old' } }; const n = mapReasons(map, s);
if (!(n === 2 && !('R2-F01-R01' in map.not_run) && /lane z/.test(map.not_run['REV-F02-1']) && /lane x/.test(map.not_run['REV-F01-2']))) { console.log(`self-test failed: the map's NOT RUN reasons: ${JSON.stringify(map.not_run)} n=${n}`); fails = 1; }
if (!fails) console.log('self-test passed: one case per required regression with the id <prefix>-<finding>-<n>, the line verbatim as title and accept, the owner from the dispatch table, finding and priority carried, NOT RUN; regenerating keeps live fields and the other suites; every unmapped case gets a NOT RUN reason naming its owner lane in the map, a mapped one loses it');

View file

@ -36,8 +36,9 @@ BS_ROUTE_STATE_1="free=1 slots=2 load1=80" BS_ROUTE_STATE_2="free=3 slots=3 load
BS_ROUTE_STATE_1="free=2 slots=2 load1=70" BS_ROUTE_STATE_2="free=3 slots=3 load1=5" expect "load 70 stays on build-1 (the line is 80)" build 1 0
# the ssh path itself under the hook's shell (/bin/bash is 3.2 on the Mac; the pool lane found `BS_SSH_OPTS[@]: unbound variable`
# at the first unpinned route, 7 Oct 2026): a host file pointing at a port nothing answers on must read "down" in a few seconds,
# not die on an unset array
printf 'build@127.0.0.1\n' > "$t/hosts"
# not die on an unset array. Port 1 in the ssh URI form (8 Oct 2026, the workers-page lane): on a build box sshd answers on
# 127.0.0.1:22 and a forwarded agent logs in, so the bare address read "free=..." there and the gate was red only off the Mac
printf 'ssh://build@127.0.0.1:1\n' > "$t/hosts"
out=$(IGNEUM_BUILD_KEY="$t/no-such-key" /bin/bash -c '. infra/build-server/lib.sh; bs_box_state 1' 2>&1 || true)
if [ "$out" = down ]; then echo "route-spill: the ssh probe under /bin/bash $(/bin/bash -c 'echo $BASH_VERSION') reads an unreachable box as down"
else echo "route-spill: the ssh probe under /bin/bash failed: '$out' (expected 'down')" >&2; fail=1; fi

View file

@ -225,8 +225,8 @@
}
},
"bench:pc1-packs": {
"command": "tools/ca3-v4-amend/pc1-ca4-packs.ps1 on PC 1 (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)",
"box_class": "PC 1 bench",
"command": "tools/ca3-v4-amend/pc1-ca4-packs.ps1 on the project's own rig (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)",
"box_class": "the project's own rig bench",
"fixtures": [
"F0",
"F1"
@ -245,8 +245,8 @@
}
},
"bench:pc1-amd": {
"command": "tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on PC 1 (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)",
"box_class": "PC 1 bench",
"command": "tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on the project's own rig (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)",
"box_class": "the project's own rig bench",
"fixtures": [
"F0",
"F1"
@ -422,7 +422,7 @@
"VER-08"
],
"coverage": {
"VER-03": "partial: a payment receipt is labelled payment only when the receipt is proven against the segment statement's receipts commitment under the finality certificate, inclusion-only receipts read inclusion, a tampered receipt and a flipped status fail; run on the Devnet 3 fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction",
"VER-03": "partial: a payment receipt is labelled payment only when the receipt is proven against the segment statement's receipts commitment under the finality certificate, inclusion-only receipts read inclusion, a tampered receipt and a flipped status fail; run on the the earlier devnet fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction",
"VER-04": "partial: the oracle's trust() names the deployer-installed table and the unchecked aggregator signature, a second hash at a stored certificate index is refused, a root claim at an unknown index is refused, another chain id is refused (tools/reference-apps/oracle/test.mjs on Sepolia); Review B F04: the Sepolia verifiers apply the two-thirds rule only, carry no lock-kind field, and an under-threshold (recovery-rule) certificate fails closed in submitCertificate, so no stored root is a recovery lock (the DEX lane's docs/bridge/light-client-bridge.md paragraph); the claimed-option review of the trust model is the reviewer's",
"VER-05": "partial: the public node and the reference reader on build-1 executed the 2.0 devnet from genesis with no snapshot (unit ExecStart without --igneum-exec-snapshot), and the pages fetch headers, certificates, coinbase bodies and account proofs from the public read RPC only; the archive and availability model beyond the 2,048-block state ring is the OPS no-founder exercise",
"VER-06": "partial: the read service never serves a certificate from another network, a withheld header in the path and a corrupted proof node are detected, stale data prints its lock age beside every balance, and a client told no certificate never falls back to a trusted RPC balance; detection on the fault network F4 is not run",
@ -576,8 +576,8 @@
}
},
"bench:amd-intel-energy": {
"command": "the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (PC 1, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/",
"box_class": "PC 1 and PC 2 bench (OpenCL)",
"command": "the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (the project's own rig, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/",
"box_class": "the project's own rig and PC 2 bench (OpenCL)",
"fixtures": [
"F0",
"F1"
@ -631,7 +631,7 @@
}
},
"pc:install-update": {
"command": "signed jobs and relay runs on PC 1 (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)",
"command": "signed jobs and relay runs on the project's own rig (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)",
"box_class": "PC (the two Windows PCs; nothing on the Mac)",
"fixtures": [
"F2"
@ -646,17 +646,68 @@
"UX-07": "partial: the install classes (the payload's stop step, the detached installer under a job, the installer refusing a wrong-version name, a stale install flag) and the no-click resume; the recovery-instruction and canary rows are the shipper's",
"OPS-03": "partial: the update path's read-back and the signed manifest's verify on the PC (a tampered or rolled-back payload is refused by the engine's manifest check); authority separation is the shipper's review"
}
},
"harness:release-manifest": {
"command": "bash tools/ci/release-manifest-check.sh (self-test, then the tree; the pins, the proof guests' hashes on disk, the fork freeze, the pool's vendored node, the proving manifest's source_commit provenance) and bash tools/ci/build-from-manifest.sh --box N on the release tree (kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host from packaging/release-manifest.json with the node vendored at the manifest's sha)",
"box_class": "gate",
"fixtures": [
"F0"
],
"cases": [
"R2-F03-R01"
],
"coverage": {
"R2-F03-R01": "full for the components the tree builds (node, miner, pool, app, prove-host from one manifest, no unpublished vendor tree); the GPU workers are the kit's cross-build and read under the shipper's kit-isa line, not this cell"
}
},
"harness:same-work": {
"command": "tools/ci/p01-vectors.py --job-context <job-context.json> --phase 1|2|3 per reader (CUDA, OpenCL, Metal workers), igneum-miner --recheck-vectors (node), igneum-pow hash-bound (CPU reference), the pool's member-side re-check; docs/plans/igneum-2.0-same-work-test.md",
"box_class": "release (the readers on their pods and boxes)",
"fixtures": [
"F0"
],
"cases": [
"R2-F03-R02",
"R2-F03-R03"
],
"coverage": {
"R2-F03-R02": "partial until every reader has run: bit-for-bit agreement per nonce across node, CPU reference, CUDA, OpenCL, Metal and pool on one job context",
"R2-F03-R03": "partial: the day-boundary transition in phase 2 (the one transition every live network crosses hourly at 60x); a class rotation is the same test on a research object until a v6 floor is set"
}
},
"canary:fresh-install": {
"command": "the rule 33 fresh-install canary on a release tip (the founder, 8 October 2026): install from the published artefact on a non-AVX-512 box with an empty datadir, sync genesis to tip, five minutes mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read back; the record tools/ci/canary/<sha>.json (one file per sha, an artefacts list: fleet, windows, mac, hive) read by tools/ci/canary-check.sh <sha> [--artefact kind], which publish-manifest.sh and publish-public.sh refuse without",
"box_class": "release (a non-AVX-512 box with an empty datadir)",
"fixtures": [
"F0"
],
"cases": [
"INT-07"
],
"coverage": {
"INT-07": "partial: V6-12's clean-install half (one published object installed fresh, synced, mined, proved and paid on one host per artefact); the cross-host agreement half (node, pool, CPU verifier, each GPU host across activation) is harness:same-work's"
}
},
"review:k-lane-shadow-k": {
"command": "floor lane 2's placed and routed cores on ASAP7 in tools/chip-model/rtl (the rows in docs/analysis/class-v6/floor/shadow-k.md); the register landing 50ff1611f recorded ADV-06 against it before the recorder existed",
"box_class": "none",
"fixtures": [],
"cases": [
"ADV-06"
],
"coverage": {
"ADV-06": "partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains"
}
}
},
"not_run": {
"GOV-02": "the approval is recorded in the registry's approval field; the automated half (thresholds frozen before any run_status) is the gate rule landing by 21:00",
"GOV-04": "the evidence vault F9 (raw and negative evidence preserved) is the gate rule landing by 21:00: a PASS must carry its evidence file",
"GOV-08": "the stale-evidence rule (evidence older than the manifest sha reads NOT RUN) is the gate rule landing by 21:00",
"GPU-04": "the memory-clock ladder has no harness tonight: PC 1 mines nothing under the Devnet 3 off order",
"GPU-04": "the memory-clock ladder has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order",
"GPU-06": "accepted work under ordinary connectivity needs the fault network F4",
"GPU-07": "the sustained thermal and power soak has no harness tonight: PC 1 mines nothing under the Devnet 3 off order",
"GPU-07": "the sustained thermal and power soak has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order",
"POW-05": "amortised cheap winning attempts are the attack lanes' grind and era harnesses (tools/attack/f7-era, f9-grind), not in the release matrix; their rows come from those lanes",
"ADV-06": "process-advantage separation is the adversary lanes' chip study",
"ROT-03": "miner-voted bring-forward needs a vote harness on the fault network F4",
"ROT-04": "seed-selection resistance is the census harness (the class v6 invention lane), not yet in the matrix",
"EVM-01": "no EVM conformance-vector harness is mapped tonight; the exec suite does not run the reference test vectors",
@ -743,7 +794,6 @@
"INT-04": "INT-04 (R1 integration gate): the gate's harness is the owner lane's (pool lane (a1c484c48a62948c2)); not yet named in the map",
"INT-05": "INT-05 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map",
"INT-06": "INT-06 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map",
"INT-07": "INT-07 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
"INT-08": "INT-08 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
"INT-09": "INT-09 (R1 integration gate): the gate's harness is the owner lane's (app lane); not yet named in the map",
"INT-10": "INT-10 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
@ -762,9 +812,6 @@
"R2-F02-R01": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"R2-F02-R02": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"R2-F02-R03": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"R2-F03-R01": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"R2-F03-R02": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"R2-F03-R03": "R2-F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"R2-F04-R01": "R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"R2-F04-R02": "R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"R2-F04-R03": "R2-F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",

View file

@ -1,6 +1,13 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="robots" content="noindex">
<title>Igneum Workers</title>
<!-- the main site's icon set (site/index.html's head, the ember mark); the files sit next to this page in /srv/workers -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
<style>
:root {
@ -195,17 +202,31 @@ function arc(pct, cls) {
}
const tone = (pct, warn = 70, bad = 90) => pct >= bad ? 'bad' : pct >= warn ? 'warn' : '';
const FEEDS = { 'igneum-build-1': 'https://build.igneum.network/workers.json', 'igneum-build-2': 'https://build-2.igneum.network/workers.json', 'igneum-build-3': 'https://build-3.igneum.network/workers.json' };
// one card per entry of boxes[] in the merged workers.json (merge-workers.mjs on build-1, every 20 s); no per-box feed map (8 October 2026:
// the old three-entry map drew build-1 to build-3 only while the merge carried nine boxes)
const STALE_S = 120;
const BOX_NOTES = { 'igneum-build-3': 'down since 16:40 UK, Hetzner switch fault' };
const PROVISIONING = 'provisioning, first build logged when the sources land';
function boxState(b) { // live | down | provisioning, with the line the cards print
const age = b && b.collected_at ? (Date.now() - Date.parse(b.collected_at)) / 1000 : null;
const stale = !b || b.down || !b.cores || (b.source && b.source.ok === false) || age === null || age > STALE_S;
if (b && stale && BOX_NOTES[b.name]) return { kind: 'down', note: BOX_NOTES[b.name] };
if (b && b.down) return { kind: 'down', note: String(b.reason || 'no answer from this box') };
if (!b || !b.cores) return { kind: 'provisioning', note: PROVISIONING };
if (!b.log || !b.log.present) return { kind: 'provisioning', note: PROVISIONING, live: true };
return { kind: 'live', stale: age !== null && age > STALE_S };
}
const buildHeld = b => (b.slots && b.slots.held || []).filter(h => /^build-\d+$/.test(h.slot)).length;
const boxesOf = d => (Array.isArray(d.boxes) && d.boxes.length ? d.boxes : d.box ? [{ ...d.box, source: d.sources && d.sources.box, headline: d.headline }] : []);
function serverSection(b, i) {
const id = `srv${i}`;
if (!b || !b.cores) return `<section class="server" aria-label="${esc(b && b.name || 'build server')}"><div class="head"><div><div class="name">${esc(b && b.name || 'build server')}<small>build server</small></div><div class="facts"><span>${esc(b && b.source && b.source.error || 'provisioning, first build logged when the sources land')}</span></div></div></div><div class="cores">${Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join('')}</div><div class="corelabel"><span>no sample</span><span></span></div></section>`;
const st = boxState(b);
if (st.kind === 'down' || !b || !b.cores) return `<section class="server" aria-label="${esc(b && b.name || 'build server')}"><div class="head"><div><div class="name">${esc(b && b.name || 'build server')}<small>build server</small></div><div class="facts"><span>${esc(st.note || PROVISIONING)}</span></div></div></div><div class="cores">${Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join('')}</div><div class="corelabel"><span>no sample</span><span></span></div></section>`;
const per = (b.cpu && b.cpu.per_core) || []; const n = Math.max(per.length, b.cores || 96);
const temps = (b.temps || []).map(x => `${esc(x.name)} <b>${x.c.toFixed(0)}°</b>`);
const m = b.mem || {}, d = b.disk || {}, sc = b.sccache;
const hit = sc && sc.hit_rate_pct !== null && sc.hit_rate_pct !== undefined ? sc.hit_rate_pct : null;
const stale = b.source && !b.source.ok;
const stale = (b.source && !b.source.ok) || st.stale;
return `<section class="server ${b.running && b.running.length ? 'hot' : ''}" aria-label="${esc(b.name)}">
<div class="head"><div><div class="name">${esc(b.name)}<small>${esc(b.os || 'build server')}</small></div><div class="facts">${[`<b>${b.cores}</b> threads`, `<b>${esc(fmtUptime(b.uptime_s))}</b> up`, `load <b>${(b.load || []).map(x => Number(x).toFixed(1)).join(' / ')}</b>`, b.kernel ? `kernel <b>${esc(b.kernel)}</b>` : ''].filter(Boolean).map(x => `<span>${x}</span>`).join('')}</div></div>
<div class="facts">${[...temps, b.net ? `net ↓<b>${esc(fmtBps(b.net.rx_bps))}</b> ↑<b>${esc(fmtBps(b.net.tx_bps))}</b>` : '', `<span title="${esc(b.collected_at)}" ${stale ? 'style="color:var(--warn)"' : ''}>${stale ? 'STALE, ' : ''}read ${esc(ago(b.collected_at))}</span>`].filter(Boolean).map(x => `<span>${x}</span>`).join('')}</div></div>
@ -219,25 +240,39 @@ function serverSection(b, i) {
<div class="g">${arc(b.slots ? (buildHeld(b) / Math.max(1, b.slots.count)) * 100 : 0, 'good')}<div><div class="l">Build slots</div><div class="v">${b.slots ? `${buildHeld(b)}/${b.slots.count}` : '–'}</div><div class="s">${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}</div></div></div>
</div></section>`;
}
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = bx.length ? bx.map(serverSection).join('') : serverSection(null, 0); }
// the Mac mini (igneum-mini, the Mac build box, M6): its own collector pushes mini.json to build-1; until then its card reads no report yet
const MINI = { name: 'igneum-mini', label: 'the Mac build box, M6' };
const miniLive = () => { const m = D && D.mini; return m && m.source && m.source.ok && m.cores ? m : null; };
function miniSection() {
const m = miniLive();
if (!m) return `<section class="server" aria-label="${MINI.name}"><div class="head"><div><div class="name">${MINI.name}<small>${MINI.label}</small></div><div class="facts"><span>no report yet</span></div></div></div></section>`;
return serverSection({ ...m, name: MINI.name, os: MINI.label, log: m.log || { present: true } }, 99);
}
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = (bx.length ? bx.map(serverSection).join('') : serverSection(null, 0)) + miniSection(); }
function slotBar(slots, count) {
const held = new Map((slots && slots.held || []).map(h => [h.slot, h]));
const names = Array.from({ length: count }, (_, i) => `build-${i}`);
return `<div class="slots">${names.map(nm => `<i class="${held.has(nm) ? 'held' : ''}" title="${esc(nm)}${held.has(nm) ? ': ' + esc(held.get(nm).label) : ': free'}"></i>`).join('')}</div>`;
}
const MAC_COPY = 'nothing builds here except the one DMG; the mini carries macOS binaries from tonight';
function renderCrew() {
const mac = D.mac, pcs = (D.pcs && D.pcs.machines) || [];
const cards = [];
for (const bx of boxesOf(D)) {
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">Hetzner, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.'}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
const st = boxState(bx);
if (st.kind === 'down') { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">build server</div></div>${pill('down', 'error')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
if (st.kind === 'provisioning' && !st.live) { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('provisioning', 'queued')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">Hetzner, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : (st.kind === 'provisioning' ? esc(st.note) : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.')}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
else cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('unreachable', 'error')}</div><div class="doing">${esc(bx && bx.source && bx.source.error || 'no facts from this box')}</div></div>`);
}
if (mac) {
const held = mac.slots.held || [], runs = held.filter(h => /^run-/.test(h.slot)), builds = held.filter(h => /^build-/.test(h.slot)), meas = held.filter(h => h.slot === 'measure');
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : 'Nothing under with-lock.sh. macOS binaries and the DMG build here; everything else goes to the box or the PCs.';
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this MacBook: the one DMG only, nothing else builds here (the rule since 7 October 2026); ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'}, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
} else cards.push(`<div class="w"><div class="top"><div><div class="card">MacBook-Pro</div><div class="meta">this Mac</div></div>${pill('no push', 'error')}</div><div class="doing">${esc(D.sources && D.sources.mac && D.sources.mac.error || 'the Mac pusher has not written yet (launchd com.igneum.workers-push, every 60 s)')}</div></div>`);
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : MAC_COPY;
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this MacBook, ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'}, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
} else cards.push(`<div class="w"><div class="top"><div><div class="card">MacBook-Pro</div><div class="meta">this MacBook</div></div>${pill('no push', 'error')}</div><div class="doing">${MAC_COPY}</div></div>`);
{ const m = miniLive(); const held = m && m.slots && m.slots.held || [];
cards.push(`<div class="w"><div class="top"><div><div class="card">${MINI.name}</div><div class="meta">${MINI.label}</div></div>${pill(!m ? 'no report' : held.length ? 'building' : 'idle', !m ? 'queued' : held.length ? 'running' : '')}</div><div class="doing">${!m ? 'no report yet' : held.length ? held.map(h => esc(`${h.slot}: ${h.worktree || h.label || ''}`)).join('<br>') : 'macOS binaries build here from tonight; slot free.'}</div>${m && m.slots ? slotBar(m.slots, m.slots.count || 1) : ''}<div class="foot"><span>${m ? `${(m.recent || []).length} builds logged` : 'its collector pushes to build-1'}</span><span>${m ? esc(ago(m.collected_at)) : ''}</span></div></div>`); }
for (const pc of pcs) {
const r = pc.running;
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(pc.name)}</div><div class="meta">${esc(pc.machine || pc.id)} · ${esc(pc.role)}</div></div>${pill(r ? 'running' : pc.queue.length ? 'queued' : 'idle', r ? 'running' : pc.queue.length ? 'queued' : '')}</div><div class="doing">${r ? `${esc(r.kind)} ${esc(r.job)}${r.title ? '<br>' + esc(r.title) : ''}${r.stage ? `<br><span class="pill">stage ${esc(r.stage)}</span>` : ''}` : pc.queue.length ? `Next: ${esc(pc.queue[0].kind)} ${esc(pc.queue[0].job)}${pc.queue[0].title ? ', ' + esc(pc.queue[0].title) : ''}` : esc(pc.note || 'idle on jobs; the relay polls every 10 min')}</div><div class="foot"><span>${pc.recent.length} reports kept</span><span>${pc.last_report_at ? 'last report ' + esc(ago(pc.last_report_at)) : 'no report in 7 days'}</span></div></div>`);
@ -363,7 +398,7 @@ function renderAnalytics() {
function renderSources() {
const s = D.sources || {};
const one = (name, v, what) => `<span>${esc(name)} <b class="${v && v.ok ? '' : v && v.at ? 'warn' : 'no'}">${v && v.ok ? 'live' : v && v.at ? 'stale' : 'missing'}</b>${v && v.at ? ' ' + esc(ago(v.at)) : ''}${v && v.error ? ': ' + esc(v.error) : ''}${!v || v.ok ? '' : ' · ' + esc(what)}</span>`;
$('sources').innerHTML = [...boxesOf(D).map(b => one(b.name || 'box', b.source || s.box, 'collect.mjs on the box, systemd timer every 30 s')), one('mac', s.mac, 'push.mjs on the Mac, launchd every 60 s'), one('pcs', s.pcs, 'relay intake read by push.mjs'), `<span>${esc(D._feed || '')}: fetched ${esc(ago(D._fetched))}, written ${t(D.generated_at)} by ${esc(D.generated_by || '?')}</span>`, ...boxesOf(D).filter(b => b.log).map(b => `<span>${esc(b.name)} log ${b.log.present ? `${b.log.total} lines${b.log.bad_lines ? `, ${b.log.bad_lines} unreadable` : ''}` : 'not written yet'}</span>`)].join('');
$('sources').innerHTML = [...boxesOf(D).map(b => one(b.name || 'box', b.source || s.box, 'collect.mjs on the box, systemd timer every 30 s')), one('mac', s.mac, 'push.mjs on the Mac, launchd every 60 s'), one('mini', D.mini && D.mini.source, 'its collector on the mini, pushed to build-1'), one('pcs', s.pcs, 'relay intake read by push.mjs'), `<span>${esc(D._feed || '')}: fetched ${esc(ago(D._fetched))}, written ${t(D.generated_at)} by ${esc(D.generated_by || '?')}</span>`, ...boxesOf(D).filter(b => b.log).map(b => `<span>${esc(b.name)} log ${b.log.present ? `${b.log.total} lines${b.log.bad_lines ? `, ${b.log.bad_lines} unreadable` : ''}` : 'not written yet'}</span>`)].join('');
}
function apply(d) {
@ -375,25 +410,14 @@ function apply(d) {
const el = $('servers'); if (el && !el.innerHTML) el.innerHTML = '';
renderServers(); renderCrew(); renderNow(); renderQueue(); renderBackground(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
}
let skipLive = 0, feed = '';
let feed = '';
async function fetchJson(url, ms) { const r = await fetch(`${url}?t=${Date.now()}`, { cache: 'no-store', signal: AbortSignal.timeout(ms) }); if (!r.ok) throw new Error(`http ${r.status}`); return r.json(); }
async function load() {
let edge = null, d = null;
try { const file = demo ? 'workers.demo.json' : 'workers.json'; edge = await fetchJson(`${base}/${file}`, 8000); }
catch (e) { if (!D && (demo || skipLive > 0)) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; } }
catch (e) { if (!D) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; } }
if (demo) { d = edge; feed = 'demo document'; }
else if (skipLive <= 0) {
// the boxes the edge copy knows (or build-1 alone before the first push), each from its own Caddy feed, 4 s each, in parallel
const names = edge ? boxesOf(edge).map(b => b.name).filter(Boolean) : ['igneum-build-1'];
const results = await Promise.all(names.map(n => FEEDS[n] ? fetchJson(FEEDS[n], 4000).then(j => ({ n, j })).catch(() => ({ n, j: null })) : Promise.resolve({ n, j: null })));
const liveOnes = results.filter(r => r.j);
if (liveOnes.length) {
const first = liveOnes[0].j;
const boxes = results.map(r => r.j ? { ...r.j.box, headline: r.j.headline, source: { ok: true, at: r.j.generated_at } } : (edge && boxesOf(edge).find(b => b.name === r.n)) || { name: r.n, source: { ok: false, error: 'no live feed and no edge copy for this box' } });
d = { ...first, boxes, mac: first.mac || (edge && edge.mac), pcs: first.pcs || (edge && edge.pcs), baselines: first.baselines || (edge && edge.baselines), sources: { ...(first.sources || {}), box: boxes[0].source } };
feed = liveOnes.length === results.length ? `live from ${liveOnes.length === 1 ? 'the box' : liveOnes.length + ' boxes'}` : `live from ${liveOnes.map(r => r.n).join(', ')}, edge copy for the rest`;
} else skipLive = 4; // nothing answered: use the edge copy for the next minute before trying again
} else skipLive--;
else { d = edge; feed = edge ? 'the merged copy from build-1' : feed; }
if (!d) { d = edge; feed = 'edge copy, up to 5 min behind the box'; }
if (!d) { if (!D) { $('now').innerHTML = $('queue').innerHTML = `<div class="empty">No workers.json next to this page yet and no box answers. The Mac pusher writes the copy every 60 s and the fleet deploy carries it within 5 min.</div>`; } return; }
d._feed = feed; apply(d);