Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+573
View File
@@ -0,0 +1,573 @@
/*
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.
*/
/**
* capsule_vm_physics.c - Dynamic VM Fleet Physics Implementation
*
* See capsule_vm_physics.h and
* docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
* for the full design rationale.
*
* Concurrency: no locking. The only interrupt-driven kernel path
* (heartbeat_tick, from the timer ISR) touches only its own isolated
* TimeTrustState and never calls into mama_word_*, vm_interpret, or any
* capsule/dictionary state. HEARTBEAT_THREAD_ENABLED=0 means no separate
* OS thread runs physics logic either. BIRTH/KILL/VM-EXEC/VM-CALL/
* VM-STEP all execute synchronously in Hera's single interpreter
* context with nothing able to preempt them mid-transfer.
*
* L8 coupling (rev q): the only place this file reaches outside its own
* VMPhysics/VMFleetWindow state is l8_regime_modulation_q16(), a
* read-only look at Hera's own ssm_l8_state via sk_get_mama_vm() --
* modulating the fleet's empirically-recovered transfer rate uniformly,
* never deciding which VM receives heat. Still a passive observer: this
* changes how fast the fleet's own rate estimate responds, not who runs
* or who gets touched.
*/
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h"
#include "starkernel/console.h"
#include "starforth_config.h" /* STADIUM_CAPACITY_TICK */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
#include "vm.h" /* VM, vm->ssm_l8_state */
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
/* Word-level ROLLING_WINDOW_SIZE defaults to 4096 because word executions
* number in the millions. Fleet touches are real VM-EXEC/VM-CALL/VM-STEP
* dispatches -- dozens to low hundreds over a full boot + acceptance run.
* 64 gives the regression enough samples to stabilize without being
* wastefully oversized for a 3-8 VM fleet. Compile-time constant, same
* tuning-knob convention as ROLLING_WINDOW_SIZE. */
#define VM_FLEET_WINDOW_DEPTH 64
/* |fleet_heat_sum - Q48_ONE| < 5% of Q48_ONE, mirrors fleet-k.4th's
* K-EPSILON (3277). */
#define VM_PHYSICS_EPSILON_Q48 3277ULL
typedef struct {
uint64_t execution_heat_q48;
uint64_t last_active_tick; /* fleet_heartbeat_tick_count value at last touch */
int touched; /* 0 until the first touch; explicit rather than
* overloading last_active_tick == 0, which is a
* legitimate tick value a first touch can land on */
int is_live;
} VMPhysics;
typedef struct vm_physics_node {
VMUuid vm_id;
VMPhysics physics;
struct vm_physics_node *next;
} vm_physics_node_t;
/* kmalloc-backed linked list, same pattern as capsule_birth.c's
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
static vm_physics_node_t *vm_physics_head = (void *)0;
/* One recorded touch's inputs to the transfer law (amount = elapsed_ticks *
* slope >> 16, clamped to available heat) -- not a derived heat value.
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
* heat-snapshot design (rev m) with this: since the transfer law is known
* exactly, the true rate can be recovered directly from any touch that
* wasn't clamped (rate = amount / elapsed_ticks), rather than curve-fitting
* a reconstructed trajectory against a synthetic time axis.
*
* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this used
* to be elapsed_us, wall-clock microseconds -- non-reproducible under TCG
* per GAP-A1 (FABRIC-0.md §25.7.1). Now counts fleet_heartbeat_tick_count
* ticks, which is execution-paced (incremented once per vm_tick() call,
* confirmed at vm_runtime.c:143). */
typedef struct {
uint64_t elapsed_ticks; /* since this vm_id's previous touch; 0 if there
* was no previous touch to compare against */
uint64_t amount; /* elapsed_ticks * slope >> 16 at touch time,
* i.e. the rate-implied transfer before any
* clamp; 0 if not computed (first touch, zero
* slope, or non-monotonic tick) */
int clamped; /* 1 if the heat actually moved was capped below
* `amount` by what the rest of the fleet held --
* carries no rate information, excluded from
* the estimator rather than treated as a zero */
} VMFleetTouchSample;
typedef struct {
VMFleetTouchSample touch_samples[VM_FLEET_WINDOW_DEPTH];
uint32_t head;
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
int is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
} VMFleetWindow;
/* One instance, fleet-wide -- not per-VM state. */
static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
/* Below this many informative (unclamped) samples in a full window, skip
* the fit and keep the current slope rather than trust a handful of
* points -- same "skip, don't substitute a degenerate value" philosophy
* as the is_warm gate. Arbitrary but conservative relative to the
* 64-deep window: needs an eighth of the window to be informative. */
#define VM_PHYSICS_MIN_INFORMATIVE_SAMPLES 8
/* Shared adaptive slope, fit from aggregate fleet statistics (one slope
* for the whole fleet), exactly mirroring how the word engine infers one
* decay_slope_q48 per VM instance rather than one per word.
*
* Seeded at 65536/3 (Q48.16 for 1/3), matching the word engine's actual
* bootstrap value (src/vm_bootstrap.c / src/starkernel/vm/vm_bootstrap.c:
* "vm->decay_slope_q48 = (1ULL << 16) / 3"). The old comment on that field
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
* here. This was previously seeded at literal 0, which cannot bootstrap:
* vm_physics_touch's transfer amount is elapsed_ticks * slope >> 16, so a
* zero slope moves zero heat on every touch forever, which starves
* vm_physics_tick's regression of the very heat-trajectory signal it
* needs to infer a better slope -- a closed loop with no way out. Found
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f).
*
* UNVALIDATED after the ns-to-tick restatement (FABRIC-0.md item 2.1,
* 2026-08-04): this value was calibrated against elapsed wall-clock
* microseconds. Elapsed fleet-heartbeat *ticks* between touches is a
* different quantity at a different scale, and the seed has not been
* re-fit against it. Left as-is rather than guessed at -- the acceptance
* boot test below only checks determinism, not transfer magnitude; a real
* re-tune is DoE work (item 5.1), not something to invent here. */
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
static uint64_t slope_fit_quality_q48 = 0;
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
* HeartbeatState.tick_count -- see the rationale in the header. Advances
* from whichever VM's own heartbeat cycle happens to fire, so the
* readiness signal reflects the fleet's aggregate activity rather than
* any one VM's (in practice, Hera's) personal word-execution rate.
* Declared here (rather than just above vm_physics_heartbeat_tick, where
* it used to live) so vm_physics_touch() can read the current tick
* without a forward declaration -- it is now this counter's second
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
static uint64_t fleet_heartbeat_tick_count = 0;
static uint64_t fleet_last_capacity_tick = 0; /* renamed 2026-08-15 from
fleet_last_inference_tick -- gated on
STADIUM_CAPACITY_TICK now, not HEARTBEAT_
INFERENCE_FREQUENCY, see vm_physics_heartbeat_tick() */
static vm_physics_node_t *vm_physics_find(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_head;
while (n) {
if (vm_uuid_equal(n->vm_id, vm_id)) return n;
n = n->next;
}
return (void *)0;
}
/**
* vm_physics_transfer - The one conservative primitive everything else
* is a special case of. Subtracts amount from from (clamped at 0) and
* adds the same amount to to. Nothing is created or destroyed:
* sum(execution_heat for all LIVE VMs) is invariant across any call.
*/
static void vm_physics_transfer(VMPhysics *from, VMPhysics *to, uint64_t amount_q48)
{
uint64_t moved = (amount_q48 > from->execution_heat_q48)
? from->execution_heat_q48
: amount_q48;
from->execution_heat_q48 -= moved;
to->execution_heat_q48 += moved;
}
void vm_physics_init(VMUuid vm_id)
{
vm_physics_node_t *node = vm_physics_find(vm_id);
/* Hera (vm_uuid_hera()) is the fleet's single structural root -- the one
* entry capsule_vm_kill refuses to ever kill, and the same fixed
* point vm_physics_find_root_id below walks every parent chain up
* to. She is where the fleet's whole Q48_ONE initially resides;
* every other VM joins an already-nonzero fleet and starts at 0,
* per the "cold mass added to a closed system" reasoning -- which
* requires a nonzero sum to already exist, so Hera's own
* registration is where that sum is seeded. */
int is_root = vm_uuid_is_hera(vm_id);
if (node) {
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
return;
}
node = (vm_physics_node_t *)kmalloc(sizeof(vm_physics_node_t));
if (!node) return;
node->vm_id = vm_id;
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
node->next = vm_physics_head;
vm_physics_head = node;
}
/**
* vm_physics_find_root_id - Walk parent_vm_id edges up to Hera
*
* Every VM's parent_vm_id (VMRegistryEntry, capsule_run.h) is set once
* at its own birth and never rewritten -- including a dead VM's, so the
* walk continues correctly through an already-killed intermediate
* parent without any special dead-parent handling. Hera's own entry is
* self-referential (parent_vm_id == vm_id == vm_uuid_hera()), which is
* what stops the walk: she is the only node with no outbound edge.
*
* The 64-hop guard is defensive only -- a well-formed chain terminates
* in O(fleet depth) hops, nowhere near 64. It exists so a corrupted
* chain (which should never happen) returns the starting vm_id instead
* of spinning.
*/
static VMUuid vm_physics_find_root_id(VMUuid vm_id)
{
VMUuid cursor = vm_id;
uint32_t guard;
for (guard = 0; guard < 64; guard++) {
VMRegistryEntry entry;
if (capsule_vm_registry_get(cursor, &entry) != 0) break;
if (vm_uuid_equal(entry.parent_vm_id, cursor)) break; /* self-referential: root */
cursor = entry.parent_vm_id;
}
return cursor;
}
void vm_physics_retire(VMUuid vm_id)
{
vm_physics_node_t *dying = vm_physics_find(vm_id);
vm_physics_node_t *root;
VMUuid root_id;
if (!dying || !dying->physics.is_live) return;
root_id = vm_physics_find_root_id(vm_id);
root = !vm_uuid_equal(root_id, vm_id) ? vm_physics_find(root_id) : (void *)0;
if (root) {
/* The dying VM's entire remaining heat flows to the root it
* chains up to (in practice always Hera today) -- no division,
* no weighting policy, no "no survivors" case, since a root is
* always there by construction. This is also what makes
* TRIPOD-TEST's kill-then-rebirth "K soak" check meaningful: a
* root VM (e.g. Hermes) being killed and later respawned by
* Hera doesn't lose heat across the gap, it just parks at Hera
* in the meantime. */
vm_physics_transfer(&dying->physics, &root->physics,
dying->physics.execution_heat_q48);
}
/* root_id == vm_id means the dying VM IS the root itself (Hera --
* shouldn't happen, she can't be killed) or the chain was broken
* (shouldn't happen, see vm_physics_find_root_id). Either way
* there's nowhere conservation-preserving to send the remainder;
* it is dropped along with the dying entry. */
dying->physics.execution_heat_q48 = 0;
dying->physics.is_live = 0;
}
void vm_physics_touch(VMUuid vm_id)
{
vm_physics_node_t *target = vm_physics_find(vm_id);
VMFleetTouchSample sample = { 0, 0, 0 };
uint64_t now_tick = fleet_heartbeat_tick_count;
if (!target || !target->physics.is_live) return;
/* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this
* used to gate on target->physics.last_active_ns > 0 and scale by
* elapsed wall-clock microseconds -- non-reproducible run to run under
* TCG (GAP-A1, FABRIC-0.md §25.7.1). fleet_heartbeat_tick_count is
* execution-paced (vm_runtime.c:143), so elapsed_ticks is now a pure
* function of the execution stream. `touched` replaces the old
* `> 0` sentinel: tick 0 is a legitimate value for a genuine first
* touch to land on, so "never touched" needs its own flag rather than
* overloading the tick value. */
if (fleet_transfer_slope_q48 > 0 &&
target->physics.touched &&
now_tick > target->physics.last_active_tick) {
uint64_t elapsed_ticks = now_tick - target->physics.last_active_tick;
uint64_t amount = (elapsed_ticks * fleet_transfer_slope_q48) >> 16;
sample.elapsed_ticks = elapsed_ticks;
sample.amount = amount;
if (amount > 0) {
/* Pull toward the touched VM, proportional to each other LIVE
* VM's current heat -- same fan-out shape as retire, mirror
* direction. Clamped to what the rest of the fleet actually
* holds, so this can never manufacture heat. */
vm_physics_node_t *n;
uint64_t others_total = 0;
uint64_t moved_total;
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
others_total += n->physics.execution_heat_q48;
}
}
/* moved_total == 0 when others_total == 0 (nothing to pull --
* the fleet is already fully concentrated elsewhere), handled
* uniformly by the same clamp as a partial cap: either way
* the actual transfer fell short of what the rate implied, so
* this touch carries no usable rate information (rev o). */
moved_total = (amount > others_total) ? others_total : amount;
sample.clamped = (moved_total != amount);
if (moved_total > 0) {
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
uint64_t share = (moved_total * n->physics.execution_heat_q48)
/ others_total;
vm_physics_transfer(&n->physics, &target->physics, share);
}
}
}
}
}
target->physics.last_active_tick = now_tick;
target->physics.touched = 1;
/* Record this touch's transfer-law inputs, not a derived heat value
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
* VMFleetTouchSample for why. */
fleet_window.touch_samples[fleet_window.head] = sample;
fleet_window.head = (fleet_window.head + 1) % VM_FLEET_WINDOW_DEPTH;
if (fleet_window.count < VM_FLEET_WINDOW_DEPTH) {
fleet_window.count++;
}
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
}
/**
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
*
* Gives L8 a real, passive-observer-respecting causal channel into the
* fleet's transfer rate (rev q, VM-FLEET-ATTRACTOR-DESIGN-20260705.md):
* previously nothing L8 did could affect fleet dynamics at all (rev j's
* architectural diagnosis for its null DoE result). Reuses Hera's
* adaptive table's current_regime -- the same 3-bit entropy/cv/temporal
* classification the bandit already stratifies its own scores by,
* rather than inventing a new signal -- as a proxy for fleet-wide
* dispatch activity, since all VM-EXEC/VM-CALL flows through her.
*
* modulation = (popcount(regime) + 1) * 0.5, i.e. 0.5x-2.0x: more of the
* three high-signal bits active scales the fleet's already-recovered
* rate up (track change more aggressively), fewer scales it down (damp
* toward the raw empirical rate). No per-VM favoritism -- this scales
* whatever vm_physics_tick() already computed uniformly, it doesn't
* decide who receives heat.
*
* Returns Q16 65536 (neutral 1.0x) if Hera or her adaptive table isn't
* available, so a missing signal never blocks or corrupts the estimate
* this multiplies.
*/
static uint32_t l8_regime_modulation_q16(void)
{
VM *mama = (VM*)sk_get_mama_vm();
ssm_l8_state_t *l8;
uint32_t popcount;
if (!mama || !mama->ssm_l8_state) return 65536u;
l8 = (ssm_l8_state_t*)mama->ssm_l8_state;
if (!l8->table) return 65536u; /* legacy path tracks no regime */
popcount = (uint32_t)(((l8->table->current_regime >> 2) & 1u) +
((l8->table->current_regime >> 1) & 1u) +
(l8->table->current_regime & 1u));
return (popcount + 1u) * 32768u;
}
void vm_physics_tick(uint64_t now_ns)
{
uint64_t rates[VM_FLEET_WINDOW_DEPTH];
uint32_t rate_count = 0;
uint32_t i;
(void)now_ns; /* Unlike the log-linear regression this replaced, the
* estimator below needs no external time reference --
* each sample already carries its own elapsed_ticks. */
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
* substitute a default slope. Expected to stay unwarmed (or warm
* with a low-quality fit) at the current 3-VM Tripod scale -- there
* simply aren't enough distinct touch events yet. */
if (!fleet_window.is_warm) return;
/* Direct rate recovery, not curve-fitting (rev o,
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
* OLS approach of rev b-n): the transfer law is known exactly
* (amount = elapsed_ticks * slope >> 16), so any unclamped touch can
* be inverted directly for an exact per-sample rate. Order doesn't
* matter here -- a median over the observation window, not a
* time-indexed fit -- so this walks the fixed-size array directly
* rather than replaying ring order from fleet_window.head. */
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
if (s->clamped || s->amount == 0 || s->elapsed_ticks == 0) continue;
rates[rate_count++] = (s->amount << 16) / s->elapsed_ticks;
}
/* slope_fit_quality_q48 as the informative fraction of the window --
* a real measure now, not a fixed placeholder. Reported even when
* there's too little to fit, same as a partial-sample regression
* would report low confidence rather than nothing at all. */
slope_fit_quality_q48 = ((uint64_t)rate_count << 16) / VM_FLEET_WINDOW_DEPTH;
/* Too few informative samples to trust: keep the current slope
* rather than overwrite it with an estimate from a handful of
* points (or zero) -- the same "skip, don't substitute a degenerate
* value" philosophy as the is_warm gate above. */
if (rate_count < VM_PHYSICS_MIN_INFORMATIVE_SAMPLES) return;
/* Insertion sort for the median -- rate_count <= VM_FLEET_WINDOW_DEPTH
* (64), no library qsort in this freestanding kernel-only file. */
for (i = 1; i < rate_count; i++) {
uint64_t key = rates[i];
uint32_t j = i;
while (j > 0 && rates[j - 1] > key) {
rates[j] = rates[j - 1];
j--;
}
rates[j] = key;
}
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
}
/* Gated on STADIUM_CAPACITY_TICK, not HEARTBEAT_INFERENCE_FREQUENCY (fixed
* 2026-08-15, FABRIC-1.md §12 Q5): fleet_heartbeat_tick_count is fed by
* EVERY live VM's own vm_tick() (see the comment above this counter's
* declaration), so it advances several times faster in wall-clock terms
* than a single VM's own tick_count -- comparing it against
* HEARTBEAT_INFERENCE_FREQUENCY (written assuming a single VM's stream)
* meant this fired far more often than §22.4's required ~1000:1 separation
* from a VM's own heat-inference loop. Still one clock, one counter --
* this is a bigger flat threshold on the same counter, not a second tick
* source or anything that reads live VM count to adjust itself. */
void vm_physics_heartbeat_tick(uint64_t now_ns)
{
fleet_heartbeat_tick_count++;
if ((fleet_heartbeat_tick_count - fleet_last_capacity_tick) >= STADIUM_CAPACITY_TICK) {
vm_physics_tick(now_ns);
fleet_last_capacity_tick = fleet_heartbeat_tick_count;
}
}
uint64_t vm_physics_fleet_heat_sum(void)
{
uint64_t total = 0;
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
if (n->physics.is_live) {
total += n->physics.execution_heat_q48;
}
}
return total;
}
int vm_physics_conserved(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
return diff < VM_PHYSICS_EPSILON_Q48;
}
uint64_t vm_physics_heat_of(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_find(vm_id);
if (!n || !n->physics.is_live) return 0;
return n->physics.execution_heat_q48;
}
/* Decimal print, same shape as parity.c's parity_put_u64 -- freestanding,
* no snprintf dependency. */
static void console_put_u64(uint64_t val)
{
char buf[21];
int i = 20;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = (char)('0' + (val % 10));
val /= 10;
}
}
console_puts(&buf[i + 1]);
}
void vm_physics_status(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
console_puts("VM-PHYSICS: fleet_heat_sum=");
console_put_u64(sum);
console_println("");
console_puts("VM-PHYSICS: conserved=");
console_println(vm_physics_conserved() ? "CONSERVED" : "DRIFTED");
console_puts("VM-PHYSICS: fleet_transfer_slope_q48=");
console_put_u64(fleet_transfer_slope_q48);
console_println("");
console_puts("VM-PHYSICS: slope_fit_quality_q48=");
console_put_u64(slope_fit_quality_q48);
console_println("");
console_puts("VM-PHYSICS: fleet_window_warm=");
console_println(fleet_window.is_warm ? "YES" : "NO");
/* Per-VM breakdown (VM-FLEET-ATTRACTOR-DESIGN-20260705.md transparency
* requirement): the aggregate above always reads Q.1 when conserved,
* which says nothing about how heat is actually split among live VMs. */
{
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
VMRegistryEntry entry;
if (!n->physics.is_live) continue;
console_puts("VM-PHYSICS: vm_id=");
{
char uuid_buf[37];
vm_uuid_format(n->vm_id, uuid_buf);
console_puts(uuid_buf);
}
if (capsule_vm_registry_get(n->vm_id, &entry) == 0) {
console_puts(" name=");
console_puts(entry.name);
}
console_puts(" heat_q48=");
console_put_u64(n->physics.execution_heat_q48);
console_println("");
}
}
}