Merge build/master into reference-apps: master's steward files carried, the reference-apps cells and batches re-recorded, pages rebuilt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 19:38:55 +00:00
commit 1ca1eed751
27 changed files with 2458 additions and 279 deletions

View file

@ -6,8 +6,8 @@ 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) | 2826f37e (2ea7b43f + the DMG README line + the node pin ef0f2ed8 + elf/prior from key-succession-pin; the app crate byte-identical to 2ea7b43f's, so the pow and app cells read on 2ea7b43f cover it by content; a re-run on 2826f37e records the literal) | the shipper's line 19:3x | shipper (ae892a8b0f78fe31c) |
| Node sha for the roll | ef0f2ed8 = 291ee6ae (key succession over 417c4a57) + the workspace version 2.0.1 | the node lane's line 19:0x | node lane (a283f5f0d364ceef0) |
| 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) |
| 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 |
| Object digest | be5f406802cec1227a5ef50d42181ab42444f79af170775b22c85cb9a917273b (4cdcc488 and every sha after it; no live digest move since) | the node lane's lines | node lane |
@ -37,6 +37,13 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
| Trust anchors (light client, oracle) | BLOCKED | reference-apps lane | VER-01 and VER-04 read BLOCKED |
| The class v6 freeze line in packaging/pow-freeze.txt | open: three D1 candidates on the node mirrors (class-v6-node 3af510ec on c245d50b9 fingerprint a65e4c5a; class-v6-node-b 46e7ac18 with the acceptance fix, fingerprint 6cdd922a; class-v6-node-review 2f6eb9e9 on 04442d9ca, fingerprint 7a1dec1c, six suites green on build-1 at 19:3x) | hash lane, node lane | class v5 stays the mining class |
## Resolved by the founder (8 October 2026, 19:57 UK, through the coordinator)
| Field | Ruling |
|---|---|
| F04 (Review B), the recovery lock | kept, and always labelled "recovery", never "final", on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site) |
| F14 (Review B), fleet control and the public client | the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root |
## Signatures (by 23:30 UK, 8 October 2026)
| Role | Name or lane | Commit signed | Time |

View file

@ -1,6 +1,6 @@
# Igneum 2.0 test harness map
Generated from tools/ci/test-map.json by tools/ci/test-map-doc.mjs; edit the JSON, never this page. Registry: docs/plans/igneum-2.0-test-registry.json (128 cases).
Generated from tools/ci/test-map.json by tools/ci/test-map-doc.mjs; edit the JSON, never this page. Registry: docs/plans/igneum-2.0-test-registry.json (172 cases).
Rule: a case maps to a cell only where the cell's tests visibly answer it; coverage names what the cell proves and what remains; a mapped cell's green writes RUNNING, PASS only when coverage is full and the evidence file exists; an automated case with no cell reads NOT RUN with its reason, never PASS by inference.
@ -335,7 +335,51 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- LEAD-05 Measure control and dependency concentration: observation window
- LEAD-06 Complete the reliability observation window: observation window
- LEAD-08 Keep leadership claims valid after release: observation window
- REV-F01-1 Valid proof A, warm cache, carried statement B: refuse exactly as a cold node does.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-2 Run valid/invalid contexts in both orders, concurrently and across cache eviction/restart.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-3 Change payout, network, kind, program ID and activation context; no accepted misbinding.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-4 A native two-node test must agree on block validity and payouts despite different cache histories.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F02-1 Release build cannot activate test bypass.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F02-2 Missing oracle or pinned keys prevents service readiness after enforcement activation.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F02-3 Missing proof bytes retry without incorrectly marking a valid block permanently invalid.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F03-1 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: 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
- REV-F03-2 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: 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
- REV-F03-3 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: 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
- REV-F04-1 Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window.: 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
- REV-F04-2 Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality.: 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
- REV-F04-3 Pause-only resume with historical backfill, missing historical data and all old keys returning.: 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
- REV-F04-4 Wallet, receipt and oracle consumers distinguish any weaker recovery state.: 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
- REV-F05-1 Native reference-vs-incremental expression equivalence across all source registers and real instruction updates.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F05-2 Full-kernel output equivalence, registers, spills, wall power and accepted throughput across target GPUs.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F05-3 Adversary physical design includes prefix caching/recomputation cost; no assumed full 63-read cost on every load.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F06-1 Acceptance/reference/emitter evaluate the same activated schedule.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F06-2 Full live-dataset census on unseen seeds and the complete v6 pack.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F06-3 A failed experimental rule does not silently exhaust generation or bypass the intended resource requirement.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F07-1 Same final v6 configuration: mine -> evict -> prove -> aggregate -> submit -> rebuild -> resume; no leaked reservations.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F07-2 OOM, process crash and stale work recover without losing wallet state or silently consuming power.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F07-3 16 GB+ simultaneous mode only after measured peak plus next-epoch headroom; smaller-card modes labelled separately.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F08-1 Report every eligible job outcome, including expired work; a bounded list cannot hide losses.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F08-2 Consumer tiers complete and receive payment for declared jobs before claims about income.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F08-3 Sustained real workload across class transitions, with restart/retry and no publisher intervention.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F09-1 Generate all pass/fail cells directly from the declared inequalities and inputs.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F09-2 Independent feasibility and cost evaluation of the strongest modeled SRAM/hybrid opponent.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F09-3 GPU owners and entrants remain viable under the approved scenario envelope without assuming chip absence or death.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F10-1 Compare exact found nonce/hash sets with full-read mode, including all-hit overflow and zero-hit cases.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F10-2 Test stale jobs, high-32 rollover, tail batches and asynchronous buffer reuse.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F10-3 Compare production throughput and wall energy, not only the isolated kernel timer.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F11-1 Goal switch from an efficiency prior can explore higher core/power when policy allows.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F11-2 Driver, workload, dataset, compiler and device changes invalidate certification while retaining optional hints.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F11-3 Thermal/error/late-share events revert safely, including process or machine crash.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F12-1 Crash before/after broadcast, response timeout, restart before snapshot: no duplicate payment or silent debt loss.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F12-2 Same signed transaction retried, fee replacement reconciled by intent, not a new payment.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F12-3 Receipt reorg and finality pause leave correct pending obligations.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-1 Repeated authorization rejected or atomically replaces and cleans prior state.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-2 Oversized unterminated frame rejected within fixed memory/time budget.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-3 Slow clients and invalid share floods cannot grow unbounded member, nonce or outgoing state.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F14-1 Public build has no default arbitrary remote execution and a documented least-privilege boundary.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
- REV-F14-2 Automatic updates off remains off for urgent manifests until explicit action.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
- REV-F14-3 A compromised fleet/update signing key cannot silently acquire wallet access or activate a consensus change.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
## Count
109 automated cases: 56 mapped to a cell, 58 NOT RUN with a reason.
153 automated cases: 56 mapped to a cell, 102 NOT RUN with a reason.

File diff suppressed because it is too large Load diff

View file

@ -112,6 +112,15 @@ static void checkSource(EmuProg* p, bool defaultDevice, bool noCuda) {
if (bad.empty() && p->headers.count("memhard.h")) bad = firstUnannotated("memhard.h", p->headers["memhard.h"], noCuda);
if (!bad.empty()) { p->log = "emu-nvrtc: " + bad + ": A function without execution space annotations (__host__/__device__/__global__) is considered a host function, and host functions are not allowed in JIT mode (pass -default-device or -DIGNEUM_NO_CUDA)"; p->ok = false; return; }
}
// The select pass (8 October 2026): the worker's own text, no pack involved; it is recorded as module 3 and run by
// d_launch as a host loop (in reverse index order, so the hits land out of nonce order and the worker's sort is exercised)
if (p->name == "igneum_select.cu") {
if (p->src.find("__global__ void igneum_select(") == std::string::npos || p->src.find("atomicAdd(count, 1u)") == std::string::npos) { p->log = "emu-nvrtc: igneum_select.cu is not the select pass this stand-in knows"; p->ok = false; return; }
p->pack = 3; p->ok = true;
std::printf("info emu-nvrtc: igneum_select.cu source check PASS: %zu bytes, the select pass runs as a host loop\n", p->src.size());
std::fflush(stdout);
return;
}
for (int which = 1; which <= 2; ++which) {
std::string dir = packDir(which);
if (dir.empty()) continue;
@ -159,7 +168,7 @@ static nvrtcResult e_compile(nvrtcProgram prog, int numOptions, const char* cons
}
static nvrtcResult e_logSize(nvrtcProgram prog, size_t* n) { *n = ((EmuProg*)prog)->log.size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_log(nvrtcProgram prog, char* out) { std::strcpy(out, ((EmuProg*)prog)->log.c_str()); return NVRTC_SUCCESS; }
static std::string image(EmuProg* p) { return std::string("EMU-IMAGE:") + (p->pack == 2 ? "B" : "A"); }
static std::string image(EmuProg* p) { return std::string("EMU-IMAGE:") + (p->pack == 3 ? "S" : p->pack == 2 ? "B" : "A"); }
static nvrtcResult e_imgSize(nvrtcProgram prog, size_t* n) { *n = image((EmuProg*)prog).size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_img(nvrtcProgram prog, char* out) { std::strcpy(out, image((EmuProg*)prog).c_str()); return NVRTC_SUCCESS; }
static nvrtcResult e_addName(nvrtcProgram prog, const char* e) { ((EmuProg*)prog)->exprs.push_back(e); return NVRTC_SUCCESS; }
@ -210,12 +219,13 @@ static CUresult d_htod(CUdeviceptr dst, const void* src, size_t n) { std::memcpy
static CUresult d_modLoad(CUmodule* m, const void* img) {
const char* s = (const char*)img;
if (std::strncmp(s, "EMU-IMAGE:", 10) != 0) return CUDA_ERROR_INVALID_IMAGE;
*m = (CUmodule)(uintptr_t)(s[10] == 'B' ? 2 : 1);
*m = (CUmodule)(uintptr_t)(s[10] == 'S' ? 3 : s[10] == 'B' ? 2 : 1);
return CUDA_SUCCESS;
}
static CUresult d_modUnload(CUmodule) { return CUDA_SUCCESS; }
static CUresult d_getFn(CUfunction* f, CUmodule m, const char* name) {
int pack = (int)(uintptr_t)m, idx;
if (pack == 3) { if (std::strcmp(name, "igneum_select") != 0) return CUDA_ERROR_NOT_FOUND; *f = (CUfunction)(uintptr_t)7; return CUDA_SUCCESS; }
if (std::strcmp(name, "igneum_cache_fill") == 0) idx = 1;
else if (std::strcmp(name, "igneum_build") == 0) idx = 2;
else if (std::strcmp(name, "igneum_hash_bound") == 0) idx = 3;
@ -255,6 +265,22 @@ static CUresult d_launch(CUfunction f, unsigned gx, unsigned, unsigned, unsigned
}
case 6: emu_launch(emu_pack_b::igneum_hash_bound, gx, bx, dptr<const uint32_t>(params, 0), dptr<uint64_t>(params, 1), arg<uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<emu_pack_b::IgneumInitWords>(params, 4)); return CUDA_SUCCESS;
#endif
case 7: {
// the select pass (8 October 2026): the kernel text's semantics as a host loop, threads visited in reverse so the
// hit table fills out of nonce order (a GPU's atomics give no order either) and the worker's sort is exercised
const uint64_t* out = dptr<const uint64_t>(params, 0); uint32_t n = arg<uint32_t>(params, 1); uint64_t target = arg<uint64_t>(params, 2);
uint32_t* count = dptr<uint32_t>(params, 3); uint64_t* hits = dptr<uint64_t>(params, 4); uint32_t maxHits = arg<uint32_t>(params, 5); uint64_t* sentinel = dptr<uint64_t>(params, 6);
uint64_t threads = (uint64_t)gx * bx;
for (uint64_t t = threads; t-- > 0;) {
uint32_t i = (uint32_t)t;
if (i >= n) continue;
uint64_t h = out[i];
if (h <= target) { uint32_t k = (*count)++; if (k < maxHits) { hits[2u * k] = i; hits[2u * k + 1u] = h; } }
if (i < 32u) sentinel[i] = h;
if (i == 0u) { sentinel[32] = out[n / 2u]; sentinel[33] = out[n - 1u]; }
}
return CUDA_SUCCESS;
}
default: return CUDA_ERROR_INVALID_HANDLE;
}
}

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# The select pass equivalence check (8 October 2026, review B finding F10): the found nonce and hash set of
# --readback select equals the full read-back's, line for line, over the cases the finding names, on one worker
# binary (the CPU emulation from test.sh, or a real GPU build). Known-failed first: IGNEUM_READBACK_FAULT_TEST=1 makes
# the worker skip the counter reset from the second chunk on, so stale hits come back again; the comparison must flag
# that run before the real one counts.
# select_fault_detected the fault run differs from the full read (the check can fail)
# found_set_equals_full_zero_hits z1: target 0, two chunks, no found line in either mode
# found_set_equals_full_partial_hits p1: target 03ff..., about 16 hits per chunk, four chunks
# found_set_equals_full_all_hit_overflow a1: target ffff..., every nonce a hit, every chunk over IG_MAX_HITS (full fallback)
# found_set_equals_full_tail_batch t1: 1120 nonces at 1024 per chunk and 64 per block: a 96-nonce tail with a 32-lane launch
# found_set_equals_full_high32_rollover r1: a job from 2^32 - 32 across the high-word step
# found_set_equals_full_stale_job_queued s1, s2: two job lines queued at once, the second served after the first
# buffer_reuse_hits_then_zero b1 then b2: a hit chunk followed by a zero-target chunk prints nothing stale
# mismatch_error_then_job m1 (no pair for its seeds) errors the same way, c1 after it is served
# Usage: select-check.sh <pack A dir> <out dir> <worker command...> (the command carries --serve --pack <pack A> etc.)
set -euo pipefail
PACK_A="$1"; OUT="$2"; shift 2
mkdir -p "$OUT"
seedline() { sed -n "s/^$2 //p" "$1/seeds.txt"; }
A_EPOCH="$(seedline "$PACK_A" epoch_seed_hex)"; A_DAY="$(seedline "$PACK_A" day_seed_hex)"
[ -n "$A_EPOCH" ] && [ -n "$A_DAY" ] || { echo "FAIL: no seeds.txt in $PACK_A"; exit 1; }
PRE="00000000000000000000000000000000000000000000000000000000000000f1"
OTHER="1111111111111111111111111111111111111111111111111111111111111111"
PART="03ffffffffffffff"
script() {
echo "job z1 $PRE 0000000000000000 0 2048 $A_EPOCH $A_DAY"
echo "job p1 $PRE $PART 2048 4096 $A_EPOCH $A_DAY"
echo "job a1 $PRE ffffffffffffffff 8192 2048 $A_EPOCH $A_DAY"
echo "job t1 $PRE $PART 16384 1120 $A_EPOCH $A_DAY"
echo "job r1 $PRE $PART 4294967264 2048 $A_EPOCH $A_DAY"
echo "job s1 $PRE $PART 32768 2048 $A_EPOCH $A_DAY"
echo "job s2 $PRE $PART 40960 2048 $A_EPOCH $A_DAY"
echo "job b1 $PRE $PART 49152 1024 $A_EPOCH $A_DAY"
echo "job b2 $PRE 0000000000000000 50176 1024 $A_EPOCH $A_DAY"
echo "job m1 $PRE $PART 0 1024 $OTHER $A_DAY"
echo "job c1 $PRE $PART 51200 1024 $A_EPOCH $A_DAY"
echo "quit"
}
run() { # <mode: full|select|fault> <log>
local mode="$1" log="$2"; shift 2
case "$mode" in
full) script | IGNEUM_READBACK=full "$@" > "$log" 2>&1 || true ;;
select) script | IGNEUM_READBACK=select "$@" > "$log" 2>&1 || true ;;
fault) script | IGNEUM_READBACK=select IGNEUM_READBACK_FAULT_TEST=1 "$@" > "$log" 2>&1 || true ;;
esac
}
# the lines both modes must agree on: found, need, error, and done with its wall time stripped
norm() { grep -E '^(found|need|error|done) ' "$1" | sed -E 's/^(done [^ ]+ [0-9]+) [0-9.]+$/\1/'; }
WORKER=("$@")
run full "$OUT/rb-full.log" "${WORKER[@]}"
run select "$OUT/rb-select.log" "${WORKER[@]}"
run fault "$OUT/rb-fault.log" "${WORKER[@]}"
norm "$OUT/rb-full.log" > "$OUT/rb-full.norm"
norm "$OUT/rb-select.log" > "$OUT/rb-select.norm"
norm "$OUT/rb-fault.log" > "$OUT/rb-fault.norm"
grep -q '^ready .* readback full ' "$OUT/rb-full.log" || { echo "FAIL: the full run has no 'readback full' ready line"; exit 1; }
grep -q '^ready .* readback select ' "$OUT/rb-select.log" || { echo "FAIL: the select run has no 'readback select' ready line"; exit 1; }
[ "$(grep -c '^done ' "$OUT/rb-full.norm")" = 10 ] || { echo "FAIL: the full run should finish 10 jobs"; grep -E '^(error|done)' "$OUT/rb-full.log"; exit 1; }
echo "== select_fault_detected (known-failed first: the counter is not reset from the second chunk on)"
if cmp -s "$OUT/rb-full.norm" "$OUT/rb-fault.norm"; then echo "FAIL: the fault run equals the full read; the comparison cannot fail"; exit 1; fi
echo "flagged: $(diff "$OUT/rb-full.norm" "$OUT/rb-fault.norm" | grep -c '^>') stale or extra lines in the fault run"
echo "== found_set_equals_full_* (every case, line for line)"
if ! cmp -s "$OUT/rb-full.norm" "$OUT/rb-select.norm"; then echo "FAIL: select differs from the full read:"; diff "$OUT/rb-full.norm" "$OUT/rb-select.norm" | head -20; exit 1; fi
count() { grep -c "^found $1 " "$2" || true; }
[ "$(count z1 "$OUT/rb-select.norm")" = 0 ] || { echo "FAIL: zero_hits: z1 found lines"; exit 1; }
[ "$(count p1 "$OUT/rb-select.norm")" -gt 0 ] || { echo "FAIL: partial_hits: p1 found nothing"; exit 1; }
[ "$(count a1 "$OUT/rb-select.norm")" = 2048 ] || { echo "FAIL: all_hit_overflow: a1 should find 2048"; exit 1; }
grep -q '^done t1 1120' "$OUT/rb-select.norm" || { echo "FAIL: tail_batch: t1 did not finish 1120"; exit 1; }
[ "$(count t1 "$OUT/rb-select.norm")" -gt 0 ] || { echo "FAIL: tail_batch: t1 found nothing"; exit 1; }
grep -q '^done r1 2048' "$OUT/rb-select.norm" || { echo "FAIL: high32_rollover: r1 did not finish"; exit 1; }
awk '$1 == "found" && $2 == "r1" && $3 + 0 >= 4294967296 { n++ } END { exit n > 0 ? 0 : 1 }' "$OUT/rb-select.norm" || { echo "FAIL: high32_rollover: no found nonce at or past 2^32"; exit 1; }
grep -q '^done s1 2048' "$OUT/rb-select.norm" && grep -q '^done s2 2048' "$OUT/rb-select.norm" || { echo "FAIL: stale_job_queued: s1 or s2 did not finish"; exit 1; }
[ "$(count b1 "$OUT/rb-select.norm")" -gt 0 ] && [ "$(count b2 "$OUT/rb-select.norm")" = 0 ] || { echo "FAIL: buffer_reuse_hits_then_zero: b1 $(count b1 "$OUT/rb-select.norm") found, b2 $(count b2 "$OUT/rb-select.norm")"; exit 1; }
grep -q '^error m1 ' "$OUT/rb-select.norm" && grep -q '^done c1 1024' "$OUT/rb-select.norm" || { echo "FAIL: mismatch_error_then_job: m1 did not error or c1 was not served"; exit 1; }
grep -q 'full fallbacks' "$OUT/rb-select.log" || { echo "FAIL: no transfers line in the select run"; exit 1; }
fb="$(sed -n 's/.*(select, \([0-9]*\) full fallbacks).*/\1/p' "$OUT/rb-select.log" | tail -1)"
[ "$fb" = 2 ] || { echo "FAIL: all_hit_overflow: expected 2 full fallbacks (a1's two chunks), the select run reports '$fb'"; exit 1; }
echo "PASS: select = full over $(grep -c '^found ' "$OUT/rb-select.norm") found lines and 10 jobs (zero hits, partial, all-hit overflow with $fb full fallbacks, a 96-nonce tail, the high-32 rollover, two queued jobs, hits then zero, a mismatch error then a job); the fault run was flagged first"
echo "transfers: $(grep 'transfers per chunk' "$OUT/rb-select.log" | tail -1 | sed 's/^info //')"
echo "transfers: $(grep 'transfers per chunk' "$OUT/rb-full.log" | tail -1 | sed 's/^info //')"

View file

@ -64,3 +64,6 @@ echo "flagged as NVRTC does: $(grep -o 'program.h([0-9]*): [^:]*' "$OUT/check-no
echo "== --serve: two jobs on A, prepare B, a job on B (swap), a job on A after the swap (error), quit"
"$HERE/serve-check.sh" "$OUT/pack-a" "$OUT/pack-b" "$OUT/serve.log" "$OUT/igneum-worker-cuda-emu" --serve --pack "$OUT/pack-a" --batch-log2 13
echo "NVRTC source check PASS for $(grep -c 'source check PASS' "$OUT/serve.log") files in --serve, $(grep -c 'source check PASS' "$OUT/check-a.log") in --check"
echo "== the select pass (8 October 2026): --readback select equals the full read-back, known-failed first (emu/select-check.sh)"
"$HERE/select-check.sh" "$OUT/pack-a" "$OUT/select" "$OUT/igneum-worker-cuda-emu" --serve --pack "$OUT/pack-a" --batch-log2 10 --block-warps 2

90
proto-cuda/nvrtc/serve-bench.sh Executable file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Serving-mode measurement of igneum-worker-cuda (8 October 2026, review B finding F10): the production number beside the
# kernel timer, never interchangeable with it. The worker runs --serve exactly as the miner drives it: a stub pool here
# feeds jobs of 2^B nonces (one queued behind the one in flight, as the miner pipelines them), reads every found line
# (the miner's submit point) and every done line, and quits after N jobs. The clock starts before the worker process
# starts, so the setup (NVRTC compile, cache, dataset, self-test) is in the production number; the steady number runs
# from the ready line. Power is nvidia-smi's board reading at 4 Hz over the same windows (the pod exposes no host
# rail); the worker's own CPU seconds (utime + stime from /proc) stand in for the host-side cost of the read-back and
# the scan. The kernel timer is --bench on the same pack and batch, printed beside the rows.
# serve-bench.sh <worker> <pack dir> <out dir> <run name> <readback: full|select> [jobs 32] [batch-log2 24] [device 0]
# Pid files: every process this script starts writes <out dir>/pids/<run name>.<what>.pid (IGNEUM_PID_DIR overrides the directory) (the fleet's rule: a job is
# stopped through its pid file, never by a name pattern). Output: <out dir>/<run name>.row (one line, key=value) plus the
# worker log, the stub's timeline and the power samples.
set -euo pipefail
W="$1"; PACK="$2"; OUT="$3"; RUN="$4"; MODE="$5"; JOBS="${6:-32}"; B="${7:-24}"; DEV="${8:-0}"
[ "$MODE" = full ] || [ "$MODE" = select ] || { echo "readback must be full or select" >&2; exit 2; }
PIDDIR="${IGNEUM_PID_DIR:-$OUT/pids}" # the fleet keeps pid files under one directory per pod
mkdir -p "$PIDDIR"
LOG="$OUT/$RUN.worker.log"; TL="$OUT/$RUN.timeline"; SMI="$OUT/$RUN.smi"; ROW="$OUT/$RUN.row"
: > "$TL"
now() { date +%s.%N; }
count=$((1 << B))
target="00000fffffffffff" # one hit per 2^20 nonces: 16 found lines per 2^24-nonce job, the serving shape (a block target is far tighter)
seedline() { sed -n "s/^$2 //p" "$1/seeds.txt"; }
EPOCH="$(seedline "$PACK" epoch_seed_hex)"; DAY="$(seedline "$PACK" day_seed_hex)"
[ -n "$EPOCH" ] && [ -n "$DAY" ] || { echo "no seeds.txt in $PACK" >&2; exit 2; }
jobline() { printf 'job %d %064x %s %d %d %s %s\n' "$1" "$1" "$target" $(( $1 * count )) "$count" "$EPOCH" "$DAY"; }
# power sampler first (its own pid file)
nvidia-smi -i "$DEV" --query-gpu=timestamp,power.draw,clocks.sm,temperature.gpu --format=csv,noheader,nounits -lms 250 > "$SMI" 2>/dev/null &
echo $! > "$PIDDIR/$RUN.smi.pid"
sleep 1
t0=$(now)
coproc WK { IGNEUM_READBACK="$MODE" "$W" --serve --pack "$PACK" --device "$DEV" --batch-log2 "$B" --race off --readback "$MODE" 2>&1; }
wkpid=$WK_PID # bash clears WK_PID when the coproc ends; the copy outlives it
echo "$wkpid" > "$PIDDIR/$RUN.worker.pid"
sent=0; done_n=0; found=0; tready=""; tlast=""; cpu=""
: > "$LOG"
while IFS= read -r line <&"${WK[0]}"; do
t=$(now)
echo "$line" >> "$LOG"
case "$line" in
ready*) tready=$t; echo "$t ready" >> "$TL"; jobline $sent >&"${WK[1]}"; sent=$((sent + 1)); jobline $sent >&"${WK[1]}"; sent=$((sent + 1)) ;;
found*) found=$((found + 1)); echo "$t submit $line" >> "$TL" ;;
done*) done_n=$((done_n + 1)); tlast=$t; echo "$t $line" >> "$TL"
if [ "$sent" -lt "$JOBS" ]; then jobline $sent >&"${WK[1]}"; sent=$((sent + 1)); fi
if [ "$done_n" -ge "$JOBS" ]; then
cpu=$(awk '{ printf "%.2f", ($14 + $15) / 100.0 }' "/proc/$wkpid/stat" 2>/dev/null || echo "")
echo "quit" >&"${WK[1]}"
fi ;;
error*) echo "$t $line" >> "$TL" ;;
esac
done
wait "$wkpid" 2>/dev/null || true
t1=$(now)
sleep 1
kill "$(cat "$PIDDIR/$RUN.smi.pid")" 2>/dev/null || true
[ -n "$tready" ] && [ -n "$tlast" ] || { echo "FAIL: no ready or no done line; see $LOG"; tail -5 "$LOG"; exit 1; }
[ "$done_n" = "$JOBS" ] || { echo "FAIL: $done_n of $JOBS jobs done; see $LOG"; exit 1; }
# the power samples over the two windows: nvidia-smi stamps local time "YYYY/MM/DD HH:MM:SS.mmm"
meanw() { # <from epoch s> <to epoch s>: mean W of the samples inside the window, and their count
awk -F', *' -v a="$1" -v b="$2" 'BEGIN { n = 0; s = 0 }
{ gsub("/", "-", $1); split($1, d, " "); cmd = "date -d \"" d[1] " " d[2] "\" +%s.%N"; cmd | getline ts; close(cmd); if (ts >= a && ts <= b) { s += $2; n++ } }
END { if (n) printf "%.1f %d", s / n, n; else printf "0 0" }' "$SMI"
}
read -r wProd nProd <<< "$(meanw "$t0" "$tlast")"
read -r wSteady nSteady <<< "$(meanw "$tready" "$tlast")"
hashes=$(( JOBS * count ))
wallProd=$(awk -v a="$t0" -v b="$tlast" 'BEGIN { printf "%.3f", b - a }')
wallSteady=$(awk -v a="$tready" -v b="$tlast" 'BEGIN { printf "%.3f", b - a }')
setup=$(awk -v a="$t0" -v b="$tready" 'BEGIN { printf "%.3f", b - a }')
mhsProd=$(awk -v h="$hashes" -v w="$wallProd" 'BEGIN { printf "%.2f", h / w / 1e6 }')
mhsSteady=$(awk -v h="$hashes" -v w="$wallSteady" 'BEGIN { printf "%.2f", h / w / 1e6 }')
jProd=$(awk -v w="$wProd" -v s="$wallProd" -v n="$JOBS" 'BEGIN { printf "%.1f", w * s / n }')
jSteady=$(awk -v w="$wSteady" -v s="$wallSteady" -v n="$JOBS" 'BEGIN { printf "%.1f", w * s / n }')
mhwProd=$(awk -v m="$mhsProd" -v w="$wProd" 'BEGIN { if (w > 0) printf "%.4f", m / w; else printf "n/a" }')
mhwSteady=$(awk -v m="$mhsSteady" -v w="$wSteady" 'BEGIN { if (w > 0) printf "%.4f", m / w; else printf "n/a" }')
card=$(nvidia-smi -i "$DEV" --query-gpu=name --format=csv,noheader | tr ' ' '_')
transfers=$(grep 'transfers per chunk' "$LOG" | tail -1 | sed 's/^info //')
ready=$(grep '^ready ' "$LOG" | head -1)
{
printf 'card=%s mode=%s jobs=%d batch_log2=%d hashes=%d found=%d setup_s=%s ' "$card" "$MODE" "$JOBS" "$B" "$hashes" "$found" "$setup"
printf 'prod_wall_s=%s prod_mhs=%s prod_mean_w=%s prod_j_per_batch=%s prod_mh_per_w=%s samples=%s ' "$wallProd" "$mhsProd" "$wProd" "$jProd" "$mhwProd" "$nProd"
printf 'steady_wall_s=%s steady_mhs=%s steady_mean_w=%s steady_j_per_batch=%s steady_mh_per_w=%s samples=%s ' "$wallSteady" "$mhsSteady" "$wSteady" "$jSteady" "$mhwSteady" "$nSteady"
printf 'worker_cpu_s=%s worker_sha=%s\n' "${cpu:-n/a}" "$(sha256sum "$W" | cut -c1-16)"
printf 'ready: %s\n' "$ready"
printf 'transfers: %s\n' "$transfers"
} > "$ROW"
cat "$ROW"

View file

