Punch list §25 item 0.3 complete.
The functional work (FDT reader, `time` CSR switch, SBI TIME extension
arm/re-arm, sie.STIE) was committed separately by Captain Bob as accd79f,
honestly labeled "NOT complete." This finishes it: stale documentation
cleanup, then rigorous verification against the item's literal acceptance
text, which the prior commit had not yet done.
Doc cleanup: every remaining reference to `rdcycle` / "assumed 1 GHz" in
riscv64/timer.c and riscv64/apic.c rewritten to describe the actual `time`
CSR / SBI-armed behavior. Caught and corrected my own arithmetic error in
the process: a draft claimed the ns-overflow bound improved to "~58 years"
under the new counter; direct computation shows the bound is actually
~3.26 days (2^48 ns) and is *frequency-independent* -- s_ns_per_tick and
tick rate scale inversely and cancel. Verified with a script before
writing the final comment, not asserted.
Verification, since "boots to prompt" was not sufficient for this item's
acceptance ("heartbeat_ticks() advances ... within measurement noise"):
- No FORTH word exposes heartbeat_ticks() to the REPL, and adding one would
be a new primitive outside this item's scope. GDB-over-QEMU-stub reads of
the static counter failed ("Cannot access memory"), likely a virtual-vs-
runtime address mismatch; abandoned rather than debugged further, since a
better instrument was available.
- QEMU's own `-d int` trap tracing (independent of all guest code) shows
2,797 `cause:5 desc=s_timer` interrupts delivered by the time boot reaches
the prompt, and the interleaved `desc=supervisor_ecall` entries are exactly
the SBI set_timer calls from riscv64_timer_rearm(), confirming the full
interrupt -> handler -> re-arm -> SBI loop.
- Measured rate over a real 10.127 s window: 1,013 further interrupts,
100.028 Hz observed against 100 Hz configured -- 0.03% deviation. The
sustained, non-decaying rate is itself proof the one-shot re-arm succeeds
on every cycle, not just the first.
- Confirmed regression-free on amd64 and aarch64 too: uefi_loader.c, uefi.h,
boot_info_offsets.h and the new fdt.c/fdt.h are shared across all three
builds (amd64 also consumes the offset constants via kernel_entry.S), so
all three were rebuilt and booted. Identical dict_hash
0x3d4e1daf289da94f on all three, matching the item 0.1/0.2 baseline.
Reported, not fixed, per rule 3:
- DOE_INJECT=1's EXEC-DOE now fails as "UNKNOWN WORD" against the pruned
Hera-alone capsule -- doe.4th was never loaded by init.4th even before
item 0.1's prune, so this is a pre-existing gap the prune surfaced, not a
new defect. Discovered while chasing why a 200-rep injected DoE run
produced no new CSV rows after 25 minutes; it had failed in the first
millisecond, not run the whole time.
- repl.c's own comment claims a "Heartbeat: N ticks" diagnostic prints to
the serial log; no such print exists anywhere in the tree.
- riscv64/arch.c's arch_read_timestamp() still uses rdcycle with a stale
1 GHz comment, but it has zero callers on this architecture -- dead code,
left alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bare-Metal DoE Experiment
This directory holds data and analysis from the LithosAnanke kernel's Design of Experiments (DoE) runs — blind full-factorial 2⁴ experiments that measure the L8 Jacquard mode selector's effect on the Steady-State Machine across all three supported architectures (amd64, aarch64, riscv64).
Directory Layout
experiments/bare_metal/
├── runs/ ← timestamped canonical CSVs from every acceptance run
├── latest/ ← arch-named copies of the most recent run (human-readable)
│ ├── amd64.csv
│ ├── aarch64.csv
│ └── riscv64.csv
└── analysis/
├── charts/ ← generated SVG/PNG charts
├── report/ ← timestamped LaTeX / Markdown reports
└── tables/ ← generated summary tables
runs/ is the canonical archive. latest/ is the eyeball-friendly shortcut
— always the most recent run per architecture, overwritten on each new run.
Running the DoE
The DoE runs automatically when the kernel boots because init.4th calls it.
The standard acceptance command runs all three architectures sequentially:
make -f Makefile.starkernel ARCH=amd64 clean qemu
make -f Makefile.starkernel ARCH=aarch64 clean qemu
make -f Makefile.starkernel ARCH=riscv64 clean qemu
Each run executes 48 trials (16 L8 configs × 3 reps, Fisher-Yates shuffled), captures ~1.27 million heartbeat rows per architecture, and writes two files:
| File | Path |
|---|---|
| Timestamped canonical CSV | experiments/bare_metal/runs/doe-<arch>-<YYYYMMDD-HHMMSS>.csv |
| Latest convenience copy | experiments/bare_metal/latest/<arch>.csv |
Run architectures sequentially, never in parallel. All three QEMU
instances use accel=tcg (software emulation). Concurrent runs compete for
host CPU and corrupt the timing signal that the DoE is measuring.
Disabling the DoE
To boot into the REPL without running the experiment, comment out the last
two lines of capsules/init.4th:
Block 2049
( first init.4th )
: STAR 42 EMIT ;
: STARS 0 DO STAR LOOP ;
: MARGIN 30 SPACES ;
: BAR MARGIN 5 STARS CR ;
: BLIP MARGIN STAR CR ;
: F CR BAR BLIP BAR BLIP BLIP CR ;
( S" Hermes" BIRTH )
( S" Artemis" BIRTH )
( S" doe.4th" EXEC ) ← comment this out
( 123456 3 L8-DOE ) ← comment this out
The kernel will boot to the ok> REPL with no experiment running.
Commenting both lines leaves doe.4th unloaded so none of its words
(L8-DOE, WL-NAME, etc.) are defined, which is the cleanest state for
interactive sessions.
Changing the Seed and Rep Count
The DoE entry point is L8-DOE ( seed reps -- ).
The call in init.4th is:
123456 3 L8-DOE
-
Seed — any non-zero integer. The same seed always produces the same shuffled run order, so results are reproducible. Change the seed to explore a different permutation; different seeds are statistically equivalent but verify shuffle-independence.
-
Reps — trials per L8 configuration (1–200). 3 reps × 16 configs = 48 runs, which takes roughly 25–30 minutes per architecture under TCG. Increase for higher statistical power; decrease for quick smoke checks.
( quick smoke check — 1 rep, 16 runs total )
42 1 L8-DOE
( full study — 10 reps, 160 runs )
987654 10 L8-DOE
What Are Capsules?
A capsule is a named blob of FORTH-79 source text stored in capsules/.
The kernel's EXEC word loads a capsule by filename and interprets it as
FORTH source. BIRTH (commented out in init.4th) would instead spawn an
isolated child VM whose sole personality is that capsule's code.
There are two roles:
| Role | Who uses it | What it does |
|---|---|---|
| Init capsule | Mama VM at boot | Defines the VM's vocabulary and behavior |
| Workload capsule | DoE machinery | Provides a computational task to time |
init.4th is the Mama VM's init capsule — executed exactly once at kernel
boot. The numbered files (init-0.4th … init-9.4th) and the L8 variant
files (init-l8-*.4th) are workload capsules used by the DoE.
.4th File Structure
Every .4th file must follow StarForth's block format. The block system
maps source text to 1024-byte logical blocks; the Block NNNN header tells
the loader which block slot to fill.
Mandatory rules:
- The first line of each logical block must be
Block NNNN(capital B, single space, decimal integer). - Block numbers must be unique within a single capsule file.
- Blocks are loaded in file order and executed top-to-bottom.
- Each block can hold up to 1024 bytes of source text.
- Comments use
( ... )— parentheses with spaces inside. - Word definitions use
: NAME ... ;— standard FORTH-79.
Minimal capsule skeleton:
Block 3100
( My capsule description )
: MY-WORD ( -- )
42 . CR ;
MY-WORD
Multi-block capsule:
Block 3100
( Block 1: helpers )
: HELPER ( n -- n*2 ) 2 * ;
Block 3101
( Block 2: main logic )
: MAIN ( -- )
10 0 DO I HELPER . CR LOOP ;
MAIN
The block number namespace is shared across all loaded capsules. Convention used in this repository:
| Range | Contents |
|---|---|
| 2048–2099 | init.4th (Mama VM boot sequence) |
| 2100–2199 | doe.4th (DoE machinery) |
| 3000–3999 | Workload capsules (init-0 … init-9, init-l8-*) |
| 4000+ | User-defined capsules |
Adding a Custom Workload Capsule
Step 1 — Create the file.
Add capsules/my-workload.4th using block numbers in the 4000+ range:
Block 4000
( my-workload.4th - description of what this measures )
: MY-COMPUTE ( n -- )
0 SWAP 0 DO I 3 * + LOOP DROP ;
Block 4001
( main entry point )
: RUN-MY-WORKLOAD ( -- )
500 0 DO I MY-COMPUTE LOOP ;
RUN-MY-WORKLOAD
The last line should execute the workload so EXEC runs it immediately when
the capsule is loaded.
Step 2 — Wire it into the DoE.
Open capsules/doe.4th and add your capsule to the workload dispatch table.
Find WL-HI (Block 2057) and replace one of the existing entries, or extend
the range:
Block 2057
: WL-HI ( n -- c-addr u )
CASE
0 OF S" init-8.4th" ENDOF
1 OF S" init-9.4th" ENDOF
2 OF S" init-l8-diverse.4th" ENDOF
3 OF S" init-l8-omni.4th" ENDOF
4 OF S" init-l8-stable.4th" ENDOF
5 OF S" init-l8-temporal.4th" ENDOF
6 OF S" init-l8-transition.4th" ENDOF
7 OF S" my-workload.4th" ENDOF ← replace slot 7
DROP S" init-0.4th"
ENDCASE ;
There are 16 workload slots total (0–7 in WL-LO, 0–7 in WL-HI).
The DoE machinery picks workloads blindly from these slots — your capsule
will appear in the shuffled run matrix alongside the built-in workloads.
Step 3 — Run the experiment.
make -f Makefile.starkernel ARCH=amd64 clean qemu
Your workload's heartbeat rows will appear in the CSV under whatever
CURR-WL index maps to my-workload.4th. Match by the DOE-RUN marker
lines in the CSV:
DOE-RUN,run_id,cfg,wl_id,rep
CSV Format
Each row emitted by the [HADES][DOE ] serial tag is one heartbeat tick
during a workload execution. Extract with:
grep -aP '\[HADES\]\[DOE \]' logs2/qemu-amd64-<timestamp>.log \
| sed 's/.*\[DOE \] //' > my.csv
Columns (15 total):
| # | Name | Type | Description |
|---|---|---|---|
| 1 | tick_number |
uint32 | Monotonic heartbeat counter |
| 2 | elapsed_ns |
uint64 | Nanoseconds since run start |
| 3 | tick_interval_ns |
uint64 | Interval from prior tick |
| 4 | cache_hits_delta |
uint32 | Hot-words cache hits this tick |
| 5 | bucket_hits_delta |
uint32 | Bucket hits this tick |
| 6 | word_executions_delta |
uint32 | Words executed this tick |
| 7 | hot_word_count |
uint64 | Words with heat ≥ threshold |
| 8 | avg_word_heat_q48 |
uint64 | Mean heat (raw Q48.16 integer) |
| 9 | window_width |
uint32 | L8's target rolling window size |
| 10 | actual_window_size |
uint32 | True analysis width: min(total_executions, window_width) |
| 11 | predicted_label_hits |
uint32 | ANOVA early-exit confirmations (L8 validation signal) |
| 12 | jitter_bits |
uint64 | Estimated jitter (IEEE 754 bit pattern) |
| 13 | apic_ticks |
uint64 | APIC timer monotonic count |
| 14 | time_trust_q48 |
uint64 | Time-trust score (Q48.16) |
| 15 | variance_q48 |
uint64 | Timing variance (Q48.16) |
avg_word_heat_q48 is a raw fixed-point integer. To convert to a human-readable
heat value: avg_word_heat = avg_word_heat_q48 / 65536.0.
jitter_bits is the IEEE 754 double-precision bit pattern of the jitter in
nanoseconds. In R: readBin(as.raw(…), "double"). In Python:
struct.unpack('d', struct.pack('Q', n))[0].
Interpreting predicted_label_hits
This column is the feedback-loop closure signal.
Each non-zero value means the inference engine ran ANOVA on the current execution window and confirmed the L8 selector's config choice correlated with the subsequent execution pattern — an "early exit" because the statistical test converged without needing all data.
- High rate → L8 chose well; the system settled quickly into a stable regime.
- Low rate → L8 is still searching; the workload is novel or transient.
- Zero throughout → The workload ended before the inference engine had enough data, or the window is too small to trigger ANOVA.
This is the metric that closes the loop between "L8 made a choice" and "that choice was actually validated by what the VM did next."