From 2da746c01c95491966a09a7c3e0d329e27c1d8e9 Mon Sep 17 00:00:00 2001 From: Robert Allan James Date: Thu, 17 Sep 2026 16:43:31 -0400 Subject: [PATCH] Build DOE-PERSIST-TRIAL: block-backed DoE campaign persistence, live-verified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FABRIC-3.md §XXXV.3/.4 designed this and it was never built -- two live campaigns got launched relying entirely on a live serial-log tail before this was caught. doe_region.c/.h mirrors log_region.c's own growable-ring pattern (own devblock_from_top fence at 98, past log_region's ceiling at 97) to persist one trial-summary record per completed trial straight to disk/artemis.img, surviving past QEMU exit with no host required. Verified end to end: booted amd64, called DOE-PERSIST-TRIAL at the console, clean BYE, then read the raw disk bytes back with QEMU fully dead -- confirmed the exact values passed in. Wired into both multiuser-doe.4th and per-isa-doe.4th's trial markers. Clean build, zero new warnings, all 3 ISAs; mkcapsule --lint passes both edited capsules. FABRIC-3.md §XXXV.10 documents the fix and the root-cause correction: two campaigns were launched on an unbuilt persistence design before this was caught and fixed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K --- FABRIC-3.md | 64 ++++++++++++ Makefile.starkernel | 6 +- capsules/BLOCK_MAP.md | 36 +++---- capsules/multiuser-doe.4th | 4 +- capsules/per-isa-doe.4th | 5 +- include/starkernel/doe_region.h | 172 ++++++++++++++++++++++++++++++++ src/starkernel/doe_region.c | 148 +++++++++++++++++++++++++++ src/word_source/log_words.c | 58 +++++++++++ 8 files changed, 471 insertions(+), 22 deletions(-) create mode 100644 include/starkernel/doe_region.h create mode 100644 src/starkernel/doe_region.c diff --git a/FABRIC-3.md b/FABRIC-3.md index 2b5c5534..0ea4167f 100644 --- a/FABRIC-3.md +++ b/FABRIC-3.md @@ -5578,3 +5578,67 @@ even one real trial. Launching the first live ~24.8h run is a separate, later st their baseline identity/auth works (confirmed live), but elevation eligibility, if any of them had it before, was not restored. Left open; re-add via `ZUSE-ELIGIBILITY-ADD` later if needed. +### XXXV.10 -- §XXXV.3/.4's persistence writer BUILT and live-verified end to end +(2026-09-17) + +**Root-cause correction first:** two live campaigns (the original single-ISA 180-trial run and +this section's own per-ISA amd64 leg) were launched between §XXXV.8 and this entry on the +mistaken assumption that the campaign was ready, without re-checking that §XXXV.9 explicitly +flagged the persistence writer as design-only. Both runs depended entirely on a live serial-log +tail and were killed mid-flight (one by a colliding peer session, one deliberately, by request) +with no durable record beyond the raw serial logs already committed. Flagged directly by Captain +Bob ("Just plain dropped the ball and ignored all previous instructions") -- correct. Saved as +`feedback_verify_design_implemented_before_launching_campaign.md`: grep the actual source for a +named primitive before treating a design section as launch-ready, don't infer readiness from a +nearby "ratified"/"executed" header. + +**Built, mirroring `log_region.c`/`log_region.h`'s exact pattern per §XXXV.3:** +- `include/starkernel/doe_region.h` / `src/starkernel/doe_region.c` -- new, separate growable + ring (own control header, own `devblock_from_top` base at 98, immediately past + `log_region.c`'s own ceiling at 97 -- confirmed by direct arithmetic, not the `log_region.h` + doc comment's slightly-off "97+" framing, which actually meant "97 is log_region's last used + devblock," so 98 is the first genuinely free one). 64-byte slots (`doe_trial_slot_t`: two + `uint64_t` aggregates -- `fleet_k_q48`, `switch_count_cumulative` -- six `uint32_t` trial + fields matching `MU-EMIT-TRIAL-MARKER` exactly -- `run_id`/`cfg`/`rep`/`nw`/`mode`/ + `fail_count` -- plus `fleet_conserved` and an 8-byte ISA tag), 64 slots/devblock, growable + 1->4 devblocks (256 slots ceiling, well over one full 3-ISA campaign's 54 trials). Plain FIFO + eviction at the ceiling (no priority-level concept for a trial summary, unlike + `log_region.c`'s `LOG_REGION_PROTECTED_MAX_LEVEL`). +- `DOE-PERSIST-TRIAL ( run cfg rep nw mode fail isa-addr isa-u -- )`, registered in + `src/word_source/log_words.c` (same file as `(LOG-APPEND-RAW)`, same kernel-only pattern). + `fleet_k_q48`/`fleet_conserved`/`switch_count_cumulative` are read directly inside the C word + (`vm_physics_fleet_heat_sum()`, `vm_physics_conserved()`, `sk_vm_switch_signal_switch_count()`) + -- not caller-supplied, same non-spoofable-attribution discipline `(LOG-APPEND-RAW)` already + established for its own source field. +- `Makefile.starkernel`: `doe_region.c` added to both `LOADER_SRCS_BASE` and `KERNEL_SRCS_BASE` + alongside `log_region.c`. +- Wired into both `multiuser-doe.4th`'s own `MU-EMIT-TRIAL-MARKER` (Block 5051, empty ISA tag -- + the base campaign driver has no ISA concept of its own) and `per-isa-doe.4th`'s redefinition + (Block 5118, real `PER-ISA-TAG-BUF`/`PER-ISA-TAG-LEN` passed through) -- both fit within the + existing block allocation with no new blocks needed (`per-isa-doe.4th` had zero spare blocks + per §XXXV.8's own note; fit entirely inside the 4 lines of headroom Block 5118 already had). + `mkcapsule --lint` PASS on both files after editing. + +**Verified live, full round trip, not just "it compiles":** +1. Clean build, zero new warnings, all 3 ISAs (amd64/aarch64/riscv64) -- confirmed `doe_region.o` + present in both loader and kernel object trees. +2. Booted amd64, loaded `multiuser-doe.4th` then `per-isa-doe.4th`, called + `111 22 3 4 5 0 S" testisa" DOE-PERSIST-TRIAL` directly at the console -- `ok`, no error; + `VM-ERROR? .` printed `0`. Clean `BYE`, zero leaked `qemu-system-x86_64` processes. +3. **With QEMU fully exited**, read `disk/artemis.img` directly off disk with an independent + host-side script (no live process, no serial socket involved) -- confirmed the control header + at `devblock_from_top=98` (magic `'DOED'`, `devblocks=1 head_slot=0 tail_slot=1 + record_count=1`) and the first slot at `devblock_from_top=99` contain exactly + `run_id=111 cfg=22 rep=3 nw=4 mode=5 fail=0 conserved=1 isa="testisa"` -- the literal values + passed at the console, byte-for-byte, persisted with no host in the loop. This is the actual + property §XXXV.1 identified as missing and required. +4. Reverted the synthetic test write from `disk/artemis.img` (`git checkout --`) before + committing, per this repo's own established convention (§XXXV.8's `mounted_time` precedent) + -- the region's first real record should be an actual campaign trial, not test data. + +**Still open, unchanged from §XXXV.9:** the host-side extraction/reader tool (reading the ring +back into a CSV, mirroring `scripts/extract_doe.sh`'s role for serial logs) is not built -- v1 +scope per §XXXV.3 was the writer only, verified here by a one-off Python script reading the raw +bytes directly, not a maintained tool. Launching the first live multi-hour campaign is still a +separate, later, explicit decision -- this entry closes the persistence-writer gap, nothing more. + diff --git a/Makefile.starkernel b/Makefile.starkernel index 760403a5..3b1593f6 100644 --- a/Makefile.starkernel +++ b/Makefile.starkernel @@ -458,7 +458,8 @@ LOADER_SRCS_BASE := \ $(KERNEL_SRC)/heartbeat.c \ $(KERNEL_SRC)/homeblocks_sig.c \ $(KERNEL_SRC)/artemis_sig.c \ - $(KERNEL_SRC)/log_region.c + $(KERNEL_SRC)/log_region.c \ + $(KERNEL_SRC)/doe_region.c LOADER_ASM := \ $(KERNEL_SRC)/arch/$(ARCH)/boot.S \ @@ -516,7 +517,8 @@ KERNEL_SRCS_BASE := \ $(KERNEL_SRC)/heartbeat.c \ $(KERNEL_SRC)/homeblocks_sig.c \ $(KERNEL_SRC)/artemis_sig.c \ - $(KERNEL_SRC)/log_region.c + $(KERNEL_SRC)/log_region.c \ + $(KERNEL_SRC)/doe_region.c KERNEL_ASM := $(wildcard $(KERNEL_SRC)/arch/$(ARCH)/*.S) diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index c041d4d5..8d5b857f 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,5 @@ # Capsule Block Manifest — Auto-generated - + @@ -27,8 +27,8 @@ | `init-l8-volatile.4th` | 4810, 4811, 4812, 4813 | `0x98caabbbd92abac4` | yes | | `init.4th` | 2049, 2050, 2057 | `0x1ef4939ed32ec1e6` | yes | | `lib.4th` | 4050 | `0x4b216635c359ef73` | yes | -| `multiuser-doe.4th` | 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055 | `0x22140588ee7c39e6` | yes | -| `per-isa-doe.4th` | 5117, 5118, 5119 | `0xe3a6c1352d3ecaed` | yes | +| `multiuser-doe.4th` | 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055 | `0xaef05d0fd502fdc4` | yes | +| `per-isa-doe.4th` | 5117, 5118, 5119 | `0x369a7c1cad0c3a7b` | yes | | `process.4th` | 4300, 4301 | `0x781afc1dbd0294f7` | yes | | `sdk.4th` | 5109, 5110, 5111, 5112, 5113, 5114, 5115 | `0x008fdbbb62c94a3a` | yes | | `turtle.4th` | 5100, 5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108 | `0x4d470418ca543365` | yes | @@ -377,18 +377,18 @@ | 5041 | `common:messaging.4th` | `0x201cfd6d39fcb22d` | ok | | 5042 | `common:messaging.4th` | `0x201cfd6d39fcb22d` | ok | | 5043 | `workload-calib1.4th` | `0x3b9f2d17b554fabc` | ok | -| 5044 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5045 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5046 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5047 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5048 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5049 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5050 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5051 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5052 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5053 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5054 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | -| 5055 | `multiuser-doe.4th` | `0x22140588ee7c39e6` | ok | +| 5044 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5045 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5046 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5047 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5048 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5049 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5050 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5051 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5052 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5053 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5054 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | +| 5055 | `multiuser-doe.4th` | `0xaef05d0fd502fdc4` | ok | | 5056 | `workload-5-lite.4th` | `0xea0303f38824f254` | ok | | 5057 | `workload-5-lite.4th` | `0xea0303f38824f254` | ok | | 5058 | `workload-1-lite.4th` | `0x44a7a7e3176dcc8d` | ok | @@ -410,9 +410,9 @@ | 5114 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok | | 5115 | `sdk.4th` | `0x008fdbbb62c94a3a` | ok | | 5116 | `hermes:init.4th` | `0x2df61924448a6812` | ok | -| 5117 | `per-isa-doe.4th` | `0xe3a6c1352d3ecaed` | ok | -| 5118 | `per-isa-doe.4th` | `0xe3a6c1352d3ecaed` | ok | -| 5119 | `per-isa-doe.4th` | `0xe3a6c1352d3ecaed` | ok | +| 5117 | `per-isa-doe.4th` | `0x369a7c1cad0c3a7b` | ok | +| 5118 | `per-isa-doe.4th` | `0x369a7c1cad0c3a7b` | ok | +| 5119 | `per-isa-doe.4th` | `0x369a7c1cad0c3a7b` | ok | ## Conflicts diff --git a/capsules/multiuser-doe.4th b/capsules/multiuser-doe.4th index cb6eaa16..0e67dbb6 100644 --- a/capsules/multiuser-doe.4th +++ b/capsules/multiuser-doe.4th @@ -110,7 +110,9 @@ VARIABLE MU-MODE VARIABLE MU-NREPS ." rep=" MU-REP @ . ." nw=" MU-NW @ . ." mode=" MU-MODE @ . - ." fail=" MU-FAIL-COUNT @ . CR ; + ." fail=" MU-FAIL-COUNT @ . CR + MU-RUN-ID @ MU-CFG @ MU-REP @ MU-NW @ + MU-MODE @ MU-FAIL-COUNT @ 0 0 DOE-PERSIST-TRIAL ; Block 5052 : MU-BIRTH-ALL ( -- ) diff --git a/capsules/per-isa-doe.4th b/capsules/per-isa-doe.4th index 45825a76..46d85643 100644 --- a/capsules/per-isa-doe.4th +++ b/capsules/per-isa-doe.4th @@ -27,7 +27,10 @@ Block 5118 ." rep=" MU-REP @ . ." nw=" MU-NW @ . ." mode=" MU-MODE @ . - ." fail=" MU-FAIL-COUNT @ . CR ; + ." fail=" MU-FAIL-COUNT @ . CR + MU-RUN-ID @ MU-CFG @ MU-REP @ MU-NW @ + MU-MODE @ MU-FAIL-COUNT @ + PER-ISA-TAG-BUF PER-ISA-TAG-LEN @ DOE-PERSIST-TRIAL ; Block 5119 ( Per-ISA entry points, single word, no args -- for boot ) diff --git a/include/starkernel/doe_region.h b/include/starkernel/doe_region.h new file mode 100644 index 00000000..9fe60482 --- /dev/null +++ b/include/starkernel/doe_region.h @@ -0,0 +1,172 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * doe_region.h - Growable per-trial DoE-campaign-summary ring on Artemis's + * own disk (FABRIC-3.md §XXXV.3/.4, built 2026-09-17) + * + * Records one summary per completed multiuser-doe.4th/per-isa-doe.4th trial + * so a campaign's pass/fail and top-line physics picture survives past the + * QEMU serial log -- §XXXV.1 identified that doe_log.c's per-tick rows + * (console_puts()-only) cannot survive real bare-metal boot at all, and + * raw per-tick mirroring to block storage was ruled out entirely on size + * grounds (§XXXV.2: 516 MB for 8 trials vs. a ~1 GiB device). This region + * persists trial SUMMARIES only (v1, §XXXV.4) -- exactly the fields + * MU-EMIT-TRIAL-MARKER already computes, plus three trial-end aggregate + * physics reads (vm_physics_fleet_heat_sum(), vm_physics_conserved(), + * sk_vm_switch_signal_switch_count()) -- not a raw tick mirror. + * + * Same fence/accessor discipline log_region.h already established: lives + * in Artemis's top-of-device system-metadata fence, reached via + * block_subsystem.h's blk_meta_zone_read()/write() (devblock_from_top + * addressing), same growable-ring control-header shape (magic/version/CRC, + * devblocks/head_slot/tail_slot/record_count). A SEPARATE region from + * log_region.c's own -- different shape (structured numeric fields, not + * free text), different growth ceiling, and isolation so a runaway DoE + * region can never crowd out real log persistence (§XXXV.3's own stated + * reasoning, reused verbatim here). + * + * Fixed devblock_from_top allocation, immediately past log_region.c's own + * ceiling (which occupies [65, 65+1+LOG_REGION_MAX_DEVBLOCKS-1] = + * [65, 97] at full growth): + * 98 -- this region's control header (DOE_REGION_DEVBLOCK_FROM_TOP_BASE) + * 99-102 -- this region's slot devblocks, growable up to DOE_REGION_MAX_DEVBLOCKS + * + * Slot granularity is small (DOE_SLOT_SIZE, 64 bytes) relative to + * log_slot_t's 1024 -- a trial summary is a handful of fixed numeric + * fields plus an 8-byte ISA tag, nothing free-text-sized. 64 slots/devblock + * x DOE_REGION_INITIAL_DEVBLOCKS(1) = 64 already exceeds one full 3-ISA + * campaign's 54 trials (§XXXV.4); the growth ceiling (4 devblocks = 256 + * slots) is headroom for repeated campaigns, not a sizing risk. + * + * No priority-eviction logic (unlike log_region.c's LOG_REGION_PROTECTED_ + * MAX_LEVEL, §XXXII.4) -- there is no level concept for a trial summary; + * plain FIFO eviction of the oldest record when the ring is full AND + * already at the growth ceiling, which in practice will not be reached by + * any campaign shape this project has run or ratified. + */ + +#ifndef STARKERNEL_DOE_REGION_H +#define STARKERNEL_DOE_REGION_H + +#include +#include "starkernel/log_region.h" /* LOG_REGION_DEVBLOCK_FROM_TOP_BASE, LOG_REGION_MAX_DEVBLOCKS -- fence math only */ + +#ifdef __cplusplus +extern "C" { +#endif + +/*=========================================================================== + * Fence allocation + *===========================================================================*/ + +#define DOE_REGION_DEVBLOCK_FROM_TOP_BASE \ + (LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + LOG_REGION_MAX_DEVBLOCKS) /* 98 */ +#define DOE_REGION_INITIAL_DEVBLOCKS 1u /* slot devblocks at first use, excludes control header */ +#define DOE_REGION_GROWTH_INCREMENT 1u +#define DOE_REGION_MAX_DEVBLOCKS 4u /* ceiling -- devblock_from_top stays within [99,102] */ + +#define DOE_SLOT_SIZE 64u +#define DOE_SLOTS_PER_DEVBLOCK (4096u / DOE_SLOT_SIZE) /* 64 */ + +/*=========================================================================== + * doe_region_ctrl_t - ring control header, one devblock at + * DOE_REGION_DEVBLOCK_FROM_TOP_BASE. Same shape as log_region_ctrl_t. + *===========================================================================*/ + +#define DOE_REGION_MAGIC 0x44454F44ull /* 'DOED' */ +#define DOE_REGION_VERSION_0 0 + +#define DOE_REGION_PACK(ver) \ + (DOE_REGION_MAGIC | ((uint64_t)(ver) << 32)) +#define DOE_REGION_GET_MAGIC(m) ((uint32_t)((m) & 0xFFFFFFFFull)) +#define DOE_REGION_GET_VERSION(m) ((uint8_t)(((m) >> 32) & 0xFF)) + +typedef struct { + uint64_t magic; /* DOE_REGION_PACK(...) */ + uint32_t devblocks; /* current slot-area size, in devblocks (excludes this header) */ + uint32_t head_slot; /* index of the oldest live record */ + uint32_t tail_slot; /* index where the NEXT record will be written */ + uint32_t record_count; /* live records, <= devblocks * DOE_SLOTS_PER_DEVBLOCK */ + uint64_t hdr_crc; /* covers every field above this one */ + uint8_t _pad[4096 - (8 + 4 + 4 + 4 + 4 + 8)]; +} doe_region_ctrl_t; + +typedef char doe_region_ctrl_size_check[(sizeof(doe_region_ctrl_t) == 4096) ? 1 : -1]; + +/*=========================================================================== + * doe_trial_slot_t - one trial summary record, exactly DOE_SLOT_SIZE bytes. + * Field order: uint64_t pair first (natural 8-byte alignment at offsets + * 0/8), then uint32_t fields (4-byte aligned from offset 16 on), then the + * ISA tag, then trailing pad -- same alignment discipline log_slot_t uses. + *===========================================================================*/ + +#define DOE_SLOT_ISA_MAX 8u /* NUL-padded, e.g. "amd64", "aarch64", "riscv64" */ + +typedef struct { + uint64_t fleet_k_q48; /* vm_physics_fleet_heat_sum() at trial end */ + uint64_t switch_count_cumulative; /* sk_vm_switch_signal_switch_count() at trial end */ + uint32_t run_id; + uint32_t cfg; + uint32_t rep; + uint32_t nw; + uint32_t mode; + uint32_t fail_count; + uint32_t fleet_conserved; /* vm_physics_conserved() at trial end, 0/1 */ + char isa[DOE_SLOT_ISA_MAX]; + uint8_t _pad[DOE_SLOT_SIZE - (8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + DOE_SLOT_ISA_MAX)]; +} doe_trial_slot_t; + +typedef char doe_trial_slot_size_check[(sizeof(doe_trial_slot_t) == DOE_SLOT_SIZE) ? 1 : -1]; + +/*=========================================================================== + * API + *===========================================================================*/ + +/* + * doe_region_append - Write one trial-summary record to the ring, growing + * it (within DOE_REGION_MAX_DEVBLOCKS) or evicting the oldest record (ring + * full and already at the growth ceiling) as needed. Initializes the ring + * on first use (control header blank). + * + * @param run_id, cfg, rep, nw, mode, fail_count Same fields + * MU-EMIT-TRIAL-MARKER already prints (multiuser-doe.4th Block 5051). + * @param fleet_k_q48, fleet_conserved, switch_count_cumulative Trial-end + * aggregate physics reads (§XXXV.4). + * @param isa ISA tag, e.g. "amd64" (may be empty for the pre-per-isa- + * doe.4th single-ISA campaign shape). + * @param isa_len Length of isa (truncated to DOE_SLOT_ISA_MAX-1). + * @return 0 on success, -1 on any read/write failure (ring left however + * the failed operation left it -- blk_meta_zone_write() itself + * never partially writes a devblock). + */ +int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw, + uint32_t mode, uint32_t fail_count, + uint64_t fleet_k_q48, uint32_t fleet_conserved, + uint64_t switch_count_cumulative, + const char *isa, uint32_t isa_len); + +#ifdef __cplusplus +} +#endif + +#endif /* STARKERNEL_DOE_REGION_H */ diff --git a/src/starkernel/doe_region.c b/src/starkernel/doe_region.c new file mode 100644 index 00000000..8604aa28 --- /dev/null +++ b/src/starkernel/doe_region.c @@ -0,0 +1,148 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * doe_region.c - Growable per-trial DoE-campaign-summary ring on Artemis's + * disk. See starkernel/doe_region.h for the format/interface design. + * + * Mirrors log_region.c's own read/write/grow/evict shape exactly (same + * blk_meta_zone_read()/write() accessor, same control-header CRC + * discipline), minus the priority-eviction logic that doesn't apply here + * (no level concept for a trial summary -- plain FIFO eviction only). + */ + +#include "starkernel/doe_region.h" + +#include +#include + +#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */ + +static uint64_t doe_region_ctrl_compute_crc(const doe_region_ctrl_t *ctrl) { + size_t crc_span = offsetof(doe_region_ctrl_t, hdr_crc); + return compute_crc64((const uint8_t *)ctrl, crc_span); +} + +/* Reads the control header; returns 0 and *out populated only if magic, + * version, and CRC all check out. Any other result (blank, foreign, + * corrupt, or a read failure) is treated identically by the caller: "not + * yet initialized," matching log_region_ctrl_read()'s own discipline. */ +static int doe_region_ctrl_read(doe_region_ctrl_t *out) { + if (blk_meta_zone_read(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1; + if (DOE_REGION_GET_MAGIC(out->magic) != (uint32_t)(DOE_REGION_MAGIC & 0xFFFFFFFFull)) return -1; + if (DOE_REGION_GET_VERSION(out->magic) != DOE_REGION_VERSION_0) return -1; + uint64_t want_crc = doe_region_ctrl_compute_crc(out); + if (want_crc != out->hdr_crc) return -1; + return 0; +} + +static int doe_region_ctrl_write(doe_region_ctrl_t *ctrl) { + ctrl->hdr_crc = doe_region_ctrl_compute_crc(ctrl); + return blk_meta_zone_write(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1; +} + +static void doe_region_ctrl_init_fresh(doe_region_ctrl_t *ctrl) { + memset(ctrl, 0, sizeof(*ctrl)); + ctrl->magic = DOE_REGION_PACK(DOE_REGION_VERSION_0); + ctrl->devblocks = DOE_REGION_INITIAL_DEVBLOCKS; + ctrl->head_slot = 0; + ctrl->tail_slot = 0; + ctrl->record_count = 0; +} + +/* Reads/writes the devblock holding slot_index (DOE_SLOTS_PER_DEVBLOCK + * slots per devblock), one devblock_from_top past the control header for + * every DOE_SLOTS_PER_DEVBLOCK slots. */ +static uint32_t doe_region_slot_devblock_from_top(uint32_t slot_index) { + return DOE_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / DOE_SLOTS_PER_DEVBLOCK); +} + +static int doe_region_write_slot(uint32_t slot_index, const doe_trial_slot_t *slot) { + uint8_t devblock_buf[4096]; + uint32_t dft = doe_region_slot_devblock_from_top(slot_index); + uint32_t slot_in_devblock = slot_index % DOE_SLOTS_PER_DEVBLOCK; + + /* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock + * granularity, and a devblock holds DOE_SLOTS_PER_DEVBLOCK slots, so a + * single-slot write must preserve its siblings. A read failure on a + * never-yet-written devblock (blank fence content) is not fatal here -- + * proceed with a zeroed buffer, matching log_region_write_slot()'s own + * discipline. */ + if (blk_meta_zone_read(dft, devblock_buf) != 0) { + memset(devblock_buf, 0, sizeof(devblock_buf)); + } + memcpy(devblock_buf + (size_t)slot_in_devblock * DOE_SLOT_SIZE, slot, sizeof(*slot)); + return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1; +} + +int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw, + uint32_t mode, uint32_t fail_count, + uint64_t fleet_k_q48, uint32_t fleet_conserved, + uint64_t switch_count_cumulative, + const char *isa, uint32_t isa_len) { + doe_region_ctrl_t ctrl; + if (doe_region_ctrl_read(&ctrl) != 0) { + doe_region_ctrl_init_fresh(&ctrl); + } + + uint32_t total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK; + if (ctrl.record_count >= total_slots) { + if (ctrl.devblocks < DOE_REGION_MAX_DEVBLOCKS) { + /* Grow: new slots appear at indices [old_total_slots .. + * new_total_slots-1], nothing already written moves -- safe + * at any point in the ring's lifecycle (see log_region.c's + * own identical reasoning). */ + ctrl.devblocks += DOE_REGION_GROWTH_INCREMENT; + if (ctrl.devblocks > DOE_REGION_MAX_DEVBLOCKS) ctrl.devblocks = DOE_REGION_MAX_DEVBLOCKS; + total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK; + } + if (ctrl.record_count >= total_slots) { + /* Still full (growth ceiling already reached) -- plain FIFO + * eviction, no priority concept for a trial summary. */ + ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots; + ctrl.record_count--; + } + } + + doe_trial_slot_t slot; + memset(&slot, 0, sizeof(slot)); + slot.fleet_k_q48 = fleet_k_q48; + slot.switch_count_cumulative = switch_count_cumulative; + slot.run_id = run_id; + slot.cfg = cfg; + slot.rep = rep; + slot.nw = nw; + slot.mode = mode; + slot.fail_count = fail_count; + slot.fleet_conserved = fleet_conserved; + + uint32_t isa_n = isa_len; + if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX; + if (isa && isa_n) memcpy(slot.isa, isa, isa_n); + + if (doe_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1; + + ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots; + if (ctrl.record_count < total_slots) ctrl.record_count++; + + return doe_region_ctrl_write(&ctrl) == 0 ? 0 : -1; +} diff --git a/src/word_source/log_words.c b/src/word_source/log_words.c index 68d03039..74edef1d 100644 --- a/src/word_source/log_words.c +++ b/src/word_source/log_words.c @@ -19,6 +19,9 @@ #include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */ #include "starkernel/capsule_birth.h" /* capsule_vm_registry_get, VMRegistryEntry */ #include "starkernel/console.h" /* console_println() -- (LOG-APPEND-RAW) diagnostics */ +#include "starkernel/doe_region.h" /* doe_region_append() -- DOE-PERSIST-TRIAL */ +#include "starkernel/capsule_vm_physics.h" /* vm_physics_fleet_heat_sum(), vm_physics_conserved() */ +#include "starkernel/capsule_vm_switch_signal.h" /* sk_vm_switch_signal_switch_count() */ #include /* strlen */ #endif @@ -296,6 +299,59 @@ static void log_word_append_raw(VM *vm) } #endif +/* ── DOE-PERSIST-TRIAL ( run cfg rep nw mode fail isa-addr isa-u -- ) ── */ +/* + * Kernel-only. Writes one trial-summary record to Artemis's on-disk DoE + * ring (doe_region.c), FABRIC-3.md §XXXV.3/.4. Called from + * MU-EMIT-TRIAL-MARKER (multiuser-doe.4th / per-isa-doe.4th) right + * alongside the existing console print, so a campaign's data survives + * past the QEMU serial log without needing a live host tail. + * + * fleet_k_q48/fleet_conserved/switch_count_cumulative are NOT caller- + * supplied -- read directly here via vm_physics_fleet_heat_sum(), + * vm_physics_conserved(), sk_vm_switch_signal_switch_count(), same + * discipline (LOG-APPEND-RAW) uses for its own source attribution: a + * trial-end snapshot no FORTH caller could spoof or get stale by passing + * the wrong value. + */ +#ifdef __STARKERNEL__ +static void log_word_doe_persist_trial(VM *vm) +{ + if (vm->dsp < 7) { + console_println("DOE-PERSIST-TRIAL: stack underflow"); + vm->error = 1; + return; + } + cell_t isa_u = vm_pop(vm); + cell_t isa_addr = vm_pop(vm); + cell_t fail = vm_pop(vm); + cell_t mode = vm_pop(vm); + cell_t nw = vm_pop(vm); + cell_t rep = vm_pop(vm); + cell_t cfg = vm_pop(vm); + cell_t run = vm_pop(vm); + + if (isa_u < 0 || isa_addr < 0 || (isa_addr + isa_u) > (cell_t)VM_MEMORY_SIZE) { + console_println("DOE-PERSIST-TRIAL: isa string address out of range"); + vm->error = 1; + return; + } + + const char *isa = (const char *)&vm->memory[isa_addr]; + + uint64_t fleet_k_q48 = vm_physics_fleet_heat_sum(); + unsigned fleet_conserved = vm_physics_conserved() ? 1u : 0u; + uint64_t switch_count = sk_vm_switch_signal_switch_count(); + + if (doe_region_append((uint32_t)run, (uint32_t)cfg, (uint32_t)rep, (uint32_t)nw, + (uint32_t)mode, (uint32_t)fail, + fleet_k_q48, fleet_conserved, switch_count, + isa, (uint32_t)isa_u) != 0) { + vm->error = 1; + } +} +#endif + /* ── Registration ────────────────────────────────────────────────────── */ /** @@ -308,6 +364,7 @@ static void log_word_append_raw(VM *vm) * String literals: LOG-ERROR" LOG-WARN" LOG-INFO" LOG-TEST" LOG-DEBUG" * Stack string: LOG-ERROR-STR LOG-WARN-STR LOG-INFO-STR * LOG-TEST-STR LOG-DEBUG-STR + * Kernel-only: (LOG-APPEND-RAW) DOE-PERSIST-TRIAL */ void register_log_words(VM *vm) { @@ -344,5 +401,6 @@ void register_log_words(VM *vm) #ifdef __STARKERNEL__ register_word(vm, "(LOG-APPEND-RAW)", log_word_append_raw); + register_word(vm, "DOE-PERSIST-TRIAL", log_word_doe_persist_trial); #endif }