@ -1038,6 +1038,7 @@ struct Options {
std::string pack, arch = "auto";
std::string race = "on", pinned, tuningPath;
int raceBenchMs = 2000, raceBudgetS = 120, raceRounds = 0; // rounds 0 = 1 in --serve, 3 in --race
bool readbackFull = false; // --readback full / IGNEUM_READBACK=full: the pre-select path, 8 bytes per nonce over the bus
};
static void usage() {
@ -1060,7 +1061,9 @@ static void usage() {
" --race-budget-s N a race stops compiling and timing after this (default 120; base is kept)\n"
" --race-rounds N interleaved rounds, best per variant (default 1 in --serve, 3 in --race)\n"
" --variant <name> use this variant without a race (also from the tuning file)\n"
" --tuning <file> the per-card tuning file (default: IGNEUM_TUNING_FILE from the environment)\n", WORKER_VERSION);
" --tuning <file> the per-card tuning file (default: IGNEUM_TUNING_FILE from the environment)\n"
" --readback select|full in --serve: the GPU selects the hits and 34 sentinel words come back (default), or every\n"
" 8-byte result comes back and the host scans it (the path before 8 October 2026; also IGNEUM_READBACK=full)\n", WORKER_VERSION);
}
static Options parseArgs(int argc, char** argv) {
@ -1087,6 +1090,7 @@ static Options parseArgs(int argc, char** argv) {
else if (a == "--batch-log2") o.batchLog2 = std::atoi(next().c_str());
else if (a == "--block-warps") o.blockWarps = std::atoi(next().c_str());
else if (a == "--arch") o.arch = next();
else if (a == "--readback") { std::string v = next(); if (v == "full") o.readbackFull = true; else if (v == "select") o.readbackFull = false; else { std::printf("--readback must be select or full\n"); std::exit(2); } }
else if (a == "--no-prepare") { /* accepted for symmetry with the other workers; prepare is always on here */ }
else if (a == "-h" || a == "--help") { usage(); std::exit(0); }
else { std::printf("unknown argument %s\n", argv[i]); usage(); std::exit(2); }
@ -1098,6 +1102,7 @@ static Options parseArgs(int argc, char** argv) {
if (o.raceBudgetS < 5 || o.raceBudgetS > 540) { std::printf("--race-budget-s must be between 5 and 540 (the prepare lead is 600 DAA)\n"); std::exit(2); }
if (o.raceRounds == 0) o.raceRounds = o.raceOnly ? 3 : 1;
if (o.tuningPath.empty()) if (const char* t = std::getenv("IGNEUM_TUNING_FILE")) o.tuningPath = t;
if (const char* rb = std::getenv("IGNEUM_READBACK")) if (std::strcmp(rb, "full") == 0) o.readbackFull = true;
if (o.pack.empty() && !o.memprobe) { std::printf("--pack <dir> is required (igneum-miner export-pack <node> <dir> writes one)\n"); std::exit(2); }
while (o.pack.size() > 1 && (o.pack.back() == '/' || o.pack.back() == '\\')) o.pack.pop_back();
return o;
@ -1144,19 +1149,95 @@ static std::string findPackFor(const std::string& root, const std::string& epoch
static std::string parentDir(const std::string& p) { size_t i = p.find_last_of("/\\"); return i == std::string::npos ? "." : p.substr(0, i); }
static uint64_t fnv1a64Bytes(const void* p, size_t n);
// The select pass (8 October 2026, review B finding F10), the shape proto-opencl/host.c has carried since 5 October 2026.
// Before it every chunk read back 8 bytes per nonce (128 MiB for a 2^24-nonce dispatch) over the bus and the host scanned
// them under the GPU mutex. Now a tiny kernel built here by NVRTC (no pack involved, nothing in the hash path changes)
// writes the hits (index, hash word) behind an atomic counter plus 34 sentinel words (the first 32 outputs, the middle
// and the last), and the host reads back a few hundred bytes. The hash word printed is the kernel's own out[i], never
// recomputed. The found lines are printed in nonce order from the sorted hits, as the full scan printed them. A chunk
// with more hits than the table holds (a target that loose is a test or the P01 campaign, not a block) falls back to
// the full read of that chunk. The sentinels feed the stale-output detector the OpenCL worker has: two consecutive
// chunks never share the signature (different nonces or init words give different outputs), so a repeat means the
// kernel did not run. --readback full or IGNEUM_READBACK=full keeps the old path for a comparison; the select pass
// that fails to build falls back to it with an info line.
#define IG_MAX_HITS 256u
static const char* SELECT_SRC =
"// igneum-worker-cuda: the select pass (8 October 2026); the same shape as proto-opencl/host.c's igneum_select\n"
"extern \"C\" __global__ void igneum_select(const unsigned long long* out, unsigned int n, unsigned long long target, unsigned int* count,\n"
" unsigned long long* hits, unsigned int maxHits, unsigned long long* sentinel) {\n"
" unsigned int i = blockIdx.x * blockDim.x + threadIdx.x;\n"
" if (i < n) {\n"
" unsigned long long h = out[i];\n"
" if (h <= target) { unsigned int k = atomicAdd(count, 1u); if (k < maxHits) { hits[2u * k] = (unsigned long long)i; hits[2u * k + 1u] = h; } }\n"
" if (i < 32u) sentinel[i] = h;\n"
" if (i == 0u) { sentinel[32] = out[n / 2u]; sentinel[33] = out[n - 1u]; }\n"
" }\n"
"}\n";
struct SelectPass {
bool on = false;
CUmodule mod = nullptr;
CUfunction fn = nullptr;
CUdeviceptr dCount = 0, dHits = 0, dSentinel = 0;
uint32_t hCount = 0;
uint64_t hHits[IG_MAX_HITS * 2];
uint64_t hSentinel[34];
unsigned long long bytesDown = 0, fullFallbacks = 0;
double kernelMsSum = 0, selectMsSum = 0, readMsSum = 0, scanMsSum = 0;
unsigned long chunks = 0;
};
static void releaseSelect(Ctx& c, SelectPass& s) {
if (s.dCount) c.drv.memFree(s.dCount);
if (s.dHits) c.drv.memFree(s.dHits);
if (s.dSentinel) c.drv.memFree(s.dSentinel);
if (s.mod) c.drv.moduleUnload(s.mod);
s.dCount = s.dHits = s.dSentinel = 0; s.mod = nullptr; s.fn = nullptr; s.on = false;
}
// Builds the pass; on any failure the worker keeps the full read-back (an info line says why), never an error.
static void buildSelect(Ctx& c, SelectPass& s) {
Compiled cs;
std::string err;
if (!rtcCompile(c, SELECT_SRC, "igneum_select.cu", "", "", {}, cs, err)) { info("the select pass did not build (" + err + "); using the full read-back"); return; }
CUresult r = c.drv.moduleLoadData(&s.mod, cs.image.data());
if (r == CUDA_SUCCESS) r = c.drv.moduleGetFunction(&s.fn, s.mod, "igneum_select");
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dCount, 4);
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dHits, (size_t)IG_MAX_HITS * 2u * 8u);
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dSentinel, 34u * 8u);
if (r != CUDA_SUCCESS) { info("the select pass did not load (" + c.err(r) + "); using the full read-back"); releaseSelect(c, s); return; }
s.on = true;
}
static int runServe(Ctx& c, const Options& o, Pair* cur) {
const uint32_t batch = 1u << o.batchLog2;
CUdeviceptr dOut = 0;
std::string err;
if (c.drv.memAlloc(&dOut, (size_t)batch * 8u) != CUDA_SUCCESS) { emit("error 0 cuMemAlloc out buffer"); return 2; }
std::vector<uint64_t> hOut(batch);
SelectPass sel;
if (!o.readbackFull) buildSelect(c, sel);
// IGNEUM_READBACK_FAULT_TEST=1 (test only): from the second chunk on the counter is not reset, so the hits of the
// previous chunk come back again; the equivalence check must flag it (the known-failed line of emu/select-check.sh).
const bool faultTest = std::getenv("IGNEUM_READBACK_FAULT_TEST") != nullptr;
uint64_t prevSig = 0; bool havePrevSig = false;
unsigned long jobsSeen = 0;
Pair* prepared = nullptr;
Pair* old = nullptr;
PrepareTask* task = nullptr;
std::string prepareRoot; // the parent of the last prepare's pack directory: where the miner writes its packs
emit(fmt("ready cuda %s pack %s dataset-log2 %u batch %u regs %d prepare 1 path nvrtc %d.%d driver %d.%d arch %s variant %s race %s worker %s",
c.name.c_str(), cur->seedString.c_str(), cur->datasetLog2, batch, cur->regs, c.rtcMajor, c.rtcMinor, c.driverVersion / 1000, (c.driverVersion % 100) / 10, c.archOpt.c_str(), cur->variant.c_str(), c.race.c_str(), WORKER_VERSION));
emit(fmt("ready cuda %s pack %s dataset-log2 %u batch %u regs %d prepare 1 path nvrtc %d.%d driver %d.%d arch %s variant %s race %s readback %s worker %s",
c.name.c_str(), cur->seedString.c_str(), cur->datasetLog2, batch, cur->regs, c.rtcMajor, c.rtcMinor, c.driverVersion / 1000, (c.driverVersion % 100) / 10, c.archOpt.c_str(), cur->variant.c_str(), c.race.c_str(), sel.on ? "select" : "full", WORKER_VERSION));
info(fmt("readback %s (per dispatch of %u nonces: %s)", sel.on ? "select" : "full", batch, sel.on ? "the hits and 34 sentinel words come back; the GPU scans" : "8 bytes per nonce come back and the host scans them"));
info(fmt("first pack %s: %s", cur->dir.c_str(), pairSummary(cur).c_str()));
auto statsLine = [&]() {
if (sel.chunks == 0) return;
info(fmt("transfers per chunk: down %.0f B (%s, %llu full fallbacks); mean per chunk: kernel+sync %.2f ms, select %.3f ms, read-back %.3f ms, scan %.3f ms; %lu jobs %lu chunks",
(double)sel.bytesDown / (double)sel.chunks, sel.on ? "select" : "full", sel.fullFallbacks,
sel.kernelMsSum / (double)sel.chunks, sel.selectMsSum / (double)sel.chunks, sel.readMsSum / (double)sel.chunks, sel.scanMsSum / (double)sel.chunks, jobsSeen, sel.chunks));
};
if (!cur->raceLine.empty()) emit(cur->raceLine);
std::string line;
while (std::getline(std::cin, line)) {
@ -1273,18 +1354,78 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) {
uint32_t block = 32u * (uint32_t)cur->blockWarps;
uint32_t main = chunk - (chunk % block);
CUresult r = CUDA_SUCCESS;
bool useSelect = sel.on;
double c0 = wallMs(), kernelMs = 0, selectMs = 0, readMs = 0, scanMs = 0, r0;
if (useSelect && !(faultTest && sel.chunks > 0)) {
const uint32_t zero = 0;
r = c.drv.memcpyHtoD(sel.dCount, &zero, 4);
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: select count reset: " + c.err(r)); failed = true; break; }
}
if (main > 0 && !launchHash(c, cur, dOut, lo, iw, main, block, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; break; }
if (main < chunk) {
for (uint32_t off = main; off < chunk && !failed; off += 32u) if (!launchHash(c, cur, dOut + (CUdeviceptr)off * 8u, lo + off, iw, 32u, 32u, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; }
if (failed) break;
}
r = c.drv.streamSynchronize(nullptr);
if (r == CUDA_SUCCESS) r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u);
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; }
for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]);
kernelMs = wallMs() - c0;
if (r == CUDA_SUCCESS && useSelect) {
// the select pass on the same (null) stream, after every launch of the chunk, over the whole chunk including its tail
r0 = wallMs();
uint32_t n = chunk, maxHits = IG_MAX_HITS;
unsigned long long t = target;
void* args[7] = { &dOut, &n, &t, &sel.dCount, &sel.dHits, &maxHits, &sel.dSentinel };
r = c.drv.launchKernel(sel.fn, (n + 255u) / 256u, 1, 1, 256, 1, 1, 0, nullptr, args, nullptr);
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(nullptr);
selectMs = wallMs() - r0;
}
r0 = wallMs();
if (r == CUDA_SUCCESS && useSelect) {
r = c.drv.memcpyDtoH(&sel.hCount, sel.dCount, 4);
sel.bytesDown += 4;
if (r == CUDA_SUCCESS) {
if (sel.hCount > IG_MAX_HITS) {
// more hits than the table holds: this chunk takes the full path (the test target case)
++sel.fullFallbacks;
useSelect = false;
} else {
if (sel.hCount) { r = c.drv.memcpyDtoH(sel.hHits, sel.dHits, (size_t)sel.hCount * 16u); sel.bytesDown += (unsigned long long)sel.hCount * 16u; }
if (r == CUDA_SUCCESS) { r = c.drv.memcpyDtoH(sel.hSentinel, sel.dSentinel, 34u * 8u); sel.bytesDown += 34u * 8u; }
}
}
}
if (r == CUDA_SUCCESS && !useSelect) { r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u); sel.bytesDown += (unsigned long long)chunk * 8u; }
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; }
readMs = wallMs() - r0;
// Stale output: the first, middle and last words plus an FNV of the first 32. Two chunks never share it.
// On the select path the same 34 words come from the sentinel buffer the select pass wrote.
if (chunk >= 64) {
uint64_t sig = useSelect ? (fnv1a64Bytes(sel.hSentinel, 32u * 8u) ^ sel.hSentinel[32] ^ sel.hSentinel[33])
: (fnv1a64Bytes(hOut.data(), 32u * 8u) ^ hOut[chunk / 2] ^ hOut[chunk - 1]);
if (havePrevSig && sig == prevSig) { emit(fmt("error %s worker fault: the output buffer is unchanged since the previous dispatch (signature %016llx): the kernel did not run; exiting 3 so the miner restarts the worker", jobId.c_str(), (unsigned long long)sig)); std::exit(3); }
prevSig = sig; havePrevSig = true;
}
r0 = wallMs();
if (useSelect) {
// nonce order, as the full scan printed them (the miner submits the first found of a job)
uint32_t nHits = sel.hCount;
for (uint32_t a = 1; a < nHits; ++a) {
uint64_t ki = sel.hHits[2 * a], kh = sel.hHits[2 * a + 1];
uint32_t b = a;
for (; b > 0 && sel.hHits[2 * (b - 1)] > ki; --b) { sel.hHits[2 * b] = sel.hHits[2 * (b - 1)]; sel.hHits[2 * b + 1] = sel.hHits[2 * (b - 1) + 1]; }
sel.hHits[2 * b] = ki; sel.hHits[2 * b + 1] = kh;
}
for (uint32_t a = 0; a < nHits; ++a) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + (uint32_t)sel.hHits[2 * a]);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)sel.hHits[2 * a + 1]);
}
} else {
for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]);
}
}
scanMs = wallMs() - r0;
sel.kernelMsSum += kernelMs; sel.selectMsSum += selectMs; sel.readMsSum += readMs; sel.scanMsSum += scanMs; ++sel.chunks;
std::fflush(stdout);
hashes += chunk;
remaining -= chunk;
@ -1292,9 +1433,12 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) {
}
if (failed) continue;
emit(fmt("done %s %llu %.2f", jobId.c_str(), (unsigned long long)hashes, wallMs() - t0));
if (++jobsSeen % 200 == 0) statsLine();
if (switched && old) { releasePair(c, old); old = nullptr; info("dropped the previous pair (its program, cache and dataset)"); }
}
statsLine();
if (task) { task->thread.join(); if (task->result) releasePair(c, task->result); delete task; }
releaseSelect(c, sel);
c.drv.memFree(dOut);
if (old) releasePair(c, old);
if (prepared) releasePair(c, prepared);

