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:
commit
4e954be8c0
27 changed files with 2458 additions and 279 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
83
proto-cuda/nvrtc/emu/select-check.sh
Executable file
83
proto-cuda/nvrtc/emu/select-check.sh
Executable 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 //')"
|
||||
|
|
@ -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
90
proto-cuda/nvrtc/serve-bench.sh
Executable 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"
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<your 40-hex payout address>.%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<your 40-hex payout address>.%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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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<your 40-hex payout address>.%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<your 40-hex payout address>.%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>
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal file
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
4
tools/ci/notes/f04-fin-20261008.json
Normal file
4
tools/ci/notes/f04-fin-20261008.json
Normal 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)"
|
||||
}
|
||||
4
tools/ci/notes/f14-ops-20261008.json
Normal file
4
tools/ci/notes/f14-ops-20261008.json
Normal 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"
|
||||
}
|
||||
4
tools/ci/notes/f14-ux-20261008.json
Normal file
4
tools/ci/notes/f14-ux-20261008.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
65
tools/ci/proof-rule-bypass-check.sh
Executable file
65
tools/ci/proof-rule-bypass-check.sh
Executable 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
5
tools/ci/review-suite-check.sh
Executable 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
75
tools/ci/review-suite.mjs
Normal 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
44
tools/ci/test-map-merge.py
Executable 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")
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue