Build DOE-PERSIST-TRIAL: block-backed DoE campaign persistence, live-verified

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
Robert Allan James
2026-09-17 16:43:46 -04:00
co-authored by Claude Sonnet 5
parent 8b8e8e1405
commit 2da746c01c
8 changed files with 471 additions and 22 deletions
+64
View File
@@ -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.
+4 -2
View File
@@ -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)
+18 -18
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-17T16:28:31Z -->
<!-- Generated by mkcapsule --manifest 2026-09-17T18:37:43Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
@@ -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
+3 -1
View File
@@ -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 ( -- )
+4 -1
View File
@@ -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 )
+172
View File
@@ -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 <stdint.h>
#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 */
+148
View File
@@ -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 <stddef.h>
#include <string.h>
#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;
}
+58
View File
@@ -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 <string.h> /* 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
}