File diff suppressed because one or more lines are too long

View file

@ -450,7 +450,7 @@
<div class="grid c2">
<div class="dl-list" style="margin:0">
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v2.0.0 · 45.9 MB</span></a>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v2.0.1 · 45.9 MB</span></a>
<a href="/miner#hive">Linux · HiveOS <span>the tarball and the flight sheet, on the miner page</span></a>
</div>
<div>

View file

@ -254,8 +254,8 @@
<div class="body">
<p>Open the disk image, drag the app across, press Start. A Mac mines on its GPU at about a fifth of a flagship card and proves on its CPU, slowly.</p>
<p data-mac-first-open>On first open macOS will say it cannot verify the app. Click Done, open System Settings, Privacy and Security, and click Open Anyway, then open Igneum Miner again. A signed and notarized build follows.</p>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><span class="osmark bare" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>Download for macOS <span class="meta" data-dl-meta="miner-mac">v2.0.0 · 45.9 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-mac">d7dff598444bc27274eb35a5276803b7bbc72876e5d7abeb154afd2cf5a5d4b2</span></div>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><span class="osmark bare" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>Download for macOS <span class="meta" data-dl-meta="miner-mac">v2.0.1 · 45.9 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-mac">7059a31c29b9798f32c04402f40c1e02db9674d15b497b08d51e2cc8e6845f45</span></div>
</div>
</article>
<article class="dl-card" id="linux">
@ -263,16 +263,16 @@
<div><h3>Ember for Linux</h3><div class="sub">one tarball, the miner and the node inside</div></div>
<div class="body">
<p>Unpack it, run <code>igneum</code> with your payout address. The same signed manifest as the desktop apps. NVIDIA through CUDA, AMD through OpenCL.</p>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.0 · 53.3 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">5c1369fc5559e8c85534da08cb6eafff9f688336f8993b44e0512fe86d6284d1</span></div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.1 · 52.7 MB</span></a>
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">a16add6a9082747bc11d9bb9a69d193530f723dd2671fe1352ad59b1ae0a46ab</span></div>
</div>
</article>
<article class="dl-card" id="hive">
<span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>
<div><h3>Ember on HiveOS</h3><div class="sub">for the rig people, the same tarball</div></div>
<div class="body">
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.0.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.0 · 53.3 MB</span></a>
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.1.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.1 · 52.7 MB</span></a>
</div>
</article>
</div>

View file

@ -237,7 +237,7 @@
<p class="lead">A GPU-secured network for Ethereum-compatible applications and verifiable computation.</p>
<div class="fold-actions" id="fold-actions">
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" data-os-button="windows" class="btn primary"><span class="osmark bare" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Download for Windows<span class="meta" data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" data-os-button="mac" class="btn"><span class="osmark bare" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>Download for macOS<span class="meta" data-dl-meta="miner-mac">v2.0.0 · 45.9 MB</span></a>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" data-os-button="mac" class="btn"><span class="osmark bare" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>Download for macOS<span class="meta" data-dl-meta="miner-mac">v2.0.1 · 45.9 MB</span></a>
</div>
<p class="fold-more fold-one">One click: node, miner, prover. The card starts.</p>
<p class="fold-more"><a href="/download">Linux and HiveOS</a></p>

View file

@ -290,16 +290,16 @@ pre b{color:var(--molten-text);font-weight:500}
<div class="download-platform"><span class="osmark" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span><div><h3>Ember for macOS</h3><p>a disk image, Apple silicon, Metal</p></div></div>
<p class="download-note">Open the disk image, drag the app across, press Start. A Mac mines on its GPU at about a fifth of a flagship card and proves on its CPU, slowly.</p>
<p class="download-note" data-mac-first-open>On first open macOS will say it cannot verify the app. Click Done, open System Settings, Privacy and Security, and click Open Anyway, then open Igneum Miner again. A signed and notarized build follows.</p>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.85">v2.0.0 · 45.9 MB</span></a>
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-mac">d7dff598444bc27274eb35a5276803b7bbc72876e5d7abeb154afd2cf5a5d4b2</span></span></div>
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.85">v2.0.1 · 45.9 MB</span></a>
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-mac">7059a31c29b9798f32c04402f40c1e02db9674d15b497b08d51e2cc8e6845f45</span></span></div>
</div>
<div class="platform-body" id="panel-hive" role="tabpanel" aria-labelledby="tab-hive" data-platform-panel="hive" hidden>
<div class="download-platform"><span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span><div><h3>Ember for Linux and HiveOS</h3><p>a tarball for rigs and Hive flight sheets</p></div></div>
<p class="download-note">For the rig people. One tarball, the miner and the node inside, the same signed manifest as the desktop apps.</p>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v2.0.0 · 53.3 MB</span></a>
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v2.0.1 · 52.7 MB</span></a>
<p class="fair" id="versions">Current build: Windows <span data-rm="versions.miner-windows.version">0.3.26</span>, macOS <span data-rm="versions.miner-mac.version">2.0.0</span>, HiveOS <span data-rm="versions.miner-hive.version">0.3.26</span>, the wallet <span data-rm="versions.wallet-mac.version">0.1.6</span>, <span data-rm="read_at_short">read 8 October 2026, 17:2x UK</span>. The machine-readable list, with every file’s SHA-256, is <a href="/release.json">/release.json</a>.</p>
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.0.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<p class="sha">sha256 <span data-dl-sha="miner-hive">5c1369fc5559e8c85534da08cb6eafff9f688336f8993b44e0512fe86d6284d1</span></p>
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.1.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x&lt;your 40-hex payout address&gt;.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
<p class="sha">sha256 <span data-dl-sha="miner-hive">a16add6a9082747bc11d9bb9a69d193530f723dd2671fe1352ad59b1ae0a46ab</span></p>
</div>
</div>
<div>

View file

@ -10,6 +10,10 @@
| the red watcher fires on cancelled and timed-out runs too (`ci-red.yml`, `red-watch.mjs`) | The watcher's `if` missing any of failure, cancelled, timed_out, or the conclusion not handed to the record step (the self-test reads the workflow file); the line names the kind: CI red, CI cancelled, CI timed out. | 7 October 2026 |
| gh's active account is the stored Igneum entry (`gh-account-check.sh`, in Igneum's own gh directory `~/.config/gh-igneum` through `gh-env.sh`, never the founder's) | A push or a landing from this Mac while Igneum's gh directory names any other account as active, or none (the refusal names the one step: the founder or main stores the Igneum token there with `GH_CONFIG_DIR=~/.config/gh-igneum gh auth login --with-token`; no lane does); skipped with a line while `github-suspended` stands. RULE: no lane switches gh accounts on this Mac, ever; the second owner's login belongs to other projects and must never touch Igneum; the stored entry's name is in ~/.config/igneum/gh-user, never in the repository. | 7 October 2026, 21:41 UK: a lane switched gh to the other login during the suspension; nobody could say which |
| F02 (Review B): the proof-rule test bypass cannot reach a release build (`proof-rule-bypass-check.sh`; a cell of the node matrix) | An env read of IGNEUM_TEST_SKIP_PROOF_RULE with no cfg(test) or cfg(feature) guard in the 12 lines above, or under a feature in the crate's default features; a release igneumd carrying the bypass string. Red on every node sha until the proving lane's change (the read under a non-default feature or cfg(test)) lands | 8 Oct 2026 |
| the test map merges structurally at a landing (`test-map-merge.py`) and the harness page regenerates from the merged map (`merge-to-master.sh`) | Nothing by itself: two lanes adding cells collided as text and the regenerated page lost rule 26's race; the merge now keeps master's cells plus the branch's, minus what the branch removed and master left, and regenerates the page | 8 Oct 2026 |
| a push that lost the ref race retries without re-running the hook (`merge-to-master.sh` `push_race`, 12 tries) | Nothing by itself: under one landing a minute a 70-second hook per try never won master's compare-and-swap (Review B's landing lost three in a row); once the hook has passed on the first try and the rejection is a ref race, later tries push --no-verify (both parents gated) | 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 |
| 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,52 @@
{
"run_id": "201-7cfa422a-aa0e0f45",
"manifest_sha": "7cfa422a",
"cut_tip": "aa0e0f45 (miner; cells on 9c844503, the crate identical); node 7cfa422a on the key-succession pairing",
"evidence_dir": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45",
"boxes": [
"build-2",
"build-4"
],
"cells": [
{
"cell": "suite:pow",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-pow.log"
},
{
"cell": "suite:app",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-app.log"
},
{
"cell": "suite:core",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-core.log"
},
{
"cell": "suite:consensus",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-consensus.log"
},
{
"cell": "suite:exec",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-exec.log"
},
{
"cell": "suite:miner",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-miner.log"
},
{
"cell": "suite:p2p-flows",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-p2p-flows.log"
},
{
"cell": "check:freeze",
"status": "RUNNING",
"evidence": "docs/plans/igneum-2.0-f0-manifest.md; packaging/pow-freeze.txt; build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-kaspad-check.log"
}
]
}

View file

@ -76,6 +76,9 @@ the harness map page is generated from tools/ci/test-map.json and current
P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)
the proving outcome ledger (review B F08): every claimed job ends in one outcome; the report's self-test reads a log and a state file to known numbers
the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)
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)
the REV suite is generated from Review B's findings and dispatch and matches them (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

@ -26,7 +26,7 @@ MERGE_PID_FILE="$(git rev-parse --git-dir)/igneum-merge.pid"
printf '%s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${IGNEUM_MERGE_TITLE:-untitled}" > "$MERGE_PID_FILE" 2>/dev/null || true
trap 'rm -f "$MERGE_PID_FILE"' EXIT
. tools/ci/gh-env.sh # every gh call here reads Igneum's own gh directory, never the founder's (8 October 2026)
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=6; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=12; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"; NOVERIFY=""
while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) CI_WAIT_MIN="$2"; shift 2 ;; --fixes-master) FIXES_MASTER=1; shift ;; --remote) REMOTE="$2"; shift 2 ;; --self-test) SELF_TEST=1; shift ;; -*) echo "unknown option $1" >&2; exit 2 ;; *) BRANCH="$1"; shift ;; esac; done
# --remote <name>: land on another remote's master (a box mirror, build@<box>:/srv/igneum.git, while GitHub is unreachable; main's
# ruling of 7 October 2026, 19:5x UK). CI runs on GitHub only, so the CI rule binds the origin remote; on a mirror the box gate
@ -107,21 +107,59 @@ mirror_master() { # <sha> [landed-remote-url]: the landed master to every othe
# merge_with_batches <tip> <sha> <message>: in the current worktree (at <tip>), the merge of <sha>; when the branch added batch files,
# the registry takes master's copy and every batch is replayed onto it, then the evidence rules run on the result; returns 1 on a
# conflict outside the registry or a red evidence rule
# push_race <push output>: 0 when a rejected push lost only the ref's compare-and-swap (another landing moved master between the
# fetch and the push), 1 when the hook refused or something else failed. On a race the merge is rebuilt on the new tip; the hook
# already passed on the first try and both parents are gated (the branch fully, master's tip on its own landing), so the retry
# pushes with --no-verify: under tonight's landing rate (one every minute, 8 October 2026, 20:0x UK) a 70-second hook per try
# never wins the swap
push_race() { case "$1" in *"REFUSED"*|*" RED "*) return 1 ;; *"cannot lock ref"*|*"failed to update ref"*|*"fetch first"*|*"non-fast-forward"*) return 0 ;; *) return 1 ;; esac; }
merge_with_batches() {
local tip="$1" sha="$2" msg="$3" conflicts f
local MAP_CHANGED="${MAP_CHANGED:-0}" MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}" PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}" BATCHES="${BATCHES:-}" NOTES="${NOTES:-}" REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
if git "${AUTHOR[@]}" merge -q --no-ff --no-commit "$sha" >/dev/null 2>&1; then :; else
conflicts=$(git diff --name-only --diff-filter=U)
if [ -z "$BATCHES" ] || [ "$conflicts" != "$REGISTRY_PATH" ]; then git merge --abort 2>/dev/null; return 1; fi
local c ok=1
for c in $conflicts; do
case "$c" in
"$REGISTRY_PATH") [ -n "$BATCHES" ] || ok=0 ;; # rebuilt from master's copy below
"$PAGE_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # regenerated from the merged map below
"$MAP_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # merged structurally below (tools/ci/test-map-merge.py)
*) ok=0 ;;
esac
done
if [ "$ok" != 1 ]; then git merge --abort 2>/dev/null; return 1; fi
[ -n "$BATCHES" ] || git checkout -q "$tip" -- "$REGISTRY_PATH" 2>/dev/null || true
fi
if [ "$MAP_CHANGED" = 1 ] && git diff --name-only --diff-filter=U 2>/dev/null | grep -qx "$MAP_PATH"; then
local base3; base3=$(git merge-base "$tip" "$sha")
git show "$base3:$MAP_PATH" > /tmp/map-base.$$ ; git show "$tip:$MAP_PATH" > /tmp/map-master.$$ ; git show "$sha:$MAP_PATH" > /tmp/map-branch.$$
python3 tools/ci/test-map-merge.py /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$ "$MAP_PATH" || { echo "merge-to-master: the map does not merge structurally" >&2; git merge --abort 2>/dev/null; return 1; }
rm -f /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$; git add "$MAP_PATH"; echo "merge-to-master: merged $MAP_PATH structurally (master's cells plus the branch's)"
fi
if [ -n "$BATCHES" ]; then
git checkout -q "$tip" -- "$REGISTRY_PATH" # master's copy, never the branch's
if [ -f tools/ci/review-suite.mjs ] && [ -f docs/analysis/review-2026-10-08-b/findings.json ]; then
node tools/ci/review-suite.mjs --findings docs/analysis/review-2026-10-08-b/findings.json --dispatch docs/analysis/review-2026-10-08-b/dispatch.md --prefix REV --write >/dev/null || { echo "merge-to-master: the REV suite does not regenerate on the merged tree" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: regenerated the REV suite on master's registry"
fi
for f in $BATCHES; do
[ "$f" = . ] && continue
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --record "$f" >/dev/null || { echo "merge-to-master: the batch $f does not replay onto master's registry" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed $f onto master's registry"
done
for f in ${NOTES:-}; do
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --note-file "$f" >/dev/null || { echo "merge-to-master: the note $f does not apply" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed the note $f"
done
git add -A
fi
if [ "$MAP_CHANGED" = 1 ] && [ -f tools/ci/test-map-doc.mjs ]; then
git checkout -q "$tip" -- "$PAGE_PATH" 2>/dev/null || true # start from master's page; the generator overwrites it from the merged map
node tools/ci/test-map-doc.mjs >/dev/null || { echo "merge-to-master: the harness map page does not regenerate from the merged map" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: regenerated $PAGE_PATH from the merged map"; git add -A
fi
git "${AUTHOR[@]}" commit -q -m "$msg" || return 1
if [ -n "$BATCHES" ]; then
bash tools/ci/registry-evidence-check.sh "$tip" HEAD || { echo "merge-to-master: REFUSED by the registry's evidence rules on the merged registry (above)" >&2; return 1; }
@ -208,7 +246,7 @@ success 4 u push run
# the merge takes master's copy and replays the batch, so both rows land (the hot-file race, 8 October 2026, 19:1x UK)
rb="$d/rb"; mkdir -p "$rb" && ( cd "$rb" && git init -q -b master . && mkdir -p docs/plans tools/ci/batches && cp "$ROOT/tools/ci/test-record.mjs" tools/ci/ && cp "$ROOT/tools/ci/registry-evidence-check.sh" tools/ci/ && cp "$ROOT/tools/ci/docs-only-check.sh" tools/ci/ 2>/dev/null
printf '{"approval":"yes","suites":[{"code":"X","tests":[{"id":"X-1","method":"Automated","accept":"a"},{"id":"X-2","method":"Automated","accept":"b"}]}]}\n' > docs/plans/igneum-2.0-test-registry.json
printf '{"cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
printf '{"title":"t","registry":"docs/plans/igneum-2.0-test-registry.json","rule":"r","cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
git add -A && git -c user.name=t -c user.email=t@t commit -q -m base && git tag base
git checkout -q -b branch; printf '{"run_id":"r-branch","manifest_sha":"m","cells":[{"cell":"c1","status":"RUNNING","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-branch.json
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"
@ -217,7 +255,24 @@ success 4 u push run
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 "
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
[ "$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"
# 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
( cd "$rb" && cp "$ROOT/tools/ci/test-map-doc.mjs" "$ROOT/tools/ci/test-map-merge.py" tools/ci/ && mkdir -p docs/plans && git checkout -q master && 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 page0; git tag pbase
git checkout -q -b pb; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c3']={'command':'z','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 c3
git checkout -q master; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c4']={'command':'w','box_class':'b','fixtures':[],'cases':['X-2']}; 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 c4 ) >/dev/null 2>&1 || { echo "self-test failed: the page-race fixture did not build"; fails=1; }
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse pb) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES="" && 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 with page" 2>&1 && grep -c -E '^### c[34]$' docs/plans/igneum-2.0-test-harness-map.md )
case "$out" in *regenerated*2) ;; *) echo "self-test failed: the merge did not regenerate the page with both sides' cells: $out"; fails=1 ;; esac
# 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","cells":[{"cell":"c1","status":"RUNNING","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')" )
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
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; }
push_race "pre-push gate: REFUSED. master takes only a commit whose own ci run is green" && { echo "self-test failed: a hook refusal was read as a race"; fails=1; }
push_race " RED 3s no conflict markers in tracked files
error: failed to push some refs" && { echo "self-test failed: a red check was read as a race"; fails=1; }
[ "$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"
exit $fails
fi
if [ "$SELF_TEST" != 1 ] && github_suspended_refusal "$REMOTE"; then exit 2; fi
@ -250,7 +305,16 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
# 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
REVGEN=0; git diff --quiet "$BASE" "$SHA" -- tools/ci/review-suite.mjs docs/analysis/review-2026-10-08-b/findings.json docs/analysis/review-2026-10-08-b/dispatch.md 2>/dev/null || REVGEN=1 # the REV suite regenerates 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"
# the harness map page is generated from tools/ci/test-map.json (test-map-doc.mjs); a branch that changed the map regenerated the
# whole page, and master's page moves under every lane (8 October 2026, 19:5x UK: the enforced-proving lane lost rule 26's race
# twice on the page alone). The merge regenerates the page from the MERGED map, as it replays batches onto master's registry.
MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}"; PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}"
MAP_CHANGED=0; git diff --quiet "$BASE" "$SHA" -- "$MAP_PATH" 2>/dev/null || MAP_CHANGED=1
[ "$MAP_CHANGED" = 1 ] && RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS $PAGE_PATH"
# rule 26 (8 October 2026, 17:5x UK): a site/ or docs/ path another lane landed since the branch point is merged, never replaced
RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS" bash tools/ci/rule26-no-revert.sh "$BASE" "$SHA" "$REMOTE/master" || { echo "merge-to-master: REFUSED by rule 26 (above)" >&2; exit 1; }
# the registry's evidence rules (8 October 2026, 18:4x UK): a PASS carries existing evidence, a touched evidence file moves with its
@ -262,14 +326,16 @@ for i in $(seq 1 "$TRIES"); do
W=$(mktemp -d "${TMPDIR:-/tmp}/merge-to-master.XXXXXX"); rmdir "$W"
git worktree add -q --detach "$W" "$TIP"
if ( cd "$W" && merge_with_batches "$TIP" "$SHA" "Merge $BRANCH ${SHA:0:8} into master ($VERDICT)" ); then
if ( cd "$W" && git push -q "$REMOTE" HEAD:master ); then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
pushout=$( cd "$W" && git push "$REMOTE" HEAD:master ${NOVERIFY:+--no-verify} 2>&1 ) && pushed=1 || pushed=0
[ "$pushed" = 1 ] || { printf '%s\n' "$pushout" | grep -E 'REFUSED| RED |rejected|error' | head -4 | cut -c1-160; }
if [ "$pushed" = 1 ]; then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
git worktree remove --force "$W"; git fetch -q "$REMOTE" master
echo "merge-to-master: pushed on try $i: $REMOTE/master $(git log -1 --format='%h %ci' "$REMOTE/master") $(TZ=Europe/London date '+%H:%M %Z')"
# the landed master to every other mirror, whichever remote took the landing (8 October 2026, 14:0x UK: a box landing never fanned
# out, so build-3 and build-4 cut branches from a tip 23 hours old)
mirror_master "$(git rev-parse "$REMOTE/master")" "$(git remote get-url "$REMOTE" 2>/dev/null)"; exit 0
fi
echo "merge-to-master: try $i: the push was rejected (master moved or the hook was red); again"
if push_race "$pushout"; then NOVERIFY=1; echo "merge-to-master: try $i: master moved under the push (the ref's compare-and-swap lost); the hook passed, the next try pushes without re-running it"; else echo "merge-to-master: try $i: the push was refused by the hook or failed; again" >&2; fi
else
echo "merge-to-master: the merge of $BRANCH onto ${TIP:0:8} does not apply cleanly; resolve on the branch (git merge origin/master) and retry" >&2
git worktree remove --force "$W"; exit 1

View file

@ -0,0 +1,4 @@
{
"suite": "FIN",
"text": "F04 (Review B), the founder's ruling 8 October 19:57 UK: the recovery lock is kept and is always labelled 'recovery', never 'final', on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site)"
}

View file

@ -0,0 +1,4 @@
{
"suite": "OPS",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -0,0 +1,4 @@
{
"suite": "UX",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -175,6 +175,9 @@ tree_checks() {
run "P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)" python3 tools/ci/p01-vectors.py --self-test
run "the proving outcome ledger (review B F08): every claimed job ends in one outcome; the report's self-test reads a log and a state file to known numbers" python3 tools/fleet/prover-outcomes.py --self-test
run "the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)" bash tools/ci/registry-evidence-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 "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 "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,65 @@
#!/usr/bin/env bash
# F02 (Review B, 8 October 2026): the proof-rule test bypass (IGNEUM_TEST_SKIP_PROOF_RULE, read in the node fork's
# consensus/src/pipeline/body_processor/body_validation_in_context.rs) must never be compiled into a release build. Two rules:
# source: every occurrence of the bypass name in a .rs file of the fork sits in a file under tests/ or inside an item guarded by
# #[cfg(test)] or #[cfg(feature = "<f>")] / cfg!(feature = "<f>") where <f> is NOT in the crate's default features
# (the guard must appear in the 12 lines above the occurrence, inside the same item)
# binary: a release node binary (igneumd), when given, does not contain the bypass name as a string (strings | grep)
# tools/ci/proof-rule-bypass-check.sh <fork tree> [<release igneumd>] exit 0 clean, 1 red (every occurrence named), 2 bad args
# tools/ci/proof-rule-bypass-check.sh --self-test
set -euo pipefail
NAME="${PROOF_RULE_BYPASS_NAME:-IGNEUM_TEST_SKIP_PROOF_RULE}"
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
source_rule() { # <tree> -> prints "red <file>:<line> <why>" lines; returns 1 when any
local tree="$1" rc=0 f n guard feat crate_toml defaults
while IFS=: read -r f n _; do
[ -n "$f" ] || continue
case "$f" in */tests/*|*/benches/*) continue ;; esac
case "$(sed -n "${n}p" "$f")" in *"//"*"$NAME"*) if ! sed -n "${n}p" "$f" | grep -qE "env::var|env!\(|\"$NAME\""; then continue; fi ;; esac
guard=$(awk -v n="$n" 'NR>=n-12 && NR<n' "$f" | grep -oE '#\[cfg\(test\)\]|cfg\(feature *= *"[^"]+"\)|cfg!\(feature *= *"[^"]+"\)|cfg\(any\([^)]*feature *= *"[^"]+"[^)]*\)\)' | tail -1 || true)
if [ -z "$guard" ]; then echo "red $f:$n $NAME read with no cfg(test) or cfg(feature) guard in the 12 lines above"; rc=1; continue; fi
case "$guard" in '#[cfg(test)]') continue ;; esac
feat=$(printf '%s' "$guard" | grep -oE 'feature *= *"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"/\1/')
crate_toml=$(d="$(dirname "$f")"; while [ "$d" != / ] && [ ! -f "$d/Cargo.toml" ]; do d=$(dirname "$d"); done; echo "$d/Cargo.toml")
defaults=$(awk '/^\[features\]/{f=1;next} /^\[/{f=0} f && /^default *=/' "$crate_toml" 2>/dev/null || true)
case "$defaults" in *"\"$feat\""*) echo "red $f:$n guarded by feature \"$feat\", which is in the crate's default features ($crate_toml)"; rc=1 ;; esac
done < <(grep -rn --include='*.rs' -F "$NAME" "$tree" 2>/dev/null || true)
return $rc
}
binary_rule() { # <igneumd> -> 1 when the string is inside
local bin="$1"
if strings "$bin" 2>/dev/null | grep -qF "$NAME"; then echo "red $bin carries the string $NAME: the bypass is compiled in"; return 1; fi
return 0
}
if [ "${1:-}" = --self-test ]; then
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0
mk() { mkdir -p "$d/$1/src"; printf '[package]\nname = "c"\nversion = "0.1.0"\n[features]\ndefault = [%s]\ntest-bypass = []\n' "$2" > "$d/$1/Cargo.toml"; printf '%s\n' "$3" > "$d/$1/src/lib.rs"; }
mk unguarded '' 'fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
mk guarded '' '#[cfg(feature = "test-bypass")]
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }
#[cfg(not(feature = "test-bypass"))]
fn f() -> bool { false }'
mk default-feature '"test-bypass"' '#[cfg(feature = "test-bypass")]
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
mk cfgtest '' '#[cfg(test)]
mod t { fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() } }'
mk comment-only '' '// the bypass IGNEUM_TEST_SKIP_PROOF_RULE is gone from this crate
fn f() -> bool { false }'
out=$(bash "$ME" "$d/unguarded" 2>&1) && { echo "self-test failed: an unguarded env read passed"; fails=1; }; case "$out" in *"no cfg(test) or cfg(feature) guard"*) ;; *) echo "self-test failed: the unguarded read was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/guarded" >/dev/null 2>&1 || { echo "self-test failed: a read under a non-default feature was refused: $(bash "$ME" "$d/guarded" 2>&1)"; fails=1; }
out=$(bash "$ME" "$d/default-feature" 2>&1) && { echo "self-test failed: a read under a DEFAULT feature passed"; fails=1; }; case "$out" in *"default features"*) ;; *) echo "self-test failed: the default feature was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/cfgtest" >/dev/null 2>&1 || { echo "self-test failed: a read under #[cfg(test)] was refused"; fails=1; }
bash "$ME" "$d/comment-only" >/dev/null 2>&1 || { echo "self-test failed: a comment naming the bypass was refused"; fails=1; }
printf 'ELF\0\0igneumd IGNEUM_TEST_SKIP_PROOF_RULE\0' > "$d/bad.bin"; printf 'ELF\0\0igneumd clean\0' > "$d/good.bin"
out=$(bash "$ME" "$d/guarded" "$d/bad.bin" 2>&1) && { echo "self-test failed: a binary carrying the bypass string passed"; fails=1; }; case "$out" in *"compiled in"*) ;; *) echo "self-test failed: the binary was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/guarded" "$d/good.bin" >/dev/null 2>&1 || { echo "self-test failed: a clean binary was refused"; fails=1; }
[ "$fails" = 0 ] && echo "self-test passed: an env read of the bypass with no guard, or under a default feature, is red and named; a read under cfg(test) or a non-default feature passes; a comment passes; a release binary carrying the bypass string is red, a clean one passes"
exit $fails
fi
[ $# -ge 1 ] || { echo "usage: proof-rule-bypass-check.sh <fork tree> [<release igneumd>] | --self-test" >&2; exit 2; }
rc=0; out=$(source_rule "$1") || rc=1
[ -n "${2:-}" ] && { bout=$(binary_rule "$2") || rc=1; out="$out${bout:+
$bout}"; }
printf '%s\n' "$out" | sed -n 's/^red /proof-rule-bypass: RED: /p' | grep . || true
[ "$rc" = 0 ] && echo "proof-rule-bypass: every $NAME read is under cfg(test) or a non-default feature${2:+; the release binary carries no bypass string}"
exit $rc

5
tools/ci/review-suite-check.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# The REV suite of the registry matches Review B's findings and dispatch (tools/ci/review-suite.mjs --check), self-test first.
set -euo pipefail
node tools/ci/review-suite.mjs --self-test >/dev/null
node tools/ci/review-suite.mjs --findings docs/analysis/review-2026-10-08-b/findings.json --dispatch docs/analysis/review-2026-10-08-b/dispatch.md --prefix REV --check

75
tools/ci/review-suite.mjs Normal file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env node
// The REV suite of the acceptance registry: one case per required regression of an external review's findings.json, the owner from
// its dispatch.md table, the finding id and priority carried as fields, status NOT RUN until a lane records a run through the batch
// tools (main through the coordinator, 8 October 2026, 19:5x UK: Review B's 44 regressions). The registry's one structural edit
// per review: generated here, never by hand; --check refuses a registry whose REV suite differs from the generator's output.
//
// node tools/ci/review-suite.mjs --findings <findings.json> --dispatch <dispatch.md> --prefix REV [--write | --check]
// node tools/ci/review-suite.mjs --self-test
import fs from 'node:fs'; import path from 'node:path';
const args = process.argv.slice(2); const arg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
const ROOT = process.env.TEST_RECORD_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
const REG = process.env.TEST_REGISTRY || path.join(ROOT, 'docs/plans/igneum-2.0-test-registry.json');
function owners(dispatchMd) { // "| F01 title | P0 | owner a, owner b | ..." -> {F01: "owner a, owner b"}
const out = {};
for (const line of dispatchMd.split('\n')) {
const m = line.match(/^\|\s*(F\d+)\b[^|]*\|\s*([^|]*)\|\s*([^|]*)\|/); if (m) out[m[1]] = m[3].trim();
}
return out;
}
function suite(findings, dispatchMd, prefix, sourceNote) {
const own = owners(dispatchMd); const tests = [];
for (const f of findings.findings || []) {
(f.required_regressions || []).forEach((line, i) => {
tests.push({ id: `${prefix}-${f.id}-${i + 1}`, title: line, setup: `The regression ${f.id} requires (review finding ${f.id}: ${f.title}).`, steps: [line],
accept: line, evidence: 'The run record of the regression as its owner lane records it through tools/ci/test-record.mjs.', priority: String(f.priority || '').split(' ')[0] || 'P1',
profile: 'P00', cadence: 'Every release candidate', method: 'Automated + independent review', status: 'NOT RUN', source: [f.id], gate: 'Review findings closed',
owner: own[f.id] || 'unassigned', manual_page: null, finding: f.id, finding_title: f.title, finding_priority: f.priority, owner_lane: own[f.id] || 'unassigned', run_status: 'NOT RUN' });
});
}
return { code: prefix, title: `${prefix}: the external review's required regressions`, source: sourceNote, gate: 'Review findings closed', owner: 'the owner lanes per the dispatch table', fixtures: ['F0', 'F5'],
summary: `${tests.length} regressions from ${(findings.findings || []).length} findings; each reads NOT RUN until its owner lane records a run`, tests };
}
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']) 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')) {
let fails = 0;
const findings = { findings: [{ id: 'F01', title: 'A', priority: 'P0 - blocker', required_regressions: ['r one', 'r two'] }, { id: 'F02', title: 'B', priority: 'P1 - x', required_regressions: ['r three'] }] };
const dispatch = '| Finding | Priority | Owner | Default |\n|---|---|---|---|\n| F01 A | P0 | lane x, lane y | d |\n| F02 B | P1 | lane z | d |\n';
const s = suite(findings, dispatch, 'REV', 'test');
if (!(s.tests.length === 3 && s.tests[0].id === 'REV-F01-1' && s.tests[2].id === 'REV-F02-1')) { console.log(`self-test failed: the ids are not <prefix>-<finding>-<n>: ${s.tests.map((t) => t.id)}`); fails = 1; }
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 === '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: 'REV-F01-1', run_status: 'RUNNING', 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].run_status === 'RUNNING' && 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: ['REV-F01-1'] } }, not_run: { 'REV-F02-1': 'old' } }; const n = mapReasons(map, s);
if (!(n === 2 && !('REV-F01-1' 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');
process.exit(fails);
}
const findings = JSON.parse(fs.readFileSync(arg('--findings'), 'utf8')); const dispatch = fs.readFileSync(arg('--dispatch'), 'utf8'); const prefix = arg('--prefix') || 'REV';
const s = suite(findings, dispatch, prefix, `${path.relative(ROOT, arg('--findings'))} and ${path.relative(ROOT, arg('--dispatch'))}`);
const reg = JSON.parse(fs.readFileSync(REG, 'utf8'));
if (args.includes('--check')) {
const cur = (reg.suites || []).find((x) => x.code === prefix); const want = merge(JSON.parse(JSON.stringify(reg)), s).suites.find((x) => x.code === prefix);
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);
if (canon(cur) !== canon(want)) { console.error(`review-suite: the registry's ${prefix} suite differs from the generator's output; run --write and commit`); process.exit(1); }
console.log(`review-suite: the ${prefix} suite matches its findings (${s.tests.length} cases)`); process.exit(0);
}
const MAP = process.env.TEST_MAP || path.join(ROOT, 'tools/ci/test-map.json');
function mapReasons(map, s) { // every generated case with no cell reads NOT RUN with the owner lane named; a case a cell maps loses its reason
const mapped = new Set(Object.values(map.cells || {}).flatMap((c) => c.cases || [])); map.not_run = map.not_run || {}; let n = 0;
for (const t of s.tests) { if (mapped.has(t.id)) { delete map.not_run[t.id]; continue; } map.not_run[t.id] = `${t.finding} (${t.priority}, the external review): the regression's harness is the owner lane's (${t.owner_lane}); not yet named in the map`; n++; }
return n;
}
if (args.includes('--write')) {
fs.writeFileSync(REG, JSON.stringify(merge(reg, s), null, 2) + '\n');
let n = 0; if (fs.existsSync(MAP)) { const map = JSON.parse(fs.readFileSync(MAP, 'utf8')); n = mapReasons(map, s); fs.writeFileSync(MAP, JSON.stringify(map, null, 2) + '\n'); }
console.log(`review-suite: ${prefix} written, ${s.tests.length} cases from ${(findings.findings || []).length} findings; ${n} NOT RUN reasons in the map`); process.exit(0);
}
console.error('usage: review-suite.mjs --findings f --dispatch d [--prefix REV] --write|--check | --self-test'); process.exit(2);

44
tools/ci/test-map-merge.py Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""A structural three-way merge of tools/ci/test-map.json (8 October 2026, 20:0x UK): two lanes adding cells on adjacent lines
collide as text; as objects they do not. Result = master's map, plus every cell (and not_run entry) the branch added or changed
against the base, minus the cells the branch removed that master left as the base had them. Everything else of the map (title,
registry, rule) is master's. Usage: test-map-merge.py <base.json> <master.json> <branch.json> <out.json>; --self-test."""
import json, sys, tempfile, os
def merge(base, master, branch):
out = json.loads(json.dumps(master))
for key in ("cells", "not_run"):
b, m, r = base.get(key, {}), master.get(key, {}), branch.get(key, {})
res = dict(m)
for k, v in r.items():
if k not in b or b[k] != v:
res[k] = v
for k in b:
if k not in r and k in m and m[k] == b[k]:
del res[k]
out[key] = res
return out
def self_test():
fails = 0
base = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}}, "not_run": {"X-5": "none"}}
master = {"title": "t2", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}, "c4": {"a": 4}}, "not_run": {"X-5": "none", "X-6": "m"}}
branch = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 22}, "c3": {"a": 3}}, "not_run": {}} # adds c3, changes c2, removes c9, clears X-5
r = merge(base, master, branch)
want_cells = {"c1": {"a": 1}, "c2": {"a": 22}, "c4": {"a": 4}, "c3": {"a": 3}}
if r["cells"] != want_cells: print("self-test failed: cells:", r["cells"]); fails = 1
if r["not_run"] != {"X-6": "m"}: print("self-test failed: not_run:", r["not_run"]); fails = 1
if r["title"] != "t2": print("self-test failed: master's other fields not kept"); fails = 1
# master changed c9 too: the branch's removal does not win
master2 = json.loads(json.dumps(master)); master2["cells"]["c9"] = {"a": 99}
if "c9" not in merge(base, master2, branch)["cells"]: print("self-test failed: a cell master changed was removed by the branch"); fails = 1
if not fails: print("self-test passed: master's map plus the branch's added and changed cells and not_run entries, minus what the branch removed and master left alone; master's other fields kept")
return fails
if __name__ == "__main__":
if "--self-test" in sys.argv: sys.exit(self_test())
base, master, branch, out = (json.load(open(p)) for p in sys.argv[1:4]), None, None, None
b, m, r = base
res = merge(b, m, r)
with open(sys.argv[4], "w") as f: json.dump(res, f, indent=2); f.write("\n")
print(f"test-map-merge: {len(res.get('cells', {}))} cells, {len(res.get('not_run', {}))} NOT RUN reasons")

View file

@ -528,6 +528,50 @@
"LEAD-04": "observation window",
"LEAD-05": "observation window",
"LEAD-06": "observation window",
"LEAD-08": "observation window"
"LEAD-08": "observation window",
"REV-F01-1": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-2": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-3": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-4": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F02-1": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F02-2": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F02-3": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F03-1": "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",
"REV-F03-2": "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",
"REV-F03-3": "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",
"REV-F04-1": "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",
"REV-F04-2": "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",
"REV-F04-3": "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",
"REV-F04-4": "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",
"REV-F05-1": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F05-2": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F05-3": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F06-1": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F06-2": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F06-3": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F07-1": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F07-2": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F07-3": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F08-1": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F08-2": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F08-3": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F09-1": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F09-2": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F09-3": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F10-1": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F10-2": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F10-3": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F11-1": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F11-2": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F11-3": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F12-1": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F12-2": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F12-3": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-1": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-2": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-3": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F14-1": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map",
"REV-F14-2": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map",
"REV-F14-3": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map"
}
}

View file

@ -10,8 +10,10 @@
// node tools/ci/test-record.mjs --record <batch.json> batch: {run_id, manifest_sha, evidence_dir, cells:[{cell, status, evidence}]}
// each cell's case ids come from the map; the registry gains status/run/evidence/manifest
// node tools/ci/test-record.mjs --cases <cell> the case ids a matrix cell answers (for the matrix scripts' column)
// node tools/ci/test-record.mjs --note <suite code> "<text>" append a dated note to a suite's notes (a ruling, a review finding's
// disposition); never a case's accept text
// node tools/ci/test-record.mjs --self-test
import fs from 'node:fs'; import path from 'node:path';
import fs from 'node:fs'; import path from 'node:path'; import child_process from 'node:child_process';
const args = process.argv.slice(2); const arg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
const ROOT = process.env.TEST_RECORD_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
const REG = process.env.TEST_REGISTRY || path.join(ROOT, 'docs/plans/igneum-2.0-test-registry.json');
@ -45,7 +47,9 @@ function record(reg, map, batch) {
touched.push(id);
}
}
for (const [id, reason] of Object.entries(map.not_run || {})) { const c = byId.get(id); if (c && (!c.run_status || c.run_status === 'NOT RUN')) { c.run_status = 'NOT RUN'; c.evidence_record = { reason, at: now }; c.updated = now; } }
// a NOT RUN row is stamped only when its status or reason changes (8 October 2026, 20:1x UK: re-stamping every NOT RUN row on every
// run made each lane's landing collide on 39 rows it never touched)
for (const [id, reason] of Object.entries(map.not_run || {})) { const c = byId.get(id); if (c && (!c.run_status || c.run_status === 'NOT RUN') && !(c.run_status === 'NOT RUN' && c.evidence_record?.reason === reason)) { c.run_status = 'NOT RUN'; c.evidence_record = { reason, at: now }; c.updated = now; } }
return touched;
}
const acceptSnapshot = (reg) => JSON.stringify(casesOf(reg).map((c) => { const o = { id: idOf(c) }; for (const k of ACCEPT_KEYS) if (k in c) o[k] = c[k]; return o; }));
@ -63,17 +67,31 @@ if (args.includes('--self-test')) {
if (acceptSnapshot(reg) !== before) { console.log('self-test failed: a record changed an accept text'); fails = 1; }
if (!(touched.length === 1 && reg.cases[0].run_status === 'PASS' && reg.cases[0].run_id === 'r1' && reg.cases[0].evidence_record.manifest_sha === 'abc' && reg.cases[0].evidence_path === '/e/pow.log' && reg.cases[0].updated)) { console.log(`self-test failed: the run was not written to the mapped case's live fields: ${JSON.stringify(reg.cases[0])}`); fails = 1; }
if (!(reg.cases[1].run_status === 'NOT RUN' && /corpus/.test(reg.cases[1].evidence_record.reason))) { console.log('self-test failed: an unmapped Automated case did not read NOT RUN with its reason'); fails = 1; }
const stamp1 = reg.cases[1].updated; record(reg, map, { run_id: 'r1b', manifest_sha: 'abc', evidence_dir: '/e', cells: [{ cell: 'pow', status: 'PASS', evidence: '/e/pow.log' }] });
if (reg.cases[1].updated !== stamp1) { console.log('self-test failed: an unchanged NOT RUN row was re-stamped on a later run'); fails = 1; }
if (reg.cases[2].run_status) { console.log('self-test failed: a manual case was given a run status'); fails = 1; }
const regS = { suites: [{ code: 'X', tests: [{ id: 'X-1', method: 'Automated', accept: 'a' }] }] }; if (casesOf(regS).length !== 1) { console.log('self-test failed: the suites/tests registry shape was not read'); fails = 1; }
const regN = { suites: [{ code: 'FIN', tests: [{ id: 'F-1', method: 'Automated', accept: 'keep' }] }] }; fs.writeFileSync(`${d}/regn.json`, JSON.stringify(regN));
const nr = child_process.spawnSync(process.execPath, [new URL(import.meta.url).pathname, '--note', 'FIN', 'the ruling'], { env: { ...process.env, TEST_REGISTRY: `${d}/regn.json`, TEST_MAP: `${d}/map.json` }, encoding: 'utf8' });
const regN2 = JSON.parse(fs.readFileSync(`${d}/regn.json`, 'utf8'));
if (!(nr.status === 0 && regN2.suites[0].notes?.length === 1 && regN2.suites[0].notes[0].text === 'the ruling' && regN2.suites[0].tests[0].accept === 'keep')) { console.log(`self-test failed: --note did not append a dated note to the suite and keep the accept text: ${nr.stdout} ${nr.stderr}`); fails = 1; }
let threw = false; try { record(reg, map, { run_id: 'r2', manifest_sha: 'x', cells: [{ cell: 'ghost', status: 'PASS' }] }); } catch { threw = true; }
if (!threw) { console.log('self-test failed: a batch naming a cell not in the map was accepted'); fails = 1; }
fs.rmSync(d, { recursive: true, force: true });
if (!fails) console.log('self-test passed: a complete map checks; an unknown case id and an unmapped Automated case are refused; a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, leaves every accept text byte-identical, gives an unmapped Automated case NOT RUN with its reason and a manual case nothing; a batch naming an unknown cell is refused');
if (!fails) console.log('self-test passed: a complete map checks; an unknown case id and an unmapped Automated case are refused; a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, leaves every accept text byte-identical, gives an unmapped Automated case NOT RUN with its reason and a manual case nothing; a batch naming an unknown cell is refused; --note appends a dated note to a suite and never touches an accept text; an unchanged NOT RUN row is not re-stamped');
process.exit(fails);
}
const reg = load(REG); const map = load(MAP);
if (args.includes('--check')) { const r = check(reg, map); for (const l of r.lines) console.error(`test-record: ${l}`); console.log(`test-record: ${r.automated} Automated cases, ${r.mapped} mapped to cells, ${r.notRun} NOT RUN with a reason${r.bad ? `, ${r.bad} problems` : ''}`); process.exit(r.bad ? 1 : 0); }
if (arg('--cases')) { console.log(((map.cells || {})[arg('--cases')]?.cases || []).join(',')); process.exit(0); }
if (arg('--note-file')) { const n = load(arg('--note-file')); args.push('--note', n.suite, n.text); }
if (arg('--note')) {
const code = arg('--note'); const text = args[args.indexOf('--note') + 2]; const suite = (reg.suites || []).find((s) => s.code === code);
if (!suite || !text) { console.error(`test-record: --note needs a suite code in the registry and a text (got ${code}, ${text ? 'text' : 'no text'})`); process.exit(2); }
const before = acceptSnapshot(reg); suite.notes = suite.notes || []; suite.notes.push({ at: new Date().toISOString(), text });
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the note would change an accept text'); process.exit(1); }
fs.writeFileSync(REG, JSON.stringify(reg, null, 2) + '\n'); console.log(`test-record: note ${suite.notes.length} on ${code}: ${text.slice(0, 80)}`); process.exit(0);
}
if (arg('--record')) {
const batch = load(arg('--record')); const before = acceptSnapshot(reg); const touched = record(reg, map, batch);
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the record would change an accept text'); process.exit(1